public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Stefan Hanreich <s.hanreich@proxmox.com>
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	[thread overview]
Message-ID: <20260504113943.159905-40-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20260504113943.159905-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.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 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 "<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) {
+                    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





  parent reply	other threads:[~2026-05-04 11:43 UTC|newest]

Thread overview: 46+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-05-04 11:38 [PATCH access-control/cluster/manager/network/proxmox{-ve-rs,-perl-rs} v3 00/44] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-05-04 11:38 ` [PATCH pve-cluster v3 01/44] cfs: add 'sdn/route-maps.cfg' to observed files Stefan Hanreich
2026-05-04 11:38 ` [PATCH pve-cluster v3 02/44] cfs: add 'sdn/prefix-lists.cfg' " Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-access-control v3 03/44] permissions: add ACL path for prefix-lists and route-maps Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 04/44] frr: add constructor to prefix list name Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 05/44] sdn-types: add common route-map helper types Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 06/44] frr: change order type to u16 Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 07/44] frr: implement routemap match/set statements via adjacent tagging Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 08/44] frr: implement support for call and exit action Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 09/44] frr-templates: change route maps template to adapt to new frr types Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 10/44] ve-config: fabrics: adapt frr config generation Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 11/44] ve-config: add prefix list section config Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 12/44] ve-config: frr: implement frr config generation for prefix lists Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 13/44] ve-config: add route map section config Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 14/44] ve-config: frr: implement frr config generation for route maps Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 15/44] ve-config: add prefix lists integration tests Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-ve-rs v3 16/44] ve-config: add route maps " Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-perl-rs v3 17/44] pve-rs: sdn: add route maps module Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-perl-rs v3 18/44] pve-rs: sdn: add prefix lists module Stefan Hanreich
2026-05-04 11:39 ` [PATCH proxmox-perl-rs v3 19/44] sdn: add prefix list / route maps to frr config generation helper Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 20/44] controller: bgp: evpn: adapt to new match / set frr config syntax Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 21/44] sdn: add prefix lists module Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 22/44] sdn: add route map module Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 23/44] api2: add prefix list module Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 24/44] api2: add route maps module Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 25/44] api2: add route map module Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 26/44] api2: add route map entry module Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 27/44] evpn controller: add route_map_{in,out} parameter Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 28/44] bgp controller: allow configuring custom route maps Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 29/44] sdn: change detection for route maps / prefix lists Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 30/44] sdn: generate route map / prefix list configuration on sdn apply Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 31/44] sdn: frr: consider route maps and prefix lists in dry-run Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 32/44] fabrics: ospf: openfabric: add route_filter property Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 33/44] tests: add simple route map test case Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 34/44] tests: add bgp evpn route map/prefix list testcase Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 35/44] tests: add route map with prefix " Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-network v3 36/44] tests: add exit node with custom route map testcase Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-manager v3 37/44] ui: sdn: add route map selector Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-manager v3 38/44] ui: sdn: add prefix list selector Stefan Hanreich
2026-05-04 11:39 ` Stefan Hanreich [this message]
2026-05-04 11:39 ` [PATCH pve-manager v3 40/44] ui: sdn: add panel for managing route map entries Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-manager v3 41/44] ui: sdn: bgp controller: allow configuring route maps Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-manager v3 42/44] ui: sdn: evpn " Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-manager v3 43/44] ui: sdn: openfabric: add route filter Stefan Hanreich
2026-05-04 11:39 ` [PATCH pve-manager v3 44/44] ui: sdn: ospf: add route filter setting Stefan Hanreich
2026-05-04 16:37 ` superseded: [PATCH access-control/cluster/manager/network/proxmox{-ve-rs,-perl-rs} v3 00/44] Add support for route maps / prefix lists to SDN 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=20260504113943.159905-40-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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal