Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:32 +02:00
parent e13d95c843
commit 63ee29e222
3 changed files with 34 additions and 103 deletions
@@ -7,6 +7,7 @@ import { ContentGrid, ActionToolbar,
DismissibleAlert, DismissibleAlert,
IconButton, IconButton,
MetricCard, MetricCard,
StatePanel,
StatusBadge, StatusBadge,
hasScope, hasScope,
type ApiSettings, type ApiSettings,
@@ -175,7 +176,7 @@ export function ProviderReportWorkspace({ settings, auth, report }: {
</div> </div>
{execution ? {execution ?
<> <>
<ActionToolbar className="reporting-output-toolbar"> <ActionToolbar surface="panel-header" className="reporting-output-toolbar">
<span>Generated {formatDateTime(execution.generated_at)}</span> <span>Generated {formatDateTime(execution.generated_at)}</span>
{report.export_formats.includes("csv") && {report.export_formats.includes("csv") &&
<IconButton label="Download CSV" icon={<Download size={17} />} variant="ghost" onClick={() => void download("csv")} /> <IconButton label="Download CSV" icon={<Download size={17} />} variant="ghost" onClick={() => void download("csv")} />
@@ -188,7 +189,7 @@ export function ProviderReportWorkspace({ settings, auth, report }: {
<ProviderResult execution={execution} /> <ProviderResult execution={execution} />
</div> </div>
</> : </> :
<div className="reporting-empty">Select the parameters and run this governed report.</div> <StatePanel size="fill" description="Select the parameters and run this governed report." />
} }
</> </>
); );
+27 -27
View File
@@ -25,13 +25,19 @@ import { FormGrid, ActionToolbar,
Dialog, Dialog,
DocumentationHelpLink, DocumentationHelpLink,
DismissibleAlert, DismissibleAlert,
FilterBar,
IconButton, IconButton,
LoadingIndicator, LoadingIndicator,
PageScrollViewport, PageScrollViewport,
StatePanel,
SegmentedControl, SegmentedControl,
SelectionList,
SelectionListItem,
SelectionListItemContent,
StatusBadge, StatusBadge,
ToggleSwitch, ToggleSwitch,
hasScope, hasScope,
WorkspaceFrame,
type DataGridColumn, type DataGridColumn,
type PlatformRouteContext type PlatformRouteContext
} from "@govoplan/core-webui"; } from "@govoplan/core-webui";
@@ -249,9 +255,9 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
return ( return (
<main className="reporting-page"> <main className="reporting-page">
<div className="reporting-shell"> <WorkspaceFrame className="reporting-shell" label="Reporting workspace" interfaceId="reporting.workspace" helpContextId="reporting.page.workspace" helpModuleId="reporting">
<ActionToolbar className="reporting-toolbar"> <ActionToolbar surface="panel-header" className="reporting-toolbar">
<form className="reporting-search" onSubmit={submitSearch}> <FilterBar as="form" surface="control" wrap="never" width="compact" className="reporting-search" onSubmit={submitSearch}>
<Search size={17} aria-hidden="true" /> <Search size={17} aria-hidden="true" />
<input <input
value={search} value={search}
@@ -259,7 +265,7 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
aria-label="Search reports" aria-label="Search reports"
placeholder="Search reports" placeholder="Search reports"
/> />
</form> </FilterBar>
<span className="reporting-count">{reports.length + providerReports.length} reports</span> <span className="reporting-count">{reports.length + providerReports.length} reports</span>
<DocumentationHelpLink <DocumentationHelpLink
reference={{ topicId: "reporting.governed-bi", documentationType: "user" }} reference={{ topicId: "reporting.governed-bi", documentationType: "user" }}
@@ -280,38 +286,32 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
<div className="reporting-workspace"> <div className="reporting-workspace">
<PageScrollViewport className="reporting-catalogue"> <PageScrollViewport className="reporting-catalogue">
{loading && <LoadingIndicator label="Loading reports" />} {loading && <LoadingIndicator label="Loading reports" />}
{!loading && reports.length + providerReports.length === 0 && <div className="reporting-empty">No active reports are available.</div>} {!loading && reports.length + providerReports.length === 0 && <StatePanel size="compact" description="No active reports are available." />}
<div className="reporting-report-list" role="list"> <SelectionList variant="navigation" label="Reports">
{providerReports.length > 0 && <div className="reporting-list-heading">Module reports</div>} {providerReports.length > 0 && <div className="reporting-list-heading" role="presentation">Module reports</div>}
{providerReports.map((item) => { {providerReports.map((item) => {
const key = `${item.provider_id}:${item.report_id}`; const key = `${item.provider_id}:${item.report_id}`;
return ( return (
<button <SelectionListItem
type="button"
role="listitem"
key={key} key={key}
className={`reporting-report-row${selectedProviderKey === key ? " is-selected" : ""}`} selected={selectedProviderKey === key}
onClick={() => { setSelectedProviderKey(key); setSelectedId(""); }}> onClick={() => { setSelectedProviderKey(key); setSelectedId(""); }}>
<BarChart3 size={17} aria-hidden="true" /> <SelectionListItemContent leading={<BarChart3 size={17} />} title={item.title} description={`${item.provider_id} · ${item.revision}`} />
<span><strong>{item.title}</strong><small>{item.provider_id} · {item.revision}</small></span>
<StatusBadge status={item.available ? "active" : "locked"} label={item.available ? "Available" : "Restricted"} /> <StatusBadge status={item.available ? "active" : "locked"} label={item.available ? "Available" : "Restricted"} />
</button> </SelectionListItem>
); );
})} })}
{reports.length > 0 && <div className="reporting-list-heading">Semantic reports</div>} {reports.length > 0 && <div className="reporting-list-heading" role="presentation">Semantic reports</div>}
{reports.map((item) => {reports.map((item) =>
<button <SelectionListItem
type="button"
role="listitem"
key={item.definition_id} key={item.definition_id}
className={`reporting-report-row${selectedId === item.definition_id ? " is-selected" : ""}`} selected={selectedId === item.definition_id}
onClick={() => { setSelectedId(item.definition_id); setSelectedProviderKey(""); }}> onClick={() => { setSelectedId(item.definition_id); setSelectedProviderKey(""); }}>
<BarChart3 size={17} aria-hidden="true" /> <SelectionListItemContent leading={<BarChart3 size={17} />} title={item.name} description={`${item.definition_key} · r${item.revision}`} />
<span><strong>{item.name}</strong><small>{item.definition_key} · r{item.revision}</small></span>
<StatusBadge status="active" label="Active" /> <StatusBadge status="active" label="Active" />
</button> </SelectionListItem>
)} )}
</div> </SelectionList>
</PageScrollViewport> </PageScrollViewport>
<section className="reporting-result-region"> <section className="reporting-result-region">
{selectedProvider ? {selectedProvider ?
@@ -345,7 +345,7 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
onQueryChange={setQuery} onQueryChange={setQuery}
onParametersChange={setParameters} onParametersChange={setParameters}
/> />
<ActionToolbar className="reporting-output-toolbar"> <ActionToolbar surface="panel-header" className="reporting-output-toolbar">
<SegmentedControl <SegmentedControl
ariaLabel="Report output" ariaLabel="Report output"
value={outputMode} value={outputMode}
@@ -367,12 +367,12 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
} }
</ActionToolbar> </ActionToolbar>
<div className="reporting-output"> <div className="reporting-output">
{!execution && <div className="reporting-empty">Run the report or select a previous execution.</div>} {!execution && <StatePanel size="fill" description="Run the report or select a previous execution." />}
{execution && outputMode === "visual" && <ReportVisual execution={execution} onDrill={execution.query.mode === "detail" ? undefined : drill} />} {execution && outputMode === "visual" && <ReportVisual execution={execution} onDrill={execution.query.mode === "detail" ? undefined : drill} />}
{execution && outputMode === "table" && <ReportTable execution={execution} onDrill={execution.query.mode === "detail" ? undefined : drill} />} {execution && outputMode === "table" && <ReportTable execution={execution} onDrill={execution.query.mode === "detail" ? undefined : drill} />}
</div> </div>
</> : </> :
<div className="reporting-empty">Select a report.</div> <StatePanel size="fill" title="Reports" description="Select a report." />
} }
</section> </section>
<PageScrollViewport className="reporting-inspector"> <PageScrollViewport className="reporting-inspector">
@@ -398,7 +398,7 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
/>} />}
</PageScrollViewport> </PageScrollViewport>
</div> </div>
</div> </WorkspaceFrame>
<SaveViewDialog <SaveViewDialog
open={saveDialogOpen} open={saveDialogOpen}
onClose={() => setSaveDialogOpen(false)} onClose={() => setSaveDialogOpen(false)}
+4 -74
View File
@@ -1,19 +1,10 @@
.reporting-page, .reporting-page {
.reporting-shell {
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
} }
.reporting-shell { .reporting-result-header {
display: flex;
flex-direction: column;
background: var(--surface);
}
.reporting-toolbar,
.reporting-result-header,
.reporting-output-toolbar {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
@@ -21,21 +12,8 @@
background: var(--surface-raised); background: var(--surface-raised);
} }
.reporting-toolbar {
min-height: 56px;
padding: 9px 14px;
}
.reporting-search { .reporting-search {
display: flex; flex: 1 1 440px;
align-items: center;
gap: 8px;
width: min(440px, 46vw);
}
.reporting-search input {
min-width: 140px;
flex: 1;
} }
.reporting-count { .reporting-count {
@@ -66,7 +44,6 @@
border-left: 1px solid var(--border); border-left: 1px solid var(--border);
} }
.reporting-report-list,
.reporting-inspector-content section { .reporting-inspector-content section {
overflow: hidden; overflow: hidden;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -84,7 +61,6 @@
text-transform: uppercase; text-transform: uppercase;
} }
.reporting-report-row,
.reporting-inspector-content section > button { .reporting-inspector-content section > button {
display: grid; display: grid;
align-items: center; align-items: center;
@@ -97,45 +73,15 @@
cursor: pointer; cursor: pointer;
} }
.reporting-report-row {
grid-template-columns: 22px minmax(0, 1fr) auto;
gap: 8px;
min-height: 58px;
padding: 8px 10px;
}
.reporting-report-row:last-child,
.reporting-inspector-content section > button:last-child { .reporting-inspector-content section > button:last-child {
border-bottom: 0; border-bottom: 0;
} }
.reporting-report-row:hover,
.reporting-report-row:focus-visible,
.reporting-report-row.is-selected,
.reporting-inspector-content section > button:hover, .reporting-inspector-content section > button:hover,
.reporting-inspector-content section > button.is-selected { .reporting-inspector-content section > button.is-selected {
background: var(--hover-bg); background: var(--hover-bg);
} }
.reporting-report-row.is-selected {
box-shadow: inset 3px 0 0 var(--accent);
}
.reporting-report-row > span:nth-child(2) {
display: flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.reporting-report-row strong,
.reporting-report-row small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reporting-report-row small,
.reporting-inspector-content small, .reporting-inspector-content small,
.reporting-inspector-content p { .reporting-inspector-content p {
color: var(--text-soft); color: var(--text-soft);
@@ -251,11 +197,6 @@
min-width: min(320px, 35vw); min-width: min(320px, 35vw);
} }
.reporting-output-toolbar {
min-height: 48px;
padding: 6px 12px;
}
.reporting-output-toolbar > span { .reporting-output-toolbar > span {
margin-left: auto; margin-left: auto;
color: var(--text-soft); color: var(--text-soft);
@@ -288,12 +229,6 @@
min-width: 100%; min-width: 100%;
} }
.reporting-empty {
padding: 38px 12px;
color: var(--text-soft);
text-align: center;
}
.reporting-inspector-content { .reporting-inspector-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -632,13 +567,8 @@
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.reporting-result-header, .reporting-result-header {
.reporting-toolbar {
flex-wrap: wrap; flex-wrap: wrap;
} }
.reporting-search {
width: 100%;
}
} }