From 953815bad671dd4ceb5ae1a91c951cccfc6f822e Mon Sep 17 00:00:00 2001 From: Stephan de Wit Date: Tue, 6 May 2025 16:42:01 +0200 Subject: [PATCH] bootgrid: replace with Tabulator (#8555) * bootgrid: replace with Tabulator (https://github.com/opnsense/core/issues/8530) Initial work for a compatibility layer between the old jQuery bootgrid implementation and the new Tabulator inclusion * whitespace * cleanup pagination logic * minor cleanup and hardcode filter_rule page command column width to 150px * hasync: bring back sync & reconfigure all button * bring back subtle horizontal/vertical lines in the right places * improve row hovering/selection behavior * refine css further and do not horizontally space commands. Also ditch resize guide as it is too much logic for little gain * dynamically wire up default commands as well for virtualDOM rendering mode * small cleanup * remove 'last page' button in case of remote fetch and an unknown total * fix 'add' command * small CSS changes and change commands default to 100px * fix 'em' parsing * fixup internal formatter scoping * skip boolean type on formatter assignment * some more noise * prevent flashing * small interfaces overview fix, also use small buffer for em parsing to prevent early ellipsis * let's make use of onRendered() * handle table height responsiveness via our own ResizeObserver implementation * cleanup * account for scrollbar gutter, fix user defined rules on ids page * final touches for scrollbar offsets * fix 'datakey' usage * tabulator: groundwork for themes, variable height tables * tabulator: leftover * tabulator: minor persistence issue, use headerClick event instead * tabulator: fix up styles and small persistence/scrollbar glitches * some more style changes with compiled scss * better translation handling * missing return here --- plist | 9 + .../OPNsense/Base/ControllerBase.php | 6 +- .../views/OPNsense/Core/hasync_status.volt | 46 +- .../mvc/app/views/OPNsense/Core/service.volt | 16 +- .../mvc/app/views/OPNsense/DHCPv4/leases.volt | 8 +- .../app/views/OPNsense/Diagnostics/arp.volt | 1 + .../app/views/OPNsense/Diagnostics/log.volt | 61 +- .../views/OPNsense/Diagnostics/routes.volt | 21 +- .../OPNsense/Diagnostics/systemactivity.volt | 31 +- .../views/OPNsense/Firewall/filter_rule.volt | 3 +- .../mvc/app/views/OPNsense/IDS/index.volt | 83 +- .../app/views/OPNsense/IPsec/sessions.volt | 6 +- .../views/OPNsense/Interface/overview.volt | 16 +- .../mvc/app/views/OPNsense/Interface/vip.volt | 4 +- .../mvc/app/views/OPNsense/Monit/index.volt | 10 - .../mvc/app/views/OPNsense/Syslog/index.volt | 14 +- .../app/views/OPNsense/Unbound/overrides.volt | 5 +- .../layout_partials/base_bootgrid_table.volt | 2 +- .../mvc/app/views/layouts/default.volt | 48 +- src/opnsense/www/css/opnsense-bootgrid.css | 268 +++ src/opnsense/www/css/tabulator.min.css | 2 + src/opnsense/www/js/opnsense_bootgrid.js | 1769 +++++++++++++++++ src/opnsense/www/js/tabulator.min.js | 3 + src/opnsense/www/js/tabulator.min.js.map | 1 + .../assets/stylesheets/config/colors.scss | 3 +- .../assets/stylesheets/opnsense-bootgrid.scss | 260 +++ .../build/css/opnsense-bootgrid.css | 276 +++ .../assets/stylesheets/config/colors.scss | 3 +- .../assets/stylesheets/opnsense-bootgrid.scss | 265 +++ .../opnsense/build/css/opnsense-bootgrid.css | 281 +++ 30 files changed, 3318 insertions(+), 203 deletions(-) create mode 100644 src/opnsense/www/css/opnsense-bootgrid.css create mode 100644 src/opnsense/www/css/tabulator.min.css create mode 100644 src/opnsense/www/js/opnsense_bootgrid.js create mode 100644 src/opnsense/www/js/tabulator.min.js create mode 100644 src/opnsense/www/js/tabulator.min.js.map create mode 100644 src/opnsense/www/themes/opnsense-dark/assets/stylesheets/opnsense-bootgrid.scss create mode 100644 src/opnsense/www/themes/opnsense-dark/build/css/opnsense-bootgrid.css create mode 100644 src/opnsense/www/themes/opnsense/assets/stylesheets/opnsense-bootgrid.scss create mode 100644 src/opnsense/www/themes/opnsense/build/css/opnsense-bootgrid.css diff --git a/plist b/plist index a201f4184d..1c2b75a41a 100644 --- a/plist +++ b/plist @@ -2056,7 +2056,9 @@ /usr/local/opnsense/www/css/jqtree.css /usr/local/opnsense/www/css/jquery.bootgrid.css /usr/local/opnsense/www/css/nv.d3.css +/usr/local/opnsense/www/css/opnsense-bootgrid.css /usr/local/opnsense/www/css/pick-a-color-1.2.3.min.css +/usr/local/opnsense/www/css/tabulator.min.css /usr/local/opnsense/www/css/tokenize2.css /usr/local/opnsense/www/fonts/FontAwesome.otf /usr/local/opnsense/www/fonts/glyphicons-halflings-regular.eot @@ -2097,6 +2099,7 @@ /usr/local/opnsense/www/js/nv.d3.min.js.LICENSE.md /usr/local/opnsense/www/js/opnsense-treeview.js /usr/local/opnsense/www/js/opnsense.js +/usr/local/opnsense/www/js/opnsense_bootgrid.js /usr/local/opnsense/www/js/opnsense_bootgrid_plugin.js /usr/local/opnsense/www/js/opnsense_health.js /usr/local/opnsense/www/js/opnsense_status.js @@ -2109,6 +2112,8 @@ /usr/local/opnsense/www/js/polyfills.js /usr/local/opnsense/www/js/qrcode.js /usr/local/opnsense/www/js/smoothie.js +/usr/local/opnsense/www/js/tabulator.min.js +/usr/local/opnsense/www/js/tabulator.min.js.map /usr/local/opnsense/www/js/theme.js /usr/local/opnsense/www/js/tinycolor-1.4.1.min.js /usr/local/opnsense/www/js/tokenize2.js @@ -2233,6 +2238,7 @@ /usr/local/opnsense/www/themes/opnsense-dark/assets/stylesheets/dashboard.scss /usr/local/opnsense/www/themes/opnsense-dark/assets/stylesheets/dns-overview.scss /usr/local/opnsense/www/themes/opnsense-dark/assets/stylesheets/main.scss +/usr/local/opnsense/www/themes/opnsense-dark/assets/stylesheets/opnsense-bootgrid.scss /usr/local/opnsense/www/themes/opnsense-dark/build/css/bootstrap-dialog.css /usr/local/opnsense/www/themes/opnsense-dark/build/css/bootstrap-select.css /usr/local/opnsense/www/themes/opnsense-dark/build/css/dashboard.css @@ -2240,6 +2246,7 @@ /usr/local/opnsense/www/themes/opnsense-dark/build/css/jquery.bootgrid.css /usr/local/opnsense/www/themes/opnsense-dark/build/css/main.css /usr/local/opnsense/www/themes/opnsense-dark/build/css/nv.d3.css +/usr/local/opnsense/www/themes/opnsense-dark/build/css/opnsense-bootgrid.css /usr/local/opnsense/www/themes/opnsense-dark/build/css/tokenize2.css /usr/local/opnsense/www/themes/opnsense-dark/build/fonts/LICENSE.SourceSansPro.txt /usr/local/opnsense/www/themes/opnsense-dark/build/fonts/SourceSansPro-Bold/SourceSansPro-Bold.eot @@ -2354,9 +2361,11 @@ /usr/local/opnsense/www/themes/opnsense/assets/stylesheets/config/colors.scss /usr/local/opnsense/www/themes/opnsense/assets/stylesheets/dashboard.scss /usr/local/opnsense/www/themes/opnsense/assets/stylesheets/main.scss +/usr/local/opnsense/www/themes/opnsense/assets/stylesheets/opnsense-bootgrid.scss /usr/local/opnsense/www/themes/opnsense/build/css/bootstrap-dialog.css /usr/local/opnsense/www/themes/opnsense/build/css/dashboard.css /usr/local/opnsense/www/themes/opnsense/build/css/main.css +/usr/local/opnsense/www/themes/opnsense/build/css/opnsense-bootgrid.css /usr/local/opnsense/www/themes/opnsense/build/fonts/LICENSE.SourceSansPro.txt /usr/local/opnsense/www/themes/opnsense/build/fonts/SourceSansPro-Bold/SourceSansPro-Bold.eot /usr/local/opnsense/www/themes/opnsense/build/fonts/SourceSansPro-Bold/SourceSansPro-Bold.otf diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php b/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php index b722574dce..1f34287529 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/Base/ControllerBase.php @@ -67,14 +67,14 @@ class ControllerBase extends ControllerRoot // JQuery Tokenize2 (https://zellerda.github.io/Tokenize2/) '/ui/js/tokenize2.js', // Bootgrid (grid system from http://www.jquery-bootgrid.com/ ) - '/ui/js/jquery.bootgrid.js', + '/ui/js/tabulator.min.js', + '/ui/js/opnsense_bootgrid.js', // Bootstrap type ahead '/ui/js/bootstrap3-typeahead.min.js', // OPNsense standard toolkit '/ui/js/opnsense.js', '/ui/js/opnsense_theme.js', '/ui/js/opnsense_ui.js', - '/ui/js/opnsense_bootgrid_plugin.js', '/ui/js/opnsense_status.js', // bootstrap script '/ui/js/bootstrap.min.js', @@ -96,6 +96,8 @@ class ControllerBase extends ControllerRoot '/css/bootstrap-select.css', // bootstrap dialog '/css/bootstrap-dialog.css', + '/css/tabulator.min.css', + '/css/opnsense-bootgrid.css', // Font awesome '/ui/assets/fontawesome/css/all.min.css', '/ui/assets/fontawesome/css/v4-shims.min.css', diff --git a/src/opnsense/mvc/app/views/OPNsense/Core/hasync_status.volt b/src/opnsense/mvc/app/views/OPNsense/Core/hasync_status.volt index 41d56ebaee..940c7af729 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Core/hasync_status.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Core/hasync_status.volt @@ -84,13 +84,13 @@ $(".xmlrpc_srv_status_act").each(function(){ switch($(this).data('service_action')) { case 'start': - $(this).tooltip({title: "{{ lang._('Start') | safe}}"}); + $(this).tooltip({title: "{{ lang._('Start') | safe}}", container: "body", trigger: "hover"}); break; case 'restart': - $(this).tooltip({title: "{{ lang._('Synchronize and Restart') | safe}}"}); + $(this).tooltip({title: "{{ lang._('Synchronize and Restart') | safe}}", container: "body", trigger: "hover"}); break; case 'stop': - $(this).tooltip({title: "{{ lang._('Stop') | safe}}"}); + $(this).tooltip({title: "{{ lang._('Stop') | safe}}", container: "body", trigger: "hover"}); break; } $(this).click(function(){ @@ -110,17 +110,25 @@ $("#status_error").show(); } $("#status_query").hide(); - }); - $("#act_restart_all").click(function(){ - let icon = $(this).find('i'); - if (icon.hasClass('spinner')) { - return; - } - icon.removeClass('fa-repeat').addClass('fa-spinner fa-pulse'); - ajaxCall('/api/core/hasync_status/restart_all', {}, function(data){ - icon.removeClass('fa-spinner fa-pulse').addClass('fa-repeat'); - $('#grid_services').bootgrid('reload'); - }); + + $("#grid_services-header > .row > .actionBar").prepend($(` +
+ {{ lang._('Synchronize and reconfigure all') }} + + + +
+ `).click(function() { + let icon = $(this).find('i'); + if (icon.hasClass('spinner')) { + return; + } + icon.removeClass('fa-repeat').addClass('fa-spinner fa-pulse'); + ajaxCall('/api/core/hasync_status/restart_all', {}, function(data){ + icon.removeClass('fa-spinner fa-pulse').addClass('fa-repeat'); + $('#grid_services').bootgrid('reload'); + }); + })); }); }); @@ -177,16 +185,6 @@ - - - {{ lang._('Synchronize and reconfigure all') }} - - - - - - - diff --git a/src/opnsense/mvc/app/views/OPNsense/Core/service.volt b/src/opnsense/mvc/app/views/OPNsense/Core/service.volt index 93a83aeda1..2146e97d9f 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Core/service.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Core/service.volt @@ -32,7 +32,6 @@ search:'/api/core/service/search', options:{ multiSelect: false, - rowSelect: true, selection: false, formatters:{ commands: function (column, row) { @@ -47,20 +46,16 @@ }, status: function (column, row) { if (row['running']) { - return ''; + return ''; } else { - return ''; + return ''; } } } } }); grid_service.on('loaded.rs.jquery.bootgrid', function () { - $('[data-toggle="tooltip"]').tooltip(); - let ids = $("#grid-service").bootgrid("getCurrentRows"); - if (ids.length > 0) { - $("#grid-service").bootgrid('select', [ids[0].name]); - } + $('[data-toggle="tooltip"]').tooltip({container: 'body', trigger: 'hover'}); $('.command-stop').click(function () { $(this).toggleClass('disabled'); $(this).children().toggleClass('fa-stop fa-spinner fa-pulse'); @@ -92,12 +87,11 @@ {{ lang._('ID') }} - + {{ lang._('Name') }} {{ lang._('Description') }} - - + diff --git a/src/opnsense/mvc/app/views/OPNsense/DHCPv4/leases.volt b/src/opnsense/mvc/app/views/OPNsense/DHCPv4/leases.volt index 45b888cefe..3e531f7266 100644 --- a/src/opnsense/mvc/app/views/OPNsense/DHCPv4/leases.volt +++ b/src/opnsense/mvc/app/views/OPNsense/DHCPv4/leases.volt @@ -54,6 +54,7 @@ search:'/api/dhcpv4/leases/searchLease/', del:'/api/dhcpv4/leases/delLease/', options: { + virtualDOM: true, selection: false, multiSelect: false, useRequestHandlerOnGet: true, @@ -94,8 +95,11 @@ "tooltipformatter": function (column, row) { return '' + row[column.id] + '
' }, - "statusformatter": function (column, row) { + "statusformatter": function (column, row, onRendered) { let connected = row.status == 'online' ? 'text-success' : 'text-danger'; + onRendered(() => { + $('[data-toggle="tooltip"]').tooltip({container: 'body', trigger: 'hover'}); + }) return '' }, "commands": function (column, row) { @@ -118,7 +122,7 @@ /* The delete action can be hooked up to the default bootgrid behaviour */ let deleteip = ''; + let severity = $('#severity_filter').val(); + let debug = (Array.isArray(severity) && severity.includes('Debug')) || severity === "Debug"; + if ($("input.search-field").val() !== "" || !debug) { + let btn = $(` + + `).on('click', function(event) { + if ($("#exact_severity").hasClass("fa-toggle-on")) { + $("#severity_filter").selectpicker('deselectAll'); + } else { + $("#severity_filter").val("Debug"); + } + $('#grid-log').bootgrid('setPageByRowId', parseInt($(this).data('row-id'))); + }); + + return btn[0]; } else { return ""; } @@ -87,7 +104,8 @@ }, search:'/api/diagnostics/log/{{module}}/{{scope}}' }); - $(".filter_act").change(function(){ + $(".filter_act").change(function(event){ + event.stopPropagation(); if (window.localStorage) { localStorage.setItem('log_severity_{{module}}_{{scope}}', $("#severity_filter").val()); localStorage.setItem('log_validFrom_filter_{{module}}_{{scope}}', $("#validFrom_filter").val()); @@ -95,25 +113,6 @@ $('#grid-log').bootgrid('reload'); }); - grid_log.on("loaded.rs.jquery.bootgrid", function(){ - if (page > 0) { - $("ul.pagination > li:last > a").data('page', page).click(); - page = 0; - } - - $(".action-page").click(function(event){ - event.preventDefault(); - $("#grid-log").bootgrid("search", ""); - page = parseInt((parseInt($(this).data('row-id')) / $("#grid-log").bootgrid("getRowCount")))+1; - $("input.search-field").val(""); - if ($("#exact_severity").hasClass("fa-toggle-on")) { - $("#severity_filter").selectpicker('deselectAll'); - } else { - $("#severity_filter").val("Debug").change(); - } - }); - }); - $("#flushlog").on('click', function(event){ event.preventDefault(); BootstrapDialog.show({ @@ -159,6 +158,9 @@ $("#filter_container").detach().prependTo('#grid-log-header > .row > .actionBar > .actions'); $(".filter_act").tooltip(); + $("#export-wrapper").detach().appendTo('#grid-log-header > .row > .actionBar > .btn-group'); + $("#exportbtn").tooltip(); + function switch_mode(value) { let select = $("#severity_filter"); let header_val = filter_exact ? m_header : s_header; @@ -229,6 +231,11 @@ +
+ +
@@ -245,14 +252,6 @@ -
- -
diff --git a/src/opnsense/mvc/app/views/OPNsense/Diagnostics/routes.volt b/src/opnsense/mvc/app/views/OPNsense/Diagnostics/routes.volt index 273a19399c..875a398df8 100644 --- a/src/opnsense/mvc/app/views/OPNsense/Diagnostics/routes.volt +++ b/src/opnsense/mvc/app/views/OPNsense/Diagnostics/routes.volt @@ -26,16 +26,19 @@ diff --git a/src/opnsense/www/css/opnsense-bootgrid.css b/src/opnsense/www/css/opnsense-bootgrid.css new file mode 100644 index 0000000000..0e5d89a6b1 --- /dev/null +++ b/src/opnsense/www/css/opnsense-bootgrid.css @@ -0,0 +1,268 @@ + +/** +* Main theming elements +*/ +.tabulator { + font-size: 15px; + background-color: transparent; +} + +/* theme: header */ +.tabulator .tabulator-header { + background-color: transparent; + border-top: 1px solid #373736; /* $table-bg-accent, was #eee */ + border-left: 1px solid #373736; /* $table-bg-accent, was #eee */ + border-right: 1px solid #373736; /* $table-bg-accent, was #eee */ + border-bottom: none; +} +.tabulator .tabulator-header .tabulator-col { + border-right: 1px solid #373736; /* $table-bg-accent, was #e0e0e0 */ + background-color: transparent; + background: transparent; + /* XXX need theme text color */ + color: #ECECEC; /* $text-color */ +} +.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover { + background-color: #373736; /* $table-bg-accent */ +} +.tabulator .tabulator-headers { + height: 100% !important; /* make sure the below border appears */ + border-bottom: 1px solid #aaa; /* separates header from table rows */ +} +/*------------*/ + +/* theme: body */ +.tabulator .tabulator-tableholder .tabulator-table { + background-color: transparent; +} +.tabulator-row { + background-color: transparent; + color: #ECECEC; /* $text-color */ +} + +.tabulator-row.tabulator-row-odd { + background-color: #373736; /* $table-bg-accent, was (#FBFBFB) */ +} +.tabulator-row.tabulator-row-odd.tabulator-selectable:hover:not(.tabulator-selected) { + background-color: #373736; /* $table-bg-accent was (#FBFBFB) */ +} + +.tabulator-row.tabulator-row-even { + background-color: transparent; +} +.tabulator-row.tabulator-row-even.tabulator-selectable:hover:not(.tabulator-selected) { + background-color: transparent; +} +.tabulator .tabulator-tableholder { + background-color: transparent; + border-left: 1px solid #373736; /* $table-bg-accent, was (#eee) */ + border-right: 1px solid #373736; /* $table-bg-accent, was (#eee) */ + /* overflow: scroll; */ + scrollbar-gutter: stable; +} +.tabulator-row.tabulator-selected { + background-color: #9abcea; /* XXX needs new theme */ +} +.tabulator-row.tabulator-selected:hover { + cursor: pointer; + background-color: #9abcea; /* XXX needs new theme */ +} +.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg { + color: #ECECEC; /* spinner icon, $text-color */ +} + +/* Command column styles */ +.tabulator-col.tabulator-frozen.tabulator-frozen-right { + background-color: #101218; /* $body-background was #eee, probably needs new style */ +} +.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right { + background-color: #101218; /* $body-background was #eee, probably needs new style */ +} + +/* Checkbox column styles */ +.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left { + border-right: 1px solid #aaa; /* separates checkbox column from table rows, needs same theme as .tabulator .tabulator-headers */ + border-left: 1px solid #373736; /* $table-bg-accent, was #eee*/ + +} +.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left { + border-right: 1px solid #aaa; /* XXX needs new theme style */ + border-top: 1px solid #373736; /* $table-bg-accent, was #eee*/ + border-bottom: 1px solid #aaa; /* XXX needs new theme style */ + border-left: 1px solid #373736; /* $table-bg-accent, was #eee*/ +} +.tabulator-col.tabulator-row-header.tabulator-frozen.tabulator-frozen-left { + background-color: #101218; /* $body-background was #eee, probably needs new style */ +} +.tabulator-cell.tabulator-row-header.tabulator-frozen.tabulator-frozen-left { + background-color: #101218; /* $body-background was #eee, probably needs new style */ +} +.tabulator-row:last-child > +.tabulator-cell.tabulator-row-header.tabulator-frozen.tabulator-frozen-left:first-child { +} +/*------------*/ + +/* theme: cells */ +.tabulator-row .tabulator-cell { + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.2; + vertical-align: top; + border-bottom: 1px solid #373736; /* $table-bg-accent (was #eee) should probably be something lighter */ + border-right: 1px solid #373736; /* $table-bg-accent (was #eee) should probably be something lighter */ +} +/*-------------*/ + +/* theme: footer */ +.tabulator .tabulator-footer { + background-color: transparent; +} +.tabulator .tabulator-footer .tabulator-page { + background-color: #101218; /* $body-background */ + color: #C03E14; /* $brand-primary */ +} +.tabulator .tabulator-footer .tabulator-page.active { + background-color: #C03E14; /* $brand-primary */ + border-color: #C03E14; /* $brand-primary */ + cursor: default; + color: #101218; /* $body-background */ +} +.tabulator .tabulator-footer .tabulator-page:not(disabled):hover { + color: #7b280d; /* XXX where does this color come from? same in light mode as in dark mode */ + background-color: #242937; /* XXX where does this color come from? light mode: #eeeeee dark mode: #242937 */ + border-color: #ddd; +} +.tabulator .tabulator-footer .tabulator-page:not(disabled).active:hover { + background-color: #C03E14; /* $brand-primary */ + border-color: #C03E14; /* $brand-primary */ + color: #101218; /* $body-background */ + cursor: default; +} +.tabulator-page-counter { + color: #ECECEC; /* $text-color */ +} +.tabulator .tabulator-footer .tabulator-paginator { + color: #ECECEC; /* $text-color */ +} +.tabulator .tabulator-footer .tabulator-page { + border-radius: 0px; + margin: 0px; + padding: 6px 12px; + border: 1px solid #E5E5E5; /* $border-default (was #ddd) */ +} +/*----------*/ + +/* end of main theming elements */ + +.tabulator-paginator { + flex: 0 !important; +} +.tabulator-page-counter { + flex: 1; + text-align: right; +} +.tabulator-col-sorter i { + display: flex; + justify-content: center; + align-items: center; +} + +.tabulator .tabulator-header .tabulator-col { + padding: 5px; +} + +.tabulator .tabulator-headers > :first-child { + padding-left: 10px; +} + + +.opnsense-bootgrid-responsive { + white-space: wrap !important; + text-overflow: inherit !important; + overflow: visible !important; + word-break: break-word !important; +} + +.opnsense-bootgrid-ellipsis { + overflow: hidden !important; + white-space: nowrap !important; + text-overflow: ellipsis !important; +} + + +.tabulator-row > :first-child { + padding-left: 10px; +} + +.tabulator .tabulator-header .tabulator-col .tabulator-col-content { + padding: 0px; +} + +.tabulator-page-counter { + padding-right: 20px; +} + +.bootgrid-footer-commands { + width: 90px; + padding-left: 8px; +} + +/* move border from footer to last row */ +.tabulator .tabulator-footer { + border-top: none; +} + +.tabulator-tableholder::after { + content: ''; + display: block; + width: 1px; + height: 1px; + min-width: 100%; +} + +.tabulator .tabulator-footer .tabulator-paginator .tabulator-page:first-child { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; +} + +.tabulator .tabulator-footer .tabulator-paginator .tabulator-page:last-child { + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; +} + +/* border line corrections and hover/selection behavior */ +.tabulator-row:first-child > .tabulator-cell { + border-top: none; +} + +.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title { + padding: 0px; +} + +/* Row highlight behavior */ +@keyframes highlight { + 0% { + background: #ffff99; + } + 100% { + background: none; + } +} +.highlight-bg { + animation: highlight 2s; +} + +/* Tabulator "loading" and "error" overrides */ +.tabulator .tabulator-alert { + background: none; +} +.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg { + border: none; +} +.tabulator .tabulator-alert .tabulator-alert-msg { + background: initial; + font-size: 18px; +} +.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error { + border: none; +} diff --git a/src/opnsense/www/css/tabulator.min.css b/src/opnsense/www/css/tabulator.min.css new file mode 100644 index 0000000000..474c2c1c8e --- /dev/null +++ b/src/opnsense/www/css/tabulator.min.css @@ -0,0 +1,2 @@ +.tabulator{background-color:#888;border:1px solid #999;font-size:14px;overflow:hidden;position:relative;text-align:left;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}.tabulator[tabulator-layout=fitDataFill] .tabulator-tableholder .tabulator-table{min-width:100%}.tabulator[tabulator-layout=fitDataTable]{display:inline-block}.tabulator.tabulator-block-select,.tabulator.tabulator-ranges .tabulator-cell:not(.tabulator-editing){user-select:none}.tabulator .tabulator-header{background-color:#e6e6e6;border-bottom:1px solid #999;box-sizing:border-box;color:#555;font-weight:700;outline:none;overflow:hidden;position:relative;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;white-space:nowrap;width:100%}.tabulator .tabulator-header.tabulator-header-hidden{display:none}.tabulator .tabulator-header .tabulator-header-contents{overflow:hidden;position:relative}.tabulator .tabulator-header .tabulator-header-contents .tabulator-headers{display:inline-block}.tabulator .tabulator-header .tabulator-col{background:#e6e6e6;border-right:1px solid #aaa;box-sizing:border-box;display:inline-flex;flex-direction:column;justify-content:flex-start;overflow:hidden;position:relative;text-align:left;vertical-align:bottom}.tabulator .tabulator-header .tabulator-col.tabulator-moving{background:#cdcdcd;border:1px solid #999;pointer-events:none;position:absolute}.tabulator .tabulator-header .tabulator-col.tabulator-range-highlight{background-color:#d6d6d6;color:#000}.tabulator .tabulator-header .tabulator-col.tabulator-range-selected{background-color:#3876ca;color:#fff}.tabulator .tabulator-header .tabulator-col .tabulator-col-content{box-sizing:border-box;padding:4px;position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button{padding:0 8px}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-header-popup-button:hover{cursor:pointer;opacity:.6}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title-holder{position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title{box-sizing:border-box;overflow:hidden;text-overflow:ellipsis;vertical-align:bottom;white-space:nowrap;width:100%}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title.tabulator-col-title-wrap{text-overflow:clip;white-space:normal}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-title-editor{background:#fff;border:1px solid #999;box-sizing:border-box;padding:1px;width:100%}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-title .tabulator-header-popup-button+.tabulator-title-editor{width:calc(100% - 22px)}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{align-items:center;bottom:0;display:flex;position:absolute;right:4px;top:0}.tabulator .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:6px solid #bbb;border-left:6px solid transparent;border-right:6px solid transparent;height:0;width:0}.tabulator .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{border-top:1px solid #aaa;display:flex;margin-right:-1px;overflow:hidden;position:relative}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter{box-sizing:border-box;margin-top:2px;position:relative;text-align:center;width:100%}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter textarea{height:auto!important}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter svg{margin-top:3px}.tabulator .tabulator-header .tabulator-col .tabulator-header-filter input::-ms-clear{height:0;width:0}.tabulator .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-right:25px}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover{background-color:#cdcdcd;cursor:pointer}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter{color:#bbb}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{border-bottom:6px solid #555;cursor:pointer}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=none] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:6px solid #bbb;border-top:none}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{border-bottom:6px solid #555;cursor:pointer}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=ascending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:6px solid #666;border-top:none}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter{color:#666}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter.tabulator-col-sorter-element .tabulator-arrow:hover{border-top:6px solid #555;cursor:pointer}}.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort=descending] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow{border-bottom:none;border-top:6px solid #666;color:#666}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical .tabulator-col-content .tabulator-col-title{align-items:center;display:flex;justify-content:center;text-orientation:mixed;writing-mode:vertical-rl}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-col-vertical-flip .tabulator-col-title{transform:rotate(180deg)}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-title{padding-right:0;padding-top:20px}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable.tabulator-col-vertical-flip .tabulator-col-title{padding-bottom:20px;padding-right:0}.tabulator .tabulator-header .tabulator-col.tabulator-col-vertical.tabulator-sortable .tabulator-col-sorter{bottom:auto;justify-content:center;left:0;right:0;top:4px}.tabulator .tabulator-header .tabulator-frozen{left:0;position:sticky;z-index:11}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-left{border-right:2px solid #aaa}.tabulator .tabulator-header .tabulator-frozen.tabulator-frozen-right{border-left:2px solid #aaa}.tabulator .tabulator-header .tabulator-calcs-holder{background:#f3f3f3!important;border-bottom:1px solid #aaa;border-top:1px solid #aaa;box-sizing:border-box;display:inline-block}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row{background:#f3f3f3!important}.tabulator .tabulator-header .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle{display:none}.tabulator .tabulator-header .tabulator-frozen-rows-holder{display:inline-block}.tabulator .tabulator-header .tabulator-frozen-rows-holder:empty{display:none}.tabulator .tabulator-tableholder{-webkit-overflow-scrolling:touch;overflow:auto;position:relative;white-space:nowrap;width:100%}.tabulator .tabulator-tableholder:focus{outline:none}.tabulator .tabulator-tableholder .tabulator-placeholder{align-items:center;box-sizing:border-box;display:flex;justify-content:center;min-width:100%;width:100%}.tabulator .tabulator-tableholder .tabulator-placeholder[tabulator-render-mode=virtual]{min-height:100%}.tabulator .tabulator-tableholder .tabulator-placeholder .tabulator-placeholder-contents{color:#ccc;display:inline-block;font-size:20px;font-weight:700;padding:10px;text-align:center;white-space:normal}.tabulator .tabulator-tableholder .tabulator-table{background-color:#fff;color:#333;display:inline-block;overflow:visible;position:relative;white-space:nowrap}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs{background:#e2e2e2!important;font-weight:700}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-top{border-bottom:2px solid #aaa}.tabulator .tabulator-tableholder .tabulator-table .tabulator-row.tabulator-calcs.tabulator-calcs-bottom{border-top:2px solid #aaa}.tabulator .tabulator-tableholder .tabulator-range-overlay{inset:0;pointer-events:none;position:absolute;z-index:10}.tabulator .tabulator-tableholder .tabulator-range-overlay .tabulator-range{border:1px solid #2975dd;box-sizing:border-box;position:absolute}.tabulator .tabulator-tableholder .tabulator-range-overlay .tabulator-range.tabulator-range-active:after{background-color:#2975dd;border-radius:999px;bottom:-3px;content:"";height:6px;position:absolute;right:-3px;width:6px}.tabulator .tabulator-tableholder .tabulator-range-overlay .tabulator-range-cell-active{border:2px solid #2975dd;box-sizing:border-box;position:absolute}.tabulator .tabulator-footer{background-color:#e6e6e6;border-top:1px solid #999;color:#555;font-weight:700;user-select:none;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;white-space:nowrap}.tabulator .tabulator-footer .tabulator-footer-contents{align-items:center;display:flex;flex-direction:row;justify-content:space-between;padding:5px 10px}.tabulator .tabulator-footer .tabulator-footer-contents:empty{display:none}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs{margin-top:-5px;overflow-x:auto}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab{border:1px solid #999;border-bottom-left-radius:5px;border-bottom-right-radius:5px;border-top:none;display:inline-block;font-size:.9em;padding:5px}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab:hover{cursor:pointer;opacity:.7}.tabulator .tabulator-footer .tabulator-spreadsheet-tabs .tabulator-spreadsheet-tab.tabulator-spreadsheet-tab-active{background:#fff}.tabulator .tabulator-footer .tabulator-calcs-holder{background:#f3f3f3!important;border-bottom:1px solid #aaa;border-top:1px solid #aaa;box-sizing:border-box;overflow:hidden;text-align:left;width:100%}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row{background:#f3f3f3!important;display:inline-block}.tabulator .tabulator-footer .tabulator-calcs-holder .tabulator-row .tabulator-col-resize-handle{display:none}.tabulator .tabulator-footer .tabulator-calcs-holder:only-child{border-bottom:none;margin-bottom:-5px}.tabulator .tabulator-footer>*+.tabulator-page-counter{margin-left:10px}.tabulator .tabulator-footer .tabulator-page-counter{font-weight:400}.tabulator .tabulator-footer .tabulator-paginator{color:#555;flex:1;font-family:inherit;font-size:inherit;font-weight:inherit;text-align:right}.tabulator .tabulator-footer .tabulator-page-size{border:1px solid #aaa;border-radius:3px;display:inline-block;margin:0 5px;padding:2px 5px}.tabulator .tabulator-footer .tabulator-pages{margin:0 7px}.tabulator .tabulator-footer .tabulator-page{background:hsla(0,0%,100%,.2);border:1px solid #aaa;border-radius:3px;display:inline-block;margin:0 2px;padding:2px 5px}.tabulator .tabulator-footer .tabulator-page.active{color:#d00}.tabulator .tabulator-footer .tabulator-page:disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-footer .tabulator-page:not(disabled):hover{background:rgba(0,0,0,.2);color:#fff;cursor:pointer}}.tabulator .tabulator-col-resize-handle{display:inline-block;margin-left:-3px;margin-right:-3px;position:relative;vertical-align:middle;width:6px;z-index:11}@media (hover:hover) and (pointer:fine){.tabulator .tabulator-col-resize-handle:hover{cursor:ew-resize}}.tabulator .tabulator-col-resize-handle:last-of-type{margin-right:0;width:3px}.tabulator .tabulator-col-resize-guide{background-color:#999;height:100%;margin-left:-.5px;opacity:.5;position:absolute;top:0;width:4px}.tabulator .tabulator-row-resize-guide{background-color:#999;height:4px;left:0;margin-top:-.5px;opacity:.5;position:absolute;width:100%}.tabulator .tabulator-alert{align-items:center;background:rgba(0,0,0,.4);display:flex;height:100%;left:0;position:absolute;text-align:center;top:0;width:100%;z-index:100}.tabulator .tabulator-alert .tabulator-alert-msg{background:#fff;border-radius:10px;display:inline-block;font-size:16px;font-weight:700;margin:0 auto;padding:10px 20px}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-msg{border:4px solid #333;color:#000}.tabulator .tabulator-alert .tabulator-alert-msg.tabulator-alert-state-error{border:4px solid #d00;color:#590000}.tabulator-row{background-color:#fff;box-sizing:border-box;min-height:22px;position:relative}.tabulator-row.tabulator-row-even{background-color:#efefef}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selectable:hover{background-color:#bbb;cursor:pointer}}.tabulator-row.tabulator-selected{background-color:#9abcea}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-selected:hover{background-color:#769bcc;cursor:pointer}}.tabulator-row.tabulator-row-moving{background:#fff;border:1px solid #000}.tabulator-row.tabulator-moving{border-bottom:1px solid #aaa;border-top:1px solid #aaa;pointer-events:none;position:absolute;z-index:15}.tabulator-row.tabulator-range-highlight .tabulator-cell.tabulator-range-row-header{background-color:#d6d6d6;color:#000}.tabulator-row.tabulator-range-highlight.tabulator-range-selected .tabulator-cell.tabulator-range-row-header,.tabulator-row.tabulator-range-selected .tabulator-cell.tabulator-range-row-header{background-color:#3876ca;color:#fff}.tabulator-row .tabulator-row-resize-handle{bottom:0;height:5px;left:0;position:absolute;right:0}.tabulator-row .tabulator-row-resize-handle.prev{bottom:auto;top:0}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-row-resize-handle:hover{cursor:ns-resize}}.tabulator-row .tabulator-responsive-collapse{border-bottom:1px solid #aaa;border-top:1px solid #aaa;box-sizing:border-box;padding:5px}.tabulator-row .tabulator-responsive-collapse:empty{display:none}.tabulator-row .tabulator-responsive-collapse table{font-size:14px}.tabulator-row .tabulator-responsive-collapse table tr td{position:relative}.tabulator-row .tabulator-responsive-collapse table tr td:first-of-type{padding-right:10px}.tabulator-row .tabulator-cell{border-right:1px solid #aaa;box-sizing:border-box;display:inline-block;outline:none;overflow:hidden;padding:4px;position:relative;text-overflow:ellipsis;vertical-align:middle;white-space:nowrap}.tabulator-row .tabulator-cell.tabulator-row-header{background:#e6e6e6;border-bottom:1px solid #aaa;border-right:1px solid #999}.tabulator-row .tabulator-cell.tabulator-frozen{background-color:inherit;display:inline-block;left:0;position:sticky;z-index:11}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-right:2px solid #aaa}.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-left:2px solid #aaa}.tabulator-row .tabulator-cell.tabulator-editing{border:1px solid #1d68cd;outline:none;padding:0}.tabulator-row .tabulator-cell.tabulator-editing input,.tabulator-row .tabulator-cell.tabulator-editing select{background:transparent;border:1px;outline:none}.tabulator-row .tabulator-cell.tabulator-validation-fail{border:1px solid #d00}.tabulator-row .tabulator-cell.tabulator-validation-fail input,.tabulator-row .tabulator-cell.tabulator-validation-fail select{background:transparent;border:1px;color:#d00}.tabulator-row .tabulator-cell.tabulator-row-handle{align-items:center;display:inline-flex;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box{width:80%}.tabulator-row .tabulator-cell.tabulator-row-handle .tabulator-row-handle-box .tabulator-row-handle-bar{background:#666;height:3px;margin-top:2px;width:100%}.tabulator-row .tabulator-cell.tabulator-range-selected:not(.tabulator-range-only-cell-selected):not(.tabulator-range-row-header){background-color:#9abcea}.tabulator-row .tabulator-cell .tabulator-data-tree-branch-empty{display:inline-block;width:7px}.tabulator-row .tabulator-cell .tabulator-data-tree-branch{border-bottom:2px solid #aaa;border-bottom-left-radius:1px;border-left:2px solid #aaa;display:inline-block;height:9px;margin-right:5px;margin-top:-9px;vertical-align:middle;width:7px}.tabulator-row .tabulator-cell .tabulator-data-tree-control{align-items:center;background:rgba(0,0,0,.1);border:1px solid #333;border-radius:2px;display:inline-flex;height:11px;justify-content:center;margin-right:5px;overflow:hidden;vertical-align:middle;width:11px}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-data-tree-control:hover{background:rgba(0,0,0,.2);cursor:pointer}}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse{background:transparent;display:inline-block;height:7px;position:relative;width:1px}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{background:#333;content:"";height:1px;left:-3px;position:absolute;top:3px;width:7px}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand{background:#333;display:inline-block;height:7px;position:relative;width:1px}.tabulator-row .tabulator-cell .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{background:#333;content:"";height:1px;left:-3px;position:absolute;top:3px;width:7px}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle{align-items:center;background:#666;border-radius:20px;color:#fff;display:inline-flex;font-size:1.1em;font-weight:700;height:15px;justify-content:center;-moz-user-select:none;-khtml-user-select:none;-webkit-user-select:none;-o-user-select:none;width:15px}@media (hover:hover) and (pointer:fine){.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle:hover{cursor:pointer;opacity:.7}}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-close{display:initial}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle.open .tabulator-responsive-collapse-toggle-open{display:none}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle svg{stroke:#fff}.tabulator-row .tabulator-cell .tabulator-responsive-collapse-toggle .tabulator-responsive-collapse-toggle-close{display:none}.tabulator-row .tabulator-cell .tabulator-traffic-light{border-radius:14px;display:inline-block;height:14px;width:14px}.tabulator-row.tabulator-group{background:#ccc;border-bottom:1px solid #999;border-right:1px solid #aaa;border-top:1px solid #999;box-sizing:border-box;font-weight:700;min-width:100%;padding:5px 5px 5px 10px}@media (hover:hover) and (pointer:fine){.tabulator-row.tabulator-group:hover{background-color:rgba(0,0,0,.1);cursor:pointer}}.tabulator-row.tabulator-group.tabulator-group-visible .tabulator-arrow{border-bottom:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;margin-right:10px}.tabulator-row.tabulator-group.tabulator-group-level-1{padding-left:30px}.tabulator-row.tabulator-group.tabulator-group-level-2{padding-left:50px}.tabulator-row.tabulator-group.tabulator-group-level-3{padding-left:70px}.tabulator-row.tabulator-group.tabulator-group-level-4{padding-left:90px}.tabulator-row.tabulator-group.tabulator-group-level-5{padding-left:110px}.tabulator-row.tabulator-group .tabulator-group-toggle{display:inline-block}.tabulator-row.tabulator-group .tabulator-arrow{border-bottom:6px solid transparent;border-left:6px solid #666;border-right:0;border-top:6px solid transparent;display:inline-block;height:0;margin-right:16px;vertical-align:middle;width:0}.tabulator-row.tabulator-group span{color:#d00;margin-left:10px}.tabulator-toggle{background:#dcdcdc;border:1px solid #ccc;box-sizing:border-box;display:flex;flex-direction:row}.tabulator-toggle.tabulator-toggle-on{background:#1c6cc2}.tabulator-toggle .tabulator-toggle-switch{background:#fff;border:1px solid #ccc;box-sizing:border-box}.tabulator-popup-container{-webkit-overflow-scrolling:touch;background:#fff;border:1px solid #aaa;box-shadow:0 0 5px 0 rgba(0,0,0,.2);box-sizing:border-box;display:inline-block;font-size:14px;overflow-y:auto;position:absolute;z-index:10000}.tabulator-popup{border-radius:3px;padding:5px}.tabulator-tooltip{border-radius:2px;box-shadow:none;font-size:12px;max-width:Min(500px,100%);padding:3px 5px;pointer-events:none}.tabulator-menu .tabulator-menu-item{box-sizing:border-box;padding:5px 10px;position:relative;user-select:none}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-disabled{opacity:.5}@media (hover:hover) and (pointer:fine){.tabulator-menu .tabulator-menu-item:not(.tabulator-menu-item-disabled):hover{background:#efefef;cursor:pointer}}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu{padding-right:25px}.tabulator-menu .tabulator-menu-item.tabulator-menu-item-submenu:after{border-color:#aaa;border-style:solid;border-width:1px 1px 0 0;content:"";display:inline-block;height:7px;position:absolute;right:10px;top:calc(5px + .4em);transform:rotate(45deg);vertical-align:top;width:7px}.tabulator-menu .tabulator-menu-separator{border-top:1px solid #aaa}.tabulator-edit-list{-webkit-overflow-scrolling:touch;font-size:14px;max-height:200px;overflow-y:auto}.tabulator-edit-list .tabulator-edit-list-item{color:#333;outline:none;padding:4px}.tabulator-edit-list .tabulator-edit-list-item.active{background:#1d68cd;color:#fff}.tabulator-edit-list .tabulator-edit-list-item.active.focused{outline:1px solid hsla(0,0%,100%,.5)}.tabulator-edit-list .tabulator-edit-list-item.focused{outline:1px solid #1d68cd}@media (hover:hover) and (pointer:fine){.tabulator-edit-list .tabulator-edit-list-item:hover{background:#1d68cd;color:#fff;cursor:pointer}}.tabulator-edit-list .tabulator-edit-list-placeholder{color:#333;padding:4px;text-align:center}.tabulator-edit-list .tabulator-edit-list-group{border-bottom:1px solid #aaa;color:#333;font-weight:700;padding:6px 4px 4px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-2,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-2{padding-left:12px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-3,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-3{padding-left:20px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-4,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-4{padding-left:28px}.tabulator-edit-list .tabulator-edit-list-group.tabulator-edit-list-group-level-5,.tabulator-edit-list .tabulator-edit-list-item.tabulator-edit-list-group-level-5{padding-left:36px}.tabulator.tabulator-ltr{direction:ltr}.tabulator.tabulator-rtl{direction:rtl;text-align:initial}.tabulator.tabulator-rtl .tabulator-header .tabulator-col{border-left:1px solid #aaa;border-right:initial;text-align:initial}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-col-group .tabulator-col-group-cols{margin-left:-1px;margin-right:0}.tabulator.tabulator-rtl .tabulator-header .tabulator-col.tabulator-sortable .tabulator-col-title{padding-left:25px;padding-right:0}.tabulator.tabulator-rtl .tabulator-header .tabulator-col .tabulator-col-content .tabulator-col-sorter{left:8px;right:auto}.tabulator.tabulator-rtl .tabulator-tableholder .tabulator-range-overlay .tabulator-range.tabulator-range-active:after{background-color:#2975dd;border-radius:999px;bottom:-3px;content:"";height:6px;left:-3px;position:absolute;right:auto;width:6px}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell{border-left:1px solid #aaa;border-right:initial}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-branch{border-bottom-left-radius:0;border-bottom-right-radius:1px;border-left:initial;border-right:2px solid #aaa;margin-left:5px;margin-right:0}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell .tabulator-data-tree-control{margin-left:5px;margin-right:0}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left{border-left:2px solid #aaa}.tabulator.tabulator-rtl .tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-right{border-right:2px solid #aaa}.tabulator.tabulator-rtl .tabulator-row .tabulator-col-resize-handle:last-of-type{margin-left:0;margin-right:-3px;width:3px}.tabulator.tabulator-rtl .tabulator-footer .tabulator-calcs-holder{text-align:initial}.tabulator-print-fullscreen{bottom:0;left:0;position:absolute;right:0;top:0;z-index:10000}body.tabulator-print-fullscreen-hide>:not(.tabulator-print-fullscreen){display:none!important}.tabulator-print-table{border-collapse:collapse}.tabulator-print-table .tabulator-data-tree-branch{border-bottom:2px solid #aaa;border-bottom-left-radius:1px;border-left:2px solid #aaa;display:inline-block;height:9px;margin-right:5px;margin-top:-9px;vertical-align:middle;width:7px}.tabulator-print-table .tabulator-print-table-group{background:#ccc;border-bottom:1px solid #999;border-right:1px solid #aaa;border-top:1px solid #999;box-sizing:border-box;font-weight:700;min-width:100%;padding:5px 5px 5px 10px}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-print-table-group:hover{background-color:rgba(0,0,0,.1);cursor:pointer}}.tabulator-print-table .tabulator-print-table-group.tabulator-group-visible .tabulator-arrow{border-bottom:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #666;margin-right:10px}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-1 td{padding-left:30px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-2 td{padding-left:50px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-3 td{padding-left:70px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-4 td{padding-left:90px!important}.tabulator-print-table .tabulator-print-table-group.tabulator-group-level-5 td{padding-left:110px!important}.tabulator-print-table .tabulator-print-table-group .tabulator-group-toggle{display:inline-block}.tabulator-print-table .tabulator-print-table-group .tabulator-arrow{border-bottom:6px solid transparent;border-left:6px solid #666;border-right:0;border-top:6px solid transparent;display:inline-block;height:0;margin-right:16px;vertical-align:middle;width:0}.tabulator-print-table .tabulator-print-table-group span{color:#d00;margin-left:10px}.tabulator-print-table .tabulator-data-tree-control{align-items:center;background:rgba(0,0,0,.1);border:1px solid #333;border-radius:2px;display:inline-flex;height:11px;justify-content:center;margin-right:5px;overflow:hidden;vertical-align:middle;width:11px}@media (hover:hover) and (pointer:fine){.tabulator-print-table .tabulator-data-tree-control:hover{background:rgba(0,0,0,.2);cursor:pointer}}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse{background:transparent;display:inline-block;height:7px;position:relative;width:1px}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-collapse:after{background:#333;content:"";height:1px;left:-3px;position:absolute;top:3px;width:7px}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand{background:#333;display:inline-block;height:7px;position:relative;width:1px}.tabulator-print-table .tabulator-data-tree-control .tabulator-data-tree-control-expand:after{background:#333;content:"";height:1px;left:-3px;position:absolute;top:3px;width:7px} +/*# sourceMappingURL=tabulator.min.css.map */ \ No newline at end of file diff --git a/src/opnsense/www/js/opnsense_bootgrid.js b/src/opnsense/www/js/opnsense_bootgrid.js new file mode 100644 index 0000000000..34814f8fb1 --- /dev/null +++ b/src/opnsense/www/js/opnsense_bootgrid.js @@ -0,0 +1,1769 @@ +/* + * Copyright (C) 2025 Deciso B.V. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +$.fn.bootgrid = function(option, ...args) { + let ret; + + this.each(function() { + const $el = $(this); + let instance = $el.data('UIBootgrid'); + + if (typeof option === 'object' || option === undefined) { + // Caller instantiated jQuery bootgrid directly, unsupported + return; + } + + if (typeof option === 'string' && instance && typeof instance[option] === 'function') { + // pass legacy bootgrid calls to new UIBootgrid implementation + ret = instance[option](...args); + return false; + } + }); + + return ret !== undefined ? ret : this; +} + +$.fn.UIBootgrid = function(params) { + let id = this.attr('id'); + const $original = this; + // while we have the element, figure out: + // - if we're in a responsive container before removing it + // this determines whether rows should break to a newline or overflow (ellipsis) + let responsive = $original.parents('.table-responsive').length > 0; + if (params?.options?.responsive ?? false) { + responsive = params.options.responsive; + } + (params.options ??= {}).responsive = responsive; + + // - parse the columns + let cols = {}; + let firstHeadRow = $original.find("thead > tr").first(); + firstHeadRow.children().each((i, col) => { + let $col = $(col); + let data = $col.data(); + let width = null; + if (data.width !== undefined && data.width !== '') { + // intentionally ignore data.visible here so there is a value to go to + $col.css({width: data.width}); + width = parseFloat($col.outerWidth()) + 5.0; + } + + cols[data.columnId] = { + data: data, + label: $col.text(), + width: width + } + }) + + const $clone = $original.clone(true, true); + const $replacement = $('
').attr('id', id); + $original.replaceWith($replacement); + + let bg = new UIBootgrid(id).translateCompatOptions(params, $clone, cols).initialize(); + + // store the instance so calls to $.bootgrid(...) are wired to the new implementation + $replacement.data('UIBootgrid', bg); + + return $replacement; +} + +$.fn.UIBootgrid.translations = {}; + +class UIBootgrid { + constructor(id, options = {}, crud = {}, tabulatorOptions = {}) { + // wrapper-specific state variables + this.id = id; + this.$element = $(`#${id}`); + this.table = null; + this.searchPhrase = ""; + this.searchTimer = null; + this.curRowCount = null; + this.navigationRendered = false; + this.originalTableHeight = null; + this.tableInitialized = false; + this.isResizing = false; + this.totalKnown = false; + this.paginationTotal = undefined; + this.persistenceID = `${window.location.pathname}#${this.id}`; + + // wrapper-specific options + this.options = { + sorting: true, + selection: true, + rowCount: [7, 14, 20, 50, 100, true], + remoteGridView: false, // parse gridview from
or via ajax? + formatters: { + ...this._internalFormatters() + }, // formatter callback functions passed to column definitions + headerFormatters: {}, + sorters: { + ...this._internalSorters() + }, + requestHandler: (request) => request, + responseHandler: (response) => response, + datakey: 'uuid', + resetButton: true, + searchSettings: { + delay: 1000 + }, + navigation: true, + ajax: true, + ajaxConfig: { + method: "POST", + dataType: "json", + headers: { + "Content-type": 'application/json;charset=utf8', + } + }, + responsive: false, + onBeforeRenderDialog: null, + addButton: false, + deleteSelectedButton: false, + commands: {}, //additional registered commands + virtualDOM: false, + stickySelect: false, + triggerEditFor: null, + ...options + }; + + this.crud = crud + + // compatibility options mapped to tabulator options through translateCompatOptions() + this.compatOptions = {}; + this.$compatElement = null; + this.compatColumns = null; + + // passed directly to tabulator + this.tabulatorOptions = tabulatorOptions; + this.translations = { + add: 'Add', + deleteSelected: 'Delete selected', + edit: 'Edit', + disable: 'Disabled', + enable: 'Enable', + delete: 'Delete', + info: 'Info', + clone: 'Clone', + all: 'All', + search: 'Search', + removeWarning: 'Remove selected item(s)?', + noresultsfound: 'No results found!', + refresh: 'Refresh', + ...$.fn.UIBootgrid.translations + }; + + // wrapper-specific single version of truth of table layout + this.gridView = null; + } + + initialize() { + if (!localStorage.getItem(`tabulator-${this.persistenceID}-persistence`)) { + // If the user didn't change anything on the table, assume we start blank + Object.keys(localStorage) + .filter(key => key.startsWith(`tabulator-${this.persistenceID}`)) + .forEach(key => localStorage.removeItem(key)); + } + + if (this.options.triggerEditFor) { + this.command_edit(null, this.options.triggerEditFor); + } + + this._parseGridView(); + this._constructTable(); + this._registerEvents(); + this._renderActionBar(); + + return this; + } + + /** + * convert old-style UIBootgrid (and jQuery bootgrid) options. + * The original defaults are already mapped to defaults in tabulatorDefaults() + * + * This function modifies this.compatOptions if options can be directly included in Tabulator. + * Otherwise, this.options is modified for wrapper-specific implementations (i.e. rowCount, requestHandler). + */ + translateCompatOptions(compatOptions, $table, columns) { + this.compatColumns = columns; + this.$compatElement = $table; + let bootGridOptions = compatOptions.options; + + if (!(bootGridOptions?.ajax ?? true)) { + this.options.ajax = false; + // unset all remote facilities + this.compatOptions['sortMode'] = 'local'; + this.compatOptions['paginationMode'] = 'local'; + this.compatOptions['filterMode'] = 'local'; + } + + if (!(bootGridOptions?.selection ?? true)) { + this.compatOptions['selectableRows'] = false; + this.compatOptions['rowHeader'] = null; + } else if (!(bootGridOptions?.multiSelect ?? true)) { + this.compatOptions['selectableRows'] = 1; + } + + if (bootGridOptions?.stickySelect ?? false) { + this.options.stickySelect = true; + } + + if (bootGridOptions?.triggerEditFor ?? null) { + this.options.triggerEditFor = bootGridOptions.triggerEditFor; + } + + // navigation, determines whether actionbar, pagination and footer is rendered + if ((bootGridOptions?.navigation ?? 3) === 0) { + this.options.navigation = false; + } + + if (bootGridOptions?.rowCount ?? false) { + // -1 signified "all" previously. rowCount and paginationSize are handled in this wrapper + this.options.rowCount = bootGridOptions.rowCount.map(item => item === -1 ? true : item); + } + + // we assume a url is unconditional + this.compatOptions['ajaxURL'] = compatOptions?.search; + + if (bootGridOptions?.initialSearchPhrase ?? false) { + this.searchPhrase = bootGridOptions.initialSearchPhrase; + } + + if (bootGridOptions?.ajaxSettings ?? false) { + if (bootGridOptions.ajaxSettings?.contentType) { + this.options.ajaxConfig.headers['Content-type'] = bootGridOptions.ajaxSettings.contentType; + } + + if (bootGridOptions.ajaxSettings?.dataType) { + this.options.ajaxConfig.dataType = bootGridOptions.ajaxSettings.dataType + } + } + + if (bootGridOptions?.requestHandler ?? false) { + this.options.requestHandler = bootGridOptions.requestHandler; + } + + if (bootGridOptions?.responseHandler ?? false) { + this.options.responseHandler = bootGridOptions.responseHandler; + } + + if (bootGridOptions?.searchSettings ?? false) { + if (bootGridOptions.searchSettings?.delay) { + this.options.searchSettings.delay = bootGridOptions.searchSettings.delay; + } + } + + if (compatOptions?.datakey) { + this.options.datakey = compatOptions.datakey; + } + + if (bootGridOptions?.onBeforeRenderDialog) { + this.options.onBeforeRenderDialog = bootGridOptions.onBeforeRenderDialog; + } + + if (!(bootGridOptions?.sorting ?? true)) { + this.options.sorting = false; + } + + if (compatOptions.get) this.crud.get = compatOptions.get; + if (compatOptions.set) this.crud.set = compatOptions.set; + if (compatOptions.add) this.crud.add = compatOptions.add; + if (compatOptions.del) this.crud.del = compatOptions.del; + if (compatOptions.info) this.crud.info = compatOptions.info; + if (compatOptions.toggle) this.crud.toggle = compatOptions.toggle; + + // any additional commands? + if ('commands' in compatOptions) { + this.options.commands = compatOptions.commands; + } + + // check if add / delete buttons are present + let add = this.$compatElement.find("*[data-action=add]"); + let del = this.$compatElement.find("*[data-action=deleteSelected]"); + + if (add.length > 0) { + this.options.addButton = true; + } + + if (del.length > 0) { + this.options.deleteSelectedButton = true; + } + + // convert old-style formatters + for (const [key, formatterFn] of Object.entries(bootGridOptions?.formatters ?? {})) { + this.options.formatters[key] = (cell, formatterParams, onRendered) => { + let def = cell.getColumn().getDefinition(); + let column = { + id: def.field, + visible: def.visible + }; + return formatterFn(column, cell.getData(), onRendered); + } + } + + // convert header formatters + for (const [key, formatterFn] of Object.entries(bootGridOptions?.headerFormatters ?? {})) { + this.options.headerFormatters[key] = (cell, formatterParams, onRendered) => { + let def = cell.getColumn().getDefinition(); + let column = { + id: def.field, + visible: def.visible + }; + return formatterFn(column); + } + } + + // convert old-style converters + // For context: these converters are relevant to have a notion of sorting or localization for column values + // in cases where the backend doesn't do sorting for us (ajax=false). + // The only relevant ones seem to be "datetime", "memsize", "string", "numeric". + // however, there are some overridden ones (IDS, voucher). For these it should be investigated + // whether these require a formatter instead (IDS ajax=true while voucher ajax=false), + // "from" function represents loading a retrieved value (from backend) into the table system in a sortable format + // "to" function represents converting the "from'ed" internal value back to something human-readable (often what the backend returned). + // in all cases where ajax=true, we need to think twice whether we need the converter and should + // apply a formatter instead (data-type="datetime" for ca.volt for example) + if (this.options.ajax && 'converters' in bootGridOptions) { + for (const [key, converter] of Object.entries(bootGridOptions.converters)) { + console.error(`Converter "${key}" should be a formatter`); + } + } + + + // Detect if old bootgrid was of 'responsive' type, meaning: + // - overflow: inherit !important + // - white-space : inherit !important + // + // which allows for newlines in the table (such as log files) + // + // otherwise: + // - text-overflow: ellipsis; + // - white-space: nowrap + // - overflow: hidden + + this.options.responsive = bootGridOptions?.responsive ?? false; + + // We set the virtualDOM rendering mode off by default, + // but if there are pages where we expect this will speed up rendering a lot, + // those pages can set 'virtualDOM: true' to force this (i.e. log pages). + // These pages must implement any event handlers for cells + // in the formatter, not on the "loaded" event. If such elements include the 'bootgrid-tooltip' class, + // tooltips will automatically be activated when dynamically inserted into the DOM. If such elements + // include any of the 'command-*' classes, these elements will be linked to the default commands + this.options.virtualDOM = bootGridOptions?.virtualDOM ?? false; + if (this.options.virtualDOM) { + this.compatOptions['renderVertical'] = 'virtual'; + } + + return this; + } + + normalizeRowHeight() { + this.table.getRows().forEach(row => { + row.normalizeHeight(); + }); + } + + _parseGridView() { + let result = {}; + if (this.options.remoteGridView) { + throw new Error('fetching remote grid view not yet implemented'); + } else { + if (!this.$compatElement || !this.compatColumns) { + throw new Error('unable to parse table headers, no table element or column structure provided'); + } + + for (const [colId, val] of Object.entries(this.compatColumns)) { + let data = val.data; + + for (const [option, value] of Object.entries(data)) { + data[option] = value === '' ? null : value; + } + + if (data.type && this.options.ajax && data.type in this.options.formatters && data.type !== 'boolean') { + // use formatters instead of converters + data.formatter = data.type; + } + + result[colId] = { + id: colId, + label: val.label, + style: data.cssClass ?? '', + type: data.type ?? 'text', + formatter: data.formatter ?? null, + headerFormatter: data.headerFormatter || + !(Object.getOwnPropertyNames(Object.prototype).includes(data.columnId)) && + data.columnId in this.options.headerFormatters ? + data.columnId : null, + visible: data.visible ?? true, + sequence: data.sequence ?? null, + width: val.width, + editable: false, + } + } + } + + this.gridView = result; + } + + _translate(key, ctx = {}) { + // resolve (formatted) translations + let template = this.translations[key]; + if (!template) { + console.error('No translation found for key ' + key); + return; + } + return template.replace(/{{ctx\.(\w+)}}/g, (_, key) => key in ctx ? ctx[key] : ''); + } + + /** + * + * @returns array of column objects in tabulator-format based on this.gridView and this.options + */ + _parseColumns() { + let result = []; + + for (const [key, field] of Object.entries(this.gridView)) { + let col = {}; + if (field.id === 'commands') { + col = { + visible: field.visible, + formatter: this.options.formatters['commands'] ?? null, + title: field.label, + resizable: false, + sequence: field.sequence ?? null, + frozen: true, + headerSort: false, + headerHozAlign: "center", + // hozAlign: 'center', + } + + if (!field.width) { + field.width = '100'; + } + } else { + col = { + visible: field.visible, + editable: field.editable, + // XXX passes unsanitized HTML, which may be of concern if the cell is editable in the future + formatter: this.options.formatters[field?.formatter] ?? this.options.formatters['default'], + title: field.label, + titleFormatter: this.options.headerFormatters[field?.headerFormatter] ?? null, + field: field.id, + resizable: true, + sequence: field.sequence ?? null, + headerSort: this.options.sorting, + cssClass: this.options.responsive ? 'opnsense-bootgrid-responsive' : '', + variableHeight: true, + sorter: this.options.sorters[field.type] ?? null, + } + } + + if (field.width && !col.resizable) { + // lock the width in place + col['minWidth'] = field.width; + col['maxWidth'] = field.width; + } else if (field.width) { + col['width'] = field.width; + } + + result.push(col); + } + + result.sort((a, b) => { + if (a.sequence == null && b.sequence == null) return 0; + if (a.sequence == null) return 1; + if (b.sequence == null) return -1; + return a.sequence - b.sequence; + }); + + result.forEach(item => delete item.sequence); + + return result; + } + + _constructTable() { + this.table = new Tabulator(`#${this.id}`, { + ...this.tabulatorDefaults(), + + /* compatibility options */ + ...this.compatOptions, + + /* custom passed options */ + ...this.tabulatorOptions + }); + } + + _destroyTable() { + this.table.destroy(); + } + + _setPersistence(value) { + if (value) { + localStorage.setItem(`tabulator-${this.persistenceID}-persistence`, value); + } else { + localStorage.removeItem(`tabulator-${this.persistenceID}-persistence`); + } + } + + _registerEvents() { + this.table.on('dataLoading', () => { + if (!this.navigationRendered) { + this._renderFooter(); + this._populateColumnSelection(); + this.navigationRendered = true; + } + }); + + this.table.on('dataLoading', () => { + // Dynamically adjust table height to prevent dead space + // (workaround for https://github.com/olifolkerd/tabulator/issues/4419: maxHeight does not work without a fixed height) + if (!this.originalTableHeight) { + // this.originalTableHeight = parseInt(parseInt(this.table.options.height) * window.innerHeight / 100); + // allow content to grow to 60vh + // XXX needs option + this.originalTableHeight = parseInt(parseInt(60) * window.innerHeight / 100); + } + + const resizeObserver = new ResizeObserver(this._debounce((entries) => { + for (let entry of entries) { + const height = entry.contentRect.height; + const width = entry.contentRect.width; + const scollbarGutterOffset = 16; + let curTotalTableHeight = $(`#${this.id}`)[0].offsetHeight; + const holderHeight = $(`#${this.id} .tabulator-tableholder`)[0].offsetHeight; + + if (holderHeight > height) { + // dead space, shrink + const diff = holderHeight - height; + this.table.setHeight((curTotalTableHeight - diff) + scollbarGutterOffset); + return; + } + + if (height > holderHeight) { + const diff = height - holderHeight; + const equal = curTotalTableHeight === this.originalTableHeight; + const was = curTotalTableHeight; + curTotalTableHeight = curTotalTableHeight + diff; + if (was < this.originalTableHeight && curTotalTableHeight > this.originalTableHeight) { + // max height reached, set it explicitly in case we're coming from a smaller size + this.table.setHeight(this.originalTableHeight + scollbarGutterOffset); + this.table.redraw(); + return; + } + + if (curTotalTableHeight < this.originalTableHeight) { + // we can grow + this.table.setHeight(curTotalTableHeight + scollbarGutterOffset); + this.table.redraw(); + return; + } + } + } + })); + + resizeObserver.observe($(`#${this.id} .tabulator-table`)[0]); + + window.onresize = this._debounce(() => { + // this is mainly intended for scaling the width of the table if + // the width of the window changes. + this.table.redraw(); + }); + + if (this.options.virtualDOM) { + // Start watching for dynamically inserted DOM elements and trigger their tooltips and commands. + // XXX This has a slight performance penalty on virtual DOM scrolling for large datasets. + let targetNode = $(`#${this.id} .tabulator-table`)[0]; + var observer = new MutationObserver((mutationsList, observer) => { + mutationsList.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === 1 && $(node).hasClass('tabulator-row')) { + let commands = $(node).find('.bootgrid-tooltip'); + if (commands.length > 0) { + this._tooltips(); + } + + let actions = $(node).find('[class*="command-"]'); + if (actions.length > 0) { + actions.each((i, el) => { + let classes = $(el).attr('class').split(/\s+/); + let commandClass = classes.find(c => c.startsWith('command-')); + if (commandClass) { + let command = commandClass.replace('command-', ''); + this._wireCommands($(el), command); + } + }) + } + } + }); + }); + }); + + observer.observe(targetNode, { + childList: true, + subtree: false + }); + } + + if (!this.options.ajax) { + this.table.setPageSize(this.curRowCount); + + this.table.on("pageLoaded", (pageno) => { + this._onDataProcessed(); + }) + } + + // make sure we redraw the table as it enters the viewport (multiple tabbed grids) + // since tabulator needs the page dimensions + const intersectObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + this.table.redraw(true); + this._onDataProcessed(); + } + }); + }); + + intersectObserver.observe(this.$element[0]); + + this.tableInitialized = true; + }); + + this.table.on('dataProcessed', () => { + this._onDataProcessed(); + + // Check if the total amount of rows is known, if not, remove the "last page" + if (!this.totalKnown && this.options.ajax) { + $(`#${this.id} .tabulator-paginator button[data-page=last]`).remove(); + } + }); + + this.table.on('dataChanged', this._debounce(() => { + // debounce this so we catch the correct event if + // data has been added (this event fires in rapid succession in this case, + // but doesn't trigger dataProcessed at the end) + this._onDataProcessed(); + })); + + this.table.on('cellMouseEnter', (e, cell) => { + // tooltip when ellipsis is used (overflow on text elements without children) + let el = cell.getElement(); + let $el = $(el); + if (el.offsetWidth < el.scrollWidth && !$el.attr('title') && $el.children().length == 0){ + $el.attr('title', $el.text()).tooltip({container: 'body', trigger: 'hover'}).tooltip('show'); + } + }); + + this.table.on('rowSelected', (row) => { + this.$element.trigger("selected.rs.jquery.bootgrid", [this.table.getSelectedData()]); + }); + + this.table.on('rowDeselected', (row) => { + this.$element.trigger("deselected.rs.jquery.bootgrid", [[row.getData()]]); + }); + + this.table.on("rowSelectionChanged", this._debounce((data, rows, selected, deselected) => { + // XXX debouncing this is a bit of a hack, but this function is run + // for both the selection & deselection, while we only want to know + // the last known action. + if (this.options.stickySelect && data.length == 0) { + this.table.selectRow(deselected[0].getData()[this.options.datakey]); + } + })); + + + // Triggers to activate persistence + this.table.on('columnResized', (column) => { + this._setPersistence(true); + }); + this.table.on('headerClick', (e, column) => { + if (this.options.sorting) { + this._setPersistence(true); + } + }); + this.table.on('columnVisibilityChanged', (column, visible) => { + this._setPersistence(true); + }); + this.table.on('columnMoved', (column, columns) => { + this._setPersistence(true); + }); + } + + _renderFooter() { + if (!this.options.navigation) { + $(`#${this.id} > .tabulator-footer`).remove(); + return; + } + + this._renderFooterCommands(); + + // swap page counter and paginator around (old look & feel). + // we hook in before tableBuilt, but after dataLoading + // since we know the footer is rendered at this point, + // but we don't want to wait on the data before swapping + // the UI elements around. + + // XXX this doesn't work on the captive portal page. why? + let a = $(`#${this.id} .tabulator-page-counter`)[0]; + let b = $(`#${this.id} .tabulator-paginator`)[0]; + var aparent = a.parentNode; + var asibling = a.nextSibling === b ? a : a.nextSibling; + b.parentNode.insertBefore(a, b); + aparent.insertBefore(b, asibling); + + // replace pagination text + $(`#${this.id} .tabulator-paginator button[data-page=first]`).html("«"); + $(`#${this.id} .tabulator-paginator button[data-page=prev]`).html("‹"); + $(`#${this.id} .tabulator-paginator button[data-page=next]`).html("›"); + $(`#${this.id} .tabulator-paginator button[data-page=last]`).html("»"); + } + + _onDataProcessed() { + // refresh tooltips + if (!this.options.virtualDOM) { + this._tooltips(); + } + + this.normalizeRowHeight(); + + if (this.options.virtualDOM) { + // redraw here to prevent pagination switches from breaking the virtualdom rendering process + this.table.redraw(); + } + + // DOM layout changed, rewire commands + this._wireCommands(); + + // backwards compat + this.$element.trigger("loaded.rs.jquery.bootgrid"); + } + + _tooltips() { + this.$element.find(".bootgrid-tooltip").each((index, el) => { + if ($(el).attr('title') !== undefined) { + // keep this tooltip + } else if ($(el).hasClass('command-add')) { + $(el).attr('title', this._translate('add')); + } else if ($(el).hasClass('command-delete-selected')) { + $(el).attr('title', this._translate('deleteSelected')); + } else if ($(el).hasClass('command-edit')) { + $(el).attr('title', this._translate('edit')); + } else if ($(el).hasClass('command-toggle')) { + if ($(el).data('value') === 1) { + $(el).attr('title', this._translate('disable')); + } else { + $(el).attr('title', this._translate('enable')); + } + } else if ($(el).hasClass('command-delete')) { + $(el).attr('title', this._translate('delete')); + } else if ($(el).hasClass('command-info')) { + $(el).attr('title', this._translate('info')); + } else if ($(el).hasClass('command-copy')) { + $(el).attr('title', this._translate('clone')); + } else { + $(el).attr('title', 'Error: no tooltip match'); + } + $(el).tooltip({container: 'body', trigger: 'hover'}); + }); + } + + _wireCommands($selector=null, command=null) { + const commands = this._getCommands(); + Object.keys(commands).map((k) => { + let has_option = true; + for (let i = 0; i < commands[k]['requires'].length; i++) { + if (!(commands[k]['requires'][i] in this.crud)) { + has_option = false; + } + } + if (has_option) { + // in both cases, make sure to stop the event from bubbling up + // to the parent so no handlers on parent containers are executed + if ($selector && command && command === k) { + $selector.unbind('click').on("click", function (event) { + event.stopPropagation(); + commands[k].method.bind(this)(event); + }); + } else { + this.$element.find(".command-" + k).unbind('click').on("click", function (event) { + event.stopPropagation(); + commands[k].method.bind(this)(event); + }); + } + } else if ($(".command-" + k).length > 0) { + console.log("not all requirements met to link " + k); + } + }); + } + + _renderActionBar() { + if (!this.options.navigation) { + return; + } + + this.$element.before($(this._getHeader())); + + // search functionality + $(`#${this.id}-search-field`).val(this.searchPhrase).on("keyup", (e) => { + e.stopPropagation(); + let searchVal = $(`#${this.id}-search-field`).val(); + if (this.searchPhrase !== searchVal || (e.which === 13 && searchVal !== "")) { + this.searchPhrase = searchVal; + if (e.which === 13 || searchVal.length === 0 || searchVal.length >= 1) { + if (this.options.ajax) { + window.clearTimeout(this.searchTimer); + this.searchTimer = window.setTimeout(() => { + this._reload(); + //this.table.setFilter('searchPhrase', '=', searchVal); + }, this.options.searchSettings.delay); + } else { + this.table.setFilter((data, filterParams) => { + return Object.values(data).some( + val => typeof val === 'string' && val.toLowerCase().includes(this.searchPhrase.toLowerCase()) + ) + }); + } + } + } else if (searchVal === "" && !this.options.ajax) { + this.table.clearFilter(); + } + }); + + // Refresh + if (this.options.ajax) { + $(`#${this.id}-refresh-button`).click(() => { + // XXX This refreshes the data, but doesn't trigger a loading screen + this._reload(); + }); + } else { + $(`#${this.id}-refresh-button`).remove(); + } + + // Rowcount + this.curRowCount = localStorage.getItem(`${this.persistenceID}-rowCount`) || this.options.rowCount[0]; + if (this.curRowCount === 'true') { + this.curRowCount = true; + } + $(`#${this.id}-rowcount-text`).text(this.curRowCount === true ? this._translate('all') : this.curRowCount); + + this.options.rowCount.forEach((count) => { + let item = $(` +
  • + ${count === true ? this._translate('all') : count} +
  • + `).on('click', (e) => { + e.preventDefault(); + + let target = $(e.currentTarget); + let newRowCount = target.data('action'); + + if (newRowCount !== this.curRowCount) { + this.curRowCount = newRowCount; + if (!this.options.ajax) { + this.table.curRowCount = this.curRowCount; + } + localStorage.setItem(`${this.persistenceID}-rowCount`, this.curRowCount); + this.table.setPageSize(newRowCount); + + $(`#${this.id}-rowcount-text`).text(newRowCount === true ? this._translate('all') : newRowCount); + + $.each($(`#${this.id}-rowcount-items li`), (i, value) => { + let $li = $(value); + let thisRowCount = $li.data('action'); + thisRowCount.toString() === this.curRowCount.toString() ? + $li.addClass("active") && $li.attr('aria-selected', 'true') : + $li.removeClass("active") && $li.attr('aria-selected', 'false') + }); + } + + }); + $(`#${this.id}-rowcount-items`).append(item); + }); + + // Column selection handled after data load (see registerEvents) + + // Reset button + if (this.options.resetButton) { + let $resetBtn = $(` + + `).on('click', (e) => { + e.stopPropagation(); + Object.keys(localStorage) + .filter(key => key.startsWith(`tabulator-${this.persistenceID}`) || key.startsWith(this.persistenceID)) + .forEach(key => localStorage.removeItem(key)); + this._setPersistence(false); + location.reload(); + }); + + $(`#${this.id}-actions-group`).append($resetBtn); + } + } + + _renderFooterCommands() { + if (!this.options.navigation) { + return; + } + + let $footer = $(`#${this.id} > .tabulator-footer > .tabulator-footer-contents > .tabulator-paginator`); + let $commandContainer = $('