From be866264bd9db6abc0295153bb3f55708e464166 Mon Sep 17 00:00:00 2001 From: Jad Seifeddine Date: Tue, 13 Oct 2026 13:58:06 +0700 Subject: [PATCH] pages: shared facet filters and one list toolbar on every list page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reports: show entry names as host over a tenant subline Session history and the most-used table showed "host (Tenant)" names wrapped across three lines; they now read as the host over a quieter tenant line, as on Connections and Recordings. Part of #254 sessions, recordings: show the session type with the shared badge (#254) The sessions page printed the type as plain text and the recordings page as a coloured word; connections and reports already used .type-badge from rustguac.css. All four now use the same badge. The Proxmox badge colour moves into the shared stylesheet with the others. Shared facet filters (static/filters.js) on the sessions, recordings, reports and connections pages: a dropdown per facet (Type, Folder, User, Owner, Status as each page has them), multi-select with counts that narrow as other facets apply, All/None, a find box for long lists, a Clear filters link, and selections kept per page in sessionStorage. They sit beside the existing text search and combine with it; the buttons are compacted to the height and padding of the other controls and the dropdown reserves room for its scrollbar so it no longer sits over the counts. The list pages share one toolbar: a borderless row with the search box, the facets and the page action on the right (New ad-hoc session, Export CSV), placeholders standardised to 'Search (fields…)'. Sessions gains a search (user, host, owner, ID, type) and a User facet; Reports moves from its own .filter-bar to the shared one. The redundant page titles go — the nav already names and marks the active page — and section headings centre their heading and action button on one line. Connections: the folder header is sticky at the top of the entries pane, so it stays under the nav and the active-sessions strip while a long table scrolls. All four pages show the session type with the shared .type-badge (the Proxmox colour moves into rustguac.css), reports and recordings show "host (Tenant)" names as the host over a quieter tenant line, and the key glyph before Credentials in the nav is sized to the text. Part of #254 tests: shared facet filters and the list toolbar on every list page --- static/admin.html | 2 +- static/connections.html | 62 +++++- static/docs.html | 2 +- static/filters.js | 265 +++++++++++++++++++++++++ static/recordings.html | 31 ++- static/reports.html | 51 +++-- static/rustguac.css | 142 ++++++++++++- static/sessions.html | 85 ++++++-- static/tokens.html | 2 +- tests/e2e/README.md | 1 + tests/e2e/specs/facet-filters.spec.mjs | 86 ++++++++ 11 files changed, 674 insertions(+), 55 deletions(-) create mode 100644 static/filters.js create mode 100644 tests/e2e/specs/facet-filters.spec.mjs diff --git a/static/admin.html b/static/admin.html index 275b0b3..c67657e 100644 --- a/static/admin.html +++ b/static/admin.html @@ -22,7 +22,7 @@ Docs Tokens Admin - 🔑 Credentials + 🔑 Credentials Logout⚙ Settings
diff --git a/static/connections.html b/static/connections.html index 9282cd4..7ec87d3 100644 --- a/static/connections.html +++ b/static/connections.html @@ -216,8 +216,6 @@ display: flex; gap: var(--s-1); align-items: center; } .session-card-info .card-meta .type-badge { font-size: 10px; min-width: 36px; } - /* Proxmox VE brand orange badge */ - .type-badge.type-proxmox { background: #e57000; color: #000; padding: 1px 6px; border-radius: 3px; font-weight: 600; } .session-card.dormant { opacity: 0.75; } .session-card.dormant .session-card-thumb { filter: brightness(0.7); } .card-actions { @@ -561,6 +559,18 @@ background: rgba(255,255,255,0.015); } .main { min-height: 0; overflow-y: auto; overflow-x: hidden; padding-right: var(--s-2); } + /* The folder header (name, description, search, actions, filters) + pins to the top of the entries pane, so it stays under the nav and + the active-sessions strip while a long table scrolls beneath it. */ + #entries-header { + position: sticky; top: 0; z-index: 30; + background: var(--bg); + padding-bottom: var(--s-2); + margin-bottom: var(--s-2); + box-shadow: 0 6px 10px -8px rgba(0, 0, 0, 0.6); + } + #entries-header .folder-actions { margin-bottom: 0; } + #entries-header #entries-filters { margin: var(--s-2) 0 0 0; } /* Sidebar panel: one pinned header band (label, new-folder button, filter) on a surface tone, then the list with hairline dividers. @@ -611,7 +621,7 @@ Docs Tokens Admin - 🔑 Credentials + 🔑 Credentials Logout ⚙ Settings @@ -681,6 +691,7 @@ +

Select a folder to view entries.

@@ -1334,7 +1345,8 @@
- + + @@ -174,12 +175,10 @@ Docs Tokens Admin - 🔑 Credentials + 🔑 Credentials Logout⚙ Settings
-

Recordings

-
@@ -197,7 +196,10 @@
- +
+ +
+
@@ -373,9 +375,24 @@ function recSearchQuery() { return (document.getElementById('rec-search').value || '').toLowerCase(); } + // Shared facet filters (filters.js): Type, Folder, User; applied on top + // of the text search. Selections persist in sessionStorage for this tab. + var recFilters = rgFilters.create({ + mount: document.getElementById('rec-filters'), + id: 'recordings', + facets: [ + { key: 'type', label: 'Type', value: function(r) { return r.session_type; }, + render: function(v) { return '' + escapeHtml(v) + ''; } }, + { key: 'folder', label: 'Folder', value: function(r) { return r.folder; } }, + { key: 'user', label: 'User', value: function(r) { return r.user; } } + ], + onChange: function() { recPage = 1; renderRecordings(filterRecordings(recSearchQuery())); } + }); function filterRecordings(q) { - if (!q) return allRecordings; - return allRecordings.filter(function(r) { + recFilters.update(allRecordings); + var list = recFilters.apply(allRecordings); + if (!q) return list; + return list.filter(function(r) { return (r.name || '').toLowerCase().indexOf(q) >= 0 || (r.user || '').toLowerCase().indexOf(q) >= 0 || (r.entry_display_name || '').toLowerCase().indexOf(q) >= 0 || @@ -453,7 +470,7 @@ slice.forEach(function(r) { var date = r.modified ? new Date(r.modified).toLocaleString() : (r.created_at ? new Date(r.created_at).toLocaleString() : ''); var entryName = r.entry_display_name || r.address_book_entry || r.name; - var typeBadge = r.session_type ? '' + r.session_type + '' : ''; + var typeBadge = r.session_type ? '' + escapeHtml(r.session_type) + '' : ''; html += '' + '' + entryNameHtml(entryName) + '' + '' + escapeHtml(r.user || '') + '' + diff --git a/static/reports.html b/static/reports.html index 0d6fe3c..75ceb42 100644 --- a/static/reports.html +++ b/static/reports.html @@ -5,17 +5,6 @@ rustguac - Reports + @@ -85,12 +75,10 @@ Docs Tokens Admin - 🔑 Credentials + 🔑 Credentials Logout⚙ Settings
-

Reports

-
Total Sessions
@@ -112,9 +100,10 @@

Session History

-
- - +
+ +
+
@@ -215,6 +204,13 @@ return h; } + // "host (Tenant)" display names, as the NetBox sync writes them, read + // as the host over a quieter tenant line (same as Connections). + function entryNameHtml(label) { + var m = /^(.*\S)\s+\(([^()]+)\)$/.exec(label || ''); + if (!m) return escapeHtml(label || ''); + return '' + escapeHtml(m[1]) + '' + escapeHtml(m[2]) + ''; + } function escapeHtml(s) { if (!s) return ''; var d = document.createElement('div'); @@ -319,7 +315,9 @@ return 0; }); - // Apply text filter + // Facet filters (filters.js), then the text filter + historyFilters.update(sessionsData); + sorted = historyFilters.apply(sorted); var filterText = (document.getElementById('filter-input').value || '').toLowerCase(); if (filterText) { sorted = sorted.filter(function(s) { @@ -350,7 +348,7 @@ } html += '' + '' + - '' + + '' + '' + '' + '' + @@ -415,6 +413,19 @@ document.getElementById('filter-input').addEventListener('input', function() { applySortAndRender(); }); + // Shared facet filters (filters.js): Type, Folder, User, Status. + // Selections persist in sessionStorage for this tab. + var historyFilters = rgFilters.create({ + mount: document.getElementById('history-filters'), + id: 'reports', + facets: [ + { key: 'type', label: 'Type', value: function(s) { return s.session_type; }, render: typeBadge }, + { key: 'folder', label: 'Folder', value: function(s) { return s.address_book_folder; } }, + { key: 'user', label: 'User', value: function(s) { return s.created_by; } }, + { key: 'status', label: 'Status', value: function(s) { return s.status; } } + ], + onChange: function() { applySortAndRender(); } + }); // ── Export CSV ── document.getElementById('export-csv-btn').addEventListener('click', function() { @@ -446,7 +457,7 @@ var html = ''; data.forEach(function(c) { html += '' + - '' + + '' + '' + '' + '' + diff --git a/static/rustguac.css b/static/rustguac.css index 57dcb2e..1bb5727 100644 --- a/static/rustguac.css +++ b/static/rustguac.css @@ -93,6 +93,8 @@ nav a { line-height: 1.4; } nav a:hover { color: var(--text); text-decoration: none; } +/* The key glyph before Credentials renders as a large colour emoji; keep it to text height. */ +nav .nav-key { font-size: 0.75em; vertical-align: 0.05em; margin-right: 0.1em; } nav a.active { color: var(--primary); font-weight: bold; } nav a.active::after { content: ''; @@ -353,12 +355,14 @@ tbody tr:last-child td { border-bottom: none; } .type-vnc { background: var(--type-vnc-bg); color: var(--type-vnc-fg); } .type-web { background: var(--type-web-bg); color: var(--type-web-fg); } .type-vdi { background: var(--type-vdi-bg); color: var(--type-vdi-fg); } +.type-proxmox { background: #e57000; color: #000; } /* ── Section heading strip (used above grids/lists) ──────── */ .section-head { display: flex; - align-items: baseline; + align-items: center; gap: var(--s-2); + min-height: var(--ctl-sm); margin-bottom: var(--s-3); padding-bottom: var(--s-2); border-bottom: 1px solid var(--border); @@ -870,3 +874,139 @@ html:not([data-role-level="4"]) .admin-only { display: none !important; } "host (Tenant)" display names read as the host over a quieter subline. */ .entry-name { display: block; color: var(--text); } .entry-sub { display: block; font-size: var(--fz-xs); color: var(--text-dim); margin-top: 1px; letter-spacing: 0.02em; } + +/* ── Facet filters (filters.js) ──────────────────────────── */ +.rg-filters { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--s-2); +} +.rg-filters-label { + color: var(--text-muted); + font-size: var(--fz-sm); + text-transform: uppercase; + letter-spacing: 0.12em; +} +.rg-facet { position: relative; } +.rg-facet[hidden] { display: none; } +.rg-facet-btn { + display: inline-flex; + align-items: center; + gap: var(--s-1); + height: var(--ctl-sm); + padding: 0 var(--s-2); + background: var(--input); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text-muted); + font-family: monospace; + font-size: var(--fz-sm); + cursor: pointer; + white-space: nowrap; +} +/* The generic button:hover paints the primary colour; these stay on the input tone. */ +.rg-facet-btn:hover, .rg-facet-btn:focus, .rg-facet.open .rg-facet-btn { background: var(--input); border-color: var(--accent); color: var(--text); } +.rg-facet-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } +.rg-facet.active .rg-facet-btn { border-color: var(--accent); color: var(--accent); } +.rg-facet.active .rg-facet-btn:hover, .rg-facet.active .rg-facet-btn:focus { color: var(--accent); } +.rg-facet-n { + background: var(--accent); + color: var(--bg); + border-radius: 999px; + padding: 0 0.55em; + font-size: var(--fz-xs); + font-weight: bold; + line-height: 1.6; + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; +} +.rg-facet-n.one { background: transparent; color: var(--accent); padding: 0; font-weight: normal; font-size: var(--fz-sm); } +.rg-facet-n.one::before { content: "\00b7\00a0"; color: var(--text-dim); } +.rg-facet-n[hidden] { display: none; } +.rg-facet-caret { font-size: 0.7em; opacity: 0.6; margin-left: 1px; } +.rg-facet.open .rg-facet-caret { transform: rotate(180deg); } +.rg-facet-menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + z-index: 600; + min-width: 260px; + max-width: min(440px, calc(100vw - 32px)); + padding: var(--s-2); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5); +} +.rg-facet-menu.right { left: auto; right: 0; } +.rg-facet-menu[hidden] { display: none; } +.rg-facet-tools-row { display: flex; align-items: center; gap: var(--s-1); margin-bottom: var(--s-2); } +.rg-facet-tools { flex: 1; min-width: 0; display: flex; } +.rg-facet-tools input[type="search"] { flex: 1; min-width: 0; height: var(--ctl-sm); font-size: var(--fz-sm); } +.rg-facet-tool { + background: transparent; + border: 1px solid transparent; + border-radius: var(--radius); + color: var(--text-muted); + font-family: monospace; + font-size: var(--fz-xs); + height: var(--ctl-sm); + padding: 0 var(--s-2); + cursor: pointer; +} +.rg-facet-tool:hover, .rg-facet-tool:focus { background: transparent; color: var(--accent); border-color: var(--border); } +.rg-facet-list { max-height: 340px; overflow-y: auto; overflow-x: hidden; padding-right: var(--s-2); scrollbar-gutter: stable; } +.rg-facet-opt { + display: flex; + align-items: center; + gap: var(--s-2); + margin: 0; + padding: 4px var(--s-1); + border-radius: var(--radius); + color: var(--text); + font-size: var(--fz-sm); + font-weight: normal; + text-transform: none; + letter-spacing: normal; + cursor: pointer; +} +.rg-facet-opt:hover { background: var(--input); } +.rg-facet-opt.zero { opacity: 0.55; } +.rg-facet-opt input[type="checkbox"] { margin: 0; flex: none; accent-color: var(--accent); width: 15px; height: 15px; } +.rg-facet-val { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.rg-facet-val .type-badge { font-size: var(--fz-xs); min-width: 44px; padding: 1px var(--s-2); } +.rg-facet-count { color: var(--text-dim); font-size: var(--fz-xs); flex: none; } +.rg-facet-empty { color: var(--text-dim); font-size: var(--fz-sm); padding: var(--s-2) var(--s-1); } +.rg-filters-clear { + background: transparent; + border: 1px solid transparent; + border-radius: var(--radius); + color: var(--text-muted); + font-family: monospace; + font-size: var(--fz-sm); + height: var(--ctl-sm); + padding: 0 var(--s-2); + cursor: pointer; +} +.rg-filters-clear:hover, .rg-filters-clear:focus { background: transparent; color: var(--primary); border-color: var(--border); } +.rg-filters-clear[hidden] { display: none; } + +/* ── List tool rows (search box + facet filters above a table) ── */ +.list-tools { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--s-2); + margin-bottom: var(--s-3); +} +.list-tools:empty { display: none; } +.list-tools .list-search { flex: 1 1 260px; min-width: 200px; max-width: 520px; height: var(--ctl-sm); font-size: var(--fz-sm); } +/* A page-specific action (New ad-hoc session, Export CSV) pinned to the right + of the shared search+filters row. */ +.list-tools .list-action { margin-left: auto; } + +/* ── Modal extras ─────────────────────────────────────── */ +.modal .modal-hint { color: var(--text-muted); font-size: var(--fz-sm); margin: 0 0 var(--s-3); } +.modal.adhoc-modal { min-width: 520px; max-width: 640px; } diff --git a/static/sessions.html b/static/sessions.html index 05b337b..b587925 100644 --- a/static/sessions.html +++ b/static/sessions.html @@ -64,7 +64,7 @@ DocsTokensAdmin - 🔑 Credentials + 🔑 CredentialsLogout⚙ Settings
@@ -188,12 +188,12 @@
-
- Sessions - - +
+ +
+
-
+
' + escapeHtml(s.created_by || s.username || '') + '' + escapeHtml(s.entry_display_name || s.address_book_entry || '') + '' + entryNameHtml(s.entry_display_name || s.address_book_entry || '') + '' + escapeHtml(s.address_book_folder || '') + '' + typeBadge(s.session_type) + '' + escapeHtml(s.hostname || '') + '
' + escapeHtml(c.name || c.address_book_entry || '') + '' + entryNameHtml(c.name || c.address_book_entry || '') + '' + typeBadge(c.session_type) + '' + (c.session_count || 0) + '' + (c.total_hours != null ? c.total_hours.toFixed(1) : '0') + '
@@ -203,6 +203,7 @@ +
IDTypeHostUserOwnerStatusViewers