-Ext.define('PVE.dc.HttpProxyEdit', {
- extend: 'PVE.window.Edit',
-
- initComponent : function() {
- var me = this;
-
- Ext.applyIf(me, {
- subject: gettext('HTTP proxy'),
- items: {
- xtype: 'proxmoxtextfield',
- name: 'http_proxy',
- vtype: 'HttpProxy',
- emptyText: Proxmox.Utils.noneText,
- deleteEmpty: true,
- value: '',
- fieldLabel: gettext('HTTP proxy')
- }
- });
-
- me.callParent();
+Ext.define('PVE.dc.OptionView', {
+ extend: 'Proxmox.grid.ObjectGrid',
+ alias: ['widget.pveDcOptionView'],
- me.load();
- }
-});
+ onlineHelp: 'datacenter_configuration_file',
-Ext.define('PVE.dc.KeyboardEdit', {
- extend: 'PVE.window.Edit',
+ monStoreErrors: true,
+ userCls: 'proxmox-tags-full',
- initComponent : function() {
+ add_inputpanel_row: function(name, text, opts) {
var me = this;
- Ext.applyIf(me, {
- subject: gettext('Keyboard Layout'),
- items: {
- xtype: 'VNCKeyboardSelector',
- name: 'keyboard',
- value: '__default__',
- fieldLabel: gettext('Keyboard Layout')
- }
- });
+ 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);
+ }
- me.callParent();
+ 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,
+ };
+ },
- me.load();
- }
-});
+ render_bwlimits: function(value) {
+ if (!value) {
+ return gettext("None");
+ }
-Ext.define('PVE.dc.ConsoleViewerEdit', {
- extend: 'PVE.window.Edit',
+ let parsed = PVE.Parser.parsePropertyString(value);
+ return Object.entries(parsed)
+ .map(([k, v]) => k + ": " + Proxmox.Utils.format_size(v * 1024) + "/s")
+ .join(',');
+ },
- initComponent : function() {
+ initComponent: function() {
var me = this;
- var data = [];
-
- Ext.Array.each(['__default__','vv', 'html5'], function(value) {
- data.push([value, PVE.Utils.render_console_viewer(value)]);
+ 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,
});
-
- Ext.applyIf(me, {
- subject: gettext('Console Viewer'),
- items: {
+ 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: 'console',
+ name: 'shutdown_policy',
+ fieldLabel: gettext('Shutdown Policy'),
+ deleteEmpty: false,
value: '__default__',
- fieldLabel: gettext('Console Viewer'),
- comboItems: data
- }
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + ' (conditional)'],
+ ['freeze', 'freeze'],
+ ['failover', 'failover'],
+ ['migrate', 'migrate'],
+ ['conditional', 'conditional'],
+ ],
+ defaultValue: '__default__',
+ }],
});
-
- me.callParent();
-
- me.load();
- }
-});
-
-Ext.define('PVE.dc.EmailFromEdit', {
- extend: 'PVE.window.Edit',
-
- initComponent : function() {
- var me = this;
-
- Ext.applyIf(me, {
- subject: gettext('Email from address'),
- items: {
- xtype: 'proxmoxtextfield',
- name: 'email_from',
- vtype: 'pveMail',
- emptyText: 'root@$hostname',
- deleteEmpty: true,
- value: '',
- fieldLabel: gettext('Email from address')
- }
+ 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.callParent();
-
- me.load();
- }
-});
-
-Ext.define('PVE.dc.MacPrefixEdit', {
- extend: 'PVE.window.Edit',
-
- initComponent : function() {
- var me = this;
-
- Ext.applyIf(me, {
- subject: gettext('MAC address prefix'),
- items: {
- xtype: 'proxmoxtextfield',
- name: 'mac_prefix',
- regex: /^[a-f0-9]{2}(?::[a-f0-9]{2}){0,2}:?$/i,
- regexText: gettext('Example') + ': 02:8f',
- emptyText: Proxmox.Utils.noneText,
+ 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: '',
- fieldLabel: gettext('MAC address prefix')
- }
+ deleteEmpty: true,
+ skipEmptyText: true,
+ submitEmptyText: false,
+ },
+ {
+ xtype: 'box',
+ height: 25,
+ html: `<span class='pmx-hint'>${gettext('Note:')}</span> `
+ + 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.callParent();
-
- me.load();
- }
-});
-
-Ext.define('PVE.dc.OptionView', {
- extend: 'Proxmox.grid.ObjectGrid',
- alias: ['widget.pveDcOptionView'],
-
- onlineHelp: 'datacenter_configuration_file',
-
- initComponent : function() {
- var me = this;
-
- var reload = function() {
- me.rstore.load();
- };
-
- var rows = {
- keyboard: {
- header: gettext('Keyboard Layout'),
- editor: 'PVE.dc.KeyboardEdit',
- renderer: PVE.Utils.render_kvm_language,
- required: true
- },
- http_proxy: {
- header: gettext('HTTP proxy'),
- editor: 'PVE.dc.HttpProxyEdit',
- required: true,
- renderer: function(value) {
- if (!value) {
- return Proxmox.Utils.noneText;
- }
- return value;
- }
+ 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,
},
- console: {
- header: gettext('Console Viewer'),
- editor: 'PVE.dc.ConsoleViewerEdit',
- required: true,
- renderer: PVE.Utils.render_console_viewer
- },
- email_from: {
- header: gettext('Email from address'),
- editor: 'PVE.dc.EmailFromEdit',
- required: true,
- renderer: function(value) {
- if (!value) {
- return 'root@$hostname';
- }
- return value;
- }
+ {
+ 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: `<span class='pmx-hint'>${gettext('Note:')}</span> `
+ + gettext('WebAuthn requires using a trusted certificate.'),
+ },
+ {
+ xtype: 'box',
+ id: 'idChangeWarning',
+ hidden: true,
+ padding: '5 0 0 0',
+ html: '<i class="fa fa-exclamation-triangle warning"></i> '
+ + 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",
},
- mac_prefix: {
- header: gettext('MAC address prefix'),
- editor: 'PVE.dc.MacPrefixEdit',
- required: true,
- renderer: function(value) {
- if (!value) {
- return Proxmox.Utils.noneText;
+ {
+ 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 value;
}
- }
+ 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 `<div class="proxmox-tags-${cls}">${tags}</div>`;
+ },
+ 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,
+ },
+ ],
+ },
+ ],
+ },
};
- var sm = Ext.create('Ext.selection.RowModel', {});
-
- var run_editor = function() {
- var rec = sm.getSelection()[0];
- if (!rec) {
- return;
- }
+ 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',
+ },
+ };
- var rowdef = rows[rec.data.key];
- if (!rowdef.editor) {
- return;
- }
-
- var win = Ext.create(rowdef.editor, {
- url: "/api2/extjs/cluster/options",
- confid: rec.data.key
- });
- win.show();
- win.on('destroy', reload);
+ 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',
+ },
};
- var edit_btn = new Proxmox.button.Button({
- text: gettext('Edit'),
- disabled: true,
- selModel: sm,
- handler: run_editor
- });
+ 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",
- interval: 1000,
- selModel: sm,
- tbar: [ edit_btn ],
- rows: rows,
+ editorConfig: {
+ url: "/api2/extjs/cluster/options",
+ },
+ interval: 5000,
+ cwidth1: 200,
listeners: {
- itemdblclick: run_editor,
- activate: reload
- }
+ 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);
+ },
});