]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/OptionView.js
ui: cloudinit: match backend privilege checks
[pve-manager.git] / www / manager6 / dc / OptionView.js
1 Ext.define('PVE.dc.OptionView', {
2 extend: 'Proxmox.grid.ObjectGrid',
3 alias: ['widget.pveDcOptionView'],
4
5 onlineHelp: 'datacenter_configuration_file',
6
7 monStoreErrors: true,
8 userCls: 'proxmox-tags-full',
9
10 add_inputpanel_row: function(name, text, opts) {
11 var me = this;
12
13 opts = opts || {};
14 me.rows = me.rows || {};
15
16 let canEdit = !Object.prototype.hasOwnProperty.call(opts, 'caps') || opts.caps;
17 me.rows[name] = {
18 required: true,
19 defaultValue: opts.defaultValue,
20 header: text,
21 renderer: opts.renderer,
22 editor: canEdit ? {
23 xtype: 'proxmoxWindowEdit',
24 width: opts.width || 350,
25 subject: text,
26 onlineHelp: opts.onlineHelp,
27 fieldDefaults: {
28 labelWidth: opts.labelWidth || 100,
29 },
30 setValues: function(values) {
31 var edit_value = values[name];
32
33 if (opts.parseBeforeSet) {
34 edit_value = PVE.Parser.parsePropertyString(edit_value);
35 }
36
37 Ext.Array.each(this.query('inputpanel'), function(panel) {
38 panel.setValues(edit_value);
39 });
40 },
41 url: opts.url,
42 items: [{
43 xtype: 'inputpanel',
44 onGetValues: function(values) {
45 if (values === undefined || Object.keys(values).length === 0) {
46 return { 'delete': name };
47 }
48 var ret_val = {};
49 ret_val[name] = PVE.Parser.printPropertyString(values);
50 return ret_val;
51 },
52 items: opts.items,
53 }],
54 } : undefined,
55 };
56 },
57
58 render_bwlimits: function(value) {
59 if (!value) {
60 return gettext("None");
61 }
62
63 let parsed = PVE.Parser.parsePropertyString(value);
64 return Object.entries(parsed)
65 .map(([k, v]) => k + ": " + Proxmox.Utils.format_size(v * 1024) + "/s")
66 .join(',');
67 },
68
69 initComponent: function() {
70 var me = this;
71
72 me.add_combobox_row('keyboard', gettext('Keyboard Layout'), {
73 renderer: PVE.Utils.render_kvm_language,
74 comboItems: Object.entries(PVE.Utils.kvm_keymaps),
75 defaultValue: '__default__',
76 deleteEmpty: true,
77 });
78 me.add_text_row('http_proxy', gettext('HTTP proxy'), {
79 defaultValue: Proxmox.Utils.noneText,
80 vtype: 'HttpProxy',
81 deleteEmpty: true,
82 });
83 me.add_combobox_row('console', gettext('Console Viewer'), {
84 renderer: PVE.Utils.render_console_viewer,
85 comboItems: Object.entries(PVE.Utils.console_map),
86 defaultValue: '__default__',
87 deleteEmpty: true,
88 });
89 me.add_text_row('email_from', gettext('Email from address'), {
90 deleteEmpty: true,
91 vtype: 'proxmoxMail',
92 defaultValue: 'root@$hostname',
93 });
94 me.add_inputpanel_row('notify', gettext('Notify'), {
95 renderer: v => !v ? 'package-updates=auto' : PVE.Parser.printPropertyString(v),
96 labelWidth: 120,
97 url: "/api2/extjs/cluster/options",
98 //onlineHelp: 'ha_manager_shutdown_policy',
99 items: [{
100 xtype: 'proxmoxKVComboBox',
101 name: 'package-updates',
102 fieldLabel: gettext('Package Updates'),
103 deleteEmpty: false,
104 value: '__default__',
105 comboItems: [
106 ['__default__', Proxmox.Utils.defaultText + ' (auto)'],
107 ['auto', gettext('Automatically')],
108 ['always', gettext('Always')],
109 ['never', gettext('Never')],
110 ],
111 defaultValue: '__default__',
112 }],
113 });
114 me.add_text_row('mac_prefix', gettext('MAC address prefix'), {
115 deleteEmpty: true,
116 vtype: 'MacPrefix',
117 defaultValue: Proxmox.Utils.noneText,
118 });
119 me.add_inputpanel_row('migration', gettext('Migration Settings'), {
120 renderer: PVE.Utils.render_as_property_string,
121 labelWidth: 120,
122 url: "/api2/extjs/cluster/options",
123 defaultKey: 'type',
124 items: [{
125 xtype: 'displayfield',
126 name: 'type',
127 fieldLabel: gettext('Type'),
128 value: 'secure',
129 submitValue: true,
130 }, {
131 xtype: 'proxmoxNetworkSelector',
132 name: 'network',
133 fieldLabel: gettext('Network'),
134 value: null,
135 emptyText: Proxmox.Utils.defaultText,
136 autoSelect: false,
137 skipEmptyText: true,
138 }],
139 });
140 me.add_inputpanel_row('ha', gettext('HA Settings'), {
141 renderer: PVE.Utils.render_dc_ha_opts,
142 labelWidth: 120,
143 url: "/api2/extjs/cluster/options",
144 onlineHelp: 'ha_manager_shutdown_policy',
145 items: [{
146 xtype: 'proxmoxKVComboBox',
147 name: 'shutdown_policy',
148 fieldLabel: gettext('Shutdown Policy'),
149 deleteEmpty: false,
150 value: '__default__',
151 comboItems: [
152 ['__default__', Proxmox.Utils.defaultText + ' (conditional)'],
153 ['freeze', 'freeze'],
154 ['failover', 'failover'],
155 ['migrate', 'migrate'],
156 ['conditional', 'conditional'],
157 ],
158 defaultValue: '__default__',
159 }],
160 });
161 me.add_inputpanel_row('crs', gettext('Cluster Resource Scheduling'), {
162 renderer: PVE.Utils.render_as_property_string,
163 width: 450,
164 labelWidth: 120,
165 url: "/api2/extjs/cluster/options",
166 onlineHelp: 'ha_manager_crs',
167 items: [{
168 xtype: 'proxmoxKVComboBox',
169 name: 'ha',
170 fieldLabel: gettext('HA Scheduling'),
171 deleteEmpty: false,
172 value: '__default__',
173 comboItems: [
174 ['__default__', Proxmox.Utils.defaultText + ' (basic)'],
175 ['basic', 'Basic (Resource Count)'],
176 ['static', 'Static Load'],
177 ],
178 defaultValue: '__default__',
179 }, {
180 xtype: 'proxmoxcheckbox',
181 name: 'ha-rebalance-on-start',
182 fieldLabel: gettext('Rebalance on Start'),
183 boxLabel: gettext('Use CRS to select the least loaded node when starting an HA service'),
184 value: 0,
185 }],
186 });
187 me.add_inputpanel_row('u2f', gettext('U2F Settings'), {
188 renderer: v => !v ? Proxmox.Utils.NoneText : PVE.Parser.printPropertyString(v),
189 width: 450,
190 url: "/api2/extjs/cluster/options",
191 onlineHelp: 'pveum_configure_u2f',
192 items: [{
193 xtype: 'textfield',
194 name: 'appid',
195 fieldLabel: gettext('U2F AppID URL'),
196 emptyText: gettext('Defaults to origin'),
197 value: '',
198 deleteEmpty: true,
199 skipEmptyText: true,
200 submitEmptyText: false,
201 }, {
202 xtype: 'textfield',
203 name: 'origin',
204 fieldLabel: gettext('U2F Origin'),
205 emptyText: gettext('Defaults to requesting host URI'),
206 value: '',
207 deleteEmpty: true,
208 skipEmptyText: true,
209 submitEmptyText: false,
210 },
211 {
212 xtype: 'box',
213 height: 25,
214 html: `<span class='pmx-hint'>${gettext('Note:')}</span> `
215 + Ext.String.format(gettext('{0} is deprecated, use {1}'), 'U2F', 'WebAuthn'),
216 },
217 {
218 xtype: 'displayfield',
219 userCls: 'pmx-hint',
220 value: gettext('NOTE: Changing an AppID breaks existing U2F registrations!'),
221 }],
222 });
223 me.add_inputpanel_row('webauthn', gettext('WebAuthn Settings'), {
224 renderer: v => !v ? Proxmox.Utils.NoneText : PVE.Parser.printPropertyString(v),
225 width: 450,
226 url: "/api2/extjs/cluster/options",
227 onlineHelp: 'pveum_configure_webauthn',
228 items: [{
229 xtype: 'textfield',
230 fieldLabel: gettext('Name'),
231 name: 'rp', // NOTE: relying party consists of name and id, this is the name
232 allowBlank: false,
233 },
234 {
235 xtype: 'textfield',
236 fieldLabel: gettext('Origin'),
237 emptyText: Ext.String.format(gettext("Domain Lockdown (e.g., {0})"), document.location.origin),
238 name: 'origin',
239 allowBlank: true,
240 },
241 {
242 xtype: 'textfield',
243 fieldLabel: 'ID',
244 name: 'id',
245 allowBlank: false,
246 listeners: {
247 dirtychange: (f, isDirty) =>
248 f.up('panel').down('box[id=idChangeWarning]').setHidden(!f.originalValue || !isDirty),
249 },
250 },
251 {
252 xtype: 'container',
253 layout: 'hbox',
254 items: [
255 {
256 xtype: 'box',
257 flex: 1,
258 },
259 {
260 xtype: 'button',
261 text: gettext('Auto-fill'),
262 iconCls: 'fa fa-fw fa-pencil-square-o',
263 handler: function(button, ev) {
264 let panel = this.up('panel');
265 let fqdn = document.location.hostname;
266
267 panel.down('field[name=rp]').setValue(fqdn);
268
269 let idField = panel.down('field[name=id]');
270 let currentID = idField.getValue();
271 if (!currentID || currentID.length === 0) {
272 idField.setValue(fqdn);
273 }
274 },
275 },
276 ],
277 },
278 {
279 xtype: 'box',
280 height: 25,
281 html: `<span class='pmx-hint'>${gettext('Note:')}</span> `
282 + gettext('WebAuthn requires using a trusted certificate.'),
283 },
284 {
285 xtype: 'box',
286 id: 'idChangeWarning',
287 hidden: true,
288 padding: '5 0 0 0',
289 html: '<i class="fa fa-exclamation-triangle warning"></i> '
290 + gettext('Changing the ID breaks existing WebAuthn TFA entries.'),
291 }],
292 });
293 me.add_inputpanel_row('bwlimit', gettext('Bandwidth Limits'), {
294 renderer: me.render_bwlimits,
295 width: 450,
296 url: "/api2/extjs/cluster/options",
297 parseBeforeSet: true,
298 labelWidth: 120,
299 items: [{
300 xtype: 'pveBandwidthField',
301 name: 'default',
302 fieldLabel: gettext('Default'),
303 emptyText: gettext('none'),
304 backendUnit: "KiB",
305 },
306 {
307 xtype: 'pveBandwidthField',
308 name: 'restore',
309 fieldLabel: gettext('Backup Restore'),
310 emptyText: gettext('default'),
311 backendUnit: "KiB",
312 },
313 {
314 xtype: 'pveBandwidthField',
315 name: 'migration',
316 fieldLabel: gettext('Migration'),
317 emptyText: gettext('default'),
318 backendUnit: "KiB",
319 },
320 {
321 xtype: 'pveBandwidthField',
322 name: 'clone',
323 fieldLabel: gettext('Clone'),
324 emptyText: gettext('default'),
325 backendUnit: "KiB",
326 },
327 {
328 xtype: 'pveBandwidthField',
329 name: 'move',
330 fieldLabel: gettext('Disk Move'),
331 emptyText: gettext('default'),
332 backendUnit: "KiB",
333 }],
334 });
335 me.add_integer_row('max_workers', gettext('Maximal Workers/bulk-action'), {
336 deleteEmpty: true,
337 defaultValue: 4,
338 minValue: 1,
339 maxValue: 64, // arbitrary but generous limit as limits are good
340 });
341 me.add_inputpanel_row('next-id', gettext('Next Free VMID Range'), {
342 renderer: PVE.Utils.render_as_property_string,
343 url: "/api2/extjs/cluster/options",
344 items: [{
345 xtype: 'proxmoxintegerfield',
346 name: 'lower',
347 fieldLabel: gettext('Lower'),
348 emptyText: '100',
349 minValue: 100,
350 maxValue: 1000 * 1000 * 1000 - 1,
351 submitValue: true,
352 }, {
353 xtype: 'proxmoxintegerfield',
354 name: 'upper',
355 fieldLabel: gettext('Upper'),
356 emptyText: '1.000.000',
357 minValue: 100,
358 maxValue: 1000 * 1000 * 1000 - 1,
359 submitValue: true,
360 }],
361 });
362 me.rows['tag-style'] = {
363 required: true,
364 renderer: (value) => {
365 if (value === undefined) {
366 return gettext('No Overrides');
367 }
368 let colors = PVE.UIOptions.parseTagOverrides(value?.['color-map']);
369 let shape = value.shape;
370 let shapeText = PVE.UIOptions.tagTreeStyles[shape ?? '__default__'];
371 let txt = Ext.String.format(gettext("Tree Shape: {0}"), shapeText);
372 let orderText = PVE.UIOptions.tagOrderOptions[value.ordering ?? '__default__'];
373 txt += `, ${Ext.String.format(gettext("Ordering: {0}"), orderText)}`;
374 if (value['case-sensitive']) {
375 txt += `, ${gettext('Case-Sensitive')}`;
376 }
377 if (Object.keys(colors).length > 0) {
378 txt += `, ${gettext('Color Overrides')}: `;
379 for (const tag of Object.keys(colors)) {
380 txt += Proxmox.Utils.getTagElement(tag, colors);
381 }
382 }
383 return txt;
384 },
385 header: gettext('Tag Style Override'),
386 editor: {
387 xtype: 'proxmoxWindowEdit',
388 width: 800,
389 subject: gettext('Tag Color Override'),
390 onlineHelp: 'datacenter_configuration_file',
391 fieldDefaults: {
392 labelWidth: 100,
393 },
394 url: '/api2/extjs/cluster/options',
395 items: [
396 {
397 xtype: 'inputpanel',
398 setValues: function(values) {
399 if (values === undefined) {
400 return undefined;
401 }
402 values = values?.['tag-style'] ?? {};
403 values.shape = values.shape || '__default__';
404 values.colors = values['color-map'];
405 return Proxmox.panel.InputPanel.prototype.setValues.call(this, values);
406 },
407 onGetValues: function(values) {
408 let style = {};
409 if (values.colors) {
410 style['color-map'] = values.colors;
411 }
412 if (values.shape && values.shape !== '__default__') {
413 style.shape = values.shape;
414 }
415 if (values.ordering) {
416 style.ordering = values.ordering;
417 }
418 if (values['case-sensitive']) {
419 style['case-sensitive'] = 1;
420 }
421 let value = PVE.Parser.printPropertyString(style);
422 if (value === '') {
423 return {
424 'delete': 'tag-style',
425 };
426 }
427 return {
428 'tag-style': value,
429 };
430 },
431 items: [
432 {
433
434 name: 'shape',
435 xtype: 'proxmoxComboGrid',
436 fieldLabel: gettext('Tree Shape'),
437 valueField: 'value',
438 displayField: 'display',
439 allowBlank: false,
440 listConfig: {
441 columns: [
442 {
443 header: gettext('Option'),
444 dataIndex: 'display',
445 flex: 1,
446 },
447 {
448 header: gettext('Preview'),
449 dataIndex: 'value',
450 renderer: function(value) {
451 let cls = value ?? '__default__';
452 if (value === '__default__') {
453 cls = 'circle';
454 }
455 let tags = PVE.Utils.renderTags('preview');
456 return `<div class="proxmox-tags-${cls}">${tags}</div>`;
457 },
458 flex: 1,
459 },
460 ],
461 },
462 store: {
463 data: Object.entries(PVE.UIOptions.tagTreeStyles).map(v => ({
464 value: v[0],
465 display: v[1],
466 })),
467 },
468 deleteDefault: true,
469 defaultValue: '__default__',
470 deleteEmpty: true,
471 },
472 {
473 name: 'ordering',
474 xtype: 'proxmoxKVComboBox',
475 fieldLabel: gettext('Ordering'),
476 comboItems: Object.entries(PVE.UIOptions.tagOrderOptions),
477 defaultValue: '__default__',
478 value: '__default__',
479 deleteEmpty: true,
480 },
481 {
482 name: 'case-sensitive',
483 xtype: 'proxmoxcheckbox',
484 fieldLabel: gettext('Case-Sensitive'),
485 boxLabel: gettext('Applies to new edits'),
486 value: 0,
487 },
488 {
489 xtype: 'displayfield',
490 fieldLabel: gettext('Color Overrides'),
491 },
492 {
493 name: 'colors',
494 xtype: 'pveTagColorGrid',
495 deleteEmpty: true,
496 height: 300,
497 },
498 ],
499 },
500 ],
501 },
502 };
503
504 me.rows['user-tag-access'] = {
505 required: true,
506 renderer: (value) => {
507 if (value === undefined) {
508 return Ext.String.format(gettext('Mode: {0}'), 'free');
509 }
510 let mode = value?.['user-allow'] ?? 'free';
511 let list = value?.['user-allow-list']?.join(',') ?? '';
512 let modeTxt = Ext.String.format(gettext('Mode: {0}'), mode);
513 let overrides = PVE.UIOptions.tagOverrides;
514 let tags = PVE.Utils.renderTags(list, overrides);
515 let listTxt = tags !== '' ? `, ${gettext('Pre-defined:')} ${tags}` : '';
516 return `${modeTxt}${listTxt}`;
517 },
518 header: gettext('User Tag Access'),
519 editor: {
520 xtype: 'pveUserTagAccessEdit',
521 },
522 };
523
524 me.rows['registered-tags'] = {
525 required: true,
526 renderer: (value) => {
527 if (value === undefined) {
528 return gettext('No Registered Tags');
529 }
530 let overrides = PVE.UIOptions.tagOverrides;
531 return PVE.Utils.renderTags(value.join(','), overrides);
532 },
533 header: gettext('Registered Tags'),
534 editor: {
535 xtype: 'pveRegisteredTagEdit',
536 },
537 };
538
539 me.selModel = Ext.create('Ext.selection.RowModel', {});
540
541 Ext.apply(me, {
542 tbar: [{
543 text: gettext('Edit'),
544 xtype: 'proxmoxButton',
545 disabled: true,
546 handler: function() { me.run_editor(); },
547 selModel: me.selModel,
548 }],
549 url: "/api2/json/cluster/options",
550 editorConfig: {
551 url: "/api2/extjs/cluster/options",
552 },
553 interval: 5000,
554 cwidth1: 200,
555 listeners: {
556 itemdblclick: me.run_editor,
557 },
558 });
559
560 me.callParent();
561
562 // set the new value for the default console
563 me.mon(me.rstore, 'load', function(store, records, success) {
564 if (!success) {
565 return;
566 }
567
568 var rec = store.getById('console');
569 PVE.UIOptions.options.console = rec.data.value;
570 if (rec.data.value === '__default__') {
571 delete PVE.UIOptions.options.console;
572 }
573
574 PVE.UIOptions.options['tag-style'] = store.getById('tag-style')?.data?.value;
575 PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']);
576 PVE.UIOptions.fireUIConfigChanged();
577 });
578
579 me.on('activate', me.rstore.startUpdate);
580 me.on('destroy', me.rstore.stopUpdate);
581 me.on('deactivate', me.rstore.stopUpdate);
582 },
583 });