www/caddy: Implement tabulator groupBy, fix tabulator 'Data Load Response Blocked' warning, remove unnecessary HTML (#4909)

* www/caddy: Implement tabulator groupBy into subdomain and handlers tabs, modernize style and html

* www/caddy: Fix search endpoints being fired multiple times on initial page load, and when using the command buttons. This fixes some tabulator warnings and improves performance.

* www/caddy: Bump version to 2.0.3 and add changelog
This commit is contained in:
Monviech
2025-08-23 15:34:23 +02:00
committed by GitHub
parent bc94cb0298
commit e0897e1430
7 changed files with 115 additions and 117 deletions
+1 -2
View File
@@ -1,6 +1,5 @@
PLUGIN_NAME= caddy
PLUGIN_VERSION= 2.0.2
PLUGIN_REVISION= 2
PLUGIN_VERSION= 2.0.3
PLUGIN_DEPENDS= caddy-custom
PLUGIN_COMMENT= Modern Reverse Proxy with Automatic HTTPS, Dynamic DNS and Layer4 Routing
PLUGIN_MAINTAINER= cedrik@pischem.com
+6
View File
@@ -6,6 +6,12 @@ DOC: https://docs.opnsense.org/manual/how-tos/caddy.html
Plugin Changelog
================
2.0.3
Add: Tabulator groupBy of domain and subdomain (opnsense/plugins/pull/4909)
Fix: Tabulator 'Data Load Response Blocked' warning
Cleanup: Grid HTML and style
2.0.2
Add: Global server timeout options (opnsense/plugins/pull/4778)
@@ -5,7 +5,7 @@
<type>checkbox</type>
<help><![CDATA[Enable this handler.]]></help>
<grid_view>
<width>100</width>
<width>20</width>
<type>boolean</type>
<formatter>rowtoggle</formatter>
</grid_view>
@@ -19,12 +19,18 @@
<label>Domain</label>
<type>dropdown</type>
<help><![CDATA[Select a domain to handle.]]></help>
<grid_view>
<visible>false</visible>
</grid_view>
</field>
<field>
<id>handle.subdomain</id>
<label>Subdomain</label>
<type>dropdown</type>
<help><![CDATA[Select a subdomain to handle. Make sure to additionaly choose a wildcard domain as "Domain". Leave unset, if not using subdomains.]]></help>
<grid_view>
<visible>false</visible>
</grid_view>
</field>
<field>
<type>header</type>
@@ -5,7 +5,7 @@
<type>checkbox</type>
<help><![CDATA[Enable this Layer4 route.]]></help>
<grid_view>
<width>100</width>
<width>20</width>
<type>boolean</type>
<formatter>rowtoggle</formatter>
</grid_view>
@@ -5,7 +5,7 @@
<type>checkbox</type>
<help><![CDATA[Enable this domain.]]></help>
<grid_view>
<width>100</width>
<width>20</width>
<type>boolean</type>
<formatter>rowtoggle</formatter>
</grid_view>
@@ -5,7 +5,7 @@
<type>checkbox</type>
<help><![CDATA[Enable this subdomain.]]></help>
<grid_view>
<width>100</width>
<width>20</width>
<type>boolean</type>
<formatter>rowtoggle</formatter>
</grid_view>
@@ -27,7 +27,6 @@
<id>subdomain.FromDomain</id>
<label>Subdomain</label>
<type>text</type>
<hint>opn.example.com</hint>
<help><![CDATA[Enter a subdomain. For example, "opn.example.com" if the wildcard domain is "*.example.com". All subdomains use the same ports and protocols as their parent wildcard domain.]]></help>
</field>
<field>
@@ -26,11 +26,6 @@
<script>
$(document).ready(function() {
// Update the URL hash when tabs are clicked
if (location.hash) {
$(`#maintabs a[href="${location.hash}"]`).tab('show');
}
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
const hash = e.target.hash;
if (history.replaceState) {
@@ -41,12 +36,18 @@
});
$(window).on('hashchange', function () {
$(`#maintabs a[href="${location.hash}"]`).tab('show');
const $targetTab = $(`#maintabs a[href="${location.hash}"]`);
if ($targetTab.length) {
$targetTab.tab('show');
}
});
// Bootgrid Setup
const all_grids = {};
// block change-driven grid reloads during programmatic updates of reverseFilter selectpicker
let suppressFilterReload = false;
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
let grid_ids = [];
@@ -96,17 +97,32 @@
if (["{{ formGridReverseProxy['table_id'] }}", "{{ formGridSubdomain['table_id'] }}"].includes(grid_id)) {
const update_filter = function (selectValues) {
$('#reverseFilter')
// Refresh selectpicker with latest data so button always works even on new domains
suppressFilterReload = true;
return $('#reverseFilter')
.fetch_options('/api/caddy/reverse_proxy/get_all_reverse_domains')
.done(function () {
$('#reverseFilter')
.selectpicker('val', selectValues)
.selectpicker('refresh')
.trigger('change');
});
.selectpicker('refresh');
$('#maintabs a[href="#handlers"]').tab('show');
// manually update icon (since we skip .change())
const hasSelection = Array.isArray(selectValues) && selectValues.length > 0;
$('#reverseFilterIcon')
.toggleClass('text-success fa-filter-circle-xmark', hasSelection)
.toggleClass('fa-filter', !hasSelection);
$('#maintabs a[href="#handlers"]').tab('show');
// manually reload just the handlers grid
const grid_id = "{{ formGridHandle['table_id'] }}";
if (all_grids[grid_id]) {
all_grids[grid_id].bootgrid('reload');
}
})
.always(function () {
suppressFilterReload = false;
});
};
commands.search_handler = {
@@ -132,7 +148,6 @@
// Ensure selectpicker has values selected before click on add button
$('#reverseFilter').one('changed.bs.select', function (e) {
$("#" + "{{ formGridHandle['table_id'] }}")
.closest('.bootgrid-box')
.find("button[data-action='add']")
.trigger('click');
});
@@ -157,6 +172,7 @@
}
{% endif %}
all_grids[grid_id] = $("#" + grid_id)
.UIBootgrid({
search: `/api/caddy/reverse_proxy/search_${grid_id}/`,
@@ -165,7 +181,35 @@
add: `/api/caddy/reverse_proxy/add_${grid_id}/`,
del: `/api/caddy/reverse_proxy/del_${grid_id}/`,
toggle: `/api/caddy/reverse_proxy/toggle_${grid_id}/`,
{% if entrypoint == 'reverse_proxy' %}
tabulatorOptions: {
groupBy: grid_id === "{{ formGridHandle['table_id'] }}"
? ["%reverse", "%subdomain"]
: (grid_id === "{{ formGridSubdomain['table_id'] }}" ? "%reverse" : false),
groupHeader: (value, count, data, group) => {
const isNested = group.getParentGroup() !== false;
if (isNested && !value) {
return ''; // remove empty subdomain group headers
}
const iconClass = isNested
? 'text-warning'
: 'text-success';
const countValue = `<span class="badge chip">${count}</span>`;
return `<i class="fa fa-fw fa-globe ${iconClass}"></i> ${value} ${countValue}`;
},
},
{% endif %}
options: {
responsive: true,
rowCount: [20,50,200,500,1000,-1],
requestHandler: function (request) {
const selectedDomains = $('#reverseFilter').val();
if (selectedDomains && selectedDomains.length > 0) {
@@ -174,6 +218,9 @@
return request;
},
headerFormatters: {
enabled: function (column) {
return '<i class="fa-solid fa-fw fa-check-square" data-toggle="tooltip" title="{{ lang._('Enabled') }}"></i>';;
},
ToDomain: function (column) { return labels.upstream; },
FromDomain: function (column) {
if (grid_id === "subdomain") {
@@ -209,9 +256,6 @@
},
commands: commands
});
$("#" + grid_id).wrap('<div class="bootgrid-box"></div>');
}
{% if entrypoint == 'reverse_proxy' %}
@@ -221,8 +265,7 @@
const header = $("#" + grid_id + "-header");
const $actionBar = header.find('.actionBar');
if ($actionBar.length) {
$('#add_filter_container').detach().insertBefore($actionBar.find('.search'));
$('#add_filter_container').show();
$('#add_filter_container').detach().insertBefore($actionBar.find('.search')).show();
}
}
@@ -289,6 +332,7 @@
// Safe reload on filter change, ensures all grids are initalized beforehand
$('#reverseFilter').change(function () {
if (suppressFilterReload) return;
Object.keys(all_grids).forEach(function (grid_id) {
if ([
'{{ formGridReverseProxy["table_id"] }}',
@@ -386,61 +430,38 @@
$("#handle\\.reverse").selectpicker("refresh");
});
// Trigger bootgrid setup for handlers tab too (even if not active) to ensure command buttons always work
$('a[href="#handlers"]').trigger('shown.bs.tab');
// Trigger initial tab
const $tabs = $('#maintabs a[data-toggle="tab"]');
const $initial = $tabs.filter(`[href="${location.hash}"]`).first();
($initial.length ? $initial : $tabs.first()).tab('show');
// Trigger handlers tab too (even if not active) to ensure command buttons always work
$(document).one('ajaxStop', function () {
$('a[href="#handlers"]').triggerHandler('shown.bs.tab');
});
updateServiceControlUI('caddy');
$('<div id="messageArea" class="alert alert-info" style="display: none;"></div>').insertBefore('#change_message_base_form');
$('a[data-toggle="tab"].active, #maintabs li.active a').trigger('shown.bs.tab');
});
</script>
<style>
#add_filter_container {
margin-left: 10px;
margin-right: 20px;
}
#add_domain_container {
float: left;
}
#add_handle_container {
margin-left: 10px;
float: left;
}
.actionBar {
padding-left: 0px;
}
.custom-header {
font-weight: 800;
font-size: 16px;
font-style: italic;
}
/* Prevent bootgrid to break out of content box*/
.content-box {
overflow-x: auto;
}
.bootgrid-header,
.bootgrid-box,
.bootgrid-footer {
width: 100%;
background: none;
border: none;
max-width: 100%;
/* Prevents the grid from collapsing all dynamic columns completely */
min-width: 700px;
}
/* Not all dropdowns support data-container="body", ensure minimal vertical space for them */
.bootgrid-box {
min-height: 150px;
}
/* Limit size of grid dropdown */
.actions .dropdown-menu.pull-right {
max-height: 200px;
max-height: 400px;
min-width: max-content;
overflow-y: auto;
overflow-x: hidden;
}
.custom-header {
padding-left: 20px;
font-weight: 800;
font-size: 16px;
font-style: italic;
}
#reverseFilterClear {
border-right: none;
}
@@ -454,7 +475,7 @@
<button type="button" id="reverseFilterClear" class="btn btn-default" title="{{ lang._('Clear Selection') }}">
<i id="reverseFilterIcon" class="fa fa-fw fa-filter"></i>
</button>
<select id="reverseFilter" class="selectpicker form-control" multiple data-live-search="true" data-width="200px" data-size="10" data-container="body" title="{{ lang._('Filter by Domain') }}">
<select id="reverseFilter" class="selectpicker form-control" multiple data-live-search="true" data-width="300px" data-size="10" data-container="body" title="{{ lang._('Filter by Domain') }}">
</select>
</div>
@@ -462,14 +483,14 @@
{% if entrypoint == 'reverse_proxy' %}
<li id="tab-domains" class="active"><a data-toggle="tab" href="#domains">{{ lang._('Domains') }}</a></li>
<li id="tab-domains"><a data-toggle="tab" href="#domains">{{ lang._('Domains') }}</a></li>
<li id="tab-handlers"><a data-toggle="tab" href="#handlers">{{ lang._('Handlers') }}</a></li>
<li id="tab-access"><a data-toggle="tab" href="#access">{{ lang._('Access') }}</a></li>
<li id="tab-headers"><a data-toggle="tab" href="#headers">{{ lang._('Headers') }}</a></li>
{% elseif entrypoint == 'layer4' %}
<li id="tab-layer4" class="active"><a data-toggle="tab" href="#routes">{{ lang._('Layer4 Routes') }}</a></li>
<li id="tab-layer4"><a data-toggle="tab" href="#routes">{{ lang._('Layer4 Routes') }}</a></li>
<li id="tab-matcher"><a data-toggle="tab" href="#matchers">{{ lang._('Layer7 Matcher Settings') }}</a></li>
{% endif %}
@@ -482,83 +503,50 @@
<!-- Combined Domains Tab -->
<div id="domains" class="tab-pane fade in active">
<div style="padding-left: 16px;">
<!-- Reverse Proxy -->
<h1 class="custom-header">{{ lang._('Domains') }}</h1>
<div style="display: block;">
{{ partial('layout_partials/base_bootgrid_table', formGridReverseProxy + {'command_width': '160'})}}
</div>
</div>
<!-- Reverse Proxy -->
<h1 class="custom-header">{{ lang._('Domains') }}</h1>
{{ partial('layout_partials/base_bootgrid_table', formGridReverseProxy + {'command_width': '160'})}}
<!-- Subdomains Tab -->
<div style="padding-left: 16px;">
<h1 class="custom-header">{{ lang._('Subdomains') }}</h1>
<div style="display: block;">
{{ partial('layout_partials/base_bootgrid_table', formGridSubdomain + {'command_width': '160'})}}
</div>
</div>
<h1 class="custom-header">{{ lang._('Subdomains') }}</h1>
{{ partial('layout_partials/base_bootgrid_table', formGridSubdomain + {'command_width': '160'})}}
</div>
<!-- Handle Tab -->
<div id="handlers" class="tab-pane fade">
<div style="padding-left: 16px;">
<h1 class="custom-header">{{ lang._('Handlers') }}</h1>
<div style="display: block;">
{{ partial('layout_partials/base_bootgrid_table', formGridHandle)}}
</div>
</div>
<h1 class="custom-header">{{ lang._('Handlers') }}</h1>
{{ partial('layout_partials/base_bootgrid_table', formGridHandle)}}
</div>
<!-- Combined Access Tab -->
<div id="access" class="tab-pane fade">
<!-- Access Lists Section -->
<div style="padding-left: 16px;">
<h1 class="custom-header">{{ lang._('Access Lists') }}</h1>
<div style="display: block;">
{{ partial('layout_partials/base_bootgrid_table', formGridAccessList)}}
</div>
</div>
<h1 class="custom-header">{{ lang._('Access Lists') }}</h1>
{{ partial('layout_partials/base_bootgrid_table', formGridAccessList)}}
<!-- Basic Auth Section -->
<div style="padding-left: 16px;">
<h1 class="custom-header">{{ lang._('Basic Auth') }}</h1>
<div style="display: block;">
{{ partial('layout_partials/base_bootgrid_table', formGridBasicAuth)}}
</div>
</div>
<h1 class="custom-header">{{ lang._('Basic Auth') }}</h1>
{{ partial('layout_partials/base_bootgrid_table', formGridBasicAuth)}}
</div>
<!-- Header Tab -->
<div id="headers" class="tab-pane fade">
<div style="padding-left: 16px;">
<h1 class="custom-header">{{ lang._('Headers') }}</h1>
<div style="display: block;">
{{ partial('layout_partials/base_bootgrid_table', formGridHeader)}}
</div>
</div>
<h1 class="custom-header">{{ lang._('Headers') }}</h1>
{{ partial('layout_partials/base_bootgrid_table', formGridHeader)}}
</div>
{% elseif entrypoint == 'layer4' %}
<!-- Layer4 Tab -->
<div id="routes" class="tab-pane fade active in">
<div style="padding-left: 16px;">
<h1 class="custom-header">{{ lang._('Layer4 Routes') }}</h1>
<div style="display: block;">
{{ partial('layout_partials/base_bootgrid_table', formGridLayer4)}}
</div>
</div>
<h1 class="custom-header">{{ lang._('Layer4 Routes') }}</h1>
{{ partial('layout_partials/base_bootgrid_table', formGridLayer4)}}
</div>
<!-- Layer7 Tab -->
<div id="matchers" class="tab-pane fade">
<div style="padding-left: 16px;">
<!-- OpenVPN Matcher -->
<h1 class="custom-header">{{ lang._('OpenVPN Static Keys') }}</h1>
<div style="display: block;">
{{ partial('layout_partials/base_bootgrid_table', formGridLayer4Openvpn)}}
</div>
</div>
<h1 class="custom-header">{{ lang._('OpenVPN Static Keys') }}</h1>
{{ partial('layout_partials/base_bootgrid_table', formGridLayer4Openvpn)}}
</div>
{% endif %}