1e37156a6b
since the namespaces are in the snapshot path we get here, we must parse them out, else we confuse the first namespace with the group. for now, show all namespaces on the same level (so not nested), and do not allow for preselecting a namespace for restoring Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
694 lines
18 KiB
JavaScript
694 lines
18 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_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) {
|
|
if (!record.data['last-run-upid']) {
|
|
return '-';
|
|
}
|
|
|
|
if (!record.data['last-run-endtime']) {
|
|
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) {
|
|
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);
|
|
},
|
|
|
|
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')],
|
|
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')],
|
|
'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')],
|
|
prune: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Prune')),
|
|
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')],
|
|
verificationjob: [gettext('Verify Job'), gettext('Scheduled Verification')],
|
|
verify: ['Datastore', gettext('Verification')],
|
|
verify_group: ['Group', gettext('Verification')],
|
|
verify_snapshot: ['Snapshot', gettext('Verification')],
|
|
zfscreate: [gettext('ZFS Storage'), gettext('Create')],
|
|
});
|
|
|
|
Proxmox.Schema.overrideAuthDomains({
|
|
pbs: {
|
|
name: 'Proxmox Backup authentication server',
|
|
add: false,
|
|
edit: false,
|
|
pwchange: true,
|
|
},
|
|
});
|
|
},
|
|
|
|
// 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);
|
|
}
|
|
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();
|
|
},
|
|
|
|
renderDriveState: function(value, md) {
|
|
if (!value) {
|
|
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}`;
|
|
},
|
|
|
|
// FIXME: this "parser" is brittle and relies on the order the arguments will appear in
|
|
parseMaintenanceMode: function(mode) {
|
|
let [type, message] = mode.split(/,(.+)/);
|
|
type = type.split("=").pop();
|
|
message = message ? message.split("=")[1]
|
|
.replace(/^"(.*)"$/, '$1')
|
|
.replaceAll('\\"', '"') : null;
|
|
return [type, message];
|
|
},
|
|
|
|
renderMaintenance: function(mode, activeTasks) {
|
|
if (!mode) {
|
|
return gettext('None');
|
|
}
|
|
|
|
let [type, message] = PBS.Utils.parseMaintenanceMode(mode);
|
|
|
|
const conflictingTasks = activeTasks.write + (type === 'offline' ? activeTasks.read : 0);
|
|
|
|
let extra = '';
|
|
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;
|
|
}
|
|
return `${modeText} ${extra}`;
|
|
},
|
|
|
|
render_optional_namespace: function(value, metadata, record) {
|
|
if (!value) return '-'; // FIXME ??
|
|
return Ext.String.htmlEncode(value);
|
|
},
|
|
|
|
});
|