feat: implement governed reporting vertical
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user