feat: add governed cross-module reporting
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user