mirror of
https://git.proxmox.com/git/proxmox-backup
synced 2025-04-29 17:04:51 +00:00

For now just in the general datacenter option view, not when editing the tuning options. For also allowing one to enter this we should first provide our backend implementation as WASM to avoid having to redo this in JavaScript. Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
872 lines
24 KiB
JavaScript
872 lines
24 KiB
JavaScript
Ext.ns('PBS');
|
|
|
|
console.log("Starting Backup Server GUI");
|
|
|
|
Ext.define('PBS.Utils', {
|
|
singleton: true,
|
|
|
|
missingText: gettext('missing'),
|
|
|
|
updateLoginData: function(data) {
|
|
Proxmox.Utils.setAuthData(data);
|
|
},
|
|
|
|
dataStorePrefix: 'DataStore-',
|
|
|
|
cryptmap: [
|
|
'none',
|
|
'mixed',
|
|
'sign-only',
|
|
'encrypt',
|
|
],
|
|
|
|
cryptText: [
|
|
Proxmox.Utils.noText,
|
|
gettext('Mixed'),
|
|
gettext('Signed'),
|
|
gettext('Encrypted'),
|
|
],
|
|
|
|
cryptIconCls: [
|
|
'',
|
|
'',
|
|
'lock faded',
|
|
'lock good',
|
|
],
|
|
|
|
calculateCryptMode: function(data) {
|
|
let mixed = data.mixed;
|
|
let encrypted = data.encrypt;
|
|
let signed = data['sign-only'];
|
|
let files = data.count;
|
|
if (mixed > 0) {
|
|
return PBS.Utils.cryptmap.indexOf('mixed');
|
|
} else if (files === encrypted && encrypted > 0) {
|
|
return PBS.Utils.cryptmap.indexOf('encrypt');
|
|
} else if (files === signed && signed > 0) {
|
|
return PBS.Utils.cryptmap.indexOf('sign-only');
|
|
} else if ((signed+encrypted) === 0) {
|
|
return PBS.Utils.cryptmap.indexOf('none');
|
|
} else {
|
|
return PBS.Utils.cryptmap.indexOf('mixed');
|
|
}
|
|
},
|
|
|
|
noSubKeyHtml: 'You do not have a valid subscription for this server. Please visit <a target="_blank" href="https://www.proxmox.com/proxmox-backup-server/pricing">www.proxmox.com</a> to get a list of available options.',
|
|
|
|
getDataStoreFromPath: function(path) {
|
|
return path.slice(PBS.Utils.dataStorePrefix.length);
|
|
},
|
|
|
|
isDataStorePath: function(path) {
|
|
return path.indexOf(PBS.Utils.dataStorePrefix) === 0;
|
|
},
|
|
|
|
parsePropertyString: function(value, defaultKey) {
|
|
var res = {},
|
|
error;
|
|
|
|
if (typeof value !== 'string' || value === '') {
|
|
return res;
|
|
}
|
|
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
var kv = p.split('=', 2);
|
|
if (Ext.isDefined(kv[1])) {
|
|
res[kv[0]] = kv[1];
|
|
} else if (Ext.isDefined(defaultKey)) {
|
|
if (Ext.isDefined(res[defaultKey])) {
|
|
error = 'defaultKey may be only defined once in propertyString';
|
|
return false; // break
|
|
}
|
|
res[defaultKey] = kv[0];
|
|
} else {
|
|
error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
|
|
return false; // break
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (error !== undefined) {
|
|
console.error(error);
|
|
return null;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printPropertyString: function(data, defaultKey) {
|
|
var stringparts = [],
|
|
gotDefaultKeyVal = false,
|
|
defaultKeyVal;
|
|
|
|
Ext.Object.each(data, function(key, value) {
|
|
if (defaultKey !== undefined && key === defaultKey) {
|
|
gotDefaultKeyVal = true;
|
|
defaultKeyVal = value;
|
|
} else if (value !== '' && value !== undefined) {
|
|
stringparts.push(key + '=' + value);
|
|
}
|
|
});
|
|
|
|
stringparts = stringparts.sort();
|
|
if (gotDefaultKeyVal) {
|
|
stringparts.unshift(defaultKeyVal);
|
|
}
|
|
|
|
return stringparts.join(',');
|
|
},
|
|
|
|
// helper for deleting field which are set to there default values
|
|
delete_if_default: function(values, fieldname, default_val, create) {
|
|
if (values[fieldname] === '' || values[fieldname] === default_val) {
|
|
if (!create) {
|
|
if (values.delete) {
|
|
if (Ext.isArray(values.delete)) {
|
|
values.delete.push(fieldname);
|
|
} else {
|
|
values.delete += ',' + fieldname;
|
|
}
|
|
} else {
|
|
values.delete = [fieldname];
|
|
}
|
|
}
|
|
|
|
delete values[fieldname];
|
|
}
|
|
},
|
|
|
|
|
|
render_datetime_utc: function(datetime) {
|
|
let pad = (number) => number < 10 ? '0' + number : number;
|
|
return datetime.getUTCFullYear() +
|
|
'-' + pad(datetime.getUTCMonth() + 1) +
|
|
'-' + pad(datetime.getUTCDate()) +
|
|
'T' + pad(datetime.getUTCHours()) +
|
|
':' + pad(datetime.getUTCMinutes()) +
|
|
':' + pad(datetime.getUTCSeconds()) +
|
|
'Z';
|
|
},
|
|
|
|
render_datastore_worker_id: function(id, what) {
|
|
const res = id.match(/^(\S+?):(\S+?)\/(\S+?)(\/(.+))?$/);
|
|
if (res) {
|
|
let datastore = res[1], backupGroup = `${res[2]}/${res[3]}`;
|
|
if (res[4] !== undefined) {
|
|
let datetime = Ext.Date.parse(parseInt(res[5], 16), 'U');
|
|
let utctime = PBS.Utils.render_datetime_utc(datetime);
|
|
return `Datastore ${datastore} ${what} ${backupGroup}/${utctime}`;
|
|
} else {
|
|
return `Datastore ${datastore} ${what} ${backupGroup}`;
|
|
}
|
|
}
|
|
return `Datastore ${what} ${id}`;
|
|
},
|
|
|
|
render_prune_job_worker_id: function(id, what) {
|
|
const res = id.match(/^(\S+?):(\S+)$/);
|
|
if (!res) {
|
|
return `${what} on Datastore ${id}`;
|
|
}
|
|
let datastore = res[1], namespace = res[2];
|
|
return `${what} on Datastore ${datastore} Namespace ${namespace}`;
|
|
},
|
|
|
|
render_tape_backup_id: function(id, what) {
|
|
const res = id.match(/^(\S+?):(\S+?):(\S+?)(:(.+))?$/);
|
|
if (res) {
|
|
let datastore = res[1];
|
|
let pool = res[2];
|
|
let drive = res[3];
|
|
return `${what} ${datastore} (pool ${pool}, drive ${drive})`;
|
|
}
|
|
return `${what} ${id}`;
|
|
},
|
|
|
|
render_drive_load_media_id: function(id, what) {
|
|
const res = id.match(/^(\S+?):(\S+?)$/);
|
|
if (res) {
|
|
let drive = res[1];
|
|
let label = res[2];
|
|
return gettext('Drive') + ` ${drive} - ${what} '${label}'`;
|
|
}
|
|
|
|
return `${what} ${id}`;
|
|
},
|
|
|
|
// mimics Display trait in backend
|
|
renderKeyID: function(fingerprint) {
|
|
return fingerprint.substring(0, 23);
|
|
},
|
|
|
|
render_task_status: function(value, metadata, record, rowIndex, colIndex, store) {
|
|
// GC tasks use 'upid' for backwards-compat, rest use 'last-run-upid'
|
|
if (
|
|
!record.data['last-run-upid'] &&
|
|
!store.getById('last-run-upid')?.data.value &&
|
|
!record.data.upid &&
|
|
!store.getById('upid')?.data.value
|
|
) {
|
|
return '-';
|
|
}
|
|
|
|
if (!record.data['last-run-endtime'] && !store.getById('last-run-endtime')?.data.value) {
|
|
metadata.tdCls = 'x-grid-row-loading';
|
|
return '';
|
|
}
|
|
|
|
let parsed = Proxmox.Utils.parse_task_status(value);
|
|
let text = value;
|
|
let icon = '';
|
|
switch (parsed) {
|
|
case 'unknown':
|
|
icon = 'question faded';
|
|
text = Proxmox.Utils.unknownText;
|
|
break;
|
|
case 'error':
|
|
icon = 'times critical';
|
|
text = Proxmox.Utils.errorText + ': ' + value;
|
|
break;
|
|
case 'warning':
|
|
icon = 'exclamation warning';
|
|
break;
|
|
case 'ok':
|
|
icon = 'check good';
|
|
text = gettext("OK");
|
|
}
|
|
|
|
return `<i class="fa fa-${icon}"></i> ${text}`;
|
|
},
|
|
|
|
render_next_task_run: function(value, metadat, record) {
|
|
if (!value) return '-';
|
|
|
|
let now = new Date();
|
|
let next = new Date(value*1000);
|
|
|
|
if (next < now) {
|
|
return gettext('pending');
|
|
}
|
|
return Proxmox.Utils.render_timestamp(value);
|
|
},
|
|
|
|
render_optional_timestamp: function(value, metadata, record) {
|
|
if (!value) return '-';
|
|
return Proxmox.Utils.render_timestamp(value);
|
|
},
|
|
|
|
parse_datastore_worker_id: function(type, id) {
|
|
let result;
|
|
let res;
|
|
if (type.startsWith('verif')) {
|
|
res = PBS.Utils.VERIFICATION_JOB_ID_RE.exec(id);
|
|
if (res) {
|
|
result = res[1];
|
|
}
|
|
} else if (type.startsWith('sync')) {
|
|
res = PBS.Utils.SYNC_JOB_ID_RE.exec(id);
|
|
if (res) {
|
|
result = res[3];
|
|
}
|
|
} else if (type === 'backup') {
|
|
res = PBS.Utils.BACKUP_JOB_ID_RE.exec(id);
|
|
if (res) {
|
|
result = res[1];
|
|
}
|
|
} else if (type === 'garbage_collection') {
|
|
return id;
|
|
} else if (type === 'prune') {
|
|
return id;
|
|
}
|
|
|
|
|
|
return result;
|
|
},
|
|
|
|
extractTokenUser: function(tokenid) {
|
|
return tokenid.match(/^(.+)!([^!]+)$/)[1];
|
|
},
|
|
|
|
extractTokenName: function(tokenid) {
|
|
return tokenid.match(/^(.+)!([^!]+)$/)[2];
|
|
},
|
|
|
|
render_estimate: function(value, metaData, record) {
|
|
if (record.data.avail === 0) {
|
|
return gettext("Full");
|
|
}
|
|
|
|
if (value === undefined) {
|
|
return gettext('Not enough data');
|
|
}
|
|
|
|
let now = new Date();
|
|
let estimate = new Date(value*1000);
|
|
|
|
let timespan = (estimate - now)/1000;
|
|
|
|
if (Number(estimate) <= Number(now) || isNaN(timespan)) {
|
|
return gettext('Never');
|
|
}
|
|
|
|
let duration = Proxmox.Utils.format_duration_human(timespan);
|
|
return Ext.String.format(gettext("in {0}"), duration);
|
|
},
|
|
|
|
// FIXME: deprecated by Proxmox.Utils.render_size_usage ?!
|
|
render_size_usage: function(val, max) {
|
|
if (max === 0) {
|
|
return gettext('N/A');
|
|
}
|
|
return (val*100/max).toFixed(2) + '% (' +
|
|
Ext.String.format(gettext('{0} of {1}'),
|
|
Proxmox.Utils.format_size(val), Proxmox.Utils.format_size(max)) + ')';
|
|
},
|
|
|
|
get_help_tool: function(blockid) {
|
|
let info = Proxmox.Utils.get_help_info(blockid);
|
|
if (info === undefined) {
|
|
info = Proxmox.Utils.get_help_info('pbs_documentation_index');
|
|
}
|
|
if (info === undefined) {
|
|
throw "get_help_info failed"; // should not happen
|
|
}
|
|
|
|
let docsURI = window.location.origin + info.link;
|
|
let title = info.title;
|
|
if (info.subtitle) {
|
|
title += ' - ' + info.subtitle;
|
|
}
|
|
return {
|
|
type: 'help',
|
|
tooltip: title,
|
|
handler: function() {
|
|
window.open(docsURI);
|
|
},
|
|
};
|
|
},
|
|
|
|
calculate_dedup_factor: function(gcstatus) {
|
|
let dedup = 1.0;
|
|
if (gcstatus['disk-bytes'] > 0) {
|
|
dedup = (gcstatus['index-data-bytes'] || 0)/gcstatus['disk-bytes'];
|
|
}
|
|
return dedup;
|
|
},
|
|
|
|
parse_snapshot_id: function(snapshot) {
|
|
if (!snapshot) {
|
|
return [undefined, undefined, undefined];
|
|
}
|
|
let nsRegex = /(?:^|\/)(ns\/([^/]+))/g;
|
|
let namespaces = [];
|
|
let nsPaths = [];
|
|
snapshot = snapshot.replace(nsRegex, (_, nsPath, ns) => { nsPaths.push(nsPath); namespaces.push(ns); return ""; });
|
|
let [_match, type, group, id] = /^\/?([^/]+)\/([^/]+)\/(.+)$/.exec(snapshot);
|
|
|
|
return [type, group, id, namespaces.join('/'), nsPaths.join('/')];
|
|
},
|
|
|
|
get_type_icon_cls: function(btype) {
|
|
var cls = '';
|
|
if (btype.startsWith('vm')) {
|
|
cls = 'fa-desktop';
|
|
} else if (btype.startsWith('ct')) {
|
|
cls = 'fa-cube';
|
|
} else if (btype.startsWith('host')) {
|
|
cls = 'fa-building';
|
|
}
|
|
return cls;
|
|
},
|
|
|
|
constructor: function() {
|
|
var me = this;
|
|
|
|
let PROXMOX_SAFE_ID_REGEX = "([A-Za-z0-9_][A-Za-z0-9._-]*)";
|
|
me.SAFE_ID_RE = new RegExp(`^${PROXMOX_SAFE_ID_REGEX}$`);
|
|
// only anchored at beginning, only parses datastore for now
|
|
me.VERIFICATION_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':?');
|
|
me.SYNC_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':' +
|
|
PROXMOX_SAFE_ID_REGEX + ':' + PROXMOX_SAFE_ID_REGEX + ':');
|
|
me.BACKUP_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':');
|
|
|
|
// do whatever you want here
|
|
Proxmox.Utils.override_task_descriptions({
|
|
'acme-deactivate': (type, id) =>
|
|
Ext.String.format(gettext("Deactivate {0} Account"), 'ACME') + ` '${id || 'default'}'`,
|
|
'acme-register': (type, id) =>
|
|
Ext.String.format(gettext("Register {0} Account"), 'ACME') + ` '${id || 'default'}'`,
|
|
'acme-update': (type, id) =>
|
|
Ext.String.format(gettext("Update {0} Account"), 'ACME') + ` '${id || 'default'}'`,
|
|
'acme-new-cert': ['', gettext('Order Certificate')],
|
|
'acme-renew-cert': ['', gettext('Renew Certificate')],
|
|
'acme-revoke-cert': ['', gettext('Revoke Certificate')],
|
|
backup: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Backup')),
|
|
'barcode-label-media': [gettext('Drive'), gettext('Barcode-Label Media')],
|
|
'catalog-media': [gettext('Drive'), gettext('Catalog Media')],
|
|
'delete-datastore': [gettext('Datastore'), gettext('Remove Datastore')],
|
|
'delete-namespace': [gettext('Namespace'), gettext('Remove Namespace')],
|
|
dircreate: [gettext('Directory Storage'), gettext('Create')],
|
|
dirremove: [gettext('Directory'), gettext('Remove')],
|
|
'eject-media': [gettext('Drive'), gettext('Eject Media')],
|
|
"format-media": [gettext('Drive'), gettext('Format media')],
|
|
"forget-group": [gettext('Group'), gettext('Remove Group')],
|
|
garbage_collection: ['Datastore', gettext('Garbage Collect')],
|
|
'realm-sync': ['Realm', gettext('User Sync')],
|
|
'inventory-update': [gettext('Drive'), gettext('Inventory Update')],
|
|
'label-media': [gettext('Drive'), gettext('Label Media')],
|
|
'load-media': (type, id) => PBS.Utils.render_drive_load_media_id(id, gettext('Load Media')),
|
|
logrotate: [null, gettext('Log Rotation')],
|
|
'mount-device': [gettext('Datastore'), gettext('Mount Device')],
|
|
prune: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Prune')),
|
|
prunejob: (type, id) => PBS.Utils.render_prune_job_worker_id(id, gettext('Prune Job')),
|
|
reader: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Read Objects')),
|
|
'rewind-media': [gettext('Drive'), gettext('Rewind Media')],
|
|
sync: ['Datastore', gettext('Remote Sync')],
|
|
syncjob: [gettext('Sync Job'), gettext('Remote Sync')],
|
|
'tape-backup': (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup')),
|
|
'tape-backup-job': (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup Job')),
|
|
'tape-restore': ['Datastore', gettext('Tape Restore')],
|
|
'unload-media': [gettext('Drive'), gettext('Unload Media')],
|
|
'unmount-device': [gettext('Datastore'), gettext('Unmount Device')],
|
|
verificationjob: [gettext('Verify Job'), gettext('Scheduled Verification')],
|
|
verify: ['Datastore', gettext('Verification')],
|
|
verify_group: ['Group', gettext('Verification')],
|
|
verify_snapshot: ['Snapshot', gettext('Verification')],
|
|
wipedisk: ['Device', gettext('Wipe Disk')],
|
|
zfscreate: [gettext('ZFS Storage'), gettext('Create')],
|
|
});
|
|
|
|
Proxmox.Utils.overrideNotificationFieldName({
|
|
'datastore': gettext('Datastore'),
|
|
'job-id': gettext('Job ID'),
|
|
'media-pool': gettext('Media Pool'),
|
|
});
|
|
|
|
Proxmox.Utils.overrideNotificationFieldValue({
|
|
'acme': gettext('ACME certificate renewal'),
|
|
'gc': gettext('Garbage collection'),
|
|
'package-updates': gettext('Package updates are available'),
|
|
'prune': gettext('Prune job'),
|
|
'sync': gettext('Sync job'),
|
|
'tape-backup': gettext('Tape backup notifications'),
|
|
'tape-load': gettext('Tape loading request'),
|
|
'verify': gettext('Verification job'),
|
|
});
|
|
|
|
Proxmox.Schema.overrideAuthDomains({
|
|
pbs: {
|
|
name: 'Proxmox Backup authentication server',
|
|
ipanel: 'pmxAuthSimplePanel',
|
|
onlineHelp: 'user-realms-pbs',
|
|
add: false,
|
|
edit: true,
|
|
pwchange: true,
|
|
sync: false,
|
|
useTypeInUrl: false,
|
|
},
|
|
});
|
|
|
|
// TODO: use `overrideEndpointTypes` later - not done right now to avoid
|
|
// breakage if widget-toolkit is not updated yet.
|
|
Proxmox.Schema.notificationEndpointTypes = {
|
|
sendmail: {
|
|
name: 'Sendmail',
|
|
ipanel: 'pmxSendmailEditPanel',
|
|
iconCls: 'fa-envelope-o',
|
|
defaultMailAuthor: 'Proxmox Backup Server - $hostname',
|
|
},
|
|
smtp: {
|
|
name: 'SMTP',
|
|
ipanel: 'pmxSmtpEditPanel',
|
|
iconCls: 'fa-envelope-o',
|
|
defaultMailAuthor: 'Proxmox Backup Server - $hostname',
|
|
},
|
|
gotify: {
|
|
name: 'Gotify',
|
|
ipanel: 'pmxGotifyEditPanel',
|
|
iconCls: 'fa-bell-o',
|
|
},
|
|
webhook: {
|
|
name: 'Webhook',
|
|
ipanel: 'pmxWebhookEditPanel',
|
|
iconCls: 'fa-bell-o',
|
|
},
|
|
};
|
|
},
|
|
|
|
// Convert an ArrayBuffer to a base64url encoded string.
|
|
// A `null` value will be preserved for convenience.
|
|
bytes_to_base64url: function(bytes) {
|
|
if (bytes === null) {
|
|
return null;
|
|
}
|
|
|
|
return btoa(Array
|
|
.from(new Uint8Array(bytes))
|
|
.map(val => String.fromCharCode(val))
|
|
.join(''),
|
|
)
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/[=]/g, '');
|
|
},
|
|
|
|
// Convert an a base64url string to an ArrayBuffer.
|
|
// A `null` value will be preserved for convenience.
|
|
base64url_to_bytes: function(b64u) {
|
|
if (b64u === null) {
|
|
return null;
|
|
}
|
|
|
|
return new Uint8Array(
|
|
atob(b64u
|
|
.replace(/-/g, '+')
|
|
.replace(/_/g, '/'),
|
|
)
|
|
.split('')
|
|
.map(val => val.charCodeAt(0)),
|
|
);
|
|
},
|
|
|
|
driveCommand: function(driveid, command, reqOpts) {
|
|
let params = Ext.apply(reqOpts, {
|
|
url: `/api2/extjs/tape/drive/${driveid}/${command}`,
|
|
timeout: 5*60*1000,
|
|
failure: function(response) {
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
|
},
|
|
});
|
|
|
|
Proxmox.Utils.API2Request(params);
|
|
},
|
|
|
|
showMediaLabelWindow: function(response) {
|
|
let list = [];
|
|
for (let [key, val] of Object.entries(response.result.data)) {
|
|
if (key === 'ctime' || key === 'media-set-ctime') {
|
|
val = Proxmox.Utils.render_timestamp(val);
|
|
}
|
|
list.push({ key: key, value: val });
|
|
}
|
|
|
|
Ext.create('Ext.window.Window', {
|
|
title: gettext('Label Information'),
|
|
modal: true,
|
|
width: 600,
|
|
height: 450,
|
|
layout: 'fit',
|
|
scrollable: true,
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
store: {
|
|
data: list,
|
|
},
|
|
columns: [
|
|
{
|
|
text: gettext('Property'),
|
|
dataIndex: 'key',
|
|
width: 120,
|
|
},
|
|
{
|
|
text: gettext('Value'),
|
|
dataIndex: 'value',
|
|
flex: 1,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}).show();
|
|
},
|
|
|
|
showCartridgeMemoryWindow: function(response) {
|
|
Ext.create('Ext.window.Window', {
|
|
title: gettext('Cartridge Memory'),
|
|
modal: true,
|
|
width: 600,
|
|
height: 450,
|
|
layout: 'fit',
|
|
scrollable: true,
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
store: {
|
|
data: response.result.data,
|
|
},
|
|
columns: [
|
|
{
|
|
text: gettext('ID'),
|
|
hidden: true,
|
|
dataIndex: 'id',
|
|
width: 60,
|
|
},
|
|
{
|
|
text: gettext('Name'),
|
|
dataIndex: 'name',
|
|
flex: 2,
|
|
},
|
|
{
|
|
text: gettext('Value'),
|
|
dataIndex: 'value',
|
|
flex: 1,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}).show();
|
|
},
|
|
|
|
showVolumeStatisticsWindow: function(response) {
|
|
let list = [];
|
|
for (let [key, val] of Object.entries(response.result.data)) {
|
|
if (key === 'total-native-capacity' ||
|
|
key === 'total-used-native-capacity' ||
|
|
key === 'lifetime-bytes-read' ||
|
|
key === 'lifetime-bytes-written' ||
|
|
key === 'last-mount-bytes-read' ||
|
|
key === 'last-mount-bytes-written') {
|
|
val = Proxmox.Utils.format_size(val);
|
|
}
|
|
list.push({ key: key, value: val });
|
|
}
|
|
Ext.create('Ext.window.Window', {
|
|
title: gettext('Volume Statistics'),
|
|
modal: true,
|
|
width: 600,
|
|
height: 450,
|
|
layout: 'fit',
|
|
scrollable: true,
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
store: {
|
|
data: list,
|
|
},
|
|
columns: [
|
|
{
|
|
text: gettext('Property'),
|
|
dataIndex: 'key',
|
|
flex: 1,
|
|
},
|
|
{
|
|
text: gettext('Value'),
|
|
dataIndex: 'value',
|
|
flex: 1,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}).show();
|
|
},
|
|
|
|
showDriveStatusWindow: function(response) {
|
|
let list = [];
|
|
for (let [key, val] of Object.entries(response.result.data)) {
|
|
if (key === 'manufactured') {
|
|
val = Proxmox.Utils.render_timestamp(val);
|
|
}
|
|
if (key === 'bytes-read' || key === 'bytes-written') {
|
|
val = Proxmox.Utils.format_size(val);
|
|
}
|
|
if (key === 'drive-activity') {
|
|
val = PBS.Utils.renderDriveActivity(val);
|
|
}
|
|
list.push({ key: key, value: val });
|
|
}
|
|
|
|
Ext.create('Ext.window.Window', {
|
|
title: gettext('Status'),
|
|
modal: true,
|
|
width: 600,
|
|
height: 450,
|
|
layout: 'fit',
|
|
scrollable: true,
|
|
items: [
|
|
{
|
|
xtype: 'grid',
|
|
store: {
|
|
data: list,
|
|
},
|
|
columns: [
|
|
{
|
|
text: gettext('Property'),
|
|
dataIndex: 'key',
|
|
width: 120,
|
|
},
|
|
{
|
|
text: gettext('Value'),
|
|
dataIndex: 'value',
|
|
flex: 1,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}).show();
|
|
},
|
|
|
|
tapeDriveActivities: {
|
|
'no-activity': gettext('No Activity'),
|
|
'cleaning': gettext('Cleaning'),
|
|
'loading': gettext('Loading'),
|
|
'unloading': gettext('Unloading'),
|
|
'other': gettext('Other Activity'),
|
|
'reading': gettext('Reading data'),
|
|
'writing': gettext('Writing data'),
|
|
'locating': gettext('Locating'),
|
|
'rewinding': gettext('Rewinding'),
|
|
'erasing': gettext('Erasing'),
|
|
'formatting': gettext('Formatting'),
|
|
'calibrating': gettext('Calibrating'),
|
|
'other-dt': gettext('Other DT Activity'),
|
|
'microcode-update': gettext('Updating Microcode'),
|
|
'reading-encrypted': gettext('Reading encrypted data'),
|
|
'writing-encrypted': gettext('Writing encrypted data'),
|
|
},
|
|
|
|
renderDriveActivity: function(value) {
|
|
if (!value) {
|
|
return Proxmox.Utils.unknownText;
|
|
}
|
|
return PBS.Utils.tapeDriveActivities[value] ?? value;
|
|
},
|
|
|
|
renderDriveState: function(value, md, rec) {
|
|
if (!value) {
|
|
if (rec?.data?.activity && rec?.data?.activity !== 'no-activity') {
|
|
return PBS.Utils.renderDriveActivity(rec.data.activity);
|
|
}
|
|
return gettext('Idle');
|
|
}
|
|
|
|
let icon = '<i class="fa fa-spinner fa-pulse fa-fw"></i>';
|
|
|
|
if (value.startsWith("UPID")) {
|
|
let upid = Proxmox.Utils.parse_task_upid(value);
|
|
md.tdCls = "pointer";
|
|
return `${icon} ${upid.desc}`;
|
|
}
|
|
|
|
return `${icon} ${value}`;
|
|
},
|
|
|
|
/**
|
|
* Parses maintenance mode property string.
|
|
* Examples:
|
|
* "offline,message=foo" -> ["offline", "foo"]
|
|
* "offline" -> ["offline", null]
|
|
* "message=foo,offline" -> ["offline", "foo"]
|
|
* null/undefined -> [null, null]
|
|
*
|
|
* @param {string|null} mode - Maintenance mode string to parse.
|
|
* @return {Array<string|null>} - Parsed maintenance mode values.
|
|
*/
|
|
parseMaintenanceMode: function(mode) {
|
|
if (!mode) {
|
|
return [null, null];
|
|
}
|
|
return mode.split(',').reduce(([m, msg], pair) => {
|
|
const [key, value] = pair.split('=');
|
|
if (key === 'message') {
|
|
return [m, value.replace(/^"(.*)"$/, '$1').replace(/\\"/g, '"')];
|
|
} else {
|
|
return [value ?? key, msg];
|
|
}
|
|
}, [null, null]);
|
|
},
|
|
|
|
renderMaintenance: function(mode, activeTasks) {
|
|
if (!mode) {
|
|
return gettext('None');
|
|
}
|
|
|
|
let [type, message] = PBS.Utils.parseMaintenanceMode(mode);
|
|
|
|
let extra = '';
|
|
|
|
if (activeTasks !== undefined) {
|
|
const conflictingTasks = activeTasks.write + (type === 'offline' || type === 'unmount' ? activeTasks.read : 0);
|
|
|
|
if (conflictingTasks > 0) {
|
|
extra += '| <i class="fa fa-spinner fa-pulse fa-fw"></i> ';
|
|
extra += Ext.String.format(gettext('{0} conflicting tasks still active.'), conflictingTasks);
|
|
} else {
|
|
extra += '<i class="fa fa-check"></i>';
|
|
}
|
|
}
|
|
|
|
if (message) {
|
|
extra += ` ("${message}")`;
|
|
}
|
|
|
|
let modeText = Proxmox.Utils.unknownText;
|
|
switch (type) {
|
|
case 'read-only': modeText = gettext("Read-only");
|
|
break;
|
|
case 'offline': modeText = gettext("Offline");
|
|
break;
|
|
case 'unmount': modeText = gettext("Unmounting");
|
|
break;
|
|
}
|
|
return `${modeText} ${extra}`;
|
|
},
|
|
|
|
render_optional_namespace: function(value, metadata, record) {
|
|
if (!value) return `- (${gettext('Root')})`;
|
|
return Ext.String.htmlEncode(value);
|
|
},
|
|
|
|
render_optional_remote: function(value, metadata, record) {
|
|
if (!value) {
|
|
return `- (${gettext('Local')})`;
|
|
}
|
|
return Ext.String.htmlEncode(value);
|
|
},
|
|
|
|
tuningOptions: {
|
|
'chunk-order': {
|
|
'__default__': Proxmox.Utils.defaultText + ` (${gettext('Inode')})`,
|
|
none: gettext('None'),
|
|
inode: gettext('Inode'),
|
|
},
|
|
'sync-level': {
|
|
'__default__': Proxmox.Utils.defaultText + ` (${gettext('Filesystem')})`,
|
|
none: gettext('None'),
|
|
file: gettext('File'),
|
|
filesystem: gettext('Filesystem'),
|
|
},
|
|
},
|
|
|
|
render_tuning_options: function(tuning) {
|
|
let options = [];
|
|
let order = tuning['chunk-order'];
|
|
delete tuning['chunk-order'];
|
|
order = PBS.Utils.tuningOptions['chunk-order'][order ?? '__default__'];
|
|
options.push(`${gettext('Chunk Order')}: ${order}`);
|
|
|
|
let sync = tuning['sync-level'];
|
|
delete tuning['sync-level'];
|
|
sync = PBS.Utils.tuningOptions['sync-level'][sync ?? '__default__'];
|
|
options.push(`${gettext('Sync Level')}: ${sync}`);
|
|
|
|
let gc_atime_safety_check = tuning['gc-atime-safety-check'];
|
|
delete tuning['gc-atime-safety-check'];
|
|
options.push(`${gettext('GC Access-Time Support Check')}: ${gc_atime_safety_check ?? true}`);
|
|
|
|
let gc_atime_cutoff = tuning['gc-atime-cutoff'];
|
|
delete tuning['gc-atime-cutoff'];
|
|
let gc_atime_cutoff_rendered = Proxmox.Utils.format_duration_human((gc_atime_cutoff ?? 1445) * 60);
|
|
options.push(`${gettext('GC Access-Time Cutoff')}: ${gc_atime_cutoff_rendered}`);
|
|
|
|
let gc_cache_capacity = tuning['gc-cache-capacity'];
|
|
delete tuning['gc-cache-capacity'];
|
|
options.push(`${gettext('GC cache capacity')}: ${gc_cache_capacity ?? Proxmox.Utils.defaultText}`);
|
|
|
|
for (const [k, v] of Object.entries(tuning)) {
|
|
options.push(`${k}: ${v}`);
|
|
}
|
|
|
|
return options.join(', ');
|
|
},
|
|
});
|