import { BarChart3, CalendarClock, Download, FileJson, History, Play, RefreshCw, Save, Search, SlidersHorizontal, Table2 } from "lucide-react"; import { useEffect, useMemo, useState, type FormEvent } from "react"; import { Button, DataGrid, Dialog, DocumentationHelpLink, DismissibleAlert, IconButton, LoadingIndicator, PageScrollViewport, SegmentedControl, StatusBadge, hasScope, type DataGridColumn, type PlatformRouteContext } from "@govoplan/core-webui"; import { createIntervalSchedule, downloadExecution, getDefinition, listDefinitions, listExecutions, listProviderReports, listSavedViews, reportPayload, runReport, saveView, semanticPayload, type ReportExecution, type ReportingDefinition, type ReportingQuery, type ReportingQueryMode, type ReportingSavedView, type ProviderReportDescriptor, type SemanticModelPayload } from "../../api/reporting"; import { ProviderReportInspector, ProviderReportWorkspace } from "./ProviderReportWorkspace"; type OutputMode = "visual" | "table"; export default function ReportingPage({ settings, auth }: PlatformRouteContext) { const [search, setSearch] = useState(""); const [submittedSearch, setSubmittedSearch] = useState(""); const [reports, setReports] = useState([]); const [providerReports, setProviderReports] = useState([]); const [selectedId, setSelectedId] = useState(""); const [selectedProviderKey, setSelectedProviderKey] = useState(""); const [semantic, setSemantic] = useState(null); const [query, setQuery] = useState(emptyQuery()); const [parameters, setParameters] = useState>({}); const [execution, setExecution] = useState(null); const [history, setHistory] = useState([]); const [savedViews, setSavedViews] = useState([]); const [outputMode, setOutputMode] = useState("visual"); const [loading, setLoading] = useState(true); const [running, setRunning] = useState(false); const [error, setError] = useState(""); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const [scheduleDialogOpen, setScheduleDialogOpen] = useState(false); const canRun = hasScope(auth, "reporting:report:run"); const canSchedule = hasScope(auth, "reporting:schedule:write"); const selected = useMemo( () => 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 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); 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)); }). finally(() => setLoading(false)); } useEffect(() => { const controller = new AbortController(); void reload(controller.signal); return () => controller.abort(); }, [settings, submittedSearch]); useEffect(() => { if (!selected || !report) { setSemantic(null); setExecution(null); setHistory([]); setSavedViews([]); return; } const controller = new AbortController(); setQuery(normalizeQuery(report.default_query)); setParameters(defaultParameters(report.parameters)); setExecution(null); Promise.all([ getDefinition(settings, "semantic_model", report.semantic_model_id, report.semantic_model_revision, controller.signal), canRun ? listExecutions(settings, selected.definition_id, controller.signal) : Promise.resolve({ executions: [] }), listSavedViews(settings, selected.definition_id, controller.signal) ]). then(([semanticDefinition, executions, views]) => { setSemantic(semanticPayload(semanticDefinition)); setHistory(executions.executions); setSavedViews(views.views); setExecution(executions.executions.find((item) => item.status === "succeeded") ?? null); }). catch((reason) => { if ((reason as Error).name !== "AbortError") setError(message(reason)); }); return () => controller.abort(); }, [settings, selectedId]); function submitSearch(event: FormEvent) { event.preventDefault(); setSubmittedSearch(search.trim()); } async function execute() { if (!selected) return; setRunning(true); setError(""); try { const result = await runReport(settings, selected, query, parameters); setExecution(result); setHistory((current) => [result, ...current.filter((item) => item.execution_id !== result.execution_id)]); setOutputMode(result.visualization?.kind === "table" ? "table" : "visual"); } catch (reason) { setError(message(reason)); } finally { setRunning(false); } } function applySavedView(view: ReportingSavedView) { if (view.state.query) setQuery(normalizeQuery(view.state.query)); } return (
{error && {error} }
{loading && } {!loading && reports.length + providerReports.length === 0 &&
No active reports are available.
}
{providerReports.length > 0 &&
Module reports
} {providerReports.map((item) => { const key = `${item.provider_id}:${item.report_id}`; return ( ); })} {reports.length > 0 &&
Semantic reports
} {reports.map((item) => )}
{selectedProvider ? : selected && report ? <>
{selected.definition_key} · revision {selected.revision}

{selected.name}

{canSchedule && } variant="ghost" onClick={() => setScheduleDialogOpen(true)} /> } } variant="ghost" onClick={() => setSaveDialogOpen(true)} />
Visual }, { id: "table", label: <> Table } ]} /> {execution && <> {execution.total_rows} rows{execution.truncated ? " (truncated)" : ""} } variant="ghost" onClick={() => void downloadExecution(settings, execution.execution_id, "csv").catch((reason) => setError(message(reason)))} /> } variant="ghost" onClick={() => void downloadExecution(settings, execution.execution_id, "json").catch((reason) => setError(message(reason)))} /> }
{!execution &&
Run the report or select a previous execution.
} {execution && outputMode === "visual" && } {execution && outputMode === "table" && }
:
Select a report.
}
{selectedProvider ? : }
setSaveDialogOpen(false)} onSave={async (name) => { if (!selected) return; const saved = await saveView(settings, selected, name, query); setSavedViews((current) => [...current, saved].sort((a, b) => a.name.localeCompare(b.name))); setSaveDialogOpen(false); }} /> setScheduleDialogOpen(false)} onSave={async (name, seconds) => { if (!selected) return; await createIntervalSchedule(settings, selected, name, seconds, query, parameters); setScheduleDialogOpen(false); }} />
); } function QueryControls({ query, semantic, parameters, parameterValues, onQueryChange, onParametersChange }: { query: ReportingQuery; semantic: SemanticModelPayload | null; parameters: Array<{ key: string; label: string; type: string; required: boolean; default?: unknown; allowed_values: unknown[] }>; parameterValues: Record; onQueryChange: (query: ReportingQuery) => void; onParametersChange: (parameters: Record) => void; }) { const dimensions = semantic?.dimensions ?? []; const measures = semantic?.measures ?? []; function setMode(mode: ReportingQueryMode) { const row = query.dimensions[0] ?? dimensions[0]?.key; const column = query.dimensions[1] ?? dimensions[1]?.key; onQueryChange({ ...query, mode, pivot: mode === "pivot" ? { rows: row ? [row] : [], columns: column ? [column] : [], measures: query.measures.length ? query.measures : measures.slice(0, 1).map((item) => item.key), include_totals: true } : null }); } return (
Dimensions
{dimensions.map((item) => )}
{query.mode !== "detail" &&
Measures
{measures.map((item) => )}
} {parameters.map((parameter) => )}
); } function ReportTable({ execution }: { execution: ReportExecution }) { const [page, setPage] = useState(0); useEffect(() => setPage(0), [execution.execution_id]); const columns = useMemo>[]>(() => execution.schema.map((field) => ({ id: field.name, header: humanize(field.name), width: "1fr", minWidth: 120, resizable: true, sortable: true, filterable: true, filterType: field.type === "integer" || field.type === "number" ? field.type : "text", value: (row) => row[field.name], render: (row) => formatValue(row[field.name]) })), [execution]); return ( `${execution.execution_id}:${index}`} initialFit="container" resizeBehavior="cover" emptyText="The report returned no rows." pagination={{ page, pageSize: 50, onPageChange: setPage }} /> ); } function ReportVisual({ execution }: { execution: ReportExecution }) { const visual = execution.visualization; if (!visual || visual.kind === "table" || !visual.category || !visual.measures?.length) { return (
{visual?.fallback_reason && {visual.fallback_reason}}
); } const measure = visual.measures[0]; const values = execution.rows.map((row) => Number(row[measure] ?? 0)); const maximum = Math.max(...values.map((value) => Math.abs(value)), 1); return (
{execution.rows.map((row, index) =>
{formatValue(row[visual.category ?? ""])}
{formatValue(row[measure])}
)}
); } function Inspector({ selected, execution, history, savedViews, onSelectExecution, onApplySavedView }: { selected: ReportingDefinition | null; execution: ReportExecution | null; history: ReportExecution[]; savedViews: ReportingSavedView[]; onSelectExecution: (execution: ReportExecution) => void; onApplySavedView: (view: ReportingSavedView) => void; }) { return (

Runs

{history.length === 0 &&

No recorded runs.

} {history.map((item) => )}

Saved views

{savedViews.length === 0 &&

No saved views.

} {savedViews.map((view) => )}
{selected &&

Definition

Report revision
{selected.revision}
Content hash
{shortHash(selected.content_hash)}
{execution && <>
Dataset
{execution.dataset_id} r{execution.dataset_revision}
} {execution?.output_hash && <>
Output hash
{shortHash(execution.output_hash)}
} {execution?.executor_version && <>
Executor
{execution.executor_version}
}
{execution?.diagnostics.map((item, index) => {item.message ?? item.code ?? "Execution diagnostic"} )}
}
); } function SaveViewDialog({ open, onClose, onSave }: { open: boolean; onClose: () => void; onSave: (name: string) => Promise }) { const [name, setName] = useState(""); const [saving, setSaving] = useState(false); return ( }> ); } function ScheduleDialog({ open, onClose, onSave }: { open: boolean; onClose: () => void; onSave: (name: string, seconds: number) => Promise }) { const [name, setName] = useState(""); const [interval, setIntervalValue] = useState("86400"); const [saving, setSaving] = useState(false); return ( }>
); } function emptyQuery(): ReportingQuery { return { mode: "summary", dimensions: [], measures: [], filters: [], sort: [], pivot: null, offset: 0, limit: 200 }; } function normalizeQuery(value: ReportingQuery): ReportingQuery { return { ...emptyQuery(), ...value, dimensions: [...(value.dimensions ?? [])], measures: [...(value.measures ?? [])], filters: [...(value.filters ?? [])], sort: [...(value.sort ?? [])] }; } function defaultParameters(parameters: Array<{ key: string; default?: unknown }>): Record { return Object.fromEntries(parameters.filter((item) => item.default !== null && item.default !== undefined).map((item) => [item.key, item.default])); } function toggle(values: string[], value: string): string[] { return values.includes(value) ? values.filter((item) => item !== value) : [...values, value]; } function typedValue(value: string, type: string): unknown { if (value === "") return null; if (type === "integer") return Number.parseInt(value, 10); if (type === "number") return Number(value); return value; } function formatValue(value: unknown): string { if (value === null || value === undefined) return "—"; if (typeof value === "number") return new Intl.NumberFormat().format(value); if (typeof value === "object") return JSON.stringify(value); return String(value); } function formatDateTime(value: string): string { const parsed = new Date(value); return Number.isNaN(parsed.valueOf()) ? value : new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(parsed); } function humanize(value: string): string { return value.replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); } function shortHash(value: string): string { return `${value.slice(0, 10)}…${value.slice(-6)}`; } function message(reason: unknown): string { return reason instanceof Error ? reason.message : "Reporting could not complete the request."; }