import { DescriptionList } from "@govoplan/core-webui"; import { Download, FileJson, Play, ShieldCheck } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { ContentGrid, Button, Card, DismissibleAlert, IconButton, MetricCard, StatePanel, StatusBadge, WorkspaceActionBar, hasScope, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { downloadProviderExecution, listProviderParameterOptions, runProviderReport, type ProviderReportDescriptor, type ProviderReportExecution, type ProviderReportField } from "../../api/reporting"; export function ProviderReportWorkspace({ settings, auth, report }: { settings: ApiSettings; auth: AuthInfo; report: ProviderReportDescriptor; }) { const [parameters, setParameters] = useState>({}); const [options, setOptions] = useState>>({}); const [purpose, setPurpose] = useState(""); const [execution, setExecution] = useState(null); const [running, setRunning] = useState(false); const [error, setError] = useState(""); const tenant = auth.active_tenant ?? auth.tenant; const canRun = hasScope(auth, "reporting:report:run"); const audienceScope = useMemo(() => ({ scope_type: "tenant", scope_id: tenant.id, label: tenant.name }), [tenant.id, tenant.name]); useEffect(() => { setParameters({}); setPurpose(""); setExecution(null); setError(""); const controller = new AbortController(); const optionParameters = report.parameters.filter((item) => item.options_from_provider); void Promise.all(optionParameters.map(async (parameter) => { const result = await listProviderParameterOptions( settings, report, parameter.key, "", controller.signal ); return [parameter.key, result.options] as const; })).then((entries) => { setOptions(Object.fromEntries(entries)); setParameters(Object.fromEntries(entries.flatMap(([key, values]) => values[0] ? [[key, values[0].value]] : [] ))); }).catch((reason) => { if ((reason as Error).name !== "AbortError") setError(message(reason)); }); return () => controller.abort(); }, [settings, report.provider_id, report.report_id, report.revision]); async function run() { setRunning(true); setError(""); try { setExecution(await runProviderReport( settings, report, parameters, purpose.trim(), audienceScope )); } catch (reason) { setError(message(reason)); } finally { setRunning(false); } } async function download(format: "json" | "csv") { if (!execution) return; try { await downloadProviderExecution( settings, execution, format, purpose.trim(), audienceScope ); } catch (reason) { setError(message(reason)); } } const missingRequired = report.parameters.some((item) => item.required && (parameters[item.key] === undefined || parameters[item.key] === "") ); const runDisabledReason = !canRun ? "Report run permission is required." : !report.available ? report.unavailable_reason ?? "Policy does not allow this report." : missingRequired ? "Complete the required report parameters." : !purpose.trim() ? "Record the purpose for this governed report run." : undefined; return ( <>
{report.provider_id} · {report.revision}

{report.title}

{report.summary}

{error && {error}} {!report.available && {report.unavailable_reason ?? "Policy does not allow this report."} }
{report.parameters.map((parameter) => )}
{execution ? <> Generated {formatDateTime(execution.generated_at)}} primaryActions={<> {report.export_formats.includes("csv") && } variant="ghost" onClick={() => void download("csv")} /> } {report.export_formats.includes("json") && } variant="ghost" onClick={() => void download("json")} /> } } />
: } ); } export function ProviderReportInspector({ report }: { report: ProviderReportDescriptor }) { return (

Governance

Risk
Retention
{report.governance.retention_days == null ? "Policy managed" : `${report.governance.retention_days} days`}
Exports
{report.governance.export_formats.join(", ") || "Disabled"}
Contract
{report.contract_version}

Privacy transforms

{report.privacy_transforms.map((item) =>

{item.label}{item.required ? " · required" : ""}

)}
); } function ProviderResult({ execution }: { execution: ProviderReportExecution }) { const groups = groupFields(execution.result_schema); return ( <> {[...groups].map(([group, fields]) => { const metrics = fields.filter((field) => field.type === "suppressed_count"); const details = fields.filter((field) => field.type !== "suppressed_count"); return ( {metrics.length > 0 && {metrics.map((field) => )} } {details.length > 0 && {details.map((field) =>
{field.label}
{displayValue(pathValue(execution.result, field.path), field)}
)}
}
); })}
Purpose
{execution.purpose}
Output hash
{shortHash(execution.output_hash)}
Source revisions
{execution.source_revisions.length}
Privacy transforms
{execution.privacy_transforms.join(", ")}
Expires
{execution.expires_at ? formatDateTime(execution.expires_at) : "Policy managed"}
); } function groupFields(fields: ProviderReportField[]): Map { const groups = new Map(); for (const field of fields) groups.set(field.group, [...(groups.get(field.group) ?? []), field]); return groups; } function pathValue(payload: Record, path: string): unknown { let value: unknown = payload; for (const part of path.split(".")) { if (!value || typeof value !== "object") return null; value = (value as Record)[part]; } return value; } function displayValue(value: unknown, field?: ProviderReportField): string | number { if (value && typeof value === "object") { const count = value as { value?: unknown; suppressed?: boolean }; if (count.suppressed) return "Suppressed"; if ("value" in count) return displayValue(count.value); return JSON.stringify(value); } if (value === null || value === undefined || value === "") return "—"; if (field?.type === "datetime" || field?.type === "date") return formatDateTime(String(value)); if (typeof value === "number") return new Intl.NumberFormat().format(value); if (typeof value === "boolean") return value ? "Yes" : "No"; return String(value); } function formatDateTime(value: string): string { const date = new Date(value); return Number.isNaN(date.valueOf()) ? value : new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(date); } 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 : "The provider report could not be loaded."; }