feat: initialize governed datasources module

This commit is contained in:
2026-07-28 12:43:02 +02:00
commit 1cb6228442
37 changed files with 5910 additions and 0 deletions

View File

@@ -0,0 +1,231 @@
import {
apiFetch,
type ApiSettings
} from "@govoplan/core-webui";
export type DatasourceMode = "live" | "cached" | "static";
export type DatasourceShape = "tabular" | "document" | "binary" | "directory" | "stream";
export type DatasourceField = {
name: string;
data_type: string;
nullable: boolean;
};
export type Datasource = {
ref: string;
source_name: string;
name: string;
description?: string | null;
kind: string;
mode: DatasourceMode;
shape: DatasourceShape;
status: string;
provider?: string | null;
provider_ref?: string | null;
schema: DatasourceField[];
schema_version: string;
fingerprint: string;
current_materialization_ref?: string | null;
row_count?: number | null;
byte_count?: number | null;
updated_at?: string | null;
capabilities: string[];
provenance: Record<string, unknown>;
metadata: Record<string, unknown>;
};
export type DatasourceMaterialization = {
ref: string;
datasource_ref: string;
revision: number;
state: string;
fingerprint: string;
schema: DatasourceField[];
row_count?: number | null;
byte_count?: number | null;
frozen_at?: string | null;
frozen_label?: string | null;
source_timestamp?: string | null;
created_at?: string | null;
provenance: Record<string, unknown>;
metadata: Record<string, unknown>;
};
export type DatasourceStage = {
ref: string;
name: string;
source_name: string;
kind: string;
mode: "static" | "cached";
shape: DatasourceShape;
state: string;
target_datasource_ref?: string | null;
fingerprint: string;
schema: DatasourceField[];
row_count?: number | null;
byte_count?: number | null;
validation: Record<string, unknown>;
created_at?: string | null;
promoted_at?: string | null;
promoted_materialization_ref?: string | null;
provenance: Record<string, unknown>;
metadata: Record<string, unknown>;
};
export type DatasourceOrigin = {
ref: string;
source_name: string;
name: string;
description?: string | null;
kind: string;
shape: DatasourceShape;
supported_modes: DatasourceMode[];
provider: string;
schema: DatasourceField[];
schema_version: string;
fingerprint: string;
row_count?: number | null;
byte_count?: number | null;
updated_at?: string | null;
capabilities: string[];
metadata: Record<string, unknown>;
};
export type DatasourcePreview = {
datasource: Datasource;
rows: Record<string, unknown>[];
total_rows: number;
truncated: boolean;
materialization?: DatasourceMaterialization | null;
};
export async function listDatasources(
settings: ApiSettings,
query = ""
): Promise<Datasource[]> {
const suffix = query.trim() ? `?query=${encodeURIComponent(query.trim())}` : "";
const response = await apiFetch<{ datasources: Datasource[] }>(
settings,
`/api/v1/datasources${suffix}`
);
return response.datasources;
}
export async function listDatasourceStages(
settings: ApiSettings
): Promise<DatasourceStage[]> {
const response = await apiFetch<{ stages: DatasourceStage[] }>(
settings,
"/api/v1/datasources/stages"
);
return response.stages;
}
export async function listDatasourceOrigins(
settings: ApiSettings
): Promise<{ available: boolean; origins: DatasourceOrigin[] }> {
return apiFetch(settings, "/api/v1/datasources/origins");
}
export async function listDatasourceMaterializations(
settings: ApiSettings,
datasourceRef: string
): Promise<DatasourceMaterialization[]> {
const response = await apiFetch<{ materializations: DatasourceMaterialization[] }>(
settings,
`/api/v1/datasources/${refId(datasourceRef)}/materializations`
);
return response.materializations;
}
export function previewDatasource(
settings: ApiSettings,
datasourceRef: string,
consistency: "current" | "live" | "frozen" = "current"
): Promise<DatasourcePreview> {
return apiFetch(
settings,
`/api/v1/datasources/${refId(datasourceRef)}/preview?limit=100&consistency=${consistency}`
);
}
export function createDatasourceStage(
settings: ApiSettings,
payload: {
name: string;
source_name: string;
description?: string | null;
mode: "static" | "cached";
target_datasource_ref?: string | null;
} & (
{ format: "json"; rows: Record<string, unknown>[] }
| { format: "csv"; csv_text: string; delimiter: string }
)
): Promise<DatasourceStage> {
return apiFetch(settings, "/api/v1/datasources/stages", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function promoteDatasourceStage(
settings: ApiSettings,
stageRef: string,
payload: { freeze?: boolean; frozen_label?: string | null } = {}
): Promise<{ datasource: Datasource; materialization: DatasourceMaterialization }> {
return apiFetch(settings, `/api/v1/datasources/stages/${refId(stageRef)}/promote`, {
method: "POST",
body: JSON.stringify(payload)
});
}
export function registerDatasourceOrigin(
settings: ApiSettings,
payload: {
origin_ref: string;
name: string;
source_name: string;
mode: "live" | "cached";
description?: string | null;
}
): Promise<Datasource> {
return apiFetch(settings, "/api/v1/datasources/origins/register", {
method: "POST",
body: JSON.stringify(payload)
});
}
export function refreshDatasource(
settings: ApiSettings,
datasourceRef: string
): Promise<{ datasource: Datasource; materialization: DatasourceMaterialization }> {
return apiFetch(settings, `/api/v1/datasources/${refId(datasourceRef)}/refresh`, {
method: "POST"
});
}
export function freezeDatasource(
settings: ApiSettings,
datasourceRef: string,
label: string
): Promise<DatasourceMaterialization> {
return apiFetch(settings, `/api/v1/datasources/${refId(datasourceRef)}/freeze`, {
method: "POST",
body: JSON.stringify({ label: label.trim() || null })
});
}
export function retireDatasource(
settings: ApiSettings,
datasourceRef: string
): Promise<{ retired: boolean; datasource_ref: string }> {
return apiFetch(settings, `/api/v1/datasources/${refId(datasourceRef)}`, {
method: "DELETE"
});
}
function refId(ref: string): string {
const separator = ref.indexOf(":");
return encodeURIComponent(separator >= 0 ? ref.slice(separator + 1) : ref);
}

File diff suppressed because it is too large Load Diff

2
webui/src/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { datasourcesModule as default, datasourcesModule } from "./module";
export * from "./api/datasources";

40
webui/src/module.ts Normal file
View File

@@ -0,0 +1,40 @@
import { createElement, lazy } from "react";
import type { PlatformWebModule } from "@govoplan/core-webui";
import "./styles/datasources.css";
const DatasourcesPage = lazy(() => import("./features/datasources/DatasourcesPage"));
const readScopes = ["datasources:catalogue:read", "datasources:source:admin"];
export const datasourcesModule: PlatformWebModule = {
id: "datasources",
label: "Datasources",
version: "0.1.14",
optionalDependencies: [
"access",
"audit",
"connectors",
"files",
"notifications",
"policy"
],
navItems: [
{
to: "/datasources",
label: "Datasources",
iconName: "database-zap",
anyOf: readScopes,
order: 70
}
],
routes: [
{
path: "/datasources",
anyOf: readScopes,
order: 70,
render: ({ settings, auth }) => createElement(DatasourcesPage, { settings, auth })
}
]
};
export default datasourcesModule;

View File

@@ -0,0 +1,478 @@
.datasources-page {
position: relative;
height: calc(100vh - 115px);
min-width: 0;
min-height: 0;
padding: 0;
overflow: hidden;
color: var(--text);
background: var(--bg);
}
.datasources-page *,
.datasources-page *::before,
.datasources-page *::after {
box-sizing: border-box;
}
.datasources-shell {
display: grid;
grid-template-columns: minmax(250px, 300px) minmax(0, 1fr);
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
border: var(--border-line);
background: var(--panel);
}
.datasources-sidebar,
.datasources-workspace,
.datasources-content,
.datasources-list-frame {
min-width: 0;
min-height: 0;
}
.datasources-sidebar {
display: flex;
flex-direction: column;
overflow: hidden;
border-right: var(--border-line);
background: var(--panel-soft);
}
.datasources-sidebar-toolbar,
.datasources-workspace-toolbar,
.datasources-section-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex: 0 0 auto;
border-bottom: var(--border-line);
background: var(--panel-header);
}
.datasources-sidebar-toolbar {
min-height: 52px;
padding: 8px 10px 8px 14px;
}
.datasources-toolbar-actions,
.datasources-current-title,
.datasources-current-title > span,
.datasources-section-heading > span {
display: flex;
align-items: center;
gap: 7px;
}
.datasources-view-switch {
padding: 8px;
border-bottom: var(--border-line);
background: var(--panel);
}
.datasources-search {
padding: 9px;
border-bottom: var(--border-line);
}
.datasources-search input {
width: 100%;
min-height: 34px;
padding: 7px 9px;
}
.datasources-list-frame {
flex: 1 1 auto;
overflow: hidden;
}
.datasources-list {
height: 100%;
overflow: auto;
padding: 6px;
}
.datasources-list > button {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
width: 100%;
min-height: 56px;
padding: 8px 9px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
cursor: pointer;
text-align: left;
}
.datasources-list > button:hover,
.datasources-list > button:focus-visible {
background: var(--primary-soft);
outline: none;
}
.datasources-list > button.is-selected {
background: var(--primary-soft-strong);
box-shadow: inset 3px 0 0 var(--accent);
}
.datasources-list > button > span:first-child {
min-width: 0;
}
.datasources-list strong,
.datasources-list small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.datasources-list strong {
color: var(--text-strong);
font-size: 13px;
}
.datasources-list small {
margin-top: 4px;
color: var(--muted);
font-size: 11px;
}
.datasources-list-empty,
.datasources-inline-empty,
.datasources-inline-loading {
display: grid;
place-items: center;
min-height: 110px;
color: var(--muted);
font-size: 13px;
text-align: center;
}
.datasources-workspace {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
}
.datasources-workspace-toolbar {
min-height: 58px;
padding: 8px 10px 8px 14px;
}
.datasources-current-title {
min-width: 0;
}
.datasources-current-title > span {
display: block;
min-width: 0;
}
.datasources-current-title strong,
.datasources-current-title small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.datasources-current-title strong {
color: var(--text-strong);
font-size: 14px;
}
.datasources-current-title small {
margin-top: 2px;
color: var(--muted);
font-size: 11px;
}
.datasources-alerts {
flex: 0 0 auto;
padding: 0 12px;
}
.datasources-alerts:empty {
display: none;
}
.datasources-alerts .alert {
margin: 10px 0 0;
}
.datasources-content {
flex: 1 1 auto;
overflow: auto;
padding: 14px;
}
.datasources-metrics {
display: grid;
grid-template-columns: repeat(5, minmax(100px, 1fr));
gap: 8px;
margin-bottom: 14px;
}
.datasources-metrics > span {
min-width: 0;
min-height: 61px;
padding: 9px 10px;
border: var(--border-line);
border-radius: var(--radius-sm);
background: var(--panel);
}
.datasources-metrics small,
.datasources-metrics strong {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.datasources-metrics small {
color: var(--muted);
font-size: 10px;
text-transform: uppercase;
}
.datasources-metrics strong {
margin-top: 6px;
color: var(--text-strong);
font-size: 14px;
text-transform: capitalize;
}
.datasources-description {
margin-bottom: 14px;
padding: 10px 12px;
border-left: 3px solid var(--accent);
background: var(--panel-soft);
color: var(--text);
font-size: 13px;
line-height: 1.45;
}
.datasources-detail-section {
min-width: 0;
margin-bottom: 14px;
border: var(--border-line);
background: var(--panel);
}
.datasources-section-heading {
min-height: 42px;
padding: 7px 10px;
}
.datasources-section-heading > span {
color: var(--text-strong);
font-size: 13px;
font-weight: 650;
}
.datasources-section-heading small {
color: var(--muted);
font-size: 11px;
}
.datasources-table-scroll {
max-width: 100%;
overflow: auto;
}
.datasources-preview-table {
max-height: 310px;
}
.datasources-table-scroll table {
width: 100%;
border-collapse: collapse;
color: var(--text);
font-size: 12px;
}
.datasources-table-scroll th,
.datasources-table-scroll td {
min-width: 100px;
max-width: 320px;
padding: 8px 10px;
border-right: var(--border-line);
border-bottom: var(--border-line);
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.datasources-table-scroll th {
position: sticky;
top: 0;
z-index: 1;
background: var(--panel-header);
color: var(--text-strong);
font-weight: 650;
}
.datasources-table-scroll tr:last-child td {
border-bottom: 0;
}
.datasources-fingerprint {
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
}
.datasources-key-values {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0;
}
.datasources-key-values > span {
min-width: 0;
padding: 12px;
border-right: var(--border-line);
}
.datasources-key-values > span:last-child {
border-right: 0;
}
.datasources-key-values small,
.datasources-key-values strong {
display: block;
}
.datasources-key-values small {
margin-bottom: 5px;
color: var(--muted);
font-size: 10px;
text-transform: uppercase;
}
.datasources-key-values strong {
overflow-wrap: anywhere;
color: var(--text-strong);
font-size: 12px;
font-weight: 600;
}
.datasources-workspace-empty {
display: grid;
place-items: center;
align-content: center;
min-height: 100%;
gap: 10px;
color: var(--muted);
text-align: center;
}
.datasources-working {
position: absolute;
right: 12px;
bottom: 12px;
z-index: 4;
padding: 7px 10px;
border: var(--border-line);
border-radius: var(--radius-sm);
background: var(--panel);
box-shadow: var(--shadow);
color: var(--text-strong);
font-size: 12px;
}
.datasources-add-dialog {
width: min(760px, calc(100vw - 32px));
}
.datasources-dialog-fields {
display: grid;
gap: 12px;
}
.datasources-dialog-fields textarea {
width: 100%;
min-height: 180px;
resize: vertical;
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 12px;
}
.datasources-dialog-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 12px;
}
.datasources-dialog-copy {
margin: 0 0 14px;
color: var(--muted);
font-size: 13px;
line-height: 1.45;
}
@media (max-width: 900px) {
.datasources-page {
height: auto;
min-height: calc(100vh - 92px);
overflow: auto;
}
.datasources-shell {
grid-template-columns: 1fr;
height: auto;
overflow: visible;
}
.datasources-sidebar {
max-height: 42vh;
border-right: 0;
border-bottom: var(--border-line);
}
.datasources-workspace {
min-height: 58vh;
}
.datasources-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.datasources-workspace-toolbar {
align-items: flex-start;
flex-wrap: wrap;
}
.datasources-toolbar-actions {
flex-wrap: wrap;
}
}
@media (max-width: 560px) {
.datasources-metrics,
.datasources-key-values,
.datasources-dialog-grid {
grid-template-columns: 1fr;
}
.datasources-key-values > span {
border-right: 0;
border-bottom: var(--border-line);
}
.datasources-key-values > span:last-child {
border-bottom: 0;
}
}

1
webui/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference path="../../../govoplan-core/webui/src/vite-env.d.ts" />