From 8405980833a2b4f856c4ef6931d38123391f2b71 Mon Sep 17 00:00:00 2001 From: Stephan de Wit Date: Fri, 13 Feb 2026 15:48:44 +0100 Subject: [PATCH] interfaces: overview: clean up UI code and fix CARP badge alignment --- .../views/OPNsense/Interface/overview.volt | 263 +++++++++--------- src/opnsense/www/js/opnsense_bootgrid.js | 4 +- 2 files changed, 132 insertions(+), 135 deletions(-) diff --git a/src/opnsense/mvc/app/views/OPNsense/Interface/overview.volt b/src/opnsense/mvc/app/views/OPNsense/Interface/overview.volt index dd35bdbb2a..bdb0eeffbe 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Interface/overview.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Interface/overview.volt @@ -70,19 +70,31 @@ } function iterate_ips(obj) { - let $elements = $('
'); + let $elements = $('
').css({ + 'whiteSpace': 'normal', + 'lineHeight': '1.2' + }); obj.forEach(function (ip) { $span = $('
').text(ip['ipaddr'] + ' '); if ('vhid' in ip) { - $carp = $('').text('vhid ' + ip['vhid']); - $carp.attr('class', 'badge badge-pill'); - $carp.css('background-color', ip['status'] == 'MASTER' ? 'green' : 'primary'); - $carp.attr('data-toggle', 'tooltip'); - let title_text = ip['status'] + ' (freq. ' + ip['advbase'] + '/' + ip['advskew'] + ')'; - if (ip['peer']) { - title_text = title_text + '
' + ip['peer'] + '
' + ip['peer6']; + const titleLines = [ + `${ip.status} (freq. ${ip.advbase}/${ip.advskew})` + ]; + + if (ip.peer) { + titleLines.push(ip.peer, ip.peer6); } - $carp.attr('title', title_text); + + const $carp = $('', { + text: `vhid ${ip.vhid}`, + css: { cursor: 'pointer' }, + 'data-toggle': 'tooltip', + 'data-html': true + }) + .addClass('badge badge-pill') + .css('background-color', ip.status === 'MASTER' ? 'green' : 'primary') + .prop('title', titleLines.join('
')); + $span.append($carp); } $elements.append($span); @@ -93,6 +105,115 @@ $("#grid-overview").UIBootgrid( { search: '/api/interfaces/overview/interfaces_info', + commands: { + interface_reload: { + filter: (cell) => { + const data = cell.getData(); + return 'link_type' in data && ["dhcp", "pppoe", "pptp", "l2tp", "ppp"].includes(data.link_type); + }, + method: (event, cell) => { + const data = cell.getData(); + const $element = $(cell.getElement()).find('.command-interface_reload'); + $element.remove('i').html(''); + ajaxCall('/api/interfaces/overview/reload_interface/' + data.identifier, {}, function (data, status) { + /* delay slightly to allow the interface to come up */ + setTimeout(function() { + $element.remove('i').html(''); + $("#grid-overview").bootgrid('reload'); + }, 1000); + }); + }, + classname: 'fa fa-fw fa-refresh', + title: "{{ lang._('Reload') }}" + }, + settings: { + filter: (cell) => { + const data = cell.getData(); + return 'identifier' in data && data.identifier && 'config' in data && data.config && !data.config.internal_dynamic; + }, + method: (event, cell) => { + window.location.href = `/interfaces.php?if=${cell.getData().identifier}`; + }, + classname: 'fa fa-fw fa-cog', + title: "{{ lang._('Settings') }}" + }, + firewall_rules: { + filter: (cell) => { + const data = cell.getData(); + return 'identifier' in data && data.identifier && data.enabled; + }, + method: (event, cell) => { + window.location.href = `/ui/firewall/filter/#interface=${cell.getData().identifier}`; + }, + classname: 'fa fa-fw fa-fire', + title: "{{ lang._('Firewall Rules') }}" + }, + interface_info: { + method: (event, cell) => { + ajaxGet('/api/interfaces/overview/get_interface/' + cell.getData().device, {}, function(data, status) { + data = data['message']; + let $table = $('
'); + let $table_body = $(''); + + for (let key in data) { + let $row = $(''); + let value = data[key]['value']; + if (key === 'line rate') { + value = format_linerate(value.split(" ")[0]); + } + if (key === 'ipv4' || key === 'ipv6') { + value = iterate_ips(value); + } + + if (!'translation' in data[key]) { + continue; + } + key = data[key]['translation']; + $row.append($('').text(key)); + if (typeof value === 'string' || Array.isArray(value)) { + value = value.toString().split(",").join("
"); + } else if (typeof value === 'object' && value !== null) { + // skip any deeper nested structures + let skip = false; + for (let key in value) { + if (typeof value[key] === 'object' && value !== null && !Array.isArray(value[key])) { + skip = true; + break; + } + } + + if (skip) { + continue; + } + + $table_sub = createTable(value); + value = $table_sub.prop('outerHTML'); + } + $row.append($('').html(value)); + $table_body.append($row); + } + + $table.append($table_body); + $('[data-toggle="tooltip"]').tooltip({container: 'body', html:true}); + BootstrapDialog.show({ + title: data['description']['value'], + message: $table.prop('outerHTML'), + type: BootstrapDialog.TYPE_INFO, + draggable: true, + cssClass: 'details-dialog', + buttons: [{ + label: "{{ lang._('Close') }}", + action: function (dialogRef) { + dialogRef.close(); + } + }] + }); + }); + }, + classname: 'fa fa-fw fa-search', + title: "{{ lang._('Details') }}" + } + }, options: { selection: false, formatters: { @@ -169,134 +290,10 @@ } return $elements.prop('outerHTML'); }, - "commands": function (column, row) { - let $commands = $('
'); - let $btn = $(''); - - /* reload action for dynamic configurations */ - if ('link_type' in row) { - if (["dhcp", "pppoe", "pptp", "l2tp", "ppp"].includes(row.link_type)) { - let $command = $btn.clone(); - $command.addClass('interface-reload').attr('title', 'Reload').attr('data-device-id', row.identifier); - $command.find('i').addClass('fa fa-fw fa-refresh'); - $commands.append($command); - } - } - - $anchor = $(''); - if ('identifier' in row && row.identifier && 'config' in row && row.config) { - if (!row.config.internal_dynamic) { - $a_interfaces = $anchor.clone().attr('href', '/interfaces.php?if=' + row.identifier); - $a_interfaces.attr('title', 'Settings'); - $a_interfaces.find('i').addClass('fa fa-fw fa-cog'); - $commands.append($a_interfaces); - } - - if (row.enabled) { - $a_fw = $anchor.clone().attr('href', '/firewall_rules.php?if=' + row.identifier); - $a_fw.attr('title', 'Firewall Rules'); - $a_fw.find('i').addClass('fa fa-fw fa-fire'); - $commands.append($a_fw); - } - } - - $btn.addClass('interface-info').attr('title', 'Details').attr('data-row-id', row.device); - $btn.find('i').addClass('fa fa-fw fa-search'); - - $commands.append($btn); - return $commands.prop('outerHTML'); - } } } } ).on("loaded.rs.jquery.bootgrid", function (e) { - $('[data-toggle="tooltip"]').tooltip({container: 'body', html:true}); - - /* attach event handler to reload buttons */ - $('.interface-reload').each(function () { - $(this).unbind('click').click(function () { - let $element = $(this); - let device = $(this).data("device-id"); - $element.remove('i').html(''); - ajaxCall('/api/interfaces/overview/reload_interface/' + device, {}, function (data, status) { - /* delay slightly to allow the interface to come up */ - setTimeout(function() { - $element.remove('i').html(''); - $("#grid-overview").bootgrid('reload'); - }, 1000); - }); - }); - }); - - /* attach event handler to the command-info button */ - $(".interface-info").each(function () { - $(this).unbind('click').click(function () { - let $element = $(this); - let device = $(this).data("row-id"); - - ajaxGet('/api/interfaces/overview/get_interface/' + device, {}, function(data, status) { - data = data['message']; - let $table = $('
'); - let $table_body = $(''); - - for (let key in data) { - let $row = $(''); - let value = data[key]['value']; - if (key === 'line rate') { - value = format_linerate(value.split(" ")[0]); - } - if (key === 'ipv4' || key === 'ipv6') { - value = iterate_ips(value); - } - - if (!'translation' in data[key]) { - continue; - } - key = data[key]['translation']; - $row.append($('').text(key)); - if (typeof value === 'string' || Array.isArray(value)) { - value = value.toString().split(",").join("
"); - } else if (typeof value === 'object' && value !== null) { - // skip any deeper nested structures - let skip = false; - for (let key in value) { - if (typeof value[key] === 'object' && value !== null && !Array.isArray(value[key])) { - skip = true; - break; - } - } - - if (skip) { - continue; - } - - $table_sub = createTable(value); - value = $table_sub.prop('outerHTML'); - } - $row.append($('').html(value)); - $table_body.append($row); - } - - $table.append($table_body); - $('[data-toggle="tooltip"]').tooltip({container: 'body', html:true}); - BootstrapDialog.show({ - title: data['description']['value'], - message: $table.prop('outerHTML'), - type: BootstrapDialog.TYPE_INFO, - draggable: true, - cssClass: 'details-dialog', - buttons: [{ - label: "{{ lang._('Close') }}", - action: function (dialogRef) { - dialogRef.close(); - } - }] - }); - }); - }); - }); - $(".route-container").each(function () { let $route_container = $(this); let count = $(this).children('.route-content').length; diff --git a/src/opnsense/www/js/opnsense_bootgrid.js b/src/opnsense/www/js/opnsense_bootgrid.js index f41444e7d6..7b2d6f1130 100644 --- a/src/opnsense/www/js/opnsense_bootgrid.js +++ b/src/opnsense/www/js/opnsense_bootgrid.js @@ -953,7 +953,7 @@ class UIBootgrid { // to the parent so no handlers on parent containers are executed $selector.unbind('click').on("click", function (event) { event.stopPropagation(); - commands[command].method?.bind(this)(event); + commands[command].method?.bind(this)(event, cell); }); } @@ -1556,7 +1556,7 @@ class UIBootgrid { * register commands * * The command object can have the following properties: - * - method: a function that is executed on command click + * - method: a function that is executed on command click. function signature is (event, cell) * - title: translated title to be shown as a tooltip. Can be a function with the cell object as param * - requires: an array of strings marking which this.crud properties are required * - sequence: order of commands rendering