feat(dashboard): text filter for Overview workplans chart
Add a search field next to the Workplans by Repository mode selector so operators can narrow large lists by title, repo, domain, filename, or owner.
This commit is contained in:
parent
c363549388
commit
b5747d1104
4 changed files with 111 additions and 2 deletions
22
dashboard/src/components/workplan-search.js
Normal file
22
dashboard/src/components/workplan-search.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Case-insensitive substring match for Overview "Workplans by Repository".
|
||||
* Matches title, repo, domain, filename, owner, and id.
|
||||
*
|
||||
* @param {object} workplan Row from overview workplan_rows
|
||||
* @param {string} query Free-text filter
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function workplanMatchesSearch(workplan, query) {
|
||||
const q = String(query ?? "").trim().toLowerCase();
|
||||
if (!q) return true;
|
||||
if (!workplan || typeof workplan !== "object") return false;
|
||||
const haystack = [
|
||||
workplan.title,
|
||||
workplan.repo_label,
|
||||
workplan.domain,
|
||||
workplan.workplan_filename,
|
||||
workplan.owner,
|
||||
workplan.id,
|
||||
].filter(Boolean).join("\n").toLowerCase();
|
||||
return haystack.includes(q);
|
||||
}
|
||||
|
|
@ -24,6 +24,13 @@ by repository. Each bar is broken into four task-status segments:
|
|||
| orange | wait |
|
||||
| light grey | todo |
|
||||
|
||||
Above the chart:
|
||||
|
||||
- **Mode selector** — lifecycle, health, or recently-changed windows (with
|
||||
matching workplan counts in each option).
|
||||
- **Text filter** — case-insensitive substring match on title, repository,
|
||||
domain, workplan filename, and owner. Useful when many rows share a mode.
|
||||
|
||||
The left axis shows the `domain / repository` label once per repository group.
|
||||
The `done/total` count is printed to the right of each bar. Workplans with no
|
||||
tasks yet show a grey "— no tasks yet" label.
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ display(html`<div class="warning" style="display:${summary.error ? '' : 'none'}"
|
|||
## Workplans by Repository
|
||||
|
||||
```js
|
||||
import {workplanMatchesSearch} from "./components/workplan-search.js";
|
||||
|
||||
// ── Filter workplans by selected mode ───────────────────────────────────────
|
||||
// Lifecycle modes match stored canonical status values.
|
||||
// Health modes are derived labels; they are not stored lifecycle states.
|
||||
|
|
@ -246,6 +248,18 @@ function _setChartMode(value) {
|
|||
globalThis.__stateHubOverviewChartMode = mode;
|
||||
_chartModeState.value = mode;
|
||||
}
|
||||
|
||||
// Free-text filter over title / repo / domain / workplan filename (persists in-tab).
|
||||
const _savedChartSearch = typeof globalThis.__stateHubOverviewChartSearch === "string"
|
||||
? globalThis.__stateHubOverviewChartSearch
|
||||
: "";
|
||||
const _chartSearchState = Mutable(_savedChartSearch);
|
||||
|
||||
function _setChartSearch(value) {
|
||||
const q = String(value ?? "");
|
||||
globalThis.__stateHubOverviewChartSearch = q;
|
||||
_chartSearchState.value = q;
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
|
|
@ -265,14 +279,32 @@ _modeSelect.addEventListener("input", () => {
|
|||
_modeSelect.addEventListener("change", () => {
|
||||
_setChartMode(_modeSelect.value);
|
||||
});
|
||||
display(_modeSelect);
|
||||
|
||||
const _searchInput = html`<input
|
||||
type="search"
|
||||
class="ws-search-input"
|
||||
placeholder="Filter by title, repo, domain…"
|
||||
aria-label="Filter workplans by text"
|
||||
title="Show only workplans matching this text (title, repository, domain, filename)"
|
||||
/>`;
|
||||
_searchInput.value = String(_chartSearchState ?? "");
|
||||
_searchInput.addEventListener("input", () => {
|
||||
_setChartSearch(_searchInput.value);
|
||||
});
|
||||
|
||||
display(html`<div class="filter-bar ws-mode-bar" data-widget-name="Workplans by Repository">
|
||||
${_modeSelect}
|
||||
<div class="filter-text-input">${_searchInput}</div>
|
||||
</div>`);
|
||||
```
|
||||
|
||||
```js
|
||||
import * as Plot from "npm:@observablehq/plot";
|
||||
|
||||
const _chartModeValue = _modeValue(_chartModeState);
|
||||
const _chartWsFiltered = _workstreamsForMode(_chartModeValue, wsAll);
|
||||
const _chartSearchValue = String(_chartSearchState ?? "");
|
||||
const _chartWsFiltered = _workstreamsForMode(_chartModeValue, wsAll)
|
||||
.filter(w => workplanMatchesSearch(w, _chartSearchValue));
|
||||
|
||||
// Sort by domain, then repository, then most recently updated workplan.
|
||||
// The axis labels show each domain/repo group once.
|
||||
|
|
@ -320,6 +352,10 @@ function _wsTitle(d) {
|
|||
|
||||
// ── Render ────────────────────────────────────────────────────────────────────
|
||||
if (chartWs.length === 0) {
|
||||
const _searchQ = _chartSearchValue.trim();
|
||||
if (_searchQ) {
|
||||
display(html`<p style="color:gray">No workplans match “${_searchQ}” for the selected mode.</p>`);
|
||||
} else {
|
||||
const _emptyMsg = {
|
||||
proposed: "No proposed workplans.",
|
||||
ready: "No ready workplans.",
|
||||
|
|
@ -339,6 +375,7 @@ if (chartWs.length === 0) {
|
|||
month: "No workplans changed this month.",
|
||||
};
|
||||
display(html`<p style="color:gray">${_emptyMsg[_chartModeValue] ?? "No workplans."}</p>`);
|
||||
}
|
||||
} else {
|
||||
display(Plot.plot({
|
||||
y: {
|
||||
|
|
@ -680,6 +717,7 @@ display(Inputs.table((summary.recent_progress ?? []).map(e => ({
|
|||
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
|
||||
.ws-mode-bar { margin-bottom: 0.75rem; }
|
||||
.ws-mode-select { min-width: 18rem; max-width: 100%; padding: 0.35rem 0.5rem; border-radius: 4px; border: 1px solid var(--theme-foreground-faint, #ddd); background: var(--theme-background); color: var(--theme-foreground); font: inherit; }
|
||||
.ws-search-input { min-width: 14rem; max-width: 100%; height: 30px; font-size: 0.85rem; padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--theme-foreground-faint, #ccc); background: var(--theme-background, #fff); font-family: inherit; color: inherit; }
|
||||
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
|
||||
.card.warn { border: 2px solid orange; }
|
||||
.card-link { cursor: pointer; transition: box-shadow 0.15s, transform 0.1s; text-decoration: none; color: inherit; display: block; }
|
||||
|
|
|
|||
42
dashboard/test/workplan-search.test.mjs
Normal file
42
dashboard/test/workplan-search.test.mjs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {readFileSync} from "node:fs";
|
||||
import {dirname, join} from "node:path";
|
||||
import {fileURLToPath} from "node:url";
|
||||
|
||||
import {workplanMatchesSearch} from "../src/components/workplan-search.js";
|
||||
|
||||
const root = dirname(fileURLToPath(import.meta.url));
|
||||
const indexMd = readFileSync(join(root, "../src/index.md"), "utf-8");
|
||||
|
||||
const sample = {
|
||||
id: "abc-123",
|
||||
title: "Overview Workstream Stage Counts",
|
||||
repo_label: "state-hub",
|
||||
domain: "infotech",
|
||||
workplan_filename: "STATE-WP-0057-overview-workstream-stage-counts.md",
|
||||
owner: "codex",
|
||||
};
|
||||
|
||||
test("empty query matches all workplans", () => {
|
||||
assert.equal(workplanMatchesSearch(sample, ""), true);
|
||||
assert.equal(workplanMatchesSearch(sample, " "), true);
|
||||
assert.equal(workplanMatchesSearch(sample, null), true);
|
||||
});
|
||||
|
||||
test("matches title, repo, domain, filename, owner, and id", () => {
|
||||
assert.equal(workplanMatchesSearch(sample, "stage counts"), true);
|
||||
assert.equal(workplanMatchesSearch(sample, "STATE-HUB"), true);
|
||||
assert.equal(workplanMatchesSearch(sample, "Infotech"), true);
|
||||
assert.equal(workplanMatchesSearch(sample, "wp-0057"), true);
|
||||
assert.equal(workplanMatchesSearch(sample, "codex"), true);
|
||||
assert.equal(workplanMatchesSearch(sample, "abc-123"), true);
|
||||
assert.equal(workplanMatchesSearch(sample, "railiance"), false);
|
||||
});
|
||||
|
||||
test("overview chart wires the text filter into Workplans by Repository", () => {
|
||||
assert.match(indexMd, /workplanMatchesSearch/);
|
||||
assert.match(indexMd, /ws-search-input/);
|
||||
assert.match(indexMd, /Filter by title, repo, domain/);
|
||||
assert.match(indexMd, /## Workplans by Repository/);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue