attachmentSettings.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { TAPi18n } from '/imports/i18n';
  3. import { Meteor } from 'meteor/meteor';
  4. import { Session } from 'meteor/session';
  5. import { Tracker } from 'meteor/tracker';
  6. import { ReactiveVar } from 'meteor/reactive-var';
  7. import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
  8. import { Chart } from 'chart.js';
  9. // Global reactive variables for attachment settings
  10. const attachmentSettings = {
  11. loading: new ReactiveVar(false),
  12. showStorageSettings: new ReactiveVar(false),
  13. showMigration: new ReactiveVar(false),
  14. showMonitoring: new ReactiveVar(false),
  15. // Storage configuration
  16. filesystemPath: new ReactiveVar(''),
  17. attachmentsPath: new ReactiveVar(''),
  18. avatarsPath: new ReactiveVar(''),
  19. gridfsEnabled: new ReactiveVar(false),
  20. s3Enabled: new ReactiveVar(false),
  21. s3Endpoint: new ReactiveVar(''),
  22. s3Bucket: new ReactiveVar(''),
  23. s3Region: new ReactiveVar(''),
  24. s3SslEnabled: new ReactiveVar(false),
  25. s3Port: new ReactiveVar(443),
  26. // Migration settings
  27. migrationBatchSize: new ReactiveVar(10),
  28. migrationDelayMs: new ReactiveVar(1000),
  29. migrationCpuThreshold: new ReactiveVar(70),
  30. migrationProgress: new ReactiveVar(0),
  31. migrationStatus: new ReactiveVar('idle'),
  32. migrationLog: new ReactiveVar(''),
  33. // Monitoring data
  34. totalAttachments: new ReactiveVar(0),
  35. filesystemAttachments: new ReactiveVar(0),
  36. gridfsAttachments: new ReactiveVar(0),
  37. s3Attachments: new ReactiveVar(0),
  38. totalSize: new ReactiveVar(0),
  39. filesystemSize: new ReactiveVar(0),
  40. gridfsSize: new ReactiveVar(0),
  41. s3Size: new ReactiveVar(0),
  42. // Migration state
  43. isMigrationRunning: new ReactiveVar(false),
  44. isMigrationPaused: new ReactiveVar(false),
  45. migrationQueue: new ReactiveVar([]),
  46. currentMigration: new ReactiveVar(null)
  47. };
  48. // Main attachment settings component
  49. BlazeComponent.extendComponent({
  50. onCreated() {
  51. this.loading = attachmentSettings.loading;
  52. this.showStorageSettings = attachmentSettings.showStorageSettings;
  53. this.showMigration = attachmentSettings.showMigration;
  54. this.showMonitoring = attachmentSettings.showMonitoring;
  55. // Set default sub-menu state
  56. this.showStorageSettings.set(true);
  57. this.showMigration.set(false);
  58. this.showMonitoring.set(false);
  59. // Load initial data
  60. this.loadStorageConfiguration();
  61. this.loadMigrationSettings();
  62. this.loadMonitoringData();
  63. },
  64. // Template helpers for this component
  65. loading() {
  66. return this.loading.get();
  67. },
  68. showStorageSettings() {
  69. return this.showStorageSettings.get();
  70. },
  71. showMigration() {
  72. return this.showMigration.get();
  73. },
  74. showMonitoring() {
  75. return this.showMonitoring.get();
  76. },
  77. events() {
  78. return [
  79. {
  80. 'click a.js-attachment-storage-settings': this.switchToStorageSettings,
  81. 'click a.js-attachment-migration': this.switchToMigration,
  82. 'click a.js-attachment-monitoring': this.switchToMonitoring,
  83. }
  84. ];
  85. },
  86. switchToStorageSettings(event) {
  87. this.switchMenu(event, 'storage-settings');
  88. this.showStorageSettings.set(true);
  89. this.showMigration.set(false);
  90. this.showMonitoring.set(false);
  91. },
  92. switchToMigration(event) {
  93. this.switchMenu(event, 'attachment-migration');
  94. this.showStorageSettings.set(false);
  95. this.showMigration.set(true);
  96. this.showMonitoring.set(false);
  97. },
  98. switchToMonitoring(event) {
  99. this.switchMenu(event, 'attachment-monitoring');
  100. this.showStorageSettings.set(false);
  101. this.showMigration.set(false);
  102. this.showMonitoring.set(true);
  103. },
  104. switchMenu(event, targetId) {
  105. const target = $(event.target);
  106. if (!target.hasClass('active')) {
  107. this.loading.set(true);
  108. $('.side-menu li.active').removeClass('active');
  109. target.parent().addClass('active');
  110. // Load data based on target
  111. if (targetId === 'storage-settings') {
  112. this.loadStorageConfiguration();
  113. } else if (targetId === 'attachment-migration') {
  114. this.loadMigrationSettings();
  115. } else if (targetId === 'attachment-monitoring') {
  116. this.loadMonitoringData();
  117. }
  118. this.loading.set(false);
  119. }
  120. },
  121. loadStorageConfiguration() {
  122. Meteor.call('getAttachmentStorageConfiguration', (error, result) => {
  123. if (!error && result) {
  124. attachmentSettings.filesystemPath.set(result.filesystemPath || '');
  125. attachmentSettings.attachmentsPath.set(result.attachmentsPath || '');
  126. attachmentSettings.avatarsPath.set(result.avatarsPath || '');
  127. attachmentSettings.gridfsEnabled.set(result.gridfsEnabled || false);
  128. attachmentSettings.s3Enabled.set(result.s3Enabled || false);
  129. attachmentSettings.s3Endpoint.set(result.s3Endpoint || '');
  130. attachmentSettings.s3Bucket.set(result.s3Bucket || '');
  131. attachmentSettings.s3Region.set(result.s3Region || '');
  132. attachmentSettings.s3SslEnabled.set(result.s3SslEnabled || false);
  133. attachmentSettings.s3Port.set(result.s3Port || 443);
  134. }
  135. });
  136. },
  137. loadMigrationSettings() {
  138. Meteor.call('getAttachmentMigrationSettings', (error, result) => {
  139. if (!error && result) {
  140. attachmentSettings.migrationBatchSize.set(result.batchSize || 10);
  141. attachmentSettings.migrationDelayMs.set(result.delayMs || 1000);
  142. attachmentSettings.migrationCpuThreshold.set(result.cpuThreshold || 70);
  143. attachmentSettings.migrationStatus.set(result.status || 'idle');
  144. attachmentSettings.migrationProgress.set(result.progress || 0);
  145. }
  146. });
  147. },
  148. loadMonitoringData() {
  149. Meteor.call('getAttachmentMonitoringData', (error, result) => {
  150. if (!error && result) {
  151. attachmentSettings.totalAttachments.set(result.totalAttachments || 0);
  152. attachmentSettings.filesystemAttachments.set(result.filesystemAttachments || 0);
  153. attachmentSettings.gridfsAttachments.set(result.gridfsAttachments || 0);
  154. attachmentSettings.s3Attachments.set(result.s3Attachments || 0);
  155. attachmentSettings.totalSize.set(result.totalSize || 0);
  156. attachmentSettings.filesystemSize.set(result.filesystemSize || 0);
  157. attachmentSettings.gridfsSize.set(result.gridfsSize || 0);
  158. attachmentSettings.s3Size.set(result.s3Size || 0);
  159. }
  160. });
  161. }
  162. }).register('attachmentSettings');
  163. // Storage settings component
  164. BlazeComponent.extendComponent({
  165. onCreated() {
  166. this.filesystemPath = attachmentSettings.filesystemPath;
  167. this.attachmentsPath = attachmentSettings.attachmentsPath;
  168. this.avatarsPath = attachmentSettings.avatarsPath;
  169. this.gridfsEnabled = attachmentSettings.gridfsEnabled;
  170. this.s3Enabled = attachmentSettings.s3Enabled;
  171. this.s3Endpoint = attachmentSettings.s3Endpoint;
  172. this.s3Bucket = attachmentSettings.s3Bucket;
  173. this.s3Region = attachmentSettings.s3Region;
  174. this.s3SslEnabled = attachmentSettings.s3SslEnabled;
  175. this.s3Port = attachmentSettings.s3Port;
  176. },
  177. events() {
  178. return [
  179. {
  180. 'click button.js-test-s3-connection': this.testS3Connection,
  181. 'click button.js-save-s3-settings': this.saveS3Settings,
  182. 'change input#s3-secret-key': this.updateS3SecretKey
  183. }
  184. ];
  185. },
  186. testS3Connection() {
  187. const secretKey = $('#s3-secret-key').val();
  188. if (!secretKey) {
  189. alert(TAPi18n.__('s3-secret-key-required'));
  190. return;
  191. }
  192. Meteor.call('testS3Connection', { secretKey }, (error, result) => {
  193. if (error) {
  194. alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
  195. } else {
  196. alert(TAPi18n.__('s3-connection-success'));
  197. }
  198. });
  199. },
  200. saveS3Settings() {
  201. const secretKey = $('#s3-secret-key').val();
  202. if (!secretKey) {
  203. alert(TAPi18n.__('s3-secret-key-required'));
  204. return;
  205. }
  206. Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
  207. if (error) {
  208. alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
  209. } else {
  210. alert(TAPi18n.__('s3-settings-saved'));
  211. $('#s3-secret-key').val(''); // Clear the password field
  212. }
  213. });
  214. },
  215. updateS3SecretKey(event) {
  216. // This method can be used to validate the secret key format
  217. const secretKey = event.target.value;
  218. // Add validation logic here if needed
  219. }
  220. }).register('storageSettings');
  221. // Migration component
  222. BlazeComponent.extendComponent({
  223. onCreated() {
  224. this.migrationBatchSize = attachmentSettings.migrationBatchSize;
  225. this.migrationDelayMs = attachmentSettings.migrationDelayMs;
  226. this.migrationCpuThreshold = attachmentSettings.migrationCpuThreshold;
  227. this.migrationProgress = attachmentSettings.migrationProgress;
  228. this.migrationStatus = attachmentSettings.migrationStatus;
  229. this.migrationLog = attachmentSettings.migrationLog;
  230. this.isMigrationRunning = attachmentSettings.isMigrationRunning;
  231. this.isMigrationPaused = attachmentSettings.isMigrationPaused;
  232. // Subscribe to migration updates
  233. this.subscription = Meteor.subscribe('attachmentMigrationStatus');
  234. // Set up reactive updates
  235. this.autorun(() => {
  236. const status = attachmentSettings.migrationStatus.get();
  237. if (status === 'running') {
  238. this.isMigrationRunning.set(true);
  239. } else {
  240. this.isMigrationRunning.set(false);
  241. }
  242. });
  243. },
  244. onDestroyed() {
  245. if (this.subscription) {
  246. this.subscription.stop();
  247. }
  248. },
  249. events() {
  250. return [
  251. {
  252. 'click button.js-migrate-all-to-filesystem': () => this.startMigration('filesystem'),
  253. 'click button.js-migrate-all-to-gridfs': () => this.startMigration('gridfs'),
  254. 'click button.js-migrate-all-to-s3': () => this.startMigration('s3'),
  255. 'click button.js-pause-migration': this.pauseMigration,
  256. 'click button.js-resume-migration': this.resumeMigration,
  257. 'click button.js-stop-migration': this.stopMigration,
  258. 'change input#migration-batch-size': this.updateBatchSize,
  259. 'change input#migration-delay-ms': this.updateDelayMs,
  260. 'change input#migration-cpu-threshold': this.updateCpuThreshold
  261. }
  262. ];
  263. },
  264. startMigration(targetStorage) {
  265. const batchSize = parseInt($('#migration-batch-size').val()) || 10;
  266. const delayMs = parseInt($('#migration-delay-ms').val()) || 1000;
  267. const cpuThreshold = parseInt($('#migration-cpu-threshold').val()) || 70;
  268. Meteor.call('startAttachmentMigration', {
  269. targetStorage,
  270. batchSize,
  271. delayMs,
  272. cpuThreshold
  273. }, (error, result) => {
  274. if (error) {
  275. alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
  276. } else {
  277. this.addToLog(TAPi18n.__('migration-started') + ': ' + targetStorage);
  278. }
  279. });
  280. },
  281. pauseMigration() {
  282. Meteor.call('pauseAttachmentMigration', (error, result) => {
  283. if (error) {
  284. alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
  285. } else {
  286. this.addToLog(TAPi18n.__('migration-paused'));
  287. }
  288. });
  289. },
  290. resumeMigration() {
  291. Meteor.call('resumeAttachmentMigration', (error, result) => {
  292. if (error) {
  293. alert(TAPi18n.__('migration-resume-failed') + ': ' + error.reason);
  294. } else {
  295. this.addToLog(TAPi18n.__('migration-resumed'));
  296. }
  297. });
  298. },
  299. stopMigration() {
  300. if (confirm(TAPi18n.__('migration-stop-confirm'))) {
  301. Meteor.call('stopAttachmentMigration', (error, result) => {
  302. if (error) {
  303. alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
  304. } else {
  305. this.addToLog(TAPi18n.__('migration-stopped'));
  306. }
  307. });
  308. }
  309. },
  310. updateBatchSize(event) {
  311. const value = parseInt(event.target.value);
  312. if (value >= 1 && value <= 100) {
  313. attachmentSettings.migrationBatchSize.set(value);
  314. }
  315. },
  316. updateDelayMs(event) {
  317. const value = parseInt(event.target.value);
  318. if (value >= 100 && value <= 10000) {
  319. attachmentSettings.migrationDelayMs.set(value);
  320. }
  321. },
  322. updateCpuThreshold(event) {
  323. const value = parseInt(event.target.value);
  324. if (value >= 10 && value <= 90) {
  325. attachmentSettings.migrationCpuThreshold.set(value);
  326. }
  327. },
  328. addToLog(message) {
  329. const timestamp = new Date().toISOString();
  330. const currentLog = attachmentSettings.migrationLog.get();
  331. const newLog = `[${timestamp}] ${message}\n${currentLog}`;
  332. attachmentSettings.migrationLog.set(newLog);
  333. }
  334. }).register('attachmentMigration');
  335. // Monitoring component
  336. BlazeComponent.extendComponent({
  337. onCreated() {
  338. this.totalAttachments = attachmentSettings.totalAttachments;
  339. this.filesystemAttachments = attachmentSettings.filesystemAttachments;
  340. this.gridfsAttachments = attachmentSettings.gridfsAttachments;
  341. this.s3Attachments = attachmentSettings.s3Attachments;
  342. this.totalSize = attachmentSettings.totalSize;
  343. this.filesystemSize = attachmentSettings.filesystemSize;
  344. this.gridfsSize = attachmentSettings.gridfsSize;
  345. this.s3Size = attachmentSettings.s3Size;
  346. // Subscribe to monitoring updates
  347. this.subscription = Meteor.subscribe('attachmentMonitoringData');
  348. // Set up chart
  349. this.autorun(() => {
  350. this.updateChart();
  351. });
  352. },
  353. onDestroyed() {
  354. if (this.subscription) {
  355. this.subscription.stop();
  356. }
  357. },
  358. events() {
  359. return [
  360. {
  361. 'click button.js-refresh-monitoring': this.refreshMonitoring,
  362. 'click button.js-export-monitoring': this.exportMonitoring
  363. }
  364. ];
  365. },
  366. refreshMonitoring() {
  367. Meteor.call('refreshAttachmentMonitoringData', (error, result) => {
  368. if (error) {
  369. alert(TAPi18n.__('monitoring-refresh-failed') + ': ' + error.reason);
  370. }
  371. });
  372. },
  373. exportMonitoring() {
  374. Meteor.call('exportAttachmentMonitoringData', (error, result) => {
  375. if (error) {
  376. alert(TAPi18n.__('monitoring-export-failed') + ': ' + error.reason);
  377. } else {
  378. // Download the exported data
  379. const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
  380. const url = URL.createObjectURL(blob);
  381. const a = document.createElement('a');
  382. a.href = url;
  383. a.download = 'wekan-attachment-monitoring.json';
  384. document.body.appendChild(a);
  385. a.click();
  386. document.body.removeChild(a);
  387. URL.revokeObjectURL(url);
  388. }
  389. });
  390. },
  391. updateChart() {
  392. const ctx = document.getElementById('storage-distribution-chart');
  393. if (!ctx) return;
  394. const filesystemCount = this.filesystemAttachments.get();
  395. const gridfsCount = this.gridfsAttachments.get();
  396. const s3Count = this.s3Attachments.get();
  397. if (this.chart) {
  398. this.chart.destroy();
  399. }
  400. this.chart = new Chart(ctx, {
  401. type: 'doughnut',
  402. data: {
  403. labels: [
  404. TAPi18n.__('filesystem-storage'),
  405. TAPi18n.__('gridfs-storage'),
  406. TAPi18n.__('s3-storage')
  407. ],
  408. datasets: [{
  409. data: [filesystemCount, gridfsCount, s3Count],
  410. backgroundColor: [
  411. '#28a745',
  412. '#007bff',
  413. '#ffc107'
  414. ]
  415. }]
  416. },
  417. options: {
  418. responsive: true,
  419. maintainAspectRatio: false,
  420. plugins: {
  421. legend: {
  422. position: 'bottom'
  423. }
  424. }
  425. }
  426. });
  427. }
  428. }).register('attachmentMonitoring');
  429. // Template helpers for attachmentSettings
  430. Template.attachmentSettings.helpers({
  431. loading() {
  432. const instance = Template.instance();
  433. return instance.loading && instance.loading.get();
  434. },
  435. showStorageSettings() {
  436. const instance = Template.instance();
  437. return instance.showStorageSettings && instance.showStorageSettings.get();
  438. },
  439. showMigration() {
  440. const instance = Template.instance();
  441. return instance.showMigration && instance.showMigration.get();
  442. },
  443. showMonitoring() {
  444. const instance = Template.instance();
  445. return instance.showMonitoring && instance.showMonitoring.get();
  446. },
  447. });
  448. // Export the attachment settings for use in other components
  449. export { attachmentSettings };