From e815c7cb5cc8e9d0421065fc09221cb93a855911 Mon Sep 17 00:00:00 2001 From: iris Date: Mon, 31 Aug 2026 18:26:06 +0200 Subject: [PATCH] swarm-ui: add the custom issue-report page New /issues route: a sortable, filterable table over open issues across every repo that has one -- repo picker (default: no filter, every repo combined), hide-blocked toggle, and a label multi-select, consuming swarm-controller's GET /api/repos + GET /api/issue-report / GET /api/repos/{org}/{repo}/issue-report (see hyperhive#3831 for the row shape). blocked and depended_on_by_count arrive pre-resolved per row -- this page does no dependency-graph walking of its own, just sort/filter over what it's given. Default sort is depended_on_by_count descending, matching mara's framing of the report's headline ordering. Widened Table's TableColumn.header from string to ComponentChildren so a column can carry a real clickable sort-toggle button instead of forking a second table primitive for this one page. --- frontend/packages/swarm-ui/src/App.tsx | 2 + .../swarm-ui/src/pages/IssueReportPage.css | 43 +++ .../swarm-ui/src/pages/IssueReportPage.tsx | 287 ++++++++++++++++++ .../packages/swarm-ui/src/shell/Shell.tsx | 1 + .../packages/swarm-ui/src/ui/table/Table.tsx | 6 +- 5 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 frontend/packages/swarm-ui/src/pages/IssueReportPage.css create mode 100644 frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx diff --git a/frontend/packages/swarm-ui/src/App.tsx b/frontend/packages/swarm-ui/src/App.tsx index 65d47afe..854f5169 100644 --- a/frontend/packages/swarm-ui/src/App.tsx +++ b/frontend/packages/swarm-ui/src/App.tsx @@ -6,6 +6,7 @@ import { AgentsPage } from './pages/AgentsPage.js'; import { ComponentsPage } from './pages/ComponentsPage.js'; import { JobsPage } from './pages/JobsPage.js'; import { HivesPage } from './pages/HivesPage.js'; +import { IssueReportPage } from './pages/IssueReportPage.js'; import { Panel } from './ui/panel/Panel.js'; function NotFound() { @@ -23,6 +24,7 @@ export function App() { + diff --git a/frontend/packages/swarm-ui/src/pages/IssueReportPage.css b/frontend/packages/swarm-ui/src/pages/IssueReportPage.css new file mode 100644 index 00000000..1419727d --- /dev/null +++ b/frontend/packages/swarm-ui/src/pages/IssueReportPage.css @@ -0,0 +1,43 @@ +/* — the repo-picker + hide-blocked toggle sit in one + row (mirrors CreateAgentForm's row/wrap pattern: flex-wrap so narrow + viewports stack instead of overflowing), the label chips get their + own row below since the set is open-ended and shouldn't fight the + controls row for width. Sort buttons live inside the table's own + `` cells (Table.tsx's `header` now accepts real markup for + exactly this) so they inherit the table's header styling for free — + only the pointer cursor + no-underline reset is this page's to own. */ +.issue-report-controls { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 1.25em; + margin-bottom: 1em; +} +.issue-report-toggle { + display: flex; + align-items: center; + gap: 0.4em; + cursor: pointer; +} +.issue-report-labels { + display: flex; + flex-wrap: wrap; + gap: 0.6em; + margin-bottom: 1em; +} +.issue-report-label-chip { + display: flex; + align-items: center; + gap: 0.35em; + cursor: pointer; + color: var(--muted); +} +.issue-report-sort-btn { + background: none; + border: none; + padding: 0; + margin: 0; + font: inherit; + color: inherit; + cursor: pointer; +} diff --git a/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx b/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx new file mode 100644 index 00000000..62171995 --- /dev/null +++ b/frontend/packages/swarm-ui/src/pages/IssueReportPage.tsx @@ -0,0 +1,287 @@ +// — mara's custom issue report: browse open issues +// across every repo that has one, sortable/filterable her own way, with +// "hide blocked (open dependency)" as the headline filter and a rank +// column for how many other open issues depend on each one. Backed by +// swarm-controller's `GET /api/repos` (dropdown source, only repos with +// an open issue), `GET /api/issue-report` (default — every repo +// combined) and `GET /api/repos/{org}/{repo}/issue-report` (once a +// specific repo is picked) — `blocked` and `depended_on_by_count` both +// arrive pre-resolved per row, so nothing here does its own +// dependency-graph walk. +// +// Sorting and the label/hide-blocked filters are client-side over +// whatever's currently loaded — only the repo selection re-fetches. +// Default sort is `depended_on_by_count` descending: mara's own framing +// ("i would also like to rank them by how many issues depend on them") +// reads as the report's headline ordering, not just one more column. +// +// No `RefreshIntervalPicker`/auto-poll like the roster pages — this +// reads as a "generate a report" action rather than a live status view +// (mara: a "generating report" spinner sub-5s is fine, doesn't need to +// be instant), so it fetches once per repo-selection change instead of +// polling. +import { useEffect, useMemo, useState } from 'preact/hooks'; +import { ApiErrorPanel } from '@hive/shared/api-error-panel.js'; +import { readApiError, type ProblemDetails } from '@hive/shared/api-error.js'; +import { Badge } from '@hive/shared/badge.js'; +import { Panel } from '../ui/panel/Panel.js'; +import { SelectField } from '../ui/select-field/SelectField.js'; +import { Table, type TableColumn } from '../ui/table/Table.js'; +import './IssueReportPage.css'; + +interface IssueReportRow { + repo: string; + number: number; + title: string; + labels: string[]; + assignee: string | null; + html_url: string | null; + blocked: boolean; + depended_on_by_count: number; +} + +type SortKey = 'repo' | 'number' | 'title' | 'assignee' | 'blocked' | 'depended_on_by_count'; +type SortDir = 'asc' | 'desc'; + +// Sentinel ` setHideBlocked((e.target as HTMLInputElement).checked)} + /> + hide blocked (open dependency) + + + {allLabels.length ? ( +
+ {allLabels.map((l) => ( + + ))} +
+ ) : null} + {error ? : null} + {!error && loading ?

generating report…

: null} + {!loading && rows ? ( + `${r.repo}#${r.number}`} + emptyMessage="no issues match the current filters" + /> + ) : null} + + ); +} diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx index 8eb9d6cd..aeef0055 100644 --- a/frontend/packages/swarm-ui/src/shell/Shell.tsx +++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx @@ -48,6 +48,7 @@ const NAV_ITEMS: { href: string; label: string; accent: string }[] = [ { href: '/', label: 'hives', accent: 'var(--purple)' }, { href: '/agents', label: 'agents', accent: 'var(--green)' }, { href: '/jobs', label: 'jobs', accent: 'var(--pink)' }, + { href: '/issues', label: 'issues', accent: 'var(--yellow)' }, { href: '/components', label: 'components', accent: 'var(--blue)' }, ]; diff --git a/frontend/packages/swarm-ui/src/ui/table/Table.tsx b/frontend/packages/swarm-ui/src/ui/table/Table.tsx index 23c1739a..5814edbd 100644 --- a/frontend/packages/swarm-ui/src/ui/table/Table.tsx +++ b/frontend/packages/swarm-ui/src/ui/table/Table.tsx @@ -14,7 +14,11 @@ import './Table.css'; export interface TableColumn { key: string; - header: string; + // `ComponentChildren`, not `string` — a plain string still satisfies + // this and renders identically, but a caller that wants a clickable + // sortable header (the issue-report page's own column-header buttons) + // can pass real markup instead of forking a second table primitive. + header: ComponentChildren; render: (row: T) => ComponentChildren; }