feat: add governed cross-module reporting

This commit is contained in:
2026-08-02 05:29:38 +02:00
parent eba35edd3c
commit 0abcc2455e
21 changed files with 2497 additions and 38 deletions
+65 -15
View File
@@ -37,6 +37,7 @@ import {
getDefinition,
listDefinitions,
listExecutions,
listProviderReports,
listSavedViews,
reportPayload,
runReport,
@@ -47,8 +48,13 @@ import {
type ReportingQuery,
type ReportingQueryMode,
type ReportingSavedView,
type ProviderReportDescriptor,
type SemanticModelPayload
} from "../../api/reporting";
import {
ProviderReportInspector,
ProviderReportWorkspace
} from "./ProviderReportWorkspace";
type OutputMode = "visual" | "table";
@@ -57,7 +63,9 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
const [search, setSearch] = useState("");
const [submittedSearch, setSubmittedSearch] = useState("");
const [reports, setReports] = useState<ReportingDefinition[]>([]);
const [providerReports, setProviderReports] = useState<ProviderReportDescriptor[]>([]);
const [selectedId, setSelectedId] = useState("");
const [selectedProviderKey, setSelectedProviderKey] = useState("");
const [semantic, setSemantic] = useState<SemanticModelPayload | null>(null);
const [query, setQuery] = useState<ReportingQuery>(emptyQuery());
const [parameters, setParameters] = useState<Record<string, unknown>>({});
@@ -77,21 +85,42 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
() => reports.find((item) => item.definition_id === selectedId) ?? null,
[reports, selectedId]
);
const selectedProvider = useMemo(
() => providerReports.find((item) => `${item.provider_id}:${item.report_id}` === selectedProviderKey) ?? null,
[providerReports, selectedProviderKey]
);
const report = selected ? reportPayload(selected) : null;
function reload(signal?: AbortSignal) {
setLoading(true);
setError("");
return listDefinitions(settings, {
kinds: ["report"],
status: ["active"],
query: submittedSearch,
limit: 200
}, signal).
then((result) => {
return Promise.all([
listDefinitions(settings, {
kinds: ["report"],
status: ["active"],
query: submittedSearch,
limit: 200
}, signal),
listProviderReports(settings, signal)
]).
then(([result, providerResult]) => {
const providerRows = providerResult.reports.filter((item) => {
const query = submittedSearch.toLocaleLowerCase();
return !query || `${item.title} ${item.summary} ${item.provider_id}`.toLocaleLowerCase().includes(query);
});
setReports(result.definitions);
setSelectedId((current) => result.definitions.some((item) => item.definition_id === current) ?
current : result.definitions[0]?.definition_id ?? "");
setProviderReports(providerRows);
const currentSemanticAvailable = result.definitions.some((item) => item.definition_id === selectedId);
const currentProviderAvailable = providerRows.some((item) => `${item.provider_id}:${item.report_id}` === selectedProviderKey);
if (!currentSemanticAvailable && !currentProviderAvailable) {
if (providerRows[0]) {
setSelectedProviderKey(`${providerRows[0].provider_id}:${providerRows[0].report_id}`);
setSelectedId("");
} else {
setSelectedId(result.definitions[0]?.definition_id ?? "");
setSelectedProviderKey("");
}
}
}).
catch((reason) => {
if ((reason as Error).name !== "AbortError") setError(message(reason));
@@ -172,7 +201,7 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
placeholder="Search reports"
/>
</form>
<span className="reporting-count">{reports.length} reports</span>
<span className="reporting-count">{reports.length + providerReports.length} reports</span>
<IconButton
label="Reload reports"
icon={<RefreshCw size={17} />}
@@ -188,15 +217,32 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
<div className="reporting-workspace">
<PageScrollViewport className="reporting-catalogue">
{loading && <LoadingIndicator label="Loading reports" />}
{!loading && reports.length === 0 && <div className="reporting-empty">No active reports are available.</div>}
{!loading && reports.length + providerReports.length === 0 && <div className="reporting-empty">No active reports are available.</div>}
<div className="reporting-report-list" role="list">
{providerReports.length > 0 && <div className="reporting-list-heading">Module reports</div>}
{providerReports.map((item) => {
const key = `${item.provider_id}:${item.report_id}`;
return (
<button
type="button"
role="listitem"
key={key}
className={`reporting-report-row${selectedProviderKey === key ? " is-selected" : ""}`}
onClick={() => { setSelectedProviderKey(key); setSelectedId(""); }}>
<BarChart3 size={17} aria-hidden="true" />
<span><strong>{item.title}</strong><small>{item.provider_id} · {item.revision}</small></span>
<StatusBadge status={item.available ? "active" : "locked"} label={item.available ? "Available" : "Restricted"} />
</button>
);
})}
{reports.length > 0 && <div className="reporting-list-heading">Semantic reports</div>}
{reports.map((item) =>
<button
type="button"
role="listitem"
key={item.definition_id}
className={`reporting-report-row${selectedId === item.definition_id ? " is-selected" : ""}`}
onClick={() => setSelectedId(item.definition_id)}>
onClick={() => { setSelectedId(item.definition_id); setSelectedProviderKey(""); }}>
<BarChart3 size={17} aria-hidden="true" />
<span><strong>{item.name}</strong><small>{item.definition_key} · r{item.revision}</small></span>
<StatusBadge status="active" label="Active" />
@@ -205,7 +251,9 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
</div>
</PageScrollViewport>
<section className="reporting-result-region">
{selected && report ?
{selectedProvider ?
<ProviderReportWorkspace settings={settings} auth={auth} report={selectedProvider} /> :
selected && report ?
<>
<header className="reporting-result-header">
<div className="reporting-title">
@@ -258,14 +306,16 @@ export default function ReportingPage({ settings, auth }: PlatformRouteContext)
}
</section>
<PageScrollViewport className="reporting-inspector">
<Inspector
{selectedProvider ?
<ProviderReportInspector report={selectedProvider} /> :
<Inspector
selected={selected}
execution={execution}
history={history}
savedViews={savedViews}
onSelectExecution={setExecution}
onApplySavedView={applySavedView}
/>
/>}
</PageScrollViewport>
</div>
</div>