From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-manager v5 41/46] ui: sdn: add panel for managing prefix lists
Date: Tue, 5 May 2026 17:37:09 +0200 [thread overview]
Message-ID: <20260505153720.412180-42-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20260505153720.412180-1-s.hanreich@proxmox.com>
This panel allows users to perform CRUD operations for prefix lists
and their entries, as well as re-ordering prefix lists entries. It
allows editing each entry in a prefix list separately via an edit
window even though they are a single property in the section config.
This is implemented by sending the full entries property to the update
endpoint everytime a single entry has been edited.
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/dc/Config.js | 8 +
www/manager6/sdn/PrefixListPanel.js | 459 ++++++++++++++++++++++++++++
3 files changed, 468 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..8becf8343
--- /dev/null
+++ b/www/manager6/sdn/PrefixListPanel.js
@@ -0,0 +1,459 @@
+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 "<i class='pve-grid-fa fa fa-fw fa-reorder cursor-move'></i>";
+ },
+ },
+ {
+ 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 || !prefixList) {
+ 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
next prev parent reply other threads:[~2026-05-05 15:44 UTC|newest]
Thread overview: 47+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-05 15:36 [PATCH access-control/cluster/manager/network/proxmox{-ve-rs,-perl-rs} v5 00/46] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-cluster v5 01/46] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-cluster v5 02/46] cfs: add 'sdn/prefix-lists.cfg' " Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-access-control v5 03/46] permissions: add ACL path for prefix-lists and route-maps Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 04/46] frr: add constructor to prefix list name Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 05/46] sdn-types: add common route-map helper types Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 06/46] frr: change order type to u16 Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 07/46] frr: implement routemap match/set statements via adjacent tagging Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 08/46] frr: implement support for call and exit action Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 09/46] frr-templates: change route maps template to adapt to new frr types Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 10/46] ve-config: fabrics: adapt frr config generation Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 11/46] ve-config: add prefix list section config Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 12/46] ve-config: frr: implement frr config generation for prefix lists Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 13/46] ve-config: add route map section config Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 14/46] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 15/46] ve-config: add prefix lists integration tests Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 16/46] ve-config: add route maps " Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 17/46] fabrics: ospf: fix deserializing OspfDeletableProperties Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 18/46] fabrics: ospf: openfabric: allow user-defined route filter Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-ve-rs v5 19/46] frr: fabrics: apply route_filter setting Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-perl-rs v5 20/46] pve-rs: sdn: add route maps module Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-perl-rs v5 21/46] pve-rs: sdn: add prefix lists module Stefan Hanreich
2026-05-05 15:36 ` [PATCH proxmox-perl-rs v5 22/46] sdn: add prefix list / route maps to frr config generation helper Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-network v5 23/46] controller: bgp: evpn: adapt to new match / set frr config syntax Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-network v5 24/46] sdn: add prefix lists module Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-network v5 25/46] sdn: add route map module Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-network v5 26/46] api2: add prefix list module Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-network v5 27/46] api2: add route maps module Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-network v5 28/46] api2: add route map module Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-network v5 29/46] api2: add route map entry module Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-network v5 30/46] evpn controller: add route_map_{in,out} parameter Stefan Hanreich
2026-05-05 15:36 ` [PATCH pve-network v5 31/46] bgp controller: allow configuring custom route maps Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-network v5 32/46] sdn: commit route map / prefix list configuration on sdn apply Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-network v5 33/46] sdn: frr: consider route maps and prefix lists in dry-run Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-network v5 34/46] fabrics: ospf: openfabric: add route_filter property Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-network v5 35/46] tests: add simple route map test case Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-network v5 36/46] tests: add bgp evpn route map/prefix list testcase Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-network v5 37/46] tests: add route map with prefix " Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-network v5 38/46] tests: add exit node with custom route map testcase Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-manager v5 39/46] ui: sdn: add route map selector Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-manager v5 40/46] ui: sdn: add prefix list selector Stefan Hanreich
2026-05-05 15:37 ` Stefan Hanreich [this message]
2026-05-05 15:37 ` [PATCH pve-manager v5 42/46] ui: sdn: add panel for managing route map entries Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-manager v5 43/46] ui: sdn: bgp controller: allow configuring route maps Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-manager v5 44/46] ui: sdn: evpn " Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-manager v5 45/46] ui: sdn: openfabric: add route filter Stefan Hanreich
2026-05-05 15:37 ` [PATCH pve-manager v5 46/46] ui: sdn: ospf: add route filter setting Stefan Hanreich
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260505153720.412180-42-s.hanreich@proxmox.com \
--to=s.hanreich@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox