mirror of
https://github.com/wekan/wekan.git
synced 2025-12-18 00:10:13 +01:00
Fixed Admin Panel Settings menus Attachments and Cron.
Thanks to xet7 !
This commit is contained in:
parent
e0013b9b63
commit
7bb1e24bda
8 changed files with 485 additions and 1302 deletions
|
|
@ -15,6 +15,3 @@ import '/client/components/migrationProgress';
|
||||||
|
|
||||||
// Import cron settings
|
// Import cron settings
|
||||||
import '/client/components/settings/cronSettings';
|
import '/client/components/settings/cronSettings';
|
||||||
|
|
||||||
// Import settings tabs CSS
|
|
||||||
import '/client/components/settings/settingsTabs.css';
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,74 @@
|
||||||
template(name="attachmentSettings")
|
template(name="attachmentSettings")
|
||||||
.attachment-settings-content
|
ul#attachment-setting.setting-detail
|
||||||
.settings-tabs
|
li
|
||||||
ul.tab-nav
|
h3 {{_ 'attachment-storage-configuration'}}
|
||||||
li(class="{{#if showStorageSettings}}active{{/if}}")
|
.form-group
|
||||||
a.js-attachment-storage-settings(data-id="storage-settings")
|
label {{_ 'writable-path'}}
|
||||||
i.fa.fa-cog
|
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
|
||||||
| {{_ 'attachment-storage-settings'}}
|
small.form-text.text-muted {{_ 'filesystem-path-description'}}
|
||||||
li(class="{{#if showMigration}}active{{/if}}")
|
|
||||||
a.js-attachment-migration(data-id="attachment-migration")
|
.form-group
|
||||||
i.fa.fa-arrow-right
|
label {{_ 'attachments-path'}}
|
||||||
| {{_ 'attachment-migration'}}
|
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
|
||||||
li(class="{{#if showMonitoring}}active{{/if}}")
|
small.form-text.text-muted {{_ 'attachments-path-description'}}
|
||||||
a.js-attachment-monitoring(data-id="attachment-monitoring")
|
|
||||||
i.fa.fa-chart-line
|
.form-group
|
||||||
| {{_ 'attachment-monitoring'}}
|
label {{_ 'avatars-path'}}
|
||||||
|
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
|
||||||
.tab-content
|
small.form-text.text-muted {{_ 'avatars-path-description'}}
|
||||||
if loading
|
|
||||||
+spinner
|
li
|
||||||
else if showStorageSettings
|
h3 {{_ 'mongodb-gridfs-storage'}}
|
||||||
+storageSettings
|
.form-group
|
||||||
else if showMigration
|
label {{_ 'gridfs-enabled'}}
|
||||||
+attachmentMigration
|
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
|
||||||
else if showMonitoring
|
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
|
||||||
+attachmentMonitoring
|
|
||||||
|
li
|
||||||
|
h3 {{_ 's3-minio-storage'}}
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-enabled'}}
|
||||||
|
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
|
||||||
|
small.form-text.text-muted {{_ 's3-enabled-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-endpoint'}}
|
||||||
|
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-bucket'}}
|
||||||
|
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-bucket-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-region'}}
|
||||||
|
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-region-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-access-key'}}
|
||||||
|
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-access-key-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-secret-key'}}
|
||||||
|
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
|
||||||
|
small.form-text.text-muted {{_ 's3-secret-key-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-ssl-enabled'}}
|
||||||
|
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
|
||||||
|
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-port'}}
|
||||||
|
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-port-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
|
||||||
|
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
|
||||||
|
|
||||||
template(name="storageSettings")
|
template(name="storageSettings")
|
||||||
.storage-settings
|
.storage-settings
|
||||||
|
|
|
||||||
|
|
@ -1,504 +0,0 @@
|
||||||
import { ReactiveCache } from '/imports/reactiveCache';
|
|
||||||
import { TAPi18n } from '/imports/i18n';
|
|
||||||
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import { Session } from 'meteor/session';
|
|
||||||
import { Tracker } from 'meteor/tracker';
|
|
||||||
import { ReactiveVar } from 'meteor/reactive-var';
|
|
||||||
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components';
|
|
||||||
import { Chart } from 'chart.js';
|
|
||||||
|
|
||||||
// Global reactive variables for attachment settings
|
|
||||||
const attachmentSettings = {
|
|
||||||
loading: new ReactiveVar(false),
|
|
||||||
showStorageSettings: new ReactiveVar(false),
|
|
||||||
showMigration: new ReactiveVar(false),
|
|
||||||
showMonitoring: new ReactiveVar(false),
|
|
||||||
|
|
||||||
// Storage configuration
|
|
||||||
filesystemPath: new ReactiveVar(''),
|
|
||||||
attachmentsPath: new ReactiveVar(''),
|
|
||||||
avatarsPath: new ReactiveVar(''),
|
|
||||||
gridfsEnabled: new ReactiveVar(false),
|
|
||||||
s3Enabled: new ReactiveVar(false),
|
|
||||||
s3Endpoint: new ReactiveVar(''),
|
|
||||||
s3Bucket: new ReactiveVar(''),
|
|
||||||
s3Region: new ReactiveVar(''),
|
|
||||||
s3SslEnabled: new ReactiveVar(false),
|
|
||||||
s3Port: new ReactiveVar(443),
|
|
||||||
|
|
||||||
// Migration settings
|
|
||||||
migrationBatchSize: new ReactiveVar(10),
|
|
||||||
migrationDelayMs: new ReactiveVar(1000),
|
|
||||||
migrationCpuThreshold: new ReactiveVar(70),
|
|
||||||
migrationProgress: new ReactiveVar(0),
|
|
||||||
migrationStatus: new ReactiveVar('idle'),
|
|
||||||
migrationLog: new ReactiveVar(''),
|
|
||||||
|
|
||||||
// Monitoring data
|
|
||||||
totalAttachments: new ReactiveVar(0),
|
|
||||||
filesystemAttachments: new ReactiveVar(0),
|
|
||||||
gridfsAttachments: new ReactiveVar(0),
|
|
||||||
s3Attachments: new ReactiveVar(0),
|
|
||||||
totalSize: new ReactiveVar(0),
|
|
||||||
filesystemSize: new ReactiveVar(0),
|
|
||||||
gridfsSize: new ReactiveVar(0),
|
|
||||||
s3Size: new ReactiveVar(0),
|
|
||||||
|
|
||||||
// Migration state
|
|
||||||
isMigrationRunning: new ReactiveVar(false),
|
|
||||||
isMigrationPaused: new ReactiveVar(false),
|
|
||||||
migrationQueue: new ReactiveVar([]),
|
|
||||||
currentMigration: new ReactiveVar(null)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main attachment settings component
|
|
||||||
BlazeComponent.extendComponent({
|
|
||||||
onCreated() {
|
|
||||||
this.loading = attachmentSettings.loading;
|
|
||||||
this.showStorageSettings = attachmentSettings.showStorageSettings;
|
|
||||||
this.showMigration = attachmentSettings.showMigration;
|
|
||||||
this.showMonitoring = attachmentSettings.showMonitoring;
|
|
||||||
|
|
||||||
// Set default sub-menu state
|
|
||||||
this.showStorageSettings.set(true);
|
|
||||||
this.showMigration.set(false);
|
|
||||||
this.showMonitoring.set(false);
|
|
||||||
|
|
||||||
// Load initial data
|
|
||||||
this.loadStorageConfiguration();
|
|
||||||
this.loadMigrationSettings();
|
|
||||||
this.loadMonitoringData();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Template helpers for this component
|
|
||||||
loading() {
|
|
||||||
return this.loading.get();
|
|
||||||
},
|
|
||||||
showStorageSettings() {
|
|
||||||
return this.showStorageSettings.get();
|
|
||||||
},
|
|
||||||
showMigration() {
|
|
||||||
return this.showMigration.get();
|
|
||||||
},
|
|
||||||
showMonitoring() {
|
|
||||||
return this.showMonitoring.get();
|
|
||||||
},
|
|
||||||
|
|
||||||
events() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'click a.js-attachment-storage-settings': this.switchToStorageSettings,
|
|
||||||
'click a.js-attachment-migration': this.switchToMigration,
|
|
||||||
'click a.js-attachment-monitoring': this.switchToMonitoring,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
switchToStorageSettings(event) {
|
|
||||||
this.switchMenu(event, 'storage-settings');
|
|
||||||
this.showStorageSettings.set(true);
|
|
||||||
this.showMigration.set(false);
|
|
||||||
this.showMonitoring.set(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
switchToMigration(event) {
|
|
||||||
this.switchMenu(event, 'attachment-migration');
|
|
||||||
this.showStorageSettings.set(false);
|
|
||||||
this.showMigration.set(true);
|
|
||||||
this.showMonitoring.set(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
switchToMonitoring(event) {
|
|
||||||
this.switchMenu(event, 'attachment-monitoring');
|
|
||||||
this.showStorageSettings.set(false);
|
|
||||||
this.showMigration.set(false);
|
|
||||||
this.showMonitoring.set(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
switchMenu(event, targetId) {
|
|
||||||
const target = $(event.target);
|
|
||||||
if (!target.hasClass('active')) {
|
|
||||||
this.loading.set(true);
|
|
||||||
|
|
||||||
$('.side-menu li.active').removeClass('active');
|
|
||||||
target.parent().addClass('active');
|
|
||||||
|
|
||||||
// Load data based on target
|
|
||||||
if (targetId === 'storage-settings') {
|
|
||||||
this.loadStorageConfiguration();
|
|
||||||
} else if (targetId === 'attachment-migration') {
|
|
||||||
this.loadMigrationSettings();
|
|
||||||
} else if (targetId === 'attachment-monitoring') {
|
|
||||||
this.loadMonitoringData();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.loading.set(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
loadStorageConfiguration() {
|
|
||||||
Meteor.call('getAttachmentStorageConfiguration', (error, result) => {
|
|
||||||
if (!error && result) {
|
|
||||||
attachmentSettings.filesystemPath.set(result.filesystemPath || '');
|
|
||||||
attachmentSettings.attachmentsPath.set(result.attachmentsPath || '');
|
|
||||||
attachmentSettings.avatarsPath.set(result.avatarsPath || '');
|
|
||||||
attachmentSettings.gridfsEnabled.set(result.gridfsEnabled || false);
|
|
||||||
attachmentSettings.s3Enabled.set(result.s3Enabled || false);
|
|
||||||
attachmentSettings.s3Endpoint.set(result.s3Endpoint || '');
|
|
||||||
attachmentSettings.s3Bucket.set(result.s3Bucket || '');
|
|
||||||
attachmentSettings.s3Region.set(result.s3Region || '');
|
|
||||||
attachmentSettings.s3SslEnabled.set(result.s3SslEnabled || false);
|
|
||||||
attachmentSettings.s3Port.set(result.s3Port || 443);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
loadMigrationSettings() {
|
|
||||||
Meteor.call('getAttachmentMigrationSettings', (error, result) => {
|
|
||||||
if (!error && result) {
|
|
||||||
attachmentSettings.migrationBatchSize.set(result.batchSize || 10);
|
|
||||||
attachmentSettings.migrationDelayMs.set(result.delayMs || 1000);
|
|
||||||
attachmentSettings.migrationCpuThreshold.set(result.cpuThreshold || 70);
|
|
||||||
attachmentSettings.migrationStatus.set(result.status || 'idle');
|
|
||||||
attachmentSettings.migrationProgress.set(result.progress || 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
loadMonitoringData() {
|
|
||||||
Meteor.call('getAttachmentMonitoringData', (error, result) => {
|
|
||||||
if (!error && result) {
|
|
||||||
attachmentSettings.totalAttachments.set(result.totalAttachments || 0);
|
|
||||||
attachmentSettings.filesystemAttachments.set(result.filesystemAttachments || 0);
|
|
||||||
attachmentSettings.gridfsAttachments.set(result.gridfsAttachments || 0);
|
|
||||||
attachmentSettings.s3Attachments.set(result.s3Attachments || 0);
|
|
||||||
attachmentSettings.totalSize.set(result.totalSize || 0);
|
|
||||||
attachmentSettings.filesystemSize.set(result.filesystemSize || 0);
|
|
||||||
attachmentSettings.gridfsSize.set(result.gridfsSize || 0);
|
|
||||||
attachmentSettings.s3Size.set(result.s3Size || 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).register('attachmentSettings');
|
|
||||||
|
|
||||||
// Storage settings component
|
|
||||||
BlazeComponent.extendComponent({
|
|
||||||
onCreated() {
|
|
||||||
this.filesystemPath = attachmentSettings.filesystemPath;
|
|
||||||
this.attachmentsPath = attachmentSettings.attachmentsPath;
|
|
||||||
this.avatarsPath = attachmentSettings.avatarsPath;
|
|
||||||
this.gridfsEnabled = attachmentSettings.gridfsEnabled;
|
|
||||||
this.s3Enabled = attachmentSettings.s3Enabled;
|
|
||||||
this.s3Endpoint = attachmentSettings.s3Endpoint;
|
|
||||||
this.s3Bucket = attachmentSettings.s3Bucket;
|
|
||||||
this.s3Region = attachmentSettings.s3Region;
|
|
||||||
this.s3SslEnabled = attachmentSettings.s3SslEnabled;
|
|
||||||
this.s3Port = attachmentSettings.s3Port;
|
|
||||||
},
|
|
||||||
|
|
||||||
events() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'click button.js-test-s3-connection': this.testS3Connection,
|
|
||||||
'click button.js-save-s3-settings': this.saveS3Settings,
|
|
||||||
'change input#s3-secret-key': this.updateS3SecretKey
|
|
||||||
}
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
testS3Connection() {
|
|
||||||
const secretKey = $('#s3-secret-key').val();
|
|
||||||
if (!secretKey) {
|
|
||||||
alert(TAPi18n.__('s3-secret-key-required'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Meteor.call('testS3Connection', { secretKey }, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
|
|
||||||
} else {
|
|
||||||
alert(TAPi18n.__('s3-connection-success'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
saveS3Settings() {
|
|
||||||
const secretKey = $('#s3-secret-key').val();
|
|
||||||
if (!secretKey) {
|
|
||||||
alert(TAPi18n.__('s3-secret-key-required'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
|
|
||||||
} else {
|
|
||||||
alert(TAPi18n.__('s3-settings-saved'));
|
|
||||||
$('#s3-secret-key').val(''); // Clear the password field
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateS3SecretKey(event) {
|
|
||||||
// This method can be used to validate the secret key format
|
|
||||||
const secretKey = event.target.value;
|
|
||||||
// Add validation logic here if needed
|
|
||||||
}
|
|
||||||
}).register('storageSettings');
|
|
||||||
|
|
||||||
// Migration component
|
|
||||||
BlazeComponent.extendComponent({
|
|
||||||
onCreated() {
|
|
||||||
this.migrationBatchSize = attachmentSettings.migrationBatchSize;
|
|
||||||
this.migrationDelayMs = attachmentSettings.migrationDelayMs;
|
|
||||||
this.migrationCpuThreshold = attachmentSettings.migrationCpuThreshold;
|
|
||||||
this.migrationProgress = attachmentSettings.migrationProgress;
|
|
||||||
this.migrationStatus = attachmentSettings.migrationStatus;
|
|
||||||
this.migrationLog = attachmentSettings.migrationLog;
|
|
||||||
this.isMigrationRunning = attachmentSettings.isMigrationRunning;
|
|
||||||
this.isMigrationPaused = attachmentSettings.isMigrationPaused;
|
|
||||||
|
|
||||||
// Subscribe to migration updates
|
|
||||||
this.subscription = Meteor.subscribe('attachmentMigrationStatus');
|
|
||||||
|
|
||||||
// Set up reactive updates
|
|
||||||
this.autorun(() => {
|
|
||||||
const status = attachmentSettings.migrationStatus.get();
|
|
||||||
if (status === 'running') {
|
|
||||||
this.isMigrationRunning.set(true);
|
|
||||||
} else {
|
|
||||||
this.isMigrationRunning.set(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onDestroyed() {
|
|
||||||
if (this.subscription) {
|
|
||||||
this.subscription.stop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
events() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'click button.js-migrate-all-to-filesystem': () => this.startMigration('filesystem'),
|
|
||||||
'click button.js-migrate-all-to-gridfs': () => this.startMigration('gridfs'),
|
|
||||||
'click button.js-migrate-all-to-s3': () => this.startMigration('s3'),
|
|
||||||
'click button.js-pause-migration': this.pauseMigration,
|
|
||||||
'click button.js-resume-migration': this.resumeMigration,
|
|
||||||
'click button.js-stop-migration': this.stopMigration,
|
|
||||||
'change input#migration-batch-size': this.updateBatchSize,
|
|
||||||
'change input#migration-delay-ms': this.updateDelayMs,
|
|
||||||
'change input#migration-cpu-threshold': this.updateCpuThreshold
|
|
||||||
}
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
startMigration(targetStorage) {
|
|
||||||
const batchSize = parseInt($('#migration-batch-size').val()) || 10;
|
|
||||||
const delayMs = parseInt($('#migration-delay-ms').val()) || 1000;
|
|
||||||
const cpuThreshold = parseInt($('#migration-cpu-threshold').val()) || 70;
|
|
||||||
|
|
||||||
Meteor.call('startAttachmentMigration', {
|
|
||||||
targetStorage,
|
|
||||||
batchSize,
|
|
||||||
delayMs,
|
|
||||||
cpuThreshold
|
|
||||||
}, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
|
||||||
} else {
|
|
||||||
this.addToLog(TAPi18n.__('migration-started') + ': ' + targetStorage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
pauseMigration() {
|
|
||||||
Meteor.call('pauseAttachmentMigration', (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
|
|
||||||
} else {
|
|
||||||
this.addToLog(TAPi18n.__('migration-paused'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
resumeMigration() {
|
|
||||||
Meteor.call('resumeAttachmentMigration', (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
alert(TAPi18n.__('migration-resume-failed') + ': ' + error.reason);
|
|
||||||
} else {
|
|
||||||
this.addToLog(TAPi18n.__('migration-resumed'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
stopMigration() {
|
|
||||||
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
|
|
||||||
Meteor.call('stopAttachmentMigration', (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
|
|
||||||
} else {
|
|
||||||
this.addToLog(TAPi18n.__('migration-stopped'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateBatchSize(event) {
|
|
||||||
const value = parseInt(event.target.value);
|
|
||||||
if (value >= 1 && value <= 100) {
|
|
||||||
attachmentSettings.migrationBatchSize.set(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateDelayMs(event) {
|
|
||||||
const value = parseInt(event.target.value);
|
|
||||||
if (value >= 100 && value <= 10000) {
|
|
||||||
attachmentSettings.migrationDelayMs.set(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateCpuThreshold(event) {
|
|
||||||
const value = parseInt(event.target.value);
|
|
||||||
if (value >= 10 && value <= 90) {
|
|
||||||
attachmentSettings.migrationCpuThreshold.set(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addToLog(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
const currentLog = attachmentSettings.migrationLog.get();
|
|
||||||
const newLog = `[${timestamp}] ${message}\n${currentLog}`;
|
|
||||||
attachmentSettings.migrationLog.set(newLog);
|
|
||||||
}
|
|
||||||
}).register('attachmentMigration');
|
|
||||||
|
|
||||||
// Monitoring component
|
|
||||||
BlazeComponent.extendComponent({
|
|
||||||
onCreated() {
|
|
||||||
this.totalAttachments = attachmentSettings.totalAttachments;
|
|
||||||
this.filesystemAttachments = attachmentSettings.filesystemAttachments;
|
|
||||||
this.gridfsAttachments = attachmentSettings.gridfsAttachments;
|
|
||||||
this.s3Attachments = attachmentSettings.s3Attachments;
|
|
||||||
this.totalSize = attachmentSettings.totalSize;
|
|
||||||
this.filesystemSize = attachmentSettings.filesystemSize;
|
|
||||||
this.gridfsSize = attachmentSettings.gridfsSize;
|
|
||||||
this.s3Size = attachmentSettings.s3Size;
|
|
||||||
|
|
||||||
// Subscribe to monitoring updates
|
|
||||||
this.subscription = Meteor.subscribe('attachmentMonitoringData');
|
|
||||||
|
|
||||||
// Set up chart
|
|
||||||
this.autorun(() => {
|
|
||||||
this.updateChart();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onDestroyed() {
|
|
||||||
if (this.subscription) {
|
|
||||||
this.subscription.stop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
events() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
'click button.js-refresh-monitoring': this.refreshMonitoring,
|
|
||||||
'click button.js-export-monitoring': this.exportMonitoring
|
|
||||||
}
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
refreshMonitoring() {
|
|
||||||
Meteor.call('refreshAttachmentMonitoringData', (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
alert(TAPi18n.__('monitoring-refresh-failed') + ': ' + error.reason);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
exportMonitoring() {
|
|
||||||
Meteor.call('exportAttachmentMonitoringData', (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
alert(TAPi18n.__('monitoring-export-failed') + ': ' + error.reason);
|
|
||||||
} else {
|
|
||||||
// Download the exported data
|
|
||||||
const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = 'wekan-attachment-monitoring.json';
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateChart() {
|
|
||||||
const ctx = document.getElementById('storage-distribution-chart');
|
|
||||||
if (!ctx) return;
|
|
||||||
|
|
||||||
const filesystemCount = this.filesystemAttachments.get();
|
|
||||||
const gridfsCount = this.gridfsAttachments.get();
|
|
||||||
const s3Count = this.s3Attachments.get();
|
|
||||||
|
|
||||||
if (this.chart) {
|
|
||||||
this.chart.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chart = new Chart(ctx, {
|
|
||||||
type: 'doughnut',
|
|
||||||
data: {
|
|
||||||
labels: [
|
|
||||||
TAPi18n.__('filesystem-storage'),
|
|
||||||
TAPi18n.__('gridfs-storage'),
|
|
||||||
TAPi18n.__('s3-storage')
|
|
||||||
],
|
|
||||||
datasets: [{
|
|
||||||
data: [filesystemCount, gridfsCount, s3Count],
|
|
||||||
backgroundColor: [
|
|
||||||
'#28a745',
|
|
||||||
'#007bff',
|
|
||||||
'#ffc107'
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'bottom'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).register('attachmentMonitoring');
|
|
||||||
|
|
||||||
// Template helpers for attachmentSettings
|
|
||||||
Template.attachmentSettings.helpers({
|
|
||||||
loading() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.loading && instance.loading.get();
|
|
||||||
},
|
|
||||||
showStorageSettings() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.showStorageSettings && instance.showStorageSettings.get();
|
|
||||||
},
|
|
||||||
showMigration() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.showMigration && instance.showMigration.get();
|
|
||||||
},
|
|
||||||
showMonitoring() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.showMonitoring && instance.showMonitoring.get();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Export the attachment settings for use in other components
|
|
||||||
export { attachmentSettings };
|
|
||||||
|
|
@ -1,35 +1,47 @@
|
||||||
template(name="cronSettings")
|
template(name="cronSettings")
|
||||||
.cron-settings-content
|
ul#cron-setting.setting-detail
|
||||||
.settings-tabs
|
li
|
||||||
ul.tab-nav
|
h3 {{_ 'cron-migrations'}}
|
||||||
li(class="{{#if showMigrations}}active{{/if}}")
|
.form-group
|
||||||
a.js-cron-migrations(data-id="cron-migrations")
|
label {{_ 'migration-status'}}
|
||||||
i.fa.fa-database
|
.status-indicator
|
||||||
| {{_ 'cron-migrations'}}
|
span.status-label {{_ 'status'}}:
|
||||||
li(class="{{#if showBoardOperations}}active{{/if}}")
|
span.status-value {{migrationStatus}}
|
||||||
a.js-cron-board-operations(data-id="cron-board-operations")
|
.progress-section
|
||||||
i.fa.fa-tasks
|
.progress
|
||||||
| {{_ 'board-operations'}}
|
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||||
li(class="{{#if showJobs}}active{{/if}}")
|
| {{migrationProgress}}%
|
||||||
a.js-cron-jobs(data-id="cron-jobs")
|
.progress-text
|
||||||
i.fa.fa-clock-o
|
| {{migrationProgress}}% {{_ 'complete'}}
|
||||||
| {{_ 'cron-jobs'}}
|
|
||||||
li(class="{{#if showAddJob}}active{{/if}}")
|
.form-group
|
||||||
a.js-cron-add(data-id="cron-add")
|
button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
|
||||||
i.fa.fa-plus
|
button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
|
||||||
| {{_ 'add-cron-job'}}
|
button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
|
||||||
|
|
||||||
.tab-content
|
li
|
||||||
if loading
|
h3 {{_ 'board-operations'}}
|
||||||
+spinner
|
.form-group
|
||||||
else if showMigrations
|
label {{_ 'scheduled-board-operations'}}
|
||||||
+cronMigrations
|
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
|
||||||
else if showBoardOperations
|
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
|
||||||
+cronBoardOperations
|
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
|
||||||
else if showJobs
|
|
||||||
+cronJobs
|
li
|
||||||
else if showAddJob
|
h3 {{_ 'cron-jobs'}}
|
||||||
+cronAddJob
|
.form-group
|
||||||
|
label {{_ 'active-cron-jobs'}}
|
||||||
|
each cronJobs
|
||||||
|
.job-item
|
||||||
|
.job-info
|
||||||
|
.job-name {{name}}
|
||||||
|
.job-schedule {{schedule}}
|
||||||
|
.job-description {{description}}
|
||||||
|
.job-actions
|
||||||
|
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
|
||||||
|
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
|
||||||
|
.add-job-section
|
||||||
|
button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
|
||||||
|
|
||||||
template(name="cronMigrations")
|
template(name="cronMigrations")
|
||||||
.cron-migrations
|
.cron-migrations
|
||||||
|
|
@ -51,7 +63,7 @@ template(name="cronMigrations")
|
||||||
.migration-progress
|
.migration-progress
|
||||||
.progress-overview
|
.progress-overview
|
||||||
.progress-bar
|
.progress-bar
|
||||||
.progress-fill(style="width: {{migrationProgress}}%")
|
.progress-fill(style="width: {{migrationProgress}}%")
|
||||||
.progress-text {{migrationProgress}}%
|
.progress-text {{migrationProgress}}%
|
||||||
.progress-label {{_ 'overall-progress'}}
|
.progress-label {{_ 'overall-progress'}}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,552 +0,0 @@
|
||||||
import { Template } from 'meteor/templating';
|
|
||||||
import { ReactiveVar } from 'meteor/reactive-var';
|
|
||||||
import { Meteor } from 'meteor/meteor';
|
|
||||||
import { TAPi18n } from '/imports/i18n';
|
|
||||||
|
|
||||||
// Reactive variables for cron settings
|
|
||||||
const migrationProgress = new ReactiveVar(0);
|
|
||||||
const migrationStatus = new ReactiveVar('');
|
|
||||||
const migrationCurrentStep = new ReactiveVar('');
|
|
||||||
const migrationSteps = new ReactiveVar([]);
|
|
||||||
const isMigrating = new ReactiveVar(false);
|
|
||||||
const cronJobs = new ReactiveVar([]);
|
|
||||||
|
|
||||||
Template.cronSettings.onCreated(function() {
|
|
||||||
this.loading = new ReactiveVar(true);
|
|
||||||
this.showMigrations = new ReactiveVar(true);
|
|
||||||
this.showBoardOperations = new ReactiveVar(false);
|
|
||||||
this.showJobs = new ReactiveVar(false);
|
|
||||||
this.showAddJob = new ReactiveVar(false);
|
|
||||||
|
|
||||||
// Board operations pagination
|
|
||||||
this.currentPage = new ReactiveVar(1);
|
|
||||||
this.pageSize = new ReactiveVar(20);
|
|
||||||
this.searchTerm = new ReactiveVar('');
|
|
||||||
this.boardOperations = new ReactiveVar([]);
|
|
||||||
this.operationStats = new ReactiveVar({});
|
|
||||||
this.pagination = new ReactiveVar({});
|
|
||||||
this.queueStats = new ReactiveVar({});
|
|
||||||
this.systemResources = new ReactiveVar({});
|
|
||||||
this.boardMigrationStats = new ReactiveVar({});
|
|
||||||
|
|
||||||
// Load initial data
|
|
||||||
loadCronData(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
Template.cronSettings.helpers({
|
|
||||||
loading() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.loading ? instance.loading.get() : true;
|
|
||||||
},
|
|
||||||
|
|
||||||
showMigrations() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.showMigrations ? instance.showMigrations.get() : true;
|
|
||||||
},
|
|
||||||
|
|
||||||
showBoardOperations() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.showBoardOperations ? instance.showBoardOperations.get() : false;
|
|
||||||
},
|
|
||||||
|
|
||||||
showJobs() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.showJobs ? instance.showJobs.get() : false;
|
|
||||||
},
|
|
||||||
|
|
||||||
showAddJob() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.showAddJob ? instance.showAddJob.get() : false;
|
|
||||||
},
|
|
||||||
|
|
||||||
migrationProgress() {
|
|
||||||
return migrationProgress.get();
|
|
||||||
},
|
|
||||||
|
|
||||||
migrationStatus() {
|
|
||||||
return migrationStatus.get();
|
|
||||||
},
|
|
||||||
|
|
||||||
migrationCurrentStep() {
|
|
||||||
return migrationCurrentStep.get();
|
|
||||||
},
|
|
||||||
|
|
||||||
migrationSteps() {
|
|
||||||
const steps = migrationSteps.get();
|
|
||||||
const currentStep = migrationCurrentStep.get();
|
|
||||||
|
|
||||||
return steps.map(step => ({
|
|
||||||
...step,
|
|
||||||
isCurrentStep: step.name === currentStep
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
cronJobs() {
|
|
||||||
return cronJobs.get();
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDate(date) {
|
|
||||||
if (!date) return '-';
|
|
||||||
return new Date(date).toLocaleString();
|
|
||||||
},
|
|
||||||
|
|
||||||
boardOperations() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.boardOperations ? instance.boardOperations.get() : [];
|
|
||||||
},
|
|
||||||
|
|
||||||
operationStats() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.operationStats ? instance.operationStats.get() : {};
|
|
||||||
},
|
|
||||||
|
|
||||||
pagination() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.pagination ? instance.pagination.get() : {};
|
|
||||||
},
|
|
||||||
|
|
||||||
queueStats() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.queueStats ? instance.queueStats.get() : {};
|
|
||||||
},
|
|
||||||
|
|
||||||
systemResources() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.systemResources ? instance.systemResources.get() : {};
|
|
||||||
},
|
|
||||||
|
|
||||||
boardMigrationStats() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance && instance.boardMigrationStats ? instance.boardMigrationStats.get() : {};
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDateTime(date) {
|
|
||||||
if (!date) return '-';
|
|
||||||
return new Date(date).toLocaleString();
|
|
||||||
},
|
|
||||||
|
|
||||||
formatDuration(startTime, endTime) {
|
|
||||||
if (!startTime) return '-';
|
|
||||||
const start = new Date(startTime);
|
|
||||||
const end = endTime ? new Date(endTime) : new Date();
|
|
||||||
const diffMs = end - start;
|
|
||||||
const diffMins = Math.floor(diffMs / 60000);
|
|
||||||
const diffSecs = Math.floor((diffMs % 60000) / 1000);
|
|
||||||
|
|
||||||
if (diffMins > 0) {
|
|
||||||
return `${diffMins}m ${diffSecs}s`;
|
|
||||||
} else {
|
|
||||||
return `${diffSecs}s`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Template.cronSettings.switchMenu = function(event, targetID) {
|
|
||||||
const instance = Template.instance();
|
|
||||||
|
|
||||||
// Reset all tabs
|
|
||||||
instance.showMigrations.set(false);
|
|
||||||
instance.showBoardOperations.set(false);
|
|
||||||
instance.showJobs.set(false);
|
|
||||||
instance.showAddJob.set(false);
|
|
||||||
|
|
||||||
// Set the selected tab
|
|
||||||
if (targetID === 'cron-migrations') {
|
|
||||||
instance.showMigrations.set(true);
|
|
||||||
} else if (targetID === 'cron-board-operations') {
|
|
||||||
instance.showBoardOperations.set(true);
|
|
||||||
} else if (targetID === 'cron-jobs') {
|
|
||||||
instance.showJobs.set(true);
|
|
||||||
} else if (targetID === 'cron-add') {
|
|
||||||
instance.showAddJob.set(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Template.cronSettings.events({
|
|
||||||
'click .js-cron-migrations'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const instance = Template.instance();
|
|
||||||
instance.showMigrations.set(true);
|
|
||||||
instance.showJobs.set(false);
|
|
||||||
instance.showAddJob.set(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-cron-board-operations'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const instance = Template.instance();
|
|
||||||
instance.showMigrations.set(false);
|
|
||||||
instance.showBoardOperations.set(true);
|
|
||||||
instance.showJobs.set(false);
|
|
||||||
instance.showAddJob.set(false);
|
|
||||||
loadBoardOperations(instance);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-cron-jobs'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const instance = Template.instance();
|
|
||||||
instance.showMigrations.set(false);
|
|
||||||
instance.showBoardOperations.set(false);
|
|
||||||
instance.showJobs.set(true);
|
|
||||||
instance.showAddJob.set(false);
|
|
||||||
loadCronJobs(instance);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-cron-add'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const instance = Template.instance();
|
|
||||||
instance.showMigrations.set(false);
|
|
||||||
instance.showJobs.set(false);
|
|
||||||
instance.showAddJob.set(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-start-all-migrations'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
Meteor.call('cron.startAllMigrations', (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to start migrations:', error);
|
|
||||||
alert('Failed to start migrations: ' + error.message);
|
|
||||||
} else {
|
|
||||||
// Migrations started successfully
|
|
||||||
pollMigrationProgress(Template.instance());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-pause-all-migrations'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
// Pause all migration cron jobs
|
|
||||||
const jobs = cronJobs.get();
|
|
||||||
jobs.forEach(job => {
|
|
||||||
if (job.name.startsWith('migration_')) {
|
|
||||||
Meteor.call('cron.pauseJob', job.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-stop-all-migrations'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
// Stop all migration cron jobs
|
|
||||||
const jobs = cronJobs.get();
|
|
||||||
jobs.forEach(job => {
|
|
||||||
if (job.name.startsWith('migration_')) {
|
|
||||||
Meteor.call('cron.stopJob', job.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-refresh-jobs'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
loadCronJobs(Template.instance());
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-start-job'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const jobName = $(event.currentTarget).data('job');
|
|
||||||
Meteor.call('cron.startJob', jobName, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to start job:', error);
|
|
||||||
alert('Failed to start job: ' + error.message);
|
|
||||||
} else {
|
|
||||||
console.log('Job started successfully');
|
|
||||||
loadCronJobs(Template.instance());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-pause-job'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const jobName = $(event.currentTarget).data('job');
|
|
||||||
Meteor.call('cron.pauseJob', jobName, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to pause job:', error);
|
|
||||||
alert('Failed to pause job: ' + error.message);
|
|
||||||
} else {
|
|
||||||
console.log('Job paused successfully');
|
|
||||||
loadCronJobs(Template.instance());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-stop-job'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const jobName = $(event.currentTarget).data('job');
|
|
||||||
Meteor.call('cron.stopJob', jobName, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to stop job:', error);
|
|
||||||
alert('Failed to stop job: ' + error.message);
|
|
||||||
} else {
|
|
||||||
console.log('Job stopped successfully');
|
|
||||||
loadCronJobs(Template.instance());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-remove-job'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const jobName = $(event.currentTarget).data('job');
|
|
||||||
if (confirm('Are you sure you want to remove this job?')) {
|
|
||||||
Meteor.call('cron.removeJob', jobName, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to remove job:', error);
|
|
||||||
alert('Failed to remove job: ' + error.message);
|
|
||||||
} else {
|
|
||||||
console.log('Job removed successfully');
|
|
||||||
loadCronJobs(Template.instance());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'submit .js-add-cron-job-form'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const form = event.currentTarget;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
|
|
||||||
const jobData = {
|
|
||||||
name: formData.get('name'),
|
|
||||||
description: formData.get('description'),
|
|
||||||
schedule: formData.get('schedule'),
|
|
||||||
weight: parseInt(formData.get('weight'))
|
|
||||||
};
|
|
||||||
|
|
||||||
Meteor.call('cron.addJob', jobData, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to add job:', error);
|
|
||||||
alert('Failed to add job: ' + error.message);
|
|
||||||
} else {
|
|
||||||
console.log('Job added successfully');
|
|
||||||
form.reset();
|
|
||||||
Template.instance().showJobs.set(true);
|
|
||||||
Template.instance().showAddJob.set(false);
|
|
||||||
loadCronJobs(Template.instance());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-cancel-add-job'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const instance = Template.instance();
|
|
||||||
instance.showJobs.set(true);
|
|
||||||
instance.showAddJob.set(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-refresh-board-operations'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
loadBoardOperations(Template.instance());
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-start-test-operation'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const testBoardId = 'test-board-' + Date.now();
|
|
||||||
const operationData = {
|
|
||||||
sourceBoardId: 'source-board',
|
|
||||||
targetBoardId: 'target-board',
|
|
||||||
copyOptions: { includeCards: true, includeAttachments: true }
|
|
||||||
};
|
|
||||||
|
|
||||||
Meteor.call('cron.startBoardOperation', testBoardId, 'copy_board', operationData, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to start test operation:', error);
|
|
||||||
alert('Failed to start test operation: ' + error.message);
|
|
||||||
} else {
|
|
||||||
console.log('Test operation started:', result);
|
|
||||||
loadBoardOperations(Template.instance());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
'input .js-search-board-operations'(event) {
|
|
||||||
const searchTerm = $(event.currentTarget).val();
|
|
||||||
const instance = Template.instance();
|
|
||||||
instance.searchTerm.set(searchTerm);
|
|
||||||
instance.currentPage.set(1);
|
|
||||||
loadBoardOperations(instance);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-prev-page'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const instance = Template.instance();
|
|
||||||
const currentPage = instance.currentPage.get();
|
|
||||||
if (currentPage > 1) {
|
|
||||||
instance.currentPage.set(currentPage - 1);
|
|
||||||
loadBoardOperations(instance);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-next-page'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const instance = Template.instance();
|
|
||||||
const currentPage = instance.currentPage.get();
|
|
||||||
const pagination = instance.pagination.get();
|
|
||||||
if (currentPage < pagination.totalPages) {
|
|
||||||
instance.currentPage.set(currentPage + 1);
|
|
||||||
loadBoardOperations(instance);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-pause-operation'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const operationId = $(event.currentTarget).data('operation');
|
|
||||||
// Implementation for pausing operation
|
|
||||||
console.log('Pause operation:', operationId);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-resume-operation'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const operationId = $(event.currentTarget).data('operation');
|
|
||||||
// Implementation for resuming operation
|
|
||||||
console.log('Resume operation:', operationId);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-stop-operation'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const operationId = $(event.currentTarget).data('operation');
|
|
||||||
if (confirm('Are you sure you want to stop this operation?')) {
|
|
||||||
// Implementation for stopping operation
|
|
||||||
console.log('Stop operation:', operationId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-view-details'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const operationId = $(event.currentTarget).data('operation');
|
|
||||||
// Implementation for viewing operation details
|
|
||||||
console.log('View details for operation:', operationId);
|
|
||||||
},
|
|
||||||
|
|
||||||
'click .js-force-board-scan'(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
Meteor.call('cron.forceBoardMigrationScan', (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to force board scan:', error);
|
|
||||||
alert('Failed to force board scan: ' + error.message);
|
|
||||||
} else {
|
|
||||||
// Board scan started successfully
|
|
||||||
// Refresh the data
|
|
||||||
loadBoardOperations(Template.instance());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Helper functions for cron settings
|
|
||||||
function loadCronData(instance) {
|
|
||||||
instance.loading.set(true);
|
|
||||||
|
|
||||||
// Load migration progress
|
|
||||||
Meteor.call('cron.getMigrationProgress', (error, result) => {
|
|
||||||
if (result) {
|
|
||||||
migrationProgress.set(result.progress);
|
|
||||||
migrationStatus.set(result.status);
|
|
||||||
migrationCurrentStep.set(result.currentStep);
|
|
||||||
migrationSteps.set(result.steps);
|
|
||||||
isMigrating.set(result.isMigrating);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load cron jobs
|
|
||||||
loadCronJobs(instance);
|
|
||||||
|
|
||||||
instance.loading.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadCronJobs(instance) {
|
|
||||||
Meteor.call('cron.getJobs', (error, result) => {
|
|
||||||
if (result) {
|
|
||||||
cronJobs.set(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadBoardOperations(instance) {
|
|
||||||
const page = instance.currentPage.get();
|
|
||||||
const limit = instance.pageSize.get();
|
|
||||||
const searchTerm = instance.searchTerm.get();
|
|
||||||
|
|
||||||
Meteor.call('cron.getAllBoardOperations', page, limit, searchTerm, (error, result) => {
|
|
||||||
if (result) {
|
|
||||||
instance.boardOperations.set(result.operations);
|
|
||||||
instance.pagination.set({
|
|
||||||
total: result.total,
|
|
||||||
page: result.page,
|
|
||||||
limit: result.limit,
|
|
||||||
totalPages: result.totalPages,
|
|
||||||
start: ((result.page - 1) * result.limit) + 1,
|
|
||||||
end: Math.min(result.page * result.limit, result.total),
|
|
||||||
hasPrev: result.page > 1,
|
|
||||||
hasNext: result.page < result.totalPages
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load operation stats
|
|
||||||
Meteor.call('cron.getBoardOperationStats', (error, result) => {
|
|
||||||
if (result) {
|
|
||||||
instance.operationStats.set(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load queue stats
|
|
||||||
Meteor.call('cron.getQueueStats', (error, result) => {
|
|
||||||
if (result) {
|
|
||||||
instance.queueStats.set(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load system resources
|
|
||||||
Meteor.call('cron.getSystemResources', (error, result) => {
|
|
||||||
if (result) {
|
|
||||||
instance.systemResources.set(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load board migration stats
|
|
||||||
Meteor.call('cron.getBoardMigrationStats', (error, result) => {
|
|
||||||
if (result) {
|
|
||||||
instance.boardMigrationStats.set(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollMigrationProgress(instance) {
|
|
||||||
const pollInterval = setInterval(() => {
|
|
||||||
Meteor.call('cron.getMigrationProgress', (error, result) => {
|
|
||||||
if (result) {
|
|
||||||
migrationProgress.set(result.progress);
|
|
||||||
migrationStatus.set(result.status);
|
|
||||||
migrationCurrentStep.set(result.currentStep);
|
|
||||||
migrationSteps.set(result.steps);
|
|
||||||
isMigrating.set(result.isMigrating);
|
|
||||||
|
|
||||||
// Stop polling if migration is complete
|
|
||||||
if (!result.isMigrating && result.progress === 100) {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Template helpers for cronSettings
|
|
||||||
Template.cronSettings.helpers({
|
|
||||||
loading() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.loading && instance.loading.get();
|
|
||||||
},
|
|
||||||
showMigrations() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.showMigrations && instance.showMigrations.get();
|
|
||||||
},
|
|
||||||
showBoardOperations() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.showBoardOperations && instance.showBoardOperations.get();
|
|
||||||
},
|
|
||||||
showJobs() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.showJobs && instance.showJobs.get();
|
|
||||||
},
|
|
||||||
showAddJob() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.showAddJob && instance.showAddJob.get();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -4,115 +4,224 @@ template(name="setting")
|
||||||
| {{_ 'error-notAuthorized'}}
|
| {{_ 'error-notAuthorized'}}
|
||||||
else
|
else
|
||||||
.content-title.ext-box
|
.content-title.ext-box
|
||||||
if loading.get
|
if isGeneralSetting
|
||||||
+spinner
|
|
||||||
else if generalSetting.get
|
|
||||||
span
|
span
|
||||||
i.fa.fa-sign-in
|
i.fa.fa-sign-in
|
||||||
| {{_ 'registration'}}
|
| {{_ 'registration'}}
|
||||||
else if emailSetting.get
|
else if isEmailSetting
|
||||||
span
|
span
|
||||||
i.fa.fa-envelope
|
i.fa.fa-envelope
|
||||||
| {{_ 'email'}}
|
| {{_ 'email'}}
|
||||||
else if accountSetting.get
|
else if isAccountSetting
|
||||||
span
|
span
|
||||||
i.fa.fa-users
|
i.fa.fa-users
|
||||||
| {{_ 'accounts'}}
|
| {{_ 'accounts'}}
|
||||||
else if tableVisibilityModeSetting.get
|
else if isTableVisibilityModeSetting
|
||||||
span
|
span
|
||||||
i.fa.fa-eye
|
i.fa.fa-eye
|
||||||
| {{_ 'tableVisibilityMode'}}
|
| {{_ 'tableVisibilityMode'}}
|
||||||
else if announcementSetting.get
|
else if isAnnouncementSetting
|
||||||
span
|
span
|
||||||
i.fa.fa-bullhorn
|
i.fa.fa-bullhorn
|
||||||
| {{_ 'admin-announcement'}}
|
| {{_ 'admin-announcement'}}
|
||||||
else if accessibilitySetting.get
|
else if isAccessibilitySetting
|
||||||
span
|
span
|
||||||
i.fa.fa-universal-access
|
i.fa.fa-universal-access
|
||||||
| {{_ 'accessibility'}}
|
| {{_ 'accessibility'}}
|
||||||
else if layoutSetting.get
|
else if isLayoutSetting
|
||||||
span
|
span
|
||||||
i.fa.fa-object-group
|
i.fa.fa-object-group
|
||||||
| {{_ 'layout'}}
|
| {{_ 'layout'}}
|
||||||
else if webhookSetting.get
|
else if isWebhookSetting
|
||||||
span
|
span
|
||||||
i.fa.fa-globe
|
i.fa.fa-globe
|
||||||
| {{_ 'global-webhook'}}
|
| {{_ 'global-webhook'}}
|
||||||
else if attachmentSettings.get
|
else if isAttachmentSettings
|
||||||
span
|
span
|
||||||
i.fa.fa-paperclip
|
i.fa.fa-paperclip
|
||||||
| {{_ 'attachments'}}
|
| {{_ 'attachments'}}
|
||||||
else if cronSettings.get
|
else if isCronSettings
|
||||||
span
|
span
|
||||||
i.fa.fa-clock-o
|
i.fa.fa-clock-o
|
||||||
| {{_ 'cron'}}
|
| {{_ 'cron'}}
|
||||||
.content-body
|
.content-body
|
||||||
.side-menu
|
.side-menu
|
||||||
ul
|
ul
|
||||||
li(class="{{#if generalSetting}}active{{/if}}")
|
li(class="{{#if isGeneralSetting}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="registration-setting")
|
a.js-setting-menu(data-id="registration-setting")
|
||||||
i.fa.fa-sign-in
|
i.fa.fa-sign-in
|
||||||
| {{_ 'registration'}}
|
| {{_ 'registration'}}
|
||||||
unless isSandstorm
|
unless isSandstorm
|
||||||
li(class="{{#if emailSetting}}active{{/if}}")
|
li(class="{{#if isEmailSetting}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="email-setting")
|
a.js-setting-menu(data-id="email-setting")
|
||||||
i.fa.fa-envelope
|
i.fa.fa-envelope
|
||||||
| {{_ 'email'}}
|
| {{_ 'email'}}
|
||||||
li(class="{{#if accountSetting}}active{{/if}}")
|
li(class="{{#if isAccountSetting}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="account-setting")
|
a.js-setting-menu(data-id="account-setting")
|
||||||
i.fa.fa-users
|
i.fa.fa-users
|
||||||
| {{_ 'accounts'}}
|
| {{_ 'accounts'}}
|
||||||
li(class="{{#if tableVisibilityModeSetting}}active{{/if}}")
|
li(class="{{#if isTableVisibilityModeSetting}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="tableVisibilityMode-setting")
|
a.js-setting-menu(data-id="tableVisibilityMode-setting")
|
||||||
i.fa.fa-eye
|
i.fa.fa-eye
|
||||||
| {{_ 'tableVisibilityMode'}}
|
| {{_ 'tableVisibilityMode'}}
|
||||||
li(class="{{#if announcementSetting}}active{{/if}}")
|
li(class="{{#if isAnnouncementSetting}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="announcement-setting")
|
a.js-setting-menu(data-id="announcement-setting")
|
||||||
i.fa.fa-bullhorn
|
i.fa.fa-bullhorn
|
||||||
| {{_ 'admin-announcement'}}
|
| {{_ 'admin-announcement'}}
|
||||||
li(class="{{#if accessibilitySetting}}active{{/if}}")
|
li(class="{{#if isAccessibilitySetting}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="accessibility-setting")
|
a.js-setting-menu(data-id="accessibility-setting")
|
||||||
i.fa.fa-universal-access
|
i.fa.fa-universal-access
|
||||||
| {{_ 'accessibility'}}
|
| {{_ 'accessibility'}}
|
||||||
li(class="{{#if layoutSetting}}active{{/if}}")
|
li(class="{{#if isLayoutSetting}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="layout-setting")
|
a.js-setting-menu(data-id="layout-setting")
|
||||||
i.fa.fa-object-group
|
i.fa.fa-object-group
|
||||||
| {{_ 'layout'}}
|
| {{_ 'layout'}}
|
||||||
li(class="{{#if webhookSetting}}active{{/if}}")
|
li(class="{{#if isWebhookSetting}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="webhook-setting")
|
a.js-setting-menu(data-id="webhook-setting")
|
||||||
i.fa.fa-globe
|
i.fa.fa-globe
|
||||||
| {{_ 'global-webhook'}}
|
| {{_ 'global-webhook'}}
|
||||||
li(class="{{#if attachmentSettings}}active{{/if}}")
|
li(class="{{#if isAttachmentSettings}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="attachment-settings")
|
a.js-setting-menu(data-id="attachment-settings")
|
||||||
i.fa.fa-paperclip
|
i.fa.fa-paperclip
|
||||||
| {{_ 'attachments'}}
|
| {{_ 'attachments'}}
|
||||||
li(class="{{#if cronSettings}}active{{/if}}")
|
li(class="{{#if isCronSettings}}active{{/if}}")
|
||||||
a.js-setting-menu(data-id="cron-settings")
|
a.js-setting-menu(data-id="cron-settings")
|
||||||
i.fa.fa-clock-o
|
i.fa.fa-clock-o
|
||||||
| {{_ 'cron'}}
|
| {{_ 'cron'}}
|
||||||
.main-body
|
.main-body
|
||||||
if loading.get
|
if isLoading
|
||||||
+spinner
|
+spinner
|
||||||
else if attachmentSettings.get
|
else if isAttachmentSettings
|
||||||
+attachmentSettings
|
ul#attachment-setting.setting-detail
|
||||||
else if cronSettings.get
|
li
|
||||||
+cronSettings
|
h3 {{_ 'attachment-storage-configuration'}}
|
||||||
else if generalSetting.get
|
.form-group
|
||||||
|
label {{_ 'writable-path'}}
|
||||||
|
input.wekan-form-control#filesystem-path(type="text" value="{{filesystemPath}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 'filesystem-path-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 'attachments-path'}}
|
||||||
|
input.wekan-form-control#attachments-path(type="text" value="{{attachmentsPath}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 'attachments-path-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 'avatars-path'}}
|
||||||
|
input.wekan-form-control#avatars-path(type="text" value="{{avatarsPath}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 'avatars-path-description'}}
|
||||||
|
|
||||||
|
li
|
||||||
|
h3 {{_ 'mongodb-gridfs-storage'}}
|
||||||
|
.form-group
|
||||||
|
label {{_ 'gridfs-enabled'}}
|
||||||
|
input.wekan-form-control#gridfs-enabled(type="checkbox" checked="{{gridfsEnabled}}" disabled)
|
||||||
|
small.form-text.text-muted {{_ 'gridfs-enabled-description'}}
|
||||||
|
|
||||||
|
li
|
||||||
|
h3 {{_ 's3-minio-storage'}}
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-enabled'}}
|
||||||
|
input.wekan-form-control#s3-enabled(type="checkbox" checked="{{s3Enabled}}" disabled)
|
||||||
|
small.form-text.text-muted {{_ 's3-enabled-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-endpoint'}}
|
||||||
|
input.wekan-form-control#s3-endpoint(type="text" value="{{s3Endpoint}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-endpoint-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-bucket'}}
|
||||||
|
input.wekan-form-control#s3-bucket(type="text" value="{{s3Bucket}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-bucket-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-region'}}
|
||||||
|
input.wekan-form-control#s3-region(type="text" value="{{s3Region}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-region-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-access-key'}}
|
||||||
|
input.wekan-form-control#s3-access-key(type="text" placeholder="{{_ 's3-access-key-placeholder'}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-access-key-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-secret-key'}}
|
||||||
|
input.wekan-form-control#s3-secret-key(type="password" placeholder="{{_ 's3-secret-key-placeholder'}}")
|
||||||
|
small.form-text.text-muted {{_ 's3-secret-key-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-ssl-enabled'}}
|
||||||
|
input.wekan-form-control#s3-ssl-enabled(type="checkbox" checked="{{s3SslEnabled}}" disabled)
|
||||||
|
small.form-text.text-muted {{_ 's3-ssl-enabled-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
label {{_ 's3-port'}}
|
||||||
|
input.wekan-form-control#s3-port(type="number" value="{{s3Port}}" readonly)
|
||||||
|
small.form-text.text-muted {{_ 's3-port-description'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
button.js-test-s3-connection.btn.btn-secondary {{_ 'test-s3-connection'}}
|
||||||
|
button.js-save-s3-settings.btn.btn-primary {{_ 'save-s3-settings'}}
|
||||||
|
else if isCronSettings
|
||||||
|
ul#cron-setting.setting-detail
|
||||||
|
li
|
||||||
|
h3 {{_ 'cron-migrations'}}
|
||||||
|
.form-group
|
||||||
|
label {{_ 'migration-status'}}
|
||||||
|
.status-indicator
|
||||||
|
span.status-label {{_ 'status'}}:
|
||||||
|
span.status-value {{migrationStatus}}
|
||||||
|
.progress-section
|
||||||
|
.progress
|
||||||
|
.progress-bar(role="progressbar" style="width: {{migrationProgress}}%" aria-valuenow="{{migrationProgress}}" aria-valuemin="0" aria-valuemax="100")
|
||||||
|
| {{migrationProgress}}%
|
||||||
|
.progress-text
|
||||||
|
| {{migrationProgress}}% {{_ 'complete'}}
|
||||||
|
|
||||||
|
.form-group
|
||||||
|
button.js-start-all-migrations.btn.btn-primary {{_ 'start-all-migrations'}}
|
||||||
|
button.js-pause-all-migrations.btn.btn-warning {{_ 'pause-all-migrations'}}
|
||||||
|
button.js-stop-all-migrations.btn.btn-danger {{_ 'stop-all-migrations'}}
|
||||||
|
|
||||||
|
li
|
||||||
|
h3 {{_ 'board-operations'}}
|
||||||
|
.form-group
|
||||||
|
label {{_ 'scheduled-board-operations'}}
|
||||||
|
button.js-schedule-board-cleanup.btn.btn-primary {{_ 'schedule-board-cleanup'}}
|
||||||
|
button.js-schedule-board-archive.btn.btn-warning {{_ 'schedule-board-archive'}}
|
||||||
|
button.js-schedule-board-backup.btn.btn-info {{_ 'schedule-board-backup'}}
|
||||||
|
|
||||||
|
li
|
||||||
|
h3 {{_ 'cron-jobs'}}
|
||||||
|
.form-group
|
||||||
|
label {{_ 'active-cron-jobs'}}
|
||||||
|
each cronJobs
|
||||||
|
.job-item
|
||||||
|
.job-info
|
||||||
|
.job-name {{name}}
|
||||||
|
.job-schedule {{schedule}}
|
||||||
|
.job-description {{description}}
|
||||||
|
.job-actions
|
||||||
|
button.js-pause-job.btn.btn-sm.btn-warning(data-job-id="{{_id}}") {{_ 'pause'}}
|
||||||
|
button.js-delete-job.btn.btn-sm.btn-danger(data-job-id="{{_id}}") {{_ 'delete'}}
|
||||||
|
.add-job-section
|
||||||
|
button.js-add-cron-job.btn.btn-success {{_ 'add-cron-job'}}
|
||||||
|
else if isGeneralSetting
|
||||||
+general
|
+general
|
||||||
else if emailSetting.get
|
else if isEmailSetting
|
||||||
unless isSandstorm
|
unless isSandstorm
|
||||||
+email
|
+email
|
||||||
else if accountSetting.get
|
else if isAccountSetting
|
||||||
+accountSettings
|
+accountSettings
|
||||||
else if tableVisibilityModeSetting.get
|
else if isTableVisibilityModeSetting
|
||||||
+tableVisibilityModeSettings
|
+tableVisibilityModeSettings
|
||||||
else if announcementSetting.get
|
else if isAnnouncementSetting
|
||||||
+announcementSettings
|
+announcementSettings
|
||||||
else if accessibilitySetting.get
|
else if isAccessibilitySetting
|
||||||
+accessibilitySettings
|
+accessibilitySettings
|
||||||
else if layoutSetting.get
|
else if isLayoutSetting
|
||||||
+layoutSettings
|
+layoutSettings
|
||||||
else if webhookSetting.get
|
else if isWebhookSetting
|
||||||
+webhookSettings
|
+webhookSettings
|
||||||
|
|
||||||
template(name="webhookSettings")
|
template(name="webhookSettings")
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,239 @@ BlazeComponent.extendComponent({
|
||||||
setError(error) {
|
setError(error) {
|
||||||
this.error.set(error);
|
this.error.set(error);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Template helpers moved to BlazeComponent - using different names to avoid conflicts
|
||||||
|
isGeneralSetting() {
|
||||||
|
return this.generalSetting && this.generalSetting.get();
|
||||||
|
},
|
||||||
|
isEmailSetting() {
|
||||||
|
return this.emailSetting && this.emailSetting.get();
|
||||||
|
},
|
||||||
|
isAccountSetting() {
|
||||||
|
return this.accountSetting && this.accountSetting.get();
|
||||||
|
},
|
||||||
|
isTableVisibilityModeSetting() {
|
||||||
|
return this.tableVisibilityModeSetting && this.tableVisibilityModeSetting.get();
|
||||||
|
},
|
||||||
|
isAnnouncementSetting() {
|
||||||
|
return this.announcementSetting && this.announcementSetting.get();
|
||||||
|
},
|
||||||
|
isAccessibilitySetting() {
|
||||||
|
return this.accessibilitySetting && this.accessibilitySetting.get();
|
||||||
|
},
|
||||||
|
isLayoutSetting() {
|
||||||
|
return this.layoutSetting && this.layoutSetting.get();
|
||||||
|
},
|
||||||
|
isWebhookSetting() {
|
||||||
|
return this.webhookSetting && this.webhookSetting.get();
|
||||||
|
},
|
||||||
|
isAttachmentSettings() {
|
||||||
|
return this.attachmentSettings && this.attachmentSettings.get();
|
||||||
|
},
|
||||||
|
isCronSettings() {
|
||||||
|
return this.cronSettings && this.cronSettings.get();
|
||||||
|
},
|
||||||
|
isLoading() {
|
||||||
|
return this.loading && this.loading.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Attachment settings helpers
|
||||||
|
filesystemPath() {
|
||||||
|
return process.env.WRITABLE_PATH || '/data';
|
||||||
|
},
|
||||||
|
|
||||||
|
attachmentsPath() {
|
||||||
|
const writablePath = process.env.WRITABLE_PATH || '/data';
|
||||||
|
return `${writablePath}/attachments`;
|
||||||
|
},
|
||||||
|
|
||||||
|
avatarsPath() {
|
||||||
|
const writablePath = process.env.WRITABLE_PATH || '/data';
|
||||||
|
return `${writablePath}/avatars`;
|
||||||
|
},
|
||||||
|
|
||||||
|
gridfsEnabled() {
|
||||||
|
return process.env.GRIDFS_ENABLED === 'true';
|
||||||
|
},
|
||||||
|
|
||||||
|
s3Enabled() {
|
||||||
|
return process.env.S3_ENABLED === 'true';
|
||||||
|
},
|
||||||
|
|
||||||
|
s3Endpoint() {
|
||||||
|
return process.env.S3_ENDPOINT || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
s3Bucket() {
|
||||||
|
return process.env.S3_BUCKET || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
s3Region() {
|
||||||
|
return process.env.S3_REGION || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
s3SslEnabled() {
|
||||||
|
return process.env.S3_SSL_ENABLED === 'true';
|
||||||
|
},
|
||||||
|
|
||||||
|
s3Port() {
|
||||||
|
return process.env.S3_PORT || 443;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cron settings helpers
|
||||||
|
migrationStatus() {
|
||||||
|
return 'idle'; // Placeholder
|
||||||
|
},
|
||||||
|
|
||||||
|
migrationProgress() {
|
||||||
|
return 0; // Placeholder
|
||||||
|
},
|
||||||
|
|
||||||
|
cronJobs() {
|
||||||
|
return []; // Placeholder
|
||||||
|
},
|
||||||
|
|
||||||
setLoading(w) {
|
setLoading(w) {
|
||||||
this.loading.set(w);
|
this.loading.set(w);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Event handlers for attachment settings
|
||||||
|
'click button.js-test-s3-connection'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const secretKey = $('#s3-secret-key').val();
|
||||||
|
if (!secretKey) {
|
||||||
|
alert(TAPi18n.__('s3-secret-key-required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Meteor.call('testS3Connection', { secretKey }, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('s3-connection-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('s3-connection-success'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'click button.js-save-s3-settings'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const secretKey = $('#s3-secret-key').val();
|
||||||
|
if (!secretKey) {
|
||||||
|
alert(TAPi18n.__('s3-secret-key-required'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Meteor.call('saveS3Settings', { secretKey }, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('s3-settings-save-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('s3-settings-saved'));
|
||||||
|
$('#s3-secret-key').val(''); // Clear the password field
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// Event handlers for cron settings
|
||||||
|
'click button.js-start-all-migrations'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
Meteor.call('startAllMigrations', (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('migration-start-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('migration-started'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'click button.js-pause-all-migrations'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
Meteor.call('pauseAllMigrations', (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('migration-pause-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('migration-paused'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'click button.js-stop-all-migrations'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (confirm(TAPi18n.__('migration-stop-confirm'))) {
|
||||||
|
Meteor.call('stopAllMigrations', (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('migration-stop-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('migration-stopped'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'click button.js-schedule-board-cleanup'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
Meteor.call('scheduleBoardCleanup', (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('board-cleanup-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('board-cleanup-scheduled'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'click button.js-schedule-board-archive'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
Meteor.call('scheduleBoardArchive', (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('board-archive-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('board-archive-scheduled'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'click button.js-schedule-board-backup'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
Meteor.call('scheduleBoardBackup', (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('board-backup-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('board-backup-scheduled'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'click button.js-pause-job'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const jobId = $(event.target).data('job-id');
|
||||||
|
Meteor.call('pauseCronJob', jobId, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('cron-job-pause-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('cron-job-paused'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
'click button.js-delete-job'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const jobId = $(event.target).data('job-id');
|
||||||
|
if (confirm(TAPi18n.__('cron-job-delete-confirm'))) {
|
||||||
|
Meteor.call('deleteCronJob', jobId, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
alert(TAPi18n.__('cron-job-delete-failed') + ': ' + error.reason);
|
||||||
|
} else {
|
||||||
|
alert(TAPi18n.__('cron-job-deleted'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'click button.js-add-cron-job'(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
// Placeholder for adding a new cron job (e.g., open a modal)
|
||||||
|
alert(TAPi18n.__('add-cron-job-placeholder'));
|
||||||
|
},
|
||||||
|
|
||||||
checkField(selector) {
|
checkField(selector) {
|
||||||
const value = $(selector).val();
|
const value = $(selector).val();
|
||||||
if (!value || value.trim() === '') {
|
if (!value || value.trim() === '') {
|
||||||
|
|
@ -104,37 +332,6 @@ BlazeComponent.extendComponent({
|
||||||
$('#display-authentication-method').toggleClass('is-checked');
|
$('#display-authentication-method').toggleClass('is-checked');
|
||||||
},
|
},
|
||||||
|
|
||||||
switchAttachmentTab(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const target = $(event.target);
|
|
||||||
const targetID = target.data('id');
|
|
||||||
|
|
||||||
// Update active tab
|
|
||||||
$('.tab-nav li.active').removeClass('active');
|
|
||||||
target.parent().addClass('active');
|
|
||||||
|
|
||||||
// Call the attachment settings component method if available
|
|
||||||
if (window.attachmentSettings && window.attachmentSettings.switchMenu) {
|
|
||||||
window.attachmentSettings.switchMenu(event, targetID);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
switchCronTab(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const target = $(event.target);
|
|
||||||
const targetID = target.data('id');
|
|
||||||
|
|
||||||
// Update active tab
|
|
||||||
$('.tab-nav li.active').removeClass('active');
|
|
||||||
target.parent().addClass('active');
|
|
||||||
|
|
||||||
// Call the cron settings template method if available
|
|
||||||
const cronTemplate = Template.instance();
|
|
||||||
if (cronTemplate && cronTemplate.switchMenu) {
|
|
||||||
cronTemplate.switchMenu(event, targetID);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
initializeAttachmentSubMenu() {
|
initializeAttachmentSubMenu() {
|
||||||
// Set default sub-menu state for attachment settings
|
// Set default sub-menu state for attachment settings
|
||||||
// This will be handled by the attachment settings component
|
// This will be handled by the attachment settings component
|
||||||
|
|
@ -357,13 +554,6 @@ BlazeComponent.extendComponent({
|
||||||
'click button.js-save-layout': this.saveLayout,
|
'click button.js-save-layout': this.saveLayout,
|
||||||
'click a.js-toggle-display-authentication-method': this
|
'click a.js-toggle-display-authentication-method': this
|
||||||
.toggleDisplayAuthenticationMethod,
|
.toggleDisplayAuthenticationMethod,
|
||||||
'click a.js-attachment-storage-settings': this.switchAttachmentTab,
|
|
||||||
'click a.js-attachment-migration': this.switchAttachmentTab,
|
|
||||||
'click a.js-attachment-monitoring': this.switchAttachmentTab,
|
|
||||||
'click a.js-cron-migrations': this.switchCronTab,
|
|
||||||
'click a.js-cron-board-operations': this.switchCronTab,
|
|
||||||
'click a.js-cron-jobs': this.switchCronTab,
|
|
||||||
'click a.js-cron-add': this.switchCronTab,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
@ -609,50 +799,3 @@ Template.selectSpinnerName.helpers({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Template helpers for the setting template
|
|
||||||
Template.setting.helpers({
|
|
||||||
generalSetting() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.generalSetting && instance.generalSetting.get();
|
|
||||||
},
|
|
||||||
emailSetting() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.emailSetting && instance.emailSetting.get();
|
|
||||||
},
|
|
||||||
accountSetting() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.accountSetting && instance.accountSetting.get();
|
|
||||||
},
|
|
||||||
tableVisibilityModeSetting() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.tableVisibilityModeSetting && instance.tableVisibilityModeSetting.get();
|
|
||||||
},
|
|
||||||
announcementSetting() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.announcementSetting && instance.announcementSetting.get();
|
|
||||||
},
|
|
||||||
accessibilitySetting() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.accessibilitySetting && instance.accessibilitySetting.get();
|
|
||||||
},
|
|
||||||
layoutSetting() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.layoutSetting && instance.layoutSetting.get();
|
|
||||||
},
|
|
||||||
webhookSetting() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.webhookSetting && instance.webhookSetting.get();
|
|
||||||
},
|
|
||||||
attachmentSettings() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.attachmentSettings && instance.attachmentSettings.get();
|
|
||||||
},
|
|
||||||
cronSettings() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.cronSettings && instance.cronSettings.get();
|
|
||||||
},
|
|
||||||
loading() {
|
|
||||||
const instance = Template.instance();
|
|
||||||
return instance.loading && instance.loading.get();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
/* Settings Tabs Styles */
|
|
||||||
.settings-tabs {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav li {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav li a {
|
|
||||||
display: block;
|
|
||||||
padding: 12px 20px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: #666;
|
|
||||||
border-bottom: 3px solid transparent;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav li a:hover {
|
|
||||||
color: #333;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav li.active a {
|
|
||||||
color: #2196F3;
|
|
||||||
border-bottom-color: #2196F3;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav li a i {
|
|
||||||
margin-right: 8px;
|
|
||||||
width: 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-content {
|
|
||||||
padding: 20px 0;
|
|
||||||
min-height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive design */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.tab-nav {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav li a {
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-nav li.active a {
|
|
||||||
border-bottom-color: #2196F3;
|
|
||||||
border-right: 3px solid #2196F3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue