Ext.define('PVE.dc.OptionView', { extend: 'Proxmox.grid.ObjectGrid', alias: ['widget.pveDcOptionView'], onlineHelp: 'datacenter_configuration_file', monStoreErrors: true, userCls: 'proxmox-tags-full', add_inputpanel_row: function(name, text, opts) { var me = this; opts = opts || {}; me.rows = me.rows || {}; let canEdit = !Object.prototype.hasOwnProperty.call(opts, 'caps') || opts.caps; me.rows[name] = { required: true, defaultValue: opts.defaultValue, header: text, renderer: opts.renderer, editor: canEdit ? { xtype: 'proxmoxWindowEdit', width: opts.width || 350, subject: text, onlineHelp: opts.onlineHelp, fieldDefaults: { labelWidth: opts.labelWidth || 100, }, setValues: function(values) { var edit_value = values[name]; if (opts.parseBeforeSet) { edit_value = PVE.Parser.parsePropertyString(edit_value); } Ext.Array.each(this.query('inputpanel'), function(panel) { panel.setValues(edit_value); }); }, url: opts.url, items: [{ xtype: 'inputpanel', onGetValues: function(values) { if (values === undefined || Object.keys(values).length === 0) { return { 'delete': name }; } var ret_val = {}; ret_val[name] = PVE.Parser.printPropertyString(values); return ret_val; }, items: opts.items, }], } : undefined, }; }, render_bwlimits: function(value) { if (!value) { return gettext("None"); } let parsed = PVE.Parser.parsePropertyString(value); return Object.entries(parsed) .map(([k, v]) => k + ": " + Proxmox.Utils.format_size(v * 1024) + "/s") .join(','); }, initComponent: function() { var me = this; me.add_combobox_row('keyboard', gettext('Keyboard Layout'), { renderer: PVE.Utils.render_kvm_language, comboItems: Object.entries(PVE.Utils.kvm_keymaps), defaultValue: '__default__', deleteEmpty: true, }); me.add_text_row('http_proxy', gettext('HTTP proxy'), { defaultValue: Proxmox.Utils.noneText, vtype: 'HttpProxy', deleteEmpty: true, }); me.add_combobox_row('console', gettext('Console Viewer'), { renderer: PVE.Utils.render_console_viewer, comboItems: Object.entries(PVE.Utils.console_map), defaultValue: '__default__', deleteEmpty: true, }); me.add_text_row('email_from', gettext('Email from address'), { deleteEmpty: true, vtype: 'proxmoxMail', defaultValue: 'root@$hostname', }); me.add_text_row('mac_prefix', gettext('MAC address prefix'), { deleteEmpty: true, vtype: 'MacPrefix', defaultValue: 'BC:24:11', }); me.add_inputpanel_row('migration', gettext('Migration Settings'), { renderer: PVE.Utils.render_as_property_string, labelWidth: 120, url: "/api2/extjs/cluster/options", defaultKey: 'type', items: [{ xtype: 'displayfield', name: 'type', fieldLabel: gettext('Type'), value: 'secure', submitValue: true, }, { xtype: 'proxmoxNetworkSelector', name: 'network', fieldLabel: gettext('Network'), value: null, emptyText: Proxmox.Utils.defaultText, autoSelect: false, skipEmptyText: true, }], }); me.add_inputpanel_row('ha', gettext('HA Settings'), { renderer: PVE.Utils.render_dc_ha_opts, labelWidth: 120, url: "/api2/extjs/cluster/options", onlineHelp: 'ha_manager_shutdown_policy', items: [{ xtype: 'proxmoxKVComboBox', name: 'shutdown_policy', fieldLabel: gettext('Shutdown Policy'), deleteEmpty: false, value: '__default__', comboItems: [ ['__default__', Proxmox.Utils.defaultText + ' (conditional)'], ['freeze', 'freeze'], ['failover', 'failover'], ['migrate', 'migrate'], ['conditional', 'conditional'], ], defaultValue: '__default__', }], }); me.add_inputpanel_row('crs', gettext('Cluster Resource Scheduling'), { renderer: PVE.Utils.render_as_property_string, width: 450, labelWidth: 120, url: "/api2/extjs/cluster/options", onlineHelp: 'ha_manager_crs', items: [{ xtype: 'proxmoxKVComboBox', name: 'ha', fieldLabel: gettext('HA Scheduling'), deleteEmpty: false, value: '__default__', comboItems: [ ['__default__', Proxmox.Utils.defaultText + ' (basic)'], ['basic', 'Basic (Resource Count)'], ['static', 'Static Load'], ], defaultValue: '__default__', }, { xtype: 'proxmoxcheckbox', name: 'ha-rebalance-on-start', fieldLabel: gettext('Rebalance on Start'), boxLabel: gettext('Use CRS to select the least loaded node when starting an HA service'), value: 0, }], }); me.add_inputpanel_row('u2f', gettext('U2F Settings'), { renderer: v => !v ? Proxmox.Utils.NoneText : PVE.Parser.printPropertyString(v), width: 450, url: "/api2/extjs/cluster/options", onlineHelp: 'pveum_configure_u2f', items: [{ xtype: 'textfield', name: 'appid', fieldLabel: gettext('U2F AppID URL'), emptyText: gettext('Defaults to origin'), value: '', deleteEmpty: true, skipEmptyText: true, submitEmptyText: false, }, { xtype: 'textfield', name: 'origin', fieldLabel: gettext('U2F Origin'), emptyText: gettext('Defaults to requesting host URI'), value: '', deleteEmpty: true, skipEmptyText: true, submitEmptyText: false, }, { xtype: 'box', height: 25, html: `${gettext('Note:')} ` + Ext.String.format(gettext('{0} is deprecated, use {1}'), 'U2F', 'WebAuthn'), }, { xtype: 'displayfield', userCls: 'pmx-hint', value: gettext('NOTE: Changing an AppID breaks existing U2F registrations!'), }], }); me.add_inputpanel_row('webauthn', gettext('WebAuthn Settings'), { renderer: v => !v ? Proxmox.Utils.NoneText : PVE.Parser.printPropertyString(v), width: 450, url: "/api2/extjs/cluster/options", onlineHelp: 'pveum_configure_webauthn', items: [{ xtype: 'textfield', fieldLabel: gettext('Name'), name: 'rp', // NOTE: relying party consists of name and id, this is the name allowBlank: false, }, { xtype: 'textfield', fieldLabel: gettext('Origin'), emptyText: Ext.String.format(gettext("Domain Lockdown (e.g., {0})"), document.location.origin), name: 'origin', allowBlank: true, }, { xtype: 'textfield', fieldLabel: 'ID', name: 'id', allowBlank: false, listeners: { dirtychange: (f, isDirty) => f.up('panel').down('box[id=idChangeWarning]').setHidden(!f.originalValue || !isDirty), }, }, { xtype: 'container', layout: 'hbox', items: [ { xtype: 'box', flex: 1, }, { xtype: 'button', text: gettext('Auto-fill'), iconCls: 'fa fa-fw fa-pencil-square-o', handler: function(button, ev) { let panel = this.up('panel'); let fqdn = document.location.hostname; panel.down('field[name=rp]').setValue(fqdn); let idField = panel.down('field[name=id]'); let currentID = idField.getValue(); if (!currentID || currentID.length === 0) { idField.setValue(fqdn); } }, }, ], }, { xtype: 'box', height: 25, html: `${gettext('Note:')} ` + gettext('WebAuthn requires using a trusted certificate.'), }, { xtype: 'box', id: 'idChangeWarning', hidden: true, padding: '5 0 0 0', html: ' ' + gettext('Changing the ID breaks existing WebAuthn TFA entries.'), }], }); me.add_inputpanel_row('bwlimit', gettext('Bandwidth Limits'), { renderer: me.render_bwlimits, width: 450, url: "/api2/extjs/cluster/options", parseBeforeSet: true, labelWidth: 120, items: [{ xtype: 'pveBandwidthField', name: 'default', fieldLabel: gettext('Default'), emptyText: gettext('none'), backendUnit: "KiB", }, { xtype: 'pveBandwidthField', name: 'restore', fieldLabel: gettext('Backup Restore'), emptyText: gettext('default'), backendUnit: "KiB", }, { xtype: 'pveBandwidthField', name: 'migration', fieldLabel: gettext('Migration'), emptyText: gettext('default'), backendUnit: "KiB", }, { xtype: 'pveBandwidthField', name: 'clone', fieldLabel: gettext('Clone'), emptyText: gettext('default'), backendUnit: "KiB", }, { xtype: 'pveBandwidthField', name: 'move', fieldLabel: gettext('Disk Move'), emptyText: gettext('default'), backendUnit: "KiB", }], }); me.add_integer_row('max_workers', gettext('Maximal Workers/bulk-action'), { deleteEmpty: true, defaultValue: 4, minValue: 1, maxValue: 64, // arbitrary but generous limit as limits are good }); me.add_inputpanel_row('next-id', gettext('Next Free VMID Range'), { renderer: PVE.Utils.render_as_property_string, url: "/api2/extjs/cluster/options", items: [{ xtype: 'proxmoxintegerfield', name: 'lower', fieldLabel: gettext('Lower'), emptyText: '100', minValue: 100, maxValue: 1000 * 1000 * 1000 - 1, submitValue: true, }, { xtype: 'proxmoxintegerfield', name: 'upper', fieldLabel: gettext('Upper'), emptyText: '1.000.000', minValue: 100, maxValue: 1000 * 1000 * 1000 - 1, submitValue: true, }], }); me.rows['tag-style'] = { required: true, renderer: (value) => { if (value === undefined) { return gettext('No Overrides'); } let colors = PVE.UIOptions.parseTagOverrides(value?.['color-map']); let shape = value.shape; let shapeText = PVE.UIOptions.tagTreeStyles[shape ?? '__default__']; let txt = Ext.String.format(gettext("Tree Shape: {0}"), shapeText); let orderText = PVE.UIOptions.tagOrderOptions[value.ordering ?? '__default__']; txt += `, ${Ext.String.format(gettext("Ordering: {0}"), orderText)}`; if (value['case-sensitive']) { txt += `, ${gettext('Case-Sensitive')}`; } if (Object.keys(colors).length > 0) { txt += `, ${gettext('Color Overrides')}: `; for (const tag of Object.keys(colors)) { txt += Proxmox.Utils.getTagElement(tag, colors); } } return txt; }, header: gettext('Tag Style Override'), editor: { xtype: 'proxmoxWindowEdit', width: 800, subject: gettext('Tag Color Override'), onlineHelp: 'datacenter_configuration_file', fieldDefaults: { labelWidth: 100, }, url: '/api2/extjs/cluster/options', items: [ { xtype: 'inputpanel', setValues: function(values) { if (values === undefined) { return undefined; } values = values?.['tag-style'] ?? {}; values.shape = values.shape || '__default__'; values.colors = values['color-map']; return Proxmox.panel.InputPanel.prototype.setValues.call(this, values); }, onGetValues: function(values) { let style = {}; if (values.colors) { style['color-map'] = values.colors; } if (values.shape && values.shape !== '__default__') { style.shape = values.shape; } if (values.ordering) { style.ordering = values.ordering; } if (values['case-sensitive']) { style['case-sensitive'] = 1; } let value = PVE.Parser.printPropertyString(style); if (value === '') { return { 'delete': 'tag-style', }; } return { 'tag-style': value, }; }, items: [ { name: 'shape', xtype: 'proxmoxComboGrid', fieldLabel: gettext('Tree Shape'), valueField: 'value', displayField: 'display', allowBlank: false, listConfig: { columns: [ { header: gettext('Option'), dataIndex: 'display', flex: 1, }, { header: gettext('Preview'), dataIndex: 'value', renderer: function(value) { let cls = value ?? '__default__'; if (value === '__default__') { cls = 'circle'; } let tags = PVE.Utils.renderTags('preview'); return `
`; }, flex: 1, }, ], }, store: { data: Object.entries(PVE.UIOptions.tagTreeStyles).map(v => ({ value: v[0], display: v[1], })), }, deleteDefault: true, defaultValue: '__default__', deleteEmpty: true, }, { name: 'ordering', xtype: 'proxmoxKVComboBox', fieldLabel: gettext('Ordering'), comboItems: Object.entries(PVE.UIOptions.tagOrderOptions), defaultValue: '__default__', value: '__default__', deleteEmpty: true, }, { name: 'case-sensitive', xtype: 'proxmoxcheckbox', fieldLabel: gettext('Case-Sensitive'), boxLabel: gettext('Applies to new edits'), value: 0, }, { xtype: 'displayfield', fieldLabel: gettext('Color Overrides'), }, { name: 'colors', xtype: 'pveTagColorGrid', deleteEmpty: true, height: 300, }, ], }, ], }, }; me.rows['user-tag-access'] = { required: true, renderer: (value) => { if (value === undefined) { return Ext.String.format(gettext('Mode: {0}'), 'free'); } let mode = value?.['user-allow'] ?? 'free'; let list = value?.['user-allow-list']?.join(',') ?? ''; let modeTxt = Ext.String.format(gettext('Mode: {0}'), mode); let overrides = PVE.UIOptions.tagOverrides; let tags = PVE.Utils.renderTags(list, overrides); let listTxt = tags !== '' ? `, ${gettext('Pre-defined:')} ${tags}` : ''; return `${modeTxt}${listTxt}`; }, header: gettext('User Tag Access'), editor: { xtype: 'pveUserTagAccessEdit', }, }; me.rows['registered-tags'] = { required: true, renderer: (value) => { if (value === undefined) { return gettext('No Registered Tags'); } let overrides = PVE.UIOptions.tagOverrides; return PVE.Utils.renderTags(value.join(','), overrides); }, header: gettext('Registered Tags'), editor: { xtype: 'pveRegisteredTagEdit', }, }; me.selModel = Ext.create('Ext.selection.RowModel', {}); Ext.apply(me, { tbar: [{ text: gettext('Edit'), xtype: 'proxmoxButton', disabled: true, handler: function() { me.run_editor(); }, selModel: me.selModel, }], url: "/api2/json/cluster/options", editorConfig: { url: "/api2/extjs/cluster/options", }, interval: 5000, cwidth1: 200, listeners: { itemdblclick: me.run_editor, }, }); me.callParent(); // set the new value for the default console me.mon(me.rstore, 'load', function(store, records, success) { if (!success) { return; } var rec = store.getById('console'); PVE.UIOptions.options.console = rec.data.value; if (rec.data.value === '__default__') { delete PVE.UIOptions.options.console; } PVE.UIOptions.options['tag-style'] = store.getById('tag-style')?.data?.value; PVE.UIOptions.updateTagSettings(PVE.UIOptions.options['tag-style']); PVE.UIOptions.fireUIConfigChanged(); }); me.on('activate', me.rstore.startUpdate); me.on('destroy', me.rstore.stopUpdate); me.on('deactivate', me.rstore.stopUpdate); }, });