From: Stefan Hanreich <s.hanreich@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Thomas Lamprecht <t.lamprecht@proxmox.com>
Subject: [PATCH pve-manager v6 12/24] ui: sdn: add panel for managing route map entries
Date: Fri, 8 May 2026 18:31:21 +0200 [thread overview]
Message-ID: <20260508163134.481912-13-s.hanreich@proxmox.com> (raw)
In-Reply-To: <20260508163134.481912-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>
Link: https://lore.proxmox.com/20260505153720.412180-43-s.hanreich@proxmox.com
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/dc/Config.js | 8 +
www/manager6/sdn/RouteMapPanel.js | 977 ++++++++++++++++++++++++++++++
3 files changed, 986 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..5fa0475c3
--- /dev/null
+++ b/www/manager6/sdn/RouteMapPanel.js
@@ -0,0 +1,977 @@
+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,
+ 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: '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}',
+ },
+ value: '__default__',
+ comboItems: [
+ ['__default__', Proxmox.Utils.defaultText + gettext('(exit)')],
+ ['on-match-next', gettext('On match next')],
+ ],
+ deleteEmpty: false,
+ editable: false,
+ isFormField: false,
+ listeners: {
+ select: 'onSelect',
+ },
+ },
+ ],
+
+ controller: {
+ onSelect: function () {
+ let me = this;
+ me.getView().fireEvent('dirtychange');
+ },
+ },
+
+ getValue: function () {
+ let me = this;
+
+ let exitAction = me.getViewModel().get('exitAction');
+
+ if (!exitAction?.key || exitAction.key === '__default__') {
+ return null;
+ }
+
+ return PVE.Parser.printPropertyString(exitAction);
+ },
+
+ setValue: function (value) {
+ let me = this;
+
+ let exitAction = PVE.Parser.parsePropertyString(value);
+
+ me.getViewModel().set('exitAction', exitAction);
+ me.resetOriginalValue();
+ },
+
+ getSubmitData: function () {
+ let me = this;
+
+ let value = me.getValue();
+
+ if (!value) {
+ 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.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: gettext('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: 'call',
+ 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) {
+ let exitAction = rec.data.pending?.['exit-action'] ?? rec.data['exit-action'];
+
+ if (exitAction) {
+ let parsedExitAction = PVE.Parser.parsePropertyString(exitAction);
+ return Ext.htmlEncode(`${parsedExitAction.key}`);
+ }
+ },
+ },
+ {
+ header: gettext('State'),
+ width: 100,
+ dataIndex: 'state',
+ renderer: function (value, metaData, rec) {
+ return PVE.Utils.render_sdn_pending_state(rec, value);
+ },
+ },
+ ],
+});
--
2.47.3
next prev parent reply other threads:[~2026-05-08 16:41 UTC|newest]
Thread overview: 25+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-08 16:31 [PATCH manager/network/proxmox{-ve-rs,-perl-rs} v6 00/24] Add support for route maps / prefix lists to SDN Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-ve-rs v6 01/24] sdn: prefix lists: refactor section config and api format Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-ve-rs v6 02/24] prefix lists: implement validation for prefix lists Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 03/24] sdn: prefix lists: refactor existing API endpoint Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 04/24] sdn: prefix lists: add crud methods for prefix list entries Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 05/24] sdn: prefix lists: validate prefix lists Stefan Hanreich
2026-05-08 16:31 ` [PATCH proxmox-perl-rs v6 06/24] sdn: route maps: add route map list method Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-network v6 07/24] api: refactor route map api structure Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-network v6 08/24] api: refactor prefix list " Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 09/24] ui: sdn: add route map selector Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 10/24] ui: sdn: add prefix list selector Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 11/24] ui: sdn: add panel for managing prefix lists Stefan Hanreich
2026-05-08 16:31 ` Stefan Hanreich [this message]
2026-05-08 16:31 ` [PATCH pve-manager v6 13/24] ui: sdn: bgp controller: allow configuring route maps Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 14/24] ui: sdn: evpn " Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 15/24] ui: sdn: openfabric: add route filter Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 16/24] ui: sdn: ospf: add route filter setting Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 17/24] ui: sdn: prefix list: add missing subjects Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 18/24] sdn: do not fail rendering record data if pending property is missing Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 19/24] ui: sdn: prefix list: adapt to changed api structure Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 20/24] ui: sdn: route maps: adapt to new route map " Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 21/24] ui: sdn: prefix lists: route maps: use integerfields for numbers Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 22/24] ui: sdn: prefix list panel: reload data on deleting prefix list entry Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 23/24] ui: prefix list panel: delete empty le and get properties Stefan Hanreich
2026-05-08 16:31 ` [PATCH pve-manager v6 24/24] ui: prefix list entry panel: make prefix required 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=20260508163134.481912-13-s.hanreich@proxmox.com \
--to=s.hanreich@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
--cc=t.lamprecht@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