feat: implement governed reporting vertical

This commit is contained in:
2026-08-01 17:48:39 +02:00
parent 720959536b
commit eba35edd3c
35 changed files with 8414 additions and 33 deletions
+260
View File
@@ -0,0 +1,260 @@
import {
apiFetch,
apiPath,
apiUrl,
authHeaders,
type ApiSettings
} from "@govoplan/core-webui";
export type ReportingDefinitionKind = "dataset" | "semantic_model" | "report" | "quality_plan";
export type ReportingQueryMode = "summary" | "detail" | "pivot";
export type ReportingQuery = {
mode: ReportingQueryMode;
dimensions: string[];
measures: string[];
filters: Array<Record<string, unknown>>;
sort: Array<{ key: string; direction: "asc" | "desc" }>;
pivot?: {
rows: string[];
columns: string[];
measures: string[];
include_totals: boolean;
} | null;
offset: number;
limit: number;
};
export type ReportingDefinition = {
tenant_id: string;
definition_kind: ReportingDefinitionKind;
definition_id: string;
definition_key: string;
revision: number;
name: string;
description?: string | null;
status: "draft" | "active" | "retired";
visibility: "tenant" | "restricted";
content_hash: string;
parent_kind?: ReportingDefinitionKind | null;
parent_id?: string | null;
parent_revision?: number | null;
recorded_at: string;
change_reason: string;
payload: Record<string, unknown>;
};
export type SemanticModelPayload = {
dataset_id: string;
dataset_revision: number;
dimensions: Array<{ key: string; field: string; label: string }>;
measures: Array<{ key: string; label: string; aggregation: string }>;
default_dimensions: string[];
default_measures: string[];
};
export type ReportPayload = {
semantic_model_id: string;
semantic_model_revision: number;
parameters: Array<{
key: string;
label: string;
type: string;
required: boolean;
default?: unknown;
allowed_values: unknown[];
}>;
default_query: ReportingQuery;
visualization: {
kind: string;
category_dimension?: string | null;
series_dimension?: string | null;
measures: string[];
tabular_fallback: boolean;
};
institutional_references: Array<Record<string, unknown>>;
};
export type ReportExecution = {
execution_id: string;
report_id: string;
report_revision: number;
semantic_model_id: string;
semantic_model_revision: number;
dataset_id: string;
dataset_revision: number;
status: "running" | "succeeded" | "failed";
parameters: Record<string, unknown>;
query: ReportingQuery;
definition_hashes: Record<string, string>;
source_fingerprints: Array<Record<string, unknown>>;
output_hash?: string | null;
executor_version?: string | null;
schema: Array<{ name: string; type: string }>;
rows: Array<Record<string, unknown>>;
total_rows: number;
truncated: boolean;
diagnostics: Array<{ severity?: string; code?: string; message?: string }>;
provenance: Record<string, unknown>;
started_at: string;
finished_at?: string | null;
visualization?: {
kind: string;
requested_kind?: string;
category?: string | null;
measures?: string[];
fallback_reason?: string | null;
};
};
export type ReportingSavedView = {
view_id: string;
report_id: string;
report_revision: number;
name: string;
revision: number;
state: { query?: ReportingQuery };
shared: boolean;
updated_at: string;
};
export function listDefinitions(
settings: ApiSettings,
options: { kinds?: ReportingDefinitionKind[]; status?: string[]; query?: string; limit?: number },
signal?: AbortSignal
): Promise<{ definitions: ReportingDefinition[]; total: number }> {
return apiFetch(settings, apiPath("/api/v1/reporting/definitions", {
definition_kind: options.kinds,
status: options.status,
query: options.query,
limit: options.limit ?? 200
}), { signal });
}
export function getDefinition(
settings: ApiSettings,
kind: ReportingDefinitionKind,
id: string,
revision?: number,
signal?: AbortSignal
): Promise<ReportingDefinition> {
return apiFetch(settings, apiPath(
`/api/v1/reporting/definitions/${encodeURIComponent(kind)}/${encodeURIComponent(id)}`,
{ revision }
), { signal });
}
export function runReport(
settings: ApiSettings,
report: ReportingDefinition,
query: ReportingQuery,
parameters: Record<string, unknown>
): Promise<ReportExecution> {
return apiFetch(settings, `/api/v1/reporting/reports/${encodeURIComponent(report.definition_id)}/executions`, {
method: "POST",
body: JSON.stringify({
report_revision: report.revision,
parameters,
query,
idempotency_key: crypto.randomUUID()
})
});
}
export function listExecutions(
settings: ApiSettings,
reportId: string,
signal?: AbortSignal
): Promise<{ executions: ReportExecution[] }> {
return apiFetch(settings, `/api/v1/reporting/reports/${encodeURIComponent(reportId)}/executions?limit=30`, { signal });
}
export function listSavedViews(
settings: ApiSettings,
reportId: string,
signal?: AbortSignal
): Promise<{ views: ReportingSavedView[] }> {
return apiFetch(settings, `/api/v1/reporting/reports/${encodeURIComponent(reportId)}/saved-views`, { signal });
}
export function saveView(
settings: ApiSettings,
report: ReportingDefinition,
name: string,
query: ReportingQuery
): Promise<ReportingSavedView> {
const viewId = crypto.randomUUID();
return apiFetch(settings, `/api/v1/reporting/reports/${encodeURIComponent(report.definition_id)}/saved-views/${viewId}`, {
method: "PUT",
body: JSON.stringify({
view_id: viewId,
report_revision: report.revision,
name,
state: { query },
shared: false,
access: {},
expected_revision: null
})
});
}
export function createIntervalSchedule(
settings: ApiSettings,
report: ReportingDefinition,
name: string,
seconds: number,
query: ReportingQuery,
parameters: Record<string, unknown>
): Promise<Record<string, unknown>> {
const scheduleId = crypto.randomUUID();
return apiFetch(settings, `/api/v1/reporting/schedules/${scheduleId}`, {
method: "PUT",
body: JSON.stringify({
schedule_id: scheduleId,
report_id: report.definition_id,
report_revision: report.revision,
name,
trigger_kind: "interval",
trigger_config: { seconds },
parameters,
query,
publication_target: {},
enabled: true,
next_run_at: null,
expected_revision: null
})
});
}
export async function downloadExecution(
settings: ApiSettings,
executionId: string,
format: "csv" | "json"
): Promise<void> {
const response = await fetch(apiUrl(settings, apiPath(
`/api/v1/reporting/executions/${encodeURIComponent(executionId)}/export`,
{ format }
)), {
headers: authHeaders(settings),
credentials: "include"
});
if (!response.ok) throw new Error(await response.text() || "The report export failed.");
const blob = await response.blob();
const disposition = response.headers.get("content-disposition") ?? "";
const filename = disposition.match(/filename="?([^";]+)"?/i)?.[1] ?? `report.${format}`;
const href = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = href;
link.download = filename;
link.click();
URL.revokeObjectURL(href);
}
export function reportPayload(definition: ReportingDefinition): ReportPayload {
return definition.payload as unknown as ReportPayload;
}
export function semanticPayload(definition: ReportingDefinition): SemanticModelPayload {
return definition.payload as unknown as SemanticModelPayload;
}