diff --git a/README.md b/README.md index 74c446e..9cf00fe 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,16 @@ # wireguard-ui +> **Fork Notice:** This is a fork of [ngoduykhanh/wireguard-ui](https://github.com/ngoduykhanh/wireguard-ui) with the following enhancements: +> +> - **Sortable columns on the Status page** - Click any column header to sort (descending → ascending → reset) +> - Supports sorting by: Name, Email, Received/Transmitted bytes, Connected status, Last Handshake, and more +> - Reset Sort button to restore original order +> +> A [pull request](https://github.com/ngoduykhanh/wireguard-ui/pull/685) has been submitted to merge these changes upstream. +> +> **Docker image:** `ghcr.io/remotetohome-io/wireguard-ui:sortable-columns` + A web user interface to manage your WireGuard setup. ## Features @@ -241,6 +251,4 @@ MIT. See [LICENSE](https://github.com/ngoduykhanh/wireguard-ui/blob/master/LICEN ## Support -If you like the project and want to support it, you can *buy me a coffee* ☕ - -Buy Me A Coffee +If you like this project, please support the original author at the [upstream repository](https://github.com/ngoduykhanh/wireguard-ui#support). diff --git a/templates/status.html b/templates/status.html index a9b770b..5c9e332 100644 --- a/templates/status.html +++ b/templates/status.html @@ -27,6 +27,139 @@ Connected Peers return parseFloat(temporal.toFixed(2)) + units[pow]+"B" } + + // Table sorting functionality + let currentSortColumn = null; + let currentSortDirection = null; + const originalRowOrders = new Map(); // Store original order per table + + function ipToNumber(ip) { + // Extract IP from formats like "10.9.0.132/32" or "99.92.101.230:56078" + const match = (ip || '').trim().match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/); + if (!match) return null; // Empty/invalid IPs will be sorted to bottom + // Use multiplication instead of bit shifting to avoid signed 32-bit integer issues + return (parseInt(match[1]) * 16777216) + (parseInt(match[2]) * 65536) + (parseInt(match[3]) * 256) + parseInt(match[4]); + } + + function getSortValue(cell, sortType) { + switch (sortType) { + case 'number': + return parseInt(cell.getAttribute('data-value') || cell.textContent || '0', 10); + case 'bytes': + return parseInt(cell.getAttribute('data-value') || '0', 10); + case 'boolean': + return cell.getAttribute('data-value') === '1' ? 1 : 0; + case 'date': + return new Date(cell.getAttribute('data-value') || 0).getTime(); + case 'ip': + return ipToNumber(cell.textContent || ''); + case 'text': + default: + return (cell.textContent || '').toLowerCase().trim(); + } + } + + function sortTableByColumn(table, columnIndex, direction, sortType) { + const tbody = table.querySelector('tbody'); + const rows = Array.from(tbody.querySelectorAll('tr')); + + rows.sort((a, b) => { + const aValue = getSortValue(a.cells[columnIndex], sortType); + const bValue = getSortValue(b.cells[columnIndex], sortType); + + // Push null/empty values to bottom regardless of sort direction + if (aValue === null && bValue === null) return 0; + if (aValue === null) return 1; + if (bValue === null) return -1; + + let comparison = 0; + if (sortType === 'text') { + comparison = aValue.localeCompare(bValue); + } else { + comparison = aValue - bValue; + } + + return direction === 'asc' ? comparison : -comparison; + }); + + rows.forEach(row => tbody.appendChild(row)); + } + + function resetTableSort(table) { + const tbody = table.querySelector('tbody'); + const originalOrder = originalRowOrders.get(table); + if (originalOrder) { + originalOrder.forEach(row => tbody.appendChild(row)); + } + currentSortColumn = null; + currentSortDirection = null; + + // Clear all sort indicators + table.querySelectorAll('th[data-sort-column]').forEach(header => { + header.innerHTML = header.getAttribute('data-original-text'); + }); + } + + function handleSortClick(event) { + const th = event.currentTarget; + const table = th.closest('table'); + const columnIndex = parseInt(th.getAttribute('data-sort-column'), 10); + const sortType = th.getAttribute('data-sort-type') || 'text'; + + // Determine new sort direction + if (currentSortColumn === columnIndex) { + // Cycle: desc -> asc -> reset + if (currentSortDirection === 'desc') { + currentSortDirection = 'asc'; + } else { + // Reset to original order + resetTableSort(table); + return; + } + } else { + currentSortColumn = columnIndex; + currentSortDirection = 'desc'; + } + + // Update all sortable headers in this table + table.querySelectorAll('th[data-sort-column]').forEach(header => { + const idx = parseInt(header.getAttribute('data-sort-column'), 10); + const text = header.getAttribute('data-original-text'); + if (idx === columnIndex) { + header.innerHTML = text + ' ' + (currentSortDirection === 'asc' ? '↑' : '↓'); + } else { + header.innerHTML = text; + } + }); + + sortTableByColumn(table, columnIndex, currentSortDirection, sortType); + } + + function handleResetClick(event) { + const button = event.currentTarget; + const table = button.closest('.table-container').querySelector('table'); + resetTableSort(table); + } + + document.addEventListener('DOMContentLoaded', function() { + // Store original row order for each table + document.querySelectorAll('table.table').forEach(table => { + const tbody = table.querySelector('tbody'); + originalRowOrders.set(table, Array.from(tbody.querySelectorAll('tr'))); + }); + + // Setup sortable headers + document.querySelectorAll('th[data-sort-column]').forEach(th => { + th.style.cursor = 'pointer'; + th.setAttribute('data-original-text', th.textContent); + th.addEventListener('click', handleSortClick); + }); + + // Setup reset buttons + document.querySelectorAll('.reset-sort-btn').forEach(btn => { + btn.addEventListener('click', handleResetClick); + }); + });
@@ -34,39 +167,45 @@ Connected Peers {{ end}} {{ range $dev := .devices }} - - - - - - - - - - - - - - - - - - {{ range $idx, $peer := $dev.Peers }} - - - - - - - - - - - - - {{ end }} - -
List of connected peers for device with name {{ $dev.Name }}
#NameEmailAllocated IPsEndpointPublic KeyReceivedTransmittedConnected (Approximation)Last Handshake
{{ $idx }}{{ $peer.Name }}{{ $peer.Email }}{{ $peer.AllocatedIP }}{{ $peer.Endpoint }}{{ $peer.PublicKey }}{{ if $peer.Connected }}✓{{end}}{{ $peer.LastHandshakeTime.Format "2006-01-02 15:04:05 MST" }}
+
+
+ + Click column headers to sort. Click sorted column again to reverse, then again to reset. +
+ + + + + + + + + + + + + + + + + + {{ range $idx, $peer := $dev.Peers }} + + + + + + + + + + + + + {{ end }} + +
List of connected peers for device with name {{ $dev.Name }}
#NameEmailAllocated IPsEndpointPublic KeyReceivedTransmittedConnectedLast Handshake
{{ $idx }}{{ $peer.Name }}{{ $peer.Email }}{{ $peer.AllocatedIP }}{{ $peer.Endpoint }}{{ $peer.PublicKey }}{{ if $peer.Connected }}✓{{end}}{{ $peer.LastHandshakeTime.Format "2006-01-02 15:04:05 MST" }}
+
{{ end }}