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 v4 43/47] ui: sdn: add panel for managing route map entries
Date: Mon,  4 May 2026 18:03:40 +0200	[thread overview]
Message-ID: <20260504160350.395470-44-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20260504160350.395470-1-s.hanreich@proxmox.com>

This panel allows users to perform CRUD operations on route map
entries and shows an overview of all existing route map entries.

Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
 www/manager6/Makefile             |   1 +
 www/manager6/dc/Config.js         |   8 +
 www/manager6/sdn/RouteMapPanel.js | 945 ++++++++++++++++++++++++++++++
 3 files changed, 954 insertions(+)
 create mode 100644 www/manager6/sdn/RouteMapPanel.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index b123a331d..597769bb9 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -309,6 +309,7 @@ JSSRC= 							\
 	sdn/ZoneView.js					\
 	sdn/IpamEdit.js					\
 	sdn/OptionsPanel.js				\
+	sdn/RouteMapPanel.js				\
 	sdn/RouteMapSelector.js				\
 	sdn/PrefixListPanel.js				\
 	sdn/PrefixListSelector.js				\
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 8784e357c..fd3a68a79 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: 'pveSDNRouteMaps',
+                        groups: ['sdn'],
+                        title: gettext('Route Maps'),
+                        hidden: true,
+                        iconCls: 'fa fa-map',
+                        itemId: 'sdnroutemaps',
+                    },
                     {
                         xtype: 'pveSDNPrefixLists',
                         groups: ['sdn'],
diff --git a/www/manager6/sdn/RouteMapPanel.js b/www/manager6/sdn/RouteMapPanel.js
new file mode 100644
index 000000000..ce21eb75f
--- /dev/null
+++ b/www/manager6/sdn/RouteMapPanel.js
@@ -0,0 +1,945 @@
+Ext.define('PVE.sdn.RouteMapEntry', {
+    extend: 'Ext.data.Model',
+    fields: ['route-map-id', 'order', 'action', 'match', 'set', 'exit-action', 'pending'],
+
+    getRouteMapId: function() {
+        let me = this;
+        return me.data.pending?.['route-map-id'] ?? me.data['route-map-id'];
+    },
+
+    getOrder: function() {
+        let me = this;
+        return me.data.pending?.order ?? me.data.order;
+    },
+});
+
+Ext.define('PVE.sdn.RouteMapExitAction', {
+    extend: 'Ext.data.Model',
+    fields: ['key', 'value'],
+});
+
+Ext.define('PVE.sdn.RouteMapSet', {
+    extend: 'Ext.data.Model',
+    fields: ['key', 'value'],
+});
+
+Ext.define('PVE.sdn.RouteMapSetValueField', {
+    extend: 'Ext.container.Container',
+    mixins: ['Ext.form.field.Field'],
+
+    alias: ['widget.pveSdnRouteMapSetValueField'],
+
+    layout: 'vbox',
+
+    config: {
+        record: null,
+    },
+
+    publishes: {
+        record: true,
+    },
+
+    defaults: {
+        width: '100%',
+    },
+
+    viewModel: {
+        data: {
+            selectedKey: null,
+        }
+    },
+
+    items: [],
+
+    getWidgetForKey: function(key) {
+        const widgets = {
+            'ip-next-hop-peer-address': {
+                xtype: 'displayfield',
+            },
+            'ip-next-hop': {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IPAddress',
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+            'ip-next-hop-unchanged': {
+                xtype: 'displayfield',
+            },
+            'ip6-next-hop-peer-address': {
+                xtype: 'displayfield',
+            },
+            'ip6-next-hop-prefer-global': {
+                xtype: 'displayfield',
+            },
+            'ip6-next-hop': {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IP6Address',
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+            'local-preference': {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+                bind: {
+                    value: '{record.value}',
+                },
+
+            },
+            'tag': {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+                bind: {
+                    value: '{record.value}',
+                },
+
+            },
+            'weight': {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+                bind: {
+                    value: '{record.value}',
+                },
+
+            },
+            'metric': {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+                bind: {
+                    value: '{record.value}',
+                },
+
+            },
+            'src': {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IP64Address',
+                bind: {
+                    value: '{record.value}',
+                },
+            },
+        };
+
+        return widgets[key] ?? {
+            xtype: 'displayfield'
+        };
+    },
+
+    applyRecord: function(record) {
+        let me = this;
+
+        if (record.data.key === me.getViewModel().get('selectedKey')) {
+            return;
+        }
+        me.getViewModel().set('selectedKey', record.data.key);
+
+        me.removeAll();
+
+        let widget = me.getWidgetForKey(record.data.key);
+
+        if (widget.xtype === 'displayfield') {
+            me.getRecord()?.set('value', null);
+        }
+
+        me.add(widget);
+
+        return record;
+    }
+});
+
+const ROUTE_MAP_SET_ACTION_LABELS = {
+    'ip-next-hop': gettext('IPv4 next-hop'),
+    'ip-next-hop-peer-address': gettext('IPv4 next-hop to peer address'),
+    'ip-next-hop-unchanged': gettext('IPv4 next-hop unchanged'),
+    'ip6-next-hop': gettext('IPv6 next-hop'),
+    'ip6-next-hop-peer-address': gettext('IPv6 next-hop to peer address'),
+    'ip6-next-hop-prefer-global': gettext('IPv6 next-hop to global address'),
+    'local-preference': gettext('Local Preference'),
+    'tag': gettext('Tag'),
+    'weight': gettext('Weight'),
+    'metric': gettext('Metric'),
+    'src': gettext('Source'),
+};
+
+Ext.define('PVE.sdn.RouteMapSetField', {
+    extend: 'Ext.grid.Panel',
+    mixins: ['Ext.form.field.Field'],
+    alias: 'widget.pveSdnRouteMapSetField',
+
+    emptyText: gettext('No set actions configured.'),
+
+    isCreate: false,
+
+    store: {
+        model: 'PVE.sdn.RouteMapSet',
+    },
+
+    columns: [
+        {
+            header: gettext('Property'),
+            xtype: 'widgetcolumn',
+            flex: 1,
+            widget: {
+                xtype: 'proxmoxKVComboBox',
+                comboItems: Object.entries(ROUTE_MAP_SET_ACTION_LABELS),
+                allowBlank: false,
+                deleteEmpty: false,
+                listeners: {
+                    select: function(_this, newValue) {
+                        let me = this;
+                        me.getWidgetRecord().set('key', newValue.id);
+                    },
+                }
+            },
+        },
+        {
+            header: gettext('Value'),
+            flex: 1,
+            xtype: 'widgetcolumn',
+            widget: {
+                xtype: 'pveSdnRouteMapSetValueField',
+                bind: {
+                    record: {
+                        bindTo: '{record}',
+                        deep: true,
+                    },
+                },
+            },
+        },
+        {
+            width: 20,
+            xtype: 'actioncolumn',
+            items: [
+                {
+                    tooltip: gettext('Delete'),
+                    handler: 'deleteSet',
+                    iconCls: 'fa critical fa-trash-o',
+                },
+            ],
+        },
+    ],
+
+    initComponent: function() {
+        let me = this;
+        me.callParent();
+
+        me.getStore().on('datachanged', function() {
+            me.fireEvent('dirtychange');
+        });
+    },
+
+    getValue: function() {
+        let me = this;
+
+        return me.getStore().getData().items.map((item) => {
+            let data = item.data;
+            delete data.id;
+
+            if (!data.value) {
+                delete data.value;
+            }
+
+            return PVE.Parser.printPropertyString(data);
+        });
+    },
+
+    setValue: function(value) {
+        let me = this;
+        me.getStore().setData(value.map(PVE.Parser.parsePropertyString))
+    },
+
+    getSubmitData: function() {
+        let me = this;
+        let value = me.getValue();
+
+        if (value.length === 0) {
+            return {
+                delete: [me.getName()],
+            }
+        }
+
+        return {
+            [me.getName()]: value,
+        }
+    },
+
+    tbar: [
+        {
+            xtype: 'button',
+            text: gettext('Add'),
+            handler: 'addEntry',
+        }
+    ],
+
+    controller: {
+        addEntry: function() {
+            let me = this;
+            me.getView().getStore().add({
+                key: null,
+                value: null,
+            });
+        },
+        deleteSet: function(_table, _rI, _cI, _item, _e, record) {
+            let me = this;
+            me.getView().getStore().remove(record);
+        },
+    }
+});
+
+Ext.define('PVE.sdn.RouteMapMatch', {
+    extend: 'Ext.data.Model',
+    fields: ['key', 'value'],
+});
+
+Ext.define('PVE.sdn.RouteMapMatchValueField', {
+    extend: 'Ext.container.Container',
+    mixins: ['Ext.form.field.Field'],
+
+    alias: ['widget.pveSdnRouteMapMatchValueField'],
+
+    layout: 'vbox',
+
+    config: {
+        key: null,
+        record: null,
+    },
+
+    publishes: {
+        record: true,
+    },
+
+    defaults: {
+        name: 'value',
+        width: '100%',
+        bind: {
+            value: '{record.value}',
+        },
+    },
+
+    items: [],
+
+    getWidgetForKey: function(key) {
+        const widgets = {
+            'route-type': {
+                xtype: 'proxmoxKVComboBox',
+                comboItems: [
+                    ['ead', gettext('Ethernet Auto-Discovery (Type 1)') ],
+                    ['macip', gettext('MAC/IP Advertisement (Type 2)') ],
+                    ['multicast', gettext('Inclusive Multicast (Type 3)') ],
+                    ['es', gettext('Ethernet Segment (Type 4)') ],
+                    ['prefix', gettext('IP Prefix (Type 5)') ],
+                ],
+                allowBlank: false,
+                deleteEmpty: false,
+            },
+            'vni': {
+                xtype: 'proxmoxintegerfield',
+                flex: 1,
+                minValue: 1,
+                maxValue: 2 ** 24 - 1,
+                step: 1,
+            },
+            'ip-address-prefix-list': {
+                xtype: 'pveSDNPrefixListSelector',
+            },
+            'ip6-address-prefix-list': {
+                xtype: 'pveSDNPrefixListSelector',
+            },
+            'ip-next-hop-prefix-list': {
+                xtype: 'pveSDNPrefixListSelector',
+            },
+            'ip6-next-hop-prefix-list': {
+                xtype: 'pveSDNPrefixListSelector',
+            },
+            'ip-next-hop-address': {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IPAddress',
+            },
+            'ip6-next-hop-address': {
+                xtype: 'proxmoxtextfield',
+                vtype: 'IP6Address',
+            },
+            'metric': {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+            },
+            'local-preference': {
+                xtype: 'proxmoxintegerfield',
+                minValue: 1,
+                maxValue: 2 ** 32 - 1,
+                step: 1,
+            },
+            'peer': {
+                xtype: 'proxmoxtextfield',
+            },
+        };
+
+        return widgets[key] ?? {
+            xtype: 'displayfield'
+        };
+    },
+
+    updateKey: function(key) {
+        let me = this;
+
+        me.removeAll();
+        me.add(me.getWidgetForKey(key));
+
+        return key;
+    }
+});
+
+const ROUTE_MAP_MATCH_ACTION_LABELS = {
+    'route-type': gettext('Route Type'),
+    'vni': gettext('VNI'),
+    'ip-address-prefix-list': gettext('IPv4 (prefix-list)'),
+    'ip6-address-prefix-list': gettext('IPv6 (prefix-list)'),
+    'ip-next-hop-prefix-list': gettext('IPv4 next-hop (prefix-list)'),
+    'ip6-next-hop-prefix-list': gettext('IPv6 next-hop (prefix-list)'),
+    'ip-next-hop-address': gettext('IPv4 next-hop'),
+    'ip6-next-hop-address': gettext('IPv6 next-hop'),
+    'metric': gettext('Metric'),
+    'local-preference': gettext('Local Preference'),
+    'peer': gettext('Peer'),
+};
+
+Ext.define('PVE.sdn.RouteMapMatchField', {
+    extend: 'Ext.grid.Panel',
+    mixins: ['Ext.form.field.Field'],
+    alias: 'widget.pveSdnRouteMapMatchField',
+
+    emptyText: gettext('No match actions configured.'),
+
+    isCreate: false,
+
+    store: {
+        model: 'PVE.sdn.RouteMapMatch',
+    },
+
+    columns: [
+        {
+            header: gettext('Property'),
+            xtype: 'widgetcolumn',
+            flex: 1,
+            widget: {
+                xtype: 'proxmoxKVComboBox',
+                comboItems: Object.entries(ROUTE_MAP_MATCH_ACTION_LABELS),
+                allowBlank: false,
+                deleteEmpty: false,
+                bind: {
+                    value: '{record.key}',
+                },
+                listeners: {
+                    select: function(_this, newValue) {
+                        let me = this;
+                        me.getWidgetRecord().set('key', newValue.id);
+                    },
+                }
+            },
+        },
+        {
+            header: gettext('Value'),
+            flex: 1,
+            xtype: 'widgetcolumn',
+            widget: {
+                xtype: 'pveSdnRouteMapMatchValueField',
+                bind: {
+                    key: '{record.key}',
+                    record: '{record}',
+                },
+            },
+        },
+        {
+            width: 20,
+            xtype: 'actioncolumn',
+            items: [
+                {
+                    tooltip: gettext('Delete'),
+                    handler: 'deleteMatch',
+                    iconCls: 'fa critical fa-trash-o',
+                },
+            ],
+        },
+    ],
+
+    initComponent: function() {
+        let me = this;
+        me.callParent();
+
+        me.getStore().on('datachanged', function() {
+            me.fireEvent('validitychange');
+            me.fireEvent('dirtychange');
+        });
+    },
+
+    getValue: function() {
+        let me = this;
+
+        return me.getStore().getData().items.map((item) => {
+            let data = item.data;
+            delete data.id;
+
+            if (!data.value) {
+                delete data.value;
+            }
+
+            return PVE.Parser.printPropertyString(data);
+        });
+    },
+
+    setValue: function(value) {
+        let me = this;
+        me.getStore().setData(value.map(PVE.Parser.parsePropertyString))
+    },
+
+    getSubmitData: function() {
+        let me = this;
+
+        let value = me.getValue();
+        if (value.length === 0) {
+            return {
+                delete: [me.getName()],
+            }
+        }
+
+        return {
+            [me.getName()]: value,
+        }
+    },
+
+    tbar: [
+        {
+            xtype: 'button',
+            text: gettext('Add'),
+            handler: 'addEntry',
+        }
+    ],
+
+    controller: {
+        addEntry: function() {
+            let me = this;
+            me.getView().getStore().add({
+                key: null,
+                value: null,
+            });
+        },
+        deleteMatch: function(_table, _rI, _cI, _item, _e, record) {
+            let me = this;
+            me.getView().getStore().remove(record);
+        },
+    }
+});
+
+Ext.define('PVE.sdn.RouteMapExitActionField', {
+    extend: 'Ext.container.Container',
+    mixins: ['Ext.form.field.Field'],
+    alias: 'widget.pveSdnRouteMapExitActionField',
+
+    layout: 'hbox',
+
+    viewModel: {
+        data: {
+            exitAction: null,
+        },
+    },
+
+    items: [
+        {
+            xtype: 'proxmoxKVComboBox',
+            flex: 1,
+            fieldLabel: gettext('Exit Policy'),
+            bind: {
+                value: '{exitAction.key}'
+            },
+            comboItems: [
+                ['on-match-next', gettext('On match next')],
+            ],
+            allowBlank: true,
+        },
+    ],
+
+    getValue: function() {
+        let me = this;
+
+        let exitAction = me.getViewModel().get('exitAction');
+
+        if (!exitAction.key) {
+            return null;
+        }
+
+        return PVE.Parser.printPropertyString(exitAction);
+    },
+
+    setValue: function(value) {
+        let me = this;
+        me.getViewModel().set('exitAction', PVE.Parser.parsePropertyString(value));
+    },
+
+    getSubmitData: function() {
+        let me = this;
+
+        let value = me.getValue();
+        if (value.length === 0) {
+            return {
+                delete: [me.getName()],
+            }
+        }
+
+        return {
+            [me.getName()]: value,
+        }
+    },
+});
+
+
+Ext.define('PVE.sdn.EditRouteMapEntryWindow', {
+    extend: 'Proxmox.window.Edit',
+    subject: gettext('Route Map Entry'),
+
+    initComponent: function() {
+        let me = this;
+        me.method = (me.isCreate) ? "POST" : "PUT";
+
+        me.callParent();
+    },
+
+    loadUrl: function() {
+        let me = this;
+        return `/api2/extjs/cluster/sdn/route-maps/${me.getRouteMapId()}/${me.getOrder()}`;
+    },
+
+    submitUrl: function() {
+        let me = this;
+        
+        if (me.isCreate) {
+            return '/api2/extjs/cluster/sdn/route-maps';
+        } else {
+            return `/api2/extjs/cluster/sdn/route-maps/${me.getRouteMapId()}/${me.getOrder()}`;
+        }
+    },
+
+    width: 600,
+
+    viewModel: {
+        formulas: {
+            routeMapId: function(get) {
+                let me = this;
+                return me.getView().getRouteMapId();
+            },
+            order: function(get) {
+                let me = this;
+                return me.getView().getOrder();
+            },
+        }
+    },
+
+    config: {
+        routeMapId: null,
+        order: null,
+    },
+
+    isCreate: false,
+
+    items: [
+        {
+            xtype: 'pveSDNRouteMapSelector',
+            name: 'route-map-id',
+            fieldLabel: gettext('Route Map ID'),
+            editable: true,
+            notFoundIsValid: true,
+            bind: {
+                disabled: '{routeMapId}',
+            }
+        },
+        {
+            xtype: 'proxmoxtextfield',
+            name: 'order',
+            fieldLabel: gettext('Order'),
+            bind: {
+                disabled: '{order}',
+            }
+        },
+        {
+            xtype: 'proxmoxKVComboBox',
+            fieldLabel: gettext('Action'),
+            name: 'action',
+            comboItems: [
+                ['permit', gettext('Permit')],
+                ['deny', gettext('Deny')],
+            ],
+            allowBlank: false,
+        },
+        {
+            xtype: 'fieldcontainer',
+            fieldLabel: gettext('Match'),
+            items: [
+                {
+                    xtype: 'pveSdnRouteMapMatchField',
+                    name: 'match',
+                }
+            ],
+        },
+        {
+            xtype: 'fieldcontainer',
+            fieldLabel: gettext('Set'),
+            items: [
+                {
+                    xtype: 'pveSdnRouteMapSetField',
+                    name: 'set',
+                },
+            ],
+        },
+        {
+            xtype: 'pveSDNRouteMapSelector',
+            fieldLabel: gettext('Call'),
+            name: 'call',
+            deleteEmpty: true,
+            skipEmptyText: true,
+        },
+        {
+            xtype: 'pveSdnRouteMapExitActionField',
+            fieldLabel: gettext('Exit Policy'),
+            name: 'exit-action',
+        },
+    ],
+});
+
+Ext.define('PVE.sdn.PendingRouteMap', {
+    extend: 'Ext.data.Model',
+    fields: ['id', 'pending'],
+});
+
+Ext.define('PVE.sdn.RouteMapPanel', {
+    extend: 'Ext.grid.Panel',
+    alias: ['widget.pveSDNRouteMaps'],
+
+    emptyText: gettext('No route maps configured.'),
+
+    store: {
+        autoLoad: true,
+        model: 'PVE.sdn.RouteMapEntry',
+        proxy: {
+            type: 'proxmox',
+            url: '/api2/extjs/cluster/sdn/route-maps?pending=1',
+        },
+        sorters: [
+            {
+                property: 'route-map-id',
+                direction: 'ASC',
+            },
+            {
+                property: 'order',
+                direction: 'ASC',
+            },
+        ],
+    },
+
+    viewModel: {
+        formulas: {
+            selection: function(get) {
+                let me = this;
+
+                let selection = me.getView().getSelection();
+                return (selection.length > 0) ? selection[0] : null;
+            }
+        }
+    },
+
+    listeners: {
+        itemdblclick: 'editRouteMapEntry'
+    },
+
+    controller: {
+        reload: function() {
+            let me = this;
+            me.getView().getStore().load();
+
+        },
+        addRouteMapEntry: function() {
+            let me = this;
+
+            Ext.create('PVE.sdn.EditRouteMapEntryWindow', {
+                autoShow: true,
+                isCreate: true,
+                listeners: {
+                    close: function() {
+                        me.reload();
+                    }
+                }
+            });
+        },
+        removeRouteMapEntry: function() {
+            let me = this;
+
+            let entry = me.getView().getSelection()[0];
+
+            if (!entry) {
+                console.warn('no route map entry selected!');
+                return;
+            }
+
+            Ext.Msg.show({
+                title: gettext('Confirm'),
+                icon: Ext.Msg.WARNING,
+                message: `Remove route map entry?`,
+                buttons: Ext.Msg.YESNO,
+                defaultFocus: 'no',
+                callback: function (btn) {
+                    if (btn !== 'yes') {
+                        return;
+                    }
+
+                    Proxmox.Async.api2({
+                        url: `/api2/extjs/cluster/sdn/route-maps/${entry.getRouteMapId()}/${entry.getOrder()}`,
+                        method: 'DELETE',
+                    })
+                        .catch(Proxmox.Utils.alertResponseFailure)
+                        .finally(() => {
+                            me.reload();
+                        });
+                },
+            });
+        },
+        editRouteMapEntry: function() {
+            let me = this;
+
+            let entry = me.getView().getSelection()[0];
+
+            if (!entry) {
+                console.warn('no route map entry selected!');
+                return;
+            }
+
+            Ext.create('PVE.sdn.EditRouteMapEntryWindow', {
+                autoShow: true,
+                autoLoad: true,
+                isCreate: false,
+                routeMapId: entry.getRouteMapId(),
+                order: entry.getOrder(),
+                listeners: {
+                    close: function() {
+                        me.reload();
+                    }
+                }
+            });
+        },
+    },
+
+    tbar: [
+        {
+            text: gettext('Add'),
+            xtype: 'button',
+            handler: 'addRouteMapEntry',
+        },
+        {
+            text: gettext('Edit'),
+            xtype: 'proxmoxButton',
+            handler: 'editRouteMapEntry',
+            bind: {
+                disabled: '{!selection}',
+            },
+        },
+        {
+            text: gettext('Remove'),
+            xtype: 'proxmoxButton',
+            handler: 'removeRouteMapEntry',
+            bind: {
+                disabled: '{!selection}',
+            },
+        },
+        {
+            text: gettext('Reload'),
+            xtype: 'button',
+            handler: 'reload',
+        },
+    ],
+
+    columns: [
+        {
+            text: gettext('Name'),
+            dataIndex: 'route-map-id',
+            flex: 1,
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending(rec, value, 'route-map-id', 1);
+            },
+        },
+        {
+            text: gettext('Order'),
+            dataIndex: 'order',
+            width: 50,
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending(rec, value, 'order', 1);
+            },
+        },
+        {
+            text: gettext('Action'),
+            dataIndex: 'action',
+            width: 80,
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending(rec, value, 'action', 1);
+            },
+        },
+        {
+            text: gettext('Match'),
+            dataIndex: 'match',
+            flex: 1,
+            renderer: function (value, metaData, rec) {
+                let actions = rec.data.pending?.match ?? rec.data.match ?? [];
+
+                return actions.map(PVE.Parser.parsePropertyString).map((match) => {
+                    let label = ROUTE_MAP_MATCH_ACTION_LABELS[match.key] ?? match.key;
+                    let value = (match.value) ? `: ${match.value}` : '';
+                    return Ext.htmlEncode(`${label}${value}`);
+                }).join("<br>");
+            },
+        },
+        {
+            text: gettext('Set'),
+            dataIndex: 'set',
+            flex: 1,
+            renderer: function (value, metaData, rec) {
+                let actions = rec.data.pending?.set ?? rec.data.set ?? [];
+
+                return actions.map(PVE.Parser.parsePropertyString).map((match) => {
+                    let label = ROUTE_MAP_SET_ACTION_LABELS[match.key] ?? match.key;
+                    let value = (match.value) ? `: ${match.value}` : '';
+                    return Ext.htmlEncode(`${label}${value}`);
+                }).join("<br>");
+            },
+        },
+        {
+            text: gettext('Call'),
+            dataIndex: 'action',
+            flex: 1,
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending(rec, value, 'call', 1);
+            },
+        },
+        {
+            header: gettext('Exit Policy'),
+            width: 100,
+            dataIndex: 'exit-action',
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending_state(rec, value);
+            },
+        },
+        {
+            header: gettext('State'),
+            width: 100,
+            dataIndex: 'state',
+            renderer: function (value, metaData, rec) {
+                return PVE.Utils.render_sdn_pending_state(rec, value);
+            },
+        },
+    ],
+});
-- 
2.47.3





  parent reply	other threads:[~2026-05-04 16:09 UTC|newest]

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