]> git.proxmox.com Git - proxmox-backup.git/blob - www/Utils.js
ui: navigation: change traffic-control icon to rotated signal
[proxmox-backup.git] / www / Utils.js
1 Ext.ns('PBS');
2
3 console.log("Starting Backup Server GUI");
4
5 Ext.define('PBS.Utils', {
6 singleton: true,
7
8 missingText: gettext('missing'),
9
10 updateLoginData: function(data) {
11 Proxmox.Utils.setAuthData(data);
12 },
13
14 dataStorePrefix: 'DataStore-',
15
16 cryptmap: [
17 'none',
18 'mixed',
19 'sign-only',
20 'encrypt',
21 ],
22
23 cryptText: [
24 Proxmox.Utils.noText,
25 gettext('Mixed'),
26 gettext('Signed'),
27 gettext('Encrypted'),
28 ],
29
30 cryptIconCls: [
31 '',
32 '',
33 'lock faded',
34 'lock good',
35 ],
36
37 calculateCryptMode: function(data) {
38 let mixed = data.mixed;
39 let encrypted = data.encrypt;
40 let signed = data['sign-only'];
41 let files = data.count;
42 if (mixed > 0) {
43 return PBS.Utils.cryptmap.indexOf('mixed');
44 } else if (files === encrypted && encrypted > 0) {
45 return PBS.Utils.cryptmap.indexOf('encrypt');
46 } else if (files === signed && signed > 0) {
47 return PBS.Utils.cryptmap.indexOf('sign-only');
48 } else if ((signed+encrypted) === 0) {
49 return PBS.Utils.cryptmap.indexOf('none');
50 } else {
51 return PBS.Utils.cryptmap.indexOf('mixed');
52 }
53 },
54
55 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.',
56
57 getDataStoreFromPath: function(path) {
58 return path.slice(PBS.Utils.dataStorePrefix.length);
59 },
60
61 isDataStorePath: function(path) {
62 return path.indexOf(PBS.Utils.dataStorePrefix) === 0;
63 },
64
65 parsePropertyString: function(value, defaultKey) {
66 var res = {},
67 error;
68
69 if (typeof value !== 'string' || value === '') {
70 return res;
71 }
72
73 Ext.Array.each(value.split(','), function(p) {
74 var kv = p.split('=', 2);
75 if (Ext.isDefined(kv[1])) {
76 res[kv[0]] = kv[1];
77 } else if (Ext.isDefined(defaultKey)) {
78 if (Ext.isDefined(res[defaultKey])) {
79 error = 'defaultKey may be only defined once in propertyString';
80 return false; // break
81 }
82 res[defaultKey] = kv[0];
83 } else {
84 error = 'invalid propertyString, not a key=value pair and no defaultKey defined';
85 return false; // break
86 }
87 return true;
88 });
89
90 if (error !== undefined) {
91 console.error(error);
92 return null;
93 }
94
95 return res;
96 },
97
98 printPropertyString: function(data, defaultKey) {
99 var stringparts = [],
100 gotDefaultKeyVal = false,
101 defaultKeyVal;
102
103 Ext.Object.each(data, function(key, value) {
104 if (defaultKey !== undefined && key === defaultKey) {
105 gotDefaultKeyVal = true;
106 defaultKeyVal = value;
107 } else if (value !== '' && value !== undefined) {
108 stringparts.push(key + '=' + value);
109 }
110 });
111
112 stringparts = stringparts.sort();
113 if (gotDefaultKeyVal) {
114 stringparts.unshift(defaultKeyVal);
115 }
116
117 return stringparts.join(',');
118 },
119
120 // helper for deleting field which are set to there default values
121 delete_if_default: function(values, fieldname, default_val, create) {
122 if (values[fieldname] === '' || values[fieldname] === default_val) {
123 if (!create) {
124 if (values.delete) {
125 if (Ext.isArray(values.delete)) {
126 values.delete.push(fieldname);
127 } else {
128 values.delete += ',' + fieldname;
129 }
130 } else {
131 values.delete = [fieldname];
132 }
133 }
134
135 delete values[fieldname];
136 }
137 },
138
139
140 render_datetime_utc: function(datetime) {
141 let pad = (number) => number < 10 ? '0' + number : number;
142 return datetime.getUTCFullYear() +
143 '-' + pad(datetime.getUTCMonth() + 1) +
144 '-' + pad(datetime.getUTCDate()) +
145 'T' + pad(datetime.getUTCHours()) +
146 ':' + pad(datetime.getUTCMinutes()) +
147 ':' + pad(datetime.getUTCSeconds()) +
148 'Z';
149 },
150
151 render_datastore_worker_id: function(id, what) {
152 const res = id.match(/^(\S+?):(\S+?)\/(\S+?)(\/(.+))?$/);
153 if (res) {
154 let datastore = res[1], backupGroup = `${res[2]}/${res[3]}`;
155 if (res[4] !== undefined) {
156 let datetime = Ext.Date.parse(parseInt(res[5], 16), 'U');
157 let utctime = PBS.Utils.render_datetime_utc(datetime);
158 return `Datastore ${datastore} ${what} ${backupGroup}/${utctime}`;
159 } else {
160 return `Datastore ${datastore} ${what} ${backupGroup}`;
161 }
162 }
163 return `Datastore ${what} ${id}`;
164 },
165
166 render_tape_backup_id: function(id, what) {
167 const res = id.match(/^(\S+?):(\S+?):(\S+?)(:(.+))?$/);
168 if (res) {
169 let datastore = res[1];
170 let pool = res[2];
171 let drive = res[3];
172 return `${what} ${datastore} (pool ${pool}, drive ${drive})`;
173 }
174 return `${what} ${id}`;
175 },
176
177 render_drive_load_media_id: function(id, what) {
178 const res = id.match(/^(\S+?):(\S+?)$/);
179 if (res) {
180 let drive = res[1];
181 let label = res[2];
182 return gettext('Drive') + ` ${drive} - ${what} '${label}'`;
183 }
184
185 return `${what} ${id}`;
186 },
187
188 // mimics Display trait in backend
189 renderKeyID: function(fingerprint) {
190 return fingerprint.substring(0, 23);
191 },
192
193 render_task_status: function(value, metadata, record) {
194 if (!record.data['last-run-upid']) {
195 return '-';
196 }
197
198 if (!record.data['last-run-endtime']) {
199 metadata.tdCls = 'x-grid-row-loading';
200 return '';
201 }
202
203 let parsed = Proxmox.Utils.parse_task_status(value);
204 let text = value;
205 let icon = '';
206 switch (parsed) {
207 case 'unknown':
208 icon = 'question faded';
209 text = Proxmox.Utils.unknownText;
210 break;
211 case 'error':
212 icon = 'times critical';
213 text = Proxmox.Utils.errorText + ': ' + value;
214 break;
215 case 'warning':
216 icon = 'exclamation warning';
217 break;
218 case 'ok':
219 icon = 'check good';
220 text = gettext("OK");
221 }
222
223 return `<i class="fa fa-${icon}"></i> ${text}`;
224 },
225
226 render_next_task_run: function(value, metadat, record) {
227 if (!value) return '-';
228
229 let now = new Date();
230 let next = new Date(value*1000);
231
232 if (next < now) {
233 return gettext('pending');
234 }
235 return Proxmox.Utils.render_timestamp(value);
236 },
237
238 render_optional_timestamp: function(value, metadata, record) {
239 if (!value) return '-';
240 return Proxmox.Utils.render_timestamp(value);
241 },
242
243 parse_datastore_worker_id: function(type, id) {
244 let result;
245 let res;
246 if (type.startsWith('verif')) {
247 res = PBS.Utils.VERIFICATION_JOB_ID_RE.exec(id);
248 if (res) {
249 result = res[1];
250 }
251 } else if (type.startsWith('sync')) {
252 res = PBS.Utils.SYNC_JOB_ID_RE.exec(id);
253 if (res) {
254 result = res[3];
255 }
256 } else if (type === 'backup') {
257 res = PBS.Utils.BACKUP_JOB_ID_RE.exec(id);
258 if (res) {
259 result = res[1];
260 }
261 } else if (type === 'garbage_collection') {
262 return id;
263 } else if (type === 'prune') {
264 return id;
265 }
266
267
268 return result;
269 },
270
271 extractTokenUser: function(tokenid) {
272 return tokenid.match(/^(.+)!([^!]+)$/)[1];
273 },
274
275 extractTokenName: function(tokenid) {
276 return tokenid.match(/^(.+)!([^!]+)$/)[2];
277 },
278
279 render_estimate: function(value) {
280 if (value === undefined) {
281 return gettext('Not enough data');
282 }
283
284 let now = new Date();
285 let estimate = new Date(value*1000);
286
287 let timespan = (estimate - now)/1000;
288
289 if (Number(estimate) <= Number(now) || isNaN(timespan)) {
290 return gettext('Never');
291 }
292
293 let duration = Proxmox.Utils.format_duration_human(timespan);
294 return Ext.String.format(gettext("in {0}"), duration);
295 },
296
297 render_size_usage: function(val, max) {
298 if (max === 0) {
299 return gettext('N/A');
300 }
301 return (val*100/max).toFixed(2) + '% (' +
302 Ext.String.format(gettext('{0} of {1}'),
303 Proxmox.Utils.format_size(val), Proxmox.Utils.format_size(max)) + ')';
304 },
305
306 get_help_tool: function(blockid) {
307 let info = Proxmox.Utils.get_help_info(blockid);
308 if (info === undefined) {
309 info = Proxmox.Utils.get_help_info('pbs_documentation_index');
310 }
311 if (info === undefined) {
312 throw "get_help_info failed"; // should not happen
313 }
314
315 let docsURI = window.location.origin + info.link;
316 let title = info.title;
317 if (info.subtitle) {
318 title += ' - ' + info.subtitle;
319 }
320 return {
321 type: 'help',
322 tooltip: title,
323 handler: function() {
324 window.open(docsURI);
325 },
326 };
327 },
328
329 calculate_dedup_factor: function(gcstatus) {
330 let dedup = 1.0;
331 if (gcstatus['disk-bytes'] > 0) {
332 dedup = (gcstatus['index-data-bytes'] || 0)/gcstatus['disk-bytes'];
333 }
334 return dedup;
335 },
336
337 parse_snapshot_id: function(snapshot) {
338 if (!snapshot) {
339 return [undefined, undefined, undefined];
340 }
341 let [_match, type, group, id] = /^([^/]+)\/([^/]+)\/(.+)$/.exec(snapshot);
342
343 return [type, group, id];
344 },
345
346 get_type_icon_cls: function(btype) {
347 var cls = '';
348 if (btype.startsWith('vm')) {
349 cls = 'fa-desktop';
350 } else if (btype.startsWith('ct')) {
351 cls = 'fa-cube';
352 } else if (btype.startsWith('host')) {
353 cls = 'fa-building';
354 }
355 return cls;
356 },
357
358 constructor: function() {
359 var me = this;
360
361 let PROXMOX_SAFE_ID_REGEX = "([A-Za-z0-9_][A-Za-z0-9._-]*)";
362 // only anchored at beginning
363 // only parses datastore for now
364 me.VERIFICATION_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':?');
365 me.SYNC_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':' +
366 PROXMOX_SAFE_ID_REGEX + ':' + PROXMOX_SAFE_ID_REGEX + ':');
367 me.BACKUP_JOB_ID_RE = new RegExp("^" + PROXMOX_SAFE_ID_REGEX + ':');
368
369 // do whatever you want here
370 Proxmox.Utils.override_task_descriptions({
371 'acme-deactivate': (type, id) =>
372 Ext.String.format(gettext("Deactivate {0} Account"), 'ACME') + ` '${id || 'default'}'`,
373 'acme-register': (type, id) =>
374 Ext.String.format(gettext("Register {0} Account"), 'ACME') + ` '${id || 'default'}'`,
375 'acme-update': (type, id) =>
376 Ext.String.format(gettext("Update {0} Account"), 'ACME') + ` '${id || 'default'}'`,
377 'acme-new-cert': ['', gettext('Order Certificate')],
378 'acme-renew-cert': ['', gettext('Renew Certificate')],
379 'acme-revoke-cert': ['', gettext('Revoke Certificate')],
380 backup: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Backup')),
381 'barcode-label-media': [gettext('Drive'), gettext('Barcode-Label Media')],
382 'catalog-media': [gettext('Drive'), gettext('Catalog Media')],
383 'delete-datastore': [gettext('Datastore'), gettext('Remove Datastore')],
384 dircreate: [gettext('Directory Storage'), gettext('Create')],
385 dirremove: [gettext('Directory'), gettext('Remove')],
386 'eject-media': [gettext('Drive'), gettext('Eject Media')],
387 "format-media": [gettext('Drive'), gettext('Format media')],
388 "forget-group": [gettext('Group'), gettext('Remove Group')],
389 garbage_collection: ['Datastore', gettext('Garbage Collect')],
390 'inventory-update': [gettext('Drive'), gettext('Inventory Update')],
391 'label-media': [gettext('Drive'), gettext('Label Media')],
392 'load-media': (type, id) => PBS.Utils.render_drive_load_media_id(id, gettext('Load Media')),
393 logrotate: [null, gettext('Log Rotation')],
394 prune: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Prune')),
395 reader: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Read Objects')),
396 'rewind-media': [gettext('Drive'), gettext('Rewind Media')],
397 sync: ['Datastore', gettext('Remote Sync')],
398 syncjob: [gettext('Sync Job'), gettext('Remote Sync')],
399 'tape-backup': (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup')),
400 'tape-backup-job': (type, id) => PBS.Utils.render_tape_backup_id(id, gettext('Tape Backup Job')),
401 'tape-restore': ['Datastore', gettext('Tape Restore')],
402 'unload-media': [gettext('Drive'), gettext('Unload Media')],
403 verificationjob: [gettext('Verify Job'), gettext('Scheduled Verification')],
404 verify: ['Datastore', gettext('Verification')],
405 verify_group: ['Group', gettext('Verification')],
406 verify_snapshot: ['Snapshot', gettext('Verification')],
407 zfscreate: [gettext('ZFS Storage'), gettext('Create')],
408 });
409
410 Proxmox.Schema.overrideAuthDomains({
411 pbs: {
412 name: 'Proxmox Backup authentication server',
413 add: false,
414 edit: false,
415 pwchange: true,
416 },
417 });
418 },
419
420 // Convert an ArrayBuffer to a base64url encoded string.
421 // A `null` value will be preserved for convenience.
422 bytes_to_base64url: function(bytes) {
423 if (bytes === null) {
424 return null;
425 }
426
427 return btoa(Array
428 .from(new Uint8Array(bytes))
429 .map(val => String.fromCharCode(val))
430 .join(''),
431 )
432 .replace(/\+/g, '-')
433 .replace(/\//g, '_')
434 .replace(/[=]/g, '');
435 },
436
437 // Convert an a base64url string to an ArrayBuffer.
438 // A `null` value will be preserved for convenience.
439 base64url_to_bytes: function(b64u) {
440 if (b64u === null) {
441 return null;
442 }
443
444 return new Uint8Array(
445 atob(b64u
446 .replace(/-/g, '+')
447 .replace(/_/g, '/'),
448 )
449 .split('')
450 .map(val => val.charCodeAt(0)),
451 );
452 },
453
454 driveCommand: function(driveid, command, reqOpts) {
455 let params = Ext.apply(reqOpts, {
456 url: `/api2/extjs/tape/drive/${driveid}/${command}`,
457 timeout: 5*60*1000,
458 failure: function(response) {
459 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
460 },
461 });
462
463 Proxmox.Utils.API2Request(params);
464 },
465
466 showMediaLabelWindow: function(response) {
467 let list = [];
468 for (let [key, val] of Object.entries(response.result.data)) {
469 if (key === 'ctime' || key === 'media-set-ctime') {
470 val = Proxmox.Utils.render_timestamp(val);
471 }
472 list.push({ key: key, value: val });
473 }
474
475 Ext.create('Ext.window.Window', {
476 title: gettext('Label Information'),
477 modal: true,
478 width: 600,
479 height: 450,
480 layout: 'fit',
481 scrollable: true,
482 items: [
483 {
484 xtype: 'grid',
485 store: {
486 data: list,
487 },
488 columns: [
489 {
490 text: gettext('Property'),
491 dataIndex: 'key',
492 width: 120,
493 },
494 {
495 text: gettext('Value'),
496 dataIndex: 'value',
497 flex: 1,
498 },
499 ],
500 },
501 ],
502 }).show();
503 },
504
505 showCartridgeMemoryWindow: function(response) {
506 Ext.create('Ext.window.Window', {
507 title: gettext('Cartridge Memory'),
508 modal: true,
509 width: 600,
510 height: 450,
511 layout: 'fit',
512 scrollable: true,
513 items: [
514 {
515 xtype: 'grid',
516 store: {
517 data: response.result.data,
518 },
519 columns: [
520 {
521 text: gettext('ID'),
522 hidden: true,
523 dataIndex: 'id',
524 width: 60,
525 },
526 {
527 text: gettext('Name'),
528 dataIndex: 'name',
529 flex: 2,
530 },
531 {
532 text: gettext('Value'),
533 dataIndex: 'value',
534 flex: 1,
535 },
536 ],
537 },
538 ],
539 }).show();
540 },
541
542 showVolumeStatisticsWindow: function(response) {
543 let list = [];
544 for (let [key, val] of Object.entries(response.result.data)) {
545 if (key === 'total-native-capacity' ||
546 key === 'total-used-native-capacity' ||
547 key === 'lifetime-bytes-read' ||
548 key === 'lifetime-bytes-written' ||
549 key === 'last-mount-bytes-read' ||
550 key === 'last-mount-bytes-written') {
551 val = Proxmox.Utils.format_size(val);
552 }
553 list.push({ key: key, value: val });
554 }
555 Ext.create('Ext.window.Window', {
556 title: gettext('Volume Statistics'),
557 modal: true,
558 width: 600,
559 height: 450,
560 layout: 'fit',
561 scrollable: true,
562 items: [
563 {
564 xtype: 'grid',
565 store: {
566 data: list,
567 },
568 columns: [
569 {
570 text: gettext('Property'),
571 dataIndex: 'key',
572 flex: 1,
573 },
574 {
575 text: gettext('Value'),
576 dataIndex: 'value',
577 flex: 1,
578 },
579 ],
580 },
581 ],
582 }).show();
583 },
584
585 showDriveStatusWindow: function(response) {
586 let list = [];
587 for (let [key, val] of Object.entries(response.result.data)) {
588 if (key === 'manufactured') {
589 val = Proxmox.Utils.render_timestamp(val);
590 }
591 if (key === 'bytes-read' || key === 'bytes-written') {
592 val = Proxmox.Utils.format_size(val);
593 }
594 list.push({ key: key, value: val });
595 }
596
597 Ext.create('Ext.window.Window', {
598 title: gettext('Status'),
599 modal: true,
600 width: 600,
601 height: 450,
602 layout: 'fit',
603 scrollable: true,
604 items: [
605 {
606 xtype: 'grid',
607 store: {
608 data: list,
609 },
610 columns: [
611 {
612 text: gettext('Property'),
613 dataIndex: 'key',
614 width: 120,
615 },
616 {
617 text: gettext('Value'),
618 dataIndex: 'value',
619 flex: 1,
620 },
621 ],
622 },
623 ],
624 }).show();
625 },
626
627 renderDriveState: function(value, md) {
628 if (!value) {
629 return gettext('Idle');
630 }
631
632 let icon = '<i class="fa fa-spinner fa-pulse fa-fw"></i>';
633
634 if (value.startsWith("UPID")) {
635 let upid = Proxmox.Utils.parse_task_upid(value);
636 md.tdCls = "pointer";
637 return `${icon} ${upid.desc}`;
638 }
639
640 return `${icon} ${value}`;
641 },
642
643 });