Files
dualfroz 91a785703f fix #5608: ui: resource mappings: validate the name like the API does
The API only accepts resource mapping IDs in the 'pve-configid' format,
but the name field of the PCI, USB and directory mapping windows takes
any value. A name like 'gpu@node1' is thus only rejected after
submitting, with a generic "invalid configuration ID" error that does
not tell which characters are allowed.

Use the existing 'ConfigId' vtype, so the field is marked invalid right
away and its tooltip lists the allowed characters.

Signed-off-by: Michal Fox <me@dualfroz.com>
Link: https://lore.proxmox.com/20260929141313.2873638-1-me@dualfroz.com
2026-09-30 11:44:39 +02:00

210 lines
6.5 KiB
JavaScript

Ext.define('PVE.window.DirMapEditWindow', {
extend: 'Proxmox.window.Edit',
mixins: ['Proxmox.Mixin.CBind'],
cbindData: function (initialConfig) {
let me = this;
me.isCreate = !me.name;
me.method = me.isCreate ? 'POST' : 'PUT';
me.hideMapping = !!me.entryOnly;
me.hideComment = me.name && !me.entryOnly;
me.hideNodeSelector = me.nodename || me.entryOnly;
me.hideNode = !me.nodename || !me.hideNodeSelector;
return {
name: me.name,
nodename: me.nodename,
};
},
submitUrl: function (_url, data) {
let me = this;
let name = me.isCreate ? '' : me.name;
return `/cluster/mapping/dir/${name}`;
},
title: gettext('Add Directory Mapping'),
onlineHelp: 'resource_mapping',
method: 'POST',
controller: {
xclass: 'Ext.app.ViewController',
onGetValues: function (values) {
let me = this;
let view = me.getView();
values.node ??= view.nodename;
let name = values.name;
let description = values.description;
let deletes = values.delete;
delete values.description;
delete values.name;
delete values.delete;
let map = [];
if (me.originalMap) {
map = PVE.Parser.filterPropertyStringList(
me.originalMap,
(e) => e.node !== values.node,
);
}
if (values.path) {
// TODO: Remove this when property string supports quotation of properties
if (!/^\/[^;,=()]+/.test(values.path)) {
let errMsg =
'Value does not look like a valid absolute path.' +
' These symbols are currently not allowed in path: ;,=()\n';
Ext.Msg.alert(gettext('Error'), errMsg);
// prevent sending a broken property string to the API
throw errMsg;
}
map.push(PVE.Parser.printPropertyString(values));
}
values = { map };
if (description) {
values.description = description;
}
if (deletes && !view.isCreate) {
values.delete = deletes;
}
if (view.isCreate) {
values.id = name;
}
return values;
},
onSetValues: function (values) {
let me = this;
let view = me.getView();
me.originalMap = [...values.map];
let configuredNodes = [];
PVE.Parser.filterPropertyStringList(values.map, (e) => {
configuredNodes.push(e.node);
if (e.node === view.nodename) {
values = e;
}
return false;
});
me.lookup('nodeselector').disallowedNodes = configuredNodes;
return values;
},
init: function (view) {
let _me = this;
if (!view.nodename) {
//throw "no nodename given";
}
},
},
items: [
{
xtype: 'inputpanel',
onGetValues: function (values) {
return this.up('window').getController().onGetValues(values);
},
onSetValues: function (values) {
return this.up('window').getController().onSetValues(values);
},
columnT: [
{
xtype: 'displayfield',
reference: 'directory-hint',
columnWidth: 1,
value: 'Make sure the directory exists.',
cbind: {
disabled: '{hideMapping}',
hidden: '{hideMapping}',
},
userCls: 'pmx-hint',
},
],
column1: [
{
xtype: 'pmxDisplayEditField',
fieldLabel: gettext('Name'),
cbind: {
editable: '{!name}',
value: '{name}',
submitValue: '{isCreate}',
},
name: 'name',
allowBlank: false,
vtype: 'ConfigId',
},
{
xtype: 'pveNodeSelector',
reference: 'nodeselector',
fieldLabel: gettext('Node'),
name: 'node',
cbind: {
disabled: '{hideNodeSelector}',
hidden: '{hideNodeSelector}',
},
allowBlank: false,
},
],
column2: [
{
xtype: 'fieldcontainer',
defaultType: 'radiofield',
layout: 'fit',
cbind: {
disabled: '{hideMapping}',
hidden: '{hideMapping}',
},
items: [
{
xtype: 'textfield',
name: 'path',
reference: 'path',
value: '',
emptyText: gettext('/some/path'),
cbind: {
nodename: '{nodename}',
disabled: '{hideMapping}',
},
allowBlank: false,
fieldLabel: gettext('Path'),
},
],
},
],
columnB: [
{
xtype: 'fieldcontainer',
defaultType: 'radiofield',
layout: 'fit',
cbind: {
disabled: '{hideComment}',
hidden: '{hideComment}',
},
items: [
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('Comment'),
submitValue: true,
name: 'description',
deleteEmpty: true,
},
],
},
],
},
],
});