feat(dashboard): add status filter pills and dim stopped services

- Add All/Running/Stopped filter pills with live counts in toolbar
- Dim stopped service cards (55% opacity, red left border)
- Cards brighten on hover for easy interaction

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 07:30:10 -06:00
parent fe76ca7456
commit 04c2e1f102
3 changed files with 128 additions and 1 deletions
+66 -1
View File
@@ -12,6 +12,7 @@ let logPanelOpen = false;
let modalOpen = false; let modalOpen = false;
let editingGroupId = null; // null = creating, string = editing let editingGroupId = null; // null = creating, string = editing
let searchQuery = ""; let searchQuery = "";
let statusFilter = "all"; // "all", "running", "stopped"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// DOM references // DOM references
@@ -199,6 +200,39 @@ function renderMain() {
renderServices(filtered); renderServices(filtered);
} }
function getPreStatusFilteredServices() {
let list = [...services];
if (currentView === "all") {
// show all
} else if (currentView.startsWith("node:")) {
const nodeName = currentView.slice(5);
list = list.filter((s) => s.node === nodeName);
} else {
const group = groups.find((g) => g.id === currentView);
if (group) {
const svcSet = new Set(
group.services.map((s) => s.node + ":" + s.container)
);
list = list.filter((s) => svcSet.has(s.node + ":" + s.name));
} else {
list = [];
}
}
if (searchQuery) {
const q = searchQuery.toLowerCase();
list = list.filter(
(s) =>
s.name.toLowerCase().includes(q) ||
s.image.toLowerCase().includes(q) ||
s.node.toLowerCase().includes(q)
);
}
return list;
}
function getFilteredServices() { function getFilteredServices() {
let list = [...services]; let list = [...services];
@@ -232,6 +266,13 @@ function getFilteredServices() {
); );
} }
// Status filter
if (statusFilter === "running") {
list = list.filter((s) => s.status === "running");
} else if (statusFilter === "stopped") {
list = list.filter((s) => s.status !== "running");
}
return list; return list;
} }
@@ -251,6 +292,20 @@ function renderToolbar(filtered) {
const countEl = $("#service-count"); const countEl = $("#service-count");
countEl.textContent = filtered.length + " service" + (filtered.length !== 1 ? "s" : ""); countEl.textContent = filtered.length + " service" + (filtered.length !== 1 ? "s" : "");
// Status filter counts (from pre-status-filtered list)
const preFiltered = getPreStatusFilteredServices();
const runningCount = preFiltered.filter((s) => s.status === "running").length;
const stoppedCount = preFiltered.filter((s) => s.status !== "running").length;
const countRunEl = $("#count-running");
const countStopEl = $("#count-stopped");
if (countRunEl) countRunEl.textContent = runningCount;
if (countStopEl) countStopEl.textContent = stoppedCount;
// Update active pill
$$(".filter-pill").forEach((pill) => {
pill.classList.toggle("active", pill.dataset.filter === statusFilter);
});
// Group actions visibility // Group actions visibility
const groupActions = $("#group-actions"); const groupActions = $("#group-actions");
const isGroupView = const isGroupView =
@@ -311,8 +366,10 @@ function renderServiceCard(s) {
? `<span class="card-swarm-badge">Swarm</span>` ? `<span class="card-swarm-badge">Swarm</span>`
: ""; : "";
const stoppedClass = !isRunning ? " stopped" : "";
return ` return `
<div class="service-card"> <div class="service-card${stoppedClass}">
<div class="card-header"> <div class="card-header">
<div> <div>
<span class="card-name">${escHtml(s.name)}</span> <span class="card-name">${escHtml(s.name)}</span>
@@ -685,6 +742,14 @@ function bindEvents() {
}); });
} }
// Status filter pills
$$(".filter-pill").forEach((pill) => {
pill.addEventListener("click", () => {
statusFilter = pill.dataset.filter;
renderMain();
});
});
// Search // Search
const searchInput = $("#search-input"); const searchInput = $("#search-input");
if (searchInput) { if (searchInput) {
+5
View File
@@ -54,6 +54,11 @@
<button class="btn btn-danger btn-sm" id="btn-group-stop" title="Stop all in group">&#9632; Stop All</button> <button class="btn btn-danger btn-sm" id="btn-group-stop" title="Stop all in group">&#9632; Stop All</button>
<button class="btn btn-warning btn-sm" id="btn-group-restart" title="Restart all in group">&#8635; Restart All</button> <button class="btn btn-warning btn-sm" id="btn-group-restart" title="Restart all in group">&#8635; Restart All</button>
</div> </div>
<div class="toolbar-filters" id="status-filters">
<button class="filter-pill active" data-filter="all">All</button>
<button class="filter-pill" data-filter="running">Running <span id="count-running" class="filter-count"></span></button>
<button class="filter-pill" data-filter="stopped">Stopped <span id="count-stopped" class="filter-count"></span></button>
</div>
<div class="toolbar-right"> <div class="toolbar-right">
<input type="text" id="search-input" class="search-input" placeholder="Search services..."> <input type="text" id="search-input" class="search-input" placeholder="Search services...">
</div> </div>
+57
View File
@@ -344,6 +344,54 @@ body {
border-radius: 10px; border-radius: 10px;
} }
.toolbar-filters {
display: flex;
gap: 4px;
}
.filter-pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 12px;
font-size: 12px;
font-weight: 500;
border: 1px solid var(--border);
border-radius: 20px;
cursor: pointer;
background: transparent;
color: var(--text-secondary);
transition: all var(--transition);
}
.filter-pill:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.filter-pill.active {
background: rgba(88, 166, 255, 0.15);
color: var(--accent);
border-color: rgba(88, 166, 255, 0.3);
}
.filter-pill[data-filter="stopped"].active {
background: rgba(248, 81, 73, 0.12);
color: var(--danger);
border-color: rgba(248, 81, 73, 0.3);
}
.filter-pill[data-filter="running"].active {
background: rgba(63, 185, 80, 0.12);
color: var(--success);
border-color: rgba(63, 185, 80, 0.3);
}
.filter-count {
font-size: 11px;
font-weight: 600;
}
.toolbar-center { .toolbar-center {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -394,6 +442,15 @@ body {
box-shadow: 0 0 0 1px rgba(88, 166, 255, 0.15); box-shadow: 0 0 0 1px rgba(88, 166, 255, 0.15);
} }
.service-card.stopped {
opacity: 0.55;
border-left: 3px solid var(--danger);
}
.service-card.stopped:hover {
opacity: 0.85;
}
.card-header { .card-header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;