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:
+66
-1
@@ -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) {
|
||||||
|
|||||||
@@ -54,6 +54,11 @@
|
|||||||
<button class="btn btn-danger btn-sm" id="btn-group-stop" title="Stop all in group">■ Stop All</button>
|
<button class="btn btn-danger btn-sm" id="btn-group-stop" title="Stop all in group">■ Stop All</button>
|
||||||
<button class="btn btn-warning btn-sm" id="btn-group-restart" title="Restart all in group">↻ Restart All</button>
|
<button class="btn btn-warning btn-sm" id="btn-group-restart" title="Restart all in group">↻ 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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user