From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 11CC81FF136 for ; Mon, 04 May 2026 13:43:51 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 733931D657; Mon, 4 May 2026 13:41:06 +0200 (CEST) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Subject: [PATCH pve-manager v3 39/44] ui: sdn: add panel for managing prefix lists Date: Mon, 4 May 2026 13:39:36 +0200 Message-ID: <20260504113943.159905-40-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260504113943.159905-1-s.hanreich@proxmox.com> References: <20260504113943.159905-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1777894690569 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.680 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: EOECCL3NRPH72BUT2JFJKR6UUWOPVMWH X-Message-ID-Hash: EOECCL3NRPH72BUT2JFJKR6UUWOPVMWH X-MailFrom: s.hanreich@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This panel allows users to perform CRUD operations for prefix lists and their entries, as well as re-ordering prefix lists entries. Signed-off-by: Stefan Hanreich --- www/manager6/Makefile | 1 + www/manager6/dc/Config.js | 8 + www/manager6/sdn/PrefixListPanel.js | 458 ++++++++++++++++++++++++++++ 3 files changed, 467 insertions(+) create mode 100644 www/manager6/sdn/PrefixListPanel.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 615e68662..b123a331d 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -310,6 +310,7 @@ JSSRC= \ sdn/IpamEdit.js \ sdn/OptionsPanel.js \ sdn/RouteMapSelector.js \ + sdn/PrefixListPanel.js \ sdn/PrefixListSelector.js \ sdn/controllers/Base.js \ sdn/controllers/EvpnEdit.js \ diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js index b5e27a212..8784e357c 100644 --- a/www/manager6/dc/Config.js +++ b/www/manager6/dc/Config.js @@ -312,6 +312,14 @@ Ext.define('PVE.dc.Config', { iconCls: 'fa fa-road', itemId: 'sdnfabrics', }, + { + xtype: 'pveSDNPrefixLists', + groups: ['sdn'], + title: gettext('Prefix Lists'), + hidden: true, + iconCls: 'fa fa-list-ol', + itemId: 'sdnprefixlists', + }, ); } diff --git a/www/manager6/sdn/PrefixListPanel.js b/www/manager6/sdn/PrefixListPanel.js new file mode 100644 index 000000000..d9e1b7aeb --- /dev/null +++ b/www/manager6/sdn/PrefixListPanel.js @@ -0,0 +1,458 @@ +Ext.define('PVE.sdn.PrefixList', { + extend: 'Ext.data.Model', + fields: ['id', 'entries', 'pending'], + + getId: function() { + let me = this; + return me.data.pending?.[me.idProperty] ?? me.data[me.idProperty]; + }, +}); + +Ext.define('PVE.sdn.PrefixListEntry', { + extend: 'Ext.data.Model', + fields: ['id', 'action', 'prefix', 'le', 'ge', 'pending'], +}); + +Ext.define('PVE.sdn.EditPrefixListWindow', { + extend: 'Proxmox.window.Edit', + + url: '/cluster/sdn/prefix-lists', + + config: { + entry: null, + }, + + isCreate: false, + + items: [ + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Name'), + name: 'id', + }, + ], + + initComponent: function() { + let me = this; + me.method = (me.isCreate) ? "POST" : "PUT"; + me.callParent(); + + me.setValues(me.getEntry()); + }, +}); + +Ext.define('PVE.sdn.EditPrefixListEntryWindow', { + extend: 'Proxmox.window.Edit', + + url: '/cluster/sdn/prefix-lists', + + config: { + entry: null, + }, + + isCreate: false, + + items: [ + { + xtype: 'proxmoxKVComboBox', + fieldLabel: gettext('Action'), + name: 'action', + comboItems: [ + ['permit', gettext('Permit')], + ['deny', gettext('Deny')], + ], + allowBlank: false, + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Prefix'), + name: 'prefix', + vtype: 'IP64CIDRAddress', + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Prefix <='), + name: 'le', + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Prefix >='), + name: 'ge', + }, + ], + + initComponent: function() { + let me = this; + me.method = (me.isCreate) ? "POST" : "PUT"; + me.callParent(); + + me.setValues(me.getEntry()); + }, +}); + +Ext.define('PVE.sdn.PrefixListView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveSDNPrefixListView'], + + emptyText: gettext('No prefix list configured'), + + tbar: [ + { + text: gettext('Add'), + xtype: 'button', + handler: 'addPrefixList', + }, + { + text: gettext('Remove'), + xtype: 'button', + handler: 'removePrefixList', + bind: { + disabled: '{!prefixListGrid.selection}', + }, + }, + { + text: gettext('Reload'), + xtype: 'button', + handler: 'reload', + }, + ], + + columns: [ + { + text: gettext('Name'), + dataIndex: 'id', + flex: 1, + renderer: function (value, metaData, rec) { + return PVE.Utils.render_sdn_pending(rec, value, 'id', 1); + }, + }, + { + text: gettext('State'), + width: 100, + dataIndex: 'state', + renderer: function (value, metaData, rec) { + return PVE.Utils.render_sdn_pending_state(rec, value); + }, + }, + ], +}); + +Ext.define('PVE.sdn.PrefixListEntriesView', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.pveSDNPrefixListEntriesView'], + + emptyText: gettext('Prefix List has no entries configured.'), + + config: { + prefixList: null, + }, + + viewConfig: { + plugins: [ + { + ptype: 'gridviewdragdrop', + }, + ], + }, + + listeners: { + drop: "saveEntries", + itemdblclick: 'editPrefixListEntry', + }, + + columns: [ + { + width: 40, + resizable: false, + sortable: false, + hideable: false, + menuDisabled: true, + renderer: function (value, metaData, record, rowIdx, colIdx) { + metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special'; + return ""; + }, + }, + { + text: gettext('Action'), + dataIndex: 'action', + flex: 1, + }, + { + text: gettext('Prefix'), + dataIndex: 'prefix', + flex: 1, + }, + { + fieldLabel: gettext('Prefix <='), + dataIndex: 'le', + flex: 1, + }, + { + fieldLabel: gettext('Prefix >='), + dataIndex: 'ge', + flex: 1, + }, + ], + + tbar: [ + { + text: gettext('Add'), + xtype: 'button', + handler: 'addPrefixListEntry', + bind: { + disabled: '{!prefixListGrid.selection}', + }, + }, + { + text: gettext('Edit'), + xtype: 'button', + handler: 'editPrefixListEntry', + bind: { + disabled: '{!prefixListEntriesGrid.selection}', + }, + }, + { + text: gettext('Remove'), + xtype: 'button', + handler: 'removePrefixListEntry', + bind: { + disabled: '{!prefixListEntriesGrid.selection}', + }, + }, + ], + +}); + +Ext.define('PVE.sdn.PrefixListPanel', { + extend: 'Ext.panel.Panel', + alias: ['widget.pveSDNPrefixLists'], + + emptyText: gettext('No prefix list configured'), + + viewModel: { + stores: { + prefixLists: { + autoLoad: true, + model: 'PVE.sdn.PrefixList', + proxy: { + type: 'proxmox', + url: '/api2/json/cluster/sdn/prefix-lists?pending=1', + }, + }, + prefixListEntries: { + model: 'PVE.sdn.PrefixListEntry', + proxy: { + type: 'proxmox', + reader: { + transform: { + fn: function(response) { + let entries = response.data.entries ?? []; + return entries.map(PVE.Parser.parsePropertyString); + } + } + } + }, + }, + }, + formulas: { + entryGridEmptyText: function(get) { + let selection = get('prefixListGrid.selection'); + + return (selection) + ? gettext('Prefix List has no entries configured.') + : gettext('no Prefix List selected'); + } + } + }, + + controller: { + reload: function() { + let me = this; + let prefixList = me.getViewModel().get('prefixListGrid.selection'); + + me.getViewModel().getStore('prefixLists').load((records, _operation, success) => { + if (!success) { + return; + } + + let newPrefixList = records.find((record) => { + return record.getId() === prefixList.getId(); + }); + + me.lookupReference('prefixListGrid').setSelection(newPrefixList); + }); + }, + saveEntries: function() { + let me = this; + + let prefixList = me.getViewModel().get('prefixListGrid.selection'); + + let entries = me + .getViewModel() + .getStore('prefixListEntries') + .getData() + .items + .map((item) => { + let data = item.data; + delete data.id; + + return PVE.Parser.printPropertyString(data); + }); + + let params = {}; + + if (entries.length > 0) { + params.entries = entries; + } else { + params = { delete: ["entries"] }; + } + + Proxmox.Async.api2({ + url: `/api2/extjs/cluster/sdn/prefix-lists/${prefixList.getId()}`, + params, + method: 'PUT', + }) + .catch(Proxmox.Utils.alertResponseFailure) + .finally(() => { + me.reload(prefixList); + }); + }, + selectPrefixList: function(gridPanel, record, index, options) { + let me = this; + + let url = `/api2/extjs/cluster/sdn/prefix-lists/${record.getId()}`; + let entryStore = me.getViewModel().getStore('prefixListEntries'); + + entryStore.getProxy().setUrl(url); + entryStore.load(); + }, + addPrefixList: function() { + let me = this; + + Ext.create('PVE.sdn.EditPrefixListWindow', { + autoShow: true, + isCreate: true, + listeners: { + close: () => me.reload(), + } + }); + }, + removePrefixList: function() { + let me = this; + let prefixList = me.getViewModel().get('prefixListGrid.selection'); + + Ext.Msg.show({ + title: gettext('Confirm'), + icon: Ext.Msg.WARNING, + message: `Remove prefix list "${prefixList.getId()}"?`, + buttons: Ext.Msg.YESNO, + defaultFocus: 'no', + callback: function (btn) { + if (btn !== 'yes') { + return; + } + + Proxmox.Async.api2({ + url: `/api2/extjs/cluster/sdn/prefix-lists/${prefixList.getId()}`, + method: 'DELETE', + }) + .catch(Proxmox.Utils.alertResponseFailure) + .finally(() => { + me.reload(prefixList); + }); + }, + }); + }, + addPrefixListEntry: function() { + let panel = this; + + Ext.create('PVE.sdn.EditPrefixListEntryWindow', { + autoShow: true, + isCreate: true, + submit: function() { + let me = this; + + panel.getViewModel().getStore('prefixListEntries').add(me.getValues()); + panel.saveEntries(); + + me.close(); + }, + }); + }, + editPrefixListEntry: function() { + let panel = this; + + let entry = panel.getViewModel().get('prefixListEntriesGrid.selection'); + + if (!entry) { + console.warn('no prefix list entry selected!'); + return; + } + + Ext.create('PVE.sdn.EditPrefixListEntryWindow', { + autoShow: true, + isCreate: false, + entry: entry.data, + submit: function() { + let me = this; + entry.set(me.getValues()); + + panel.saveEntries(); + + me.close(); + }, + }); + }, + removePrefixListEntry: function() { + let me = this; + + let entry = me.getViewModel().get('prefixListEntriesGrid.selection'); + + Ext.Msg.show({ + title: gettext('Confirm'), + icon: Ext.Msg.WARNING, + message: `Remove prefix list entry?`, + buttons: Ext.Msg.YESNO, + defaultFocus: 'no', + callback: function (btn) { + if (btn !== 'yes') { + return; + } + + me.getViewModel().getStore('prefixListEntries').remove(entry); + me.saveEntries(); + }, + }); + }, + }, + + layout: 'border', + + items: [ + { + xtype: 'pveSDNPrefixListView', + region: 'west', + width: '50%', + border: false, + split: true, + reference: 'prefixListGrid', + bind: { + store: '{prefixLists}', + }, + listeners: { + select: 'selectPrefixList', + }, + }, + { + xtype: 'pveSDNPrefixListEntriesView', + region: 'center', + border: false, + bind: { + prefixList: '{prefixListGrid.selection}', + store: '{prefixListEntries}', + emptyText: '{entryGridEmptyText}', + }, + reference: 'prefixListEntriesGrid', + }, + ] +}); -- 2.47.3