Migrate Datasources interface patterns
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
# Datasources Interface Pattern Migration
|
||||||
|
|
||||||
|
This migration applies the GovOPlaN interface pattern language to the governed
|
||||||
|
Datasource catalogue, staging area, connector-origin catalogue, governance
|
||||||
|
editor, previews, and immutable materialization history.
|
||||||
|
|
||||||
|
## Surface Inventory
|
||||||
|
|
||||||
|
| Surface | Archetype | Consequence class | Contract |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `/datasources` catalogue | Governed directory | Select, register, refresh, freeze, govern, or retire | Shared loading/empty/error, permission, disabled-reason, contextual-help, and read-only states |
|
||||||
|
| Staging | Review/preflight queue | Upload, validate, inspect, and promote | Non-consumable bounded stage, explicit promotion confirmation, immutable resulting revision |
|
||||||
|
| Connector origins | Optional-provider directory | Register a live or cached source | Provider availability and supported modes without hard Connectors dependency |
|
||||||
|
| Governance editor | Effective authority/provenance editor | Change institutional data context | Guarded draft, authority/source/owner/purpose/quality/freshness semantics |
|
||||||
|
| Preview/materializations | Evidence register | Inspect current sample and immutable revisions | Row/schema bounds, freshness, provenance, fingerprints, hashes, and frozen labels |
|
||||||
|
|
||||||
|
## Consequence And Availability Rules
|
||||||
|
|
||||||
|
- Static uploads become usable only after staged review and explicit promotion.
|
||||||
|
- Cached refresh and stage promotion append immutable materializations; they do
|
||||||
|
not rewrite prior execution evidence.
|
||||||
|
- Freezing creates a labelled immutable state for reproducible consumers.
|
||||||
|
- Retirement blocks new definitions while retained materialization references
|
||||||
|
continue under their governing retention policy.
|
||||||
|
- Connector origin absence is an explained optional-capability state. Local
|
||||||
|
catalogue, staging, and static data remain usable.
|
||||||
|
- Governance metadata may be discoverable independently of protected rows and
|
||||||
|
never grants row access.
|
||||||
|
|
||||||
|
Backend and WebUI manifests publish matching route/section/action surfaces.
|
||||||
|
English and German catalogues cover owned vocabulary, registration/governance/
|
||||||
|
freeze drafts are guarded, and module integrations remain capability-based.
|
||||||
@@ -34,6 +34,7 @@ from govoplan_core.core.provider_governance import (
|
|||||||
ModuleArchitectureDocumentation,
|
ModuleArchitectureDocumentation,
|
||||||
ModuleMaturityEvidence,
|
ModuleMaturityEvidence,
|
||||||
)
|
)
|
||||||
|
from govoplan_core.core.views import ViewSurface
|
||||||
from govoplan_core.db.base import Base
|
from govoplan_core.db.base import Base
|
||||||
from govoplan_datasources.backend.db import models as datasource_models
|
from govoplan_datasources.backend.db import models as datasource_models
|
||||||
from govoplan_datasources.backend.service import (
|
from govoplan_datasources.backend.service import (
|
||||||
@@ -284,6 +285,14 @@ manifest = ModuleManifest(
|
|||||||
order=70,
|
order=70,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
view_surfaces=(
|
||||||
|
ViewSurface(id="datasources.page", module_id=MODULE_ID, kind="route", label="Datasources", order=70),
|
||||||
|
ViewSurface(id="datasources.catalogue", module_id=MODULE_ID, kind="section", label="Datasource catalogue", order=10),
|
||||||
|
ViewSurface(id="datasources.staging", module_id=MODULE_ID, kind="section", label="Datasource staging", order=20),
|
||||||
|
ViewSurface(id="datasources.origins", module_id=MODULE_ID, kind="section", label="Datasource origins", order=30),
|
||||||
|
ViewSurface(id="datasources.governance", module_id=MODULE_ID, kind="action", label="Datasource governance", order=40),
|
||||||
|
ViewSurface(id="datasources.preview", module_id=MODULE_ID, kind="section", label="Datasource preview and materializations", order=50),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
route_factory=_router,
|
route_factory=_router,
|
||||||
capability_factories={
|
capability_factories={
|
||||||
@@ -352,6 +361,83 @@ manifest = ModuleManifest(
|
|||||||
"risk_compliance",
|
"risk_compliance",
|
||||||
),
|
),
|
||||||
order=70,
|
order=70,
|
||||||
|
metadata={
|
||||||
|
"seed": True,
|
||||||
|
"help_contexts": [
|
||||||
|
"datasources.page",
|
||||||
|
"datasources.catalogue",
|
||||||
|
"datasources.staging",
|
||||||
|
"datasources.origins",
|
||||||
|
"datasources.preview",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="datasources.governance",
|
||||||
|
title="Datasource authority and governance",
|
||||||
|
summary="Explain who owns data meaning, authority, correction, privacy, quality, freshness, retention, and dependent uses.",
|
||||||
|
body=(
|
||||||
|
"Authority mode states whether GovOPlaN, an external system, a synchronized projection, an overlay, or a linked reference "
|
||||||
|
"controls the data. The authoritative source, owner, steward, responsible organization/function, schema owner, privacy "
|
||||||
|
"profile, retention policy, transfer agreement, legal basis, holds, correction procedure, purposes, official keys, and "
|
||||||
|
"known limits provide discoverable institutional context. Freshness and quality policies are typed JSON contracts retained "
|
||||||
|
"with materialization evidence; enforcement remains with the provider or consuming control that declares support. Metadata "
|
||||||
|
"visibility never grants row access."
|
||||||
|
),
|
||||||
|
layer="available",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("operator", "module_admin", "data_steward", "product_owner"),
|
||||||
|
related_modules=("policy", "organizations", "idm", "dataflow", "reporting", "risk_compliance"),
|
||||||
|
order=71,
|
||||||
|
metadata={
|
||||||
|
"seed": True,
|
||||||
|
"help_contexts": [
|
||||||
|
"datasources.governance",
|
||||||
|
"datasources.field.authority-mode",
|
||||||
|
"datasources.field.authoritative-source",
|
||||||
|
"datasources.field.classification",
|
||||||
|
"datasources.field.publication-state",
|
||||||
|
"datasources.field.freshness-policy",
|
||||||
|
"datasources.field.quality-policy",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="datasources.reference.fields-and-consequences",
|
||||||
|
title="Datasource fields and lifecycle consequences",
|
||||||
|
summary="Live, cached, static, staging, promotion, refresh, freeze, and retirement semantics.",
|
||||||
|
body=(
|
||||||
|
"A Datasource key is the stable catalogue identity used by consumers. Live mode reads through an available origin; cached "
|
||||||
|
"mode refreshes an origin into immutable revisions; static mode promotes uploaded content from staging. Stages are bounded, "
|
||||||
|
"inspectable, and non-consumable until promoted. Promotion creates or updates a governed Datasource and appends an immutable "
|
||||||
|
"materialization. Refresh appends a new cached revision without rewriting older evidence. Freeze labels an immutable, "
|
||||||
|
"addressable state for reproducible execution. Retirement removes the Datasource from new definitions while retained "
|
||||||
|
"materialization references remain governed. Connector absence disables origin registration but leaves local catalogue and "
|
||||||
|
"staging behavior available."
|
||||||
|
),
|
||||||
|
layer="available",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("operator", "module_admin", "power_user", "product_owner"),
|
||||||
|
related_modules=("connectors", "dataflow", "workflow_engine", "reporting", "audit"),
|
||||||
|
order=72,
|
||||||
|
metadata={
|
||||||
|
"seed": True,
|
||||||
|
"help_contexts": [
|
||||||
|
"datasources.field.origin",
|
||||||
|
"datasources.field.key",
|
||||||
|
"datasources.field.mode",
|
||||||
|
"datasources.action.promote",
|
||||||
|
"datasources.action.refresh",
|
||||||
|
"datasources.action.freeze",
|
||||||
|
"datasources.action.retire",
|
||||||
|
],
|
||||||
|
"consequence_classes": {
|
||||||
|
"promote": "Creates or updates a governed Datasource and appends an immutable materialization revision.",
|
||||||
|
"refresh": "Reads the cached origin and appends a new immutable materialization revision.",
|
||||||
|
"freeze": "Creates a labelled immutable state for reproducible consumers and evidence.",
|
||||||
|
"retire": "Prevents new selection while retained materialization references remain governed.",
|
||||||
|
},
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_datasources.backend.manifest import manifest
|
||||||
|
|
||||||
|
|
||||||
|
class DatasourcesInterfaceDocumentationContractTests(unittest.TestCase):
|
||||||
|
def test_route_and_surfaces_remain_declared(self) -> None:
|
||||||
|
frontend = manifest.frontend
|
||||||
|
self.assertIsNotNone(frontend)
|
||||||
|
self.assertEqual({"/datasources"}, {item.path for item in frontend.routes}) # type: ignore[union-attr]
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"datasources.page",
|
||||||
|
"datasources.catalogue",
|
||||||
|
"datasources.staging",
|
||||||
|
"datasources.origins",
|
||||||
|
"datasources.governance",
|
||||||
|
"datasources.preview",
|
||||||
|
},
|
||||||
|
{item.id for item in frontend.view_surfaces}, # type: ignore[union-attr]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_help_and_consequence_metadata_remain_published(self) -> None:
|
||||||
|
topics = {topic.id: topic for topic in manifest.documentation}
|
||||||
|
lifecycle = topics["datasources.lifecycle"]
|
||||||
|
governance = topics["datasources.governance"]
|
||||||
|
reference = topics["datasources.reference.fields-and-consequences"]
|
||||||
|
|
||||||
|
self.assertIn("datasources.staging", lifecycle.metadata["help_contexts"])
|
||||||
|
self.assertIn("datasources.field.authority-mode", governance.metadata["help_contexts"])
|
||||||
|
self.assertIn("datasources.action.promote", reference.metadata["help_contexts"])
|
||||||
|
self.assertIn("freeze", reference.metadata["consequence_classes"])
|
||||||
|
self.assertIn("retire", reference.metadata["consequence_classes"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -20,9 +20,11 @@ import {
|
|||||||
Upload
|
Upload
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DocumentationHelpLink,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
FormField,
|
FormField,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -31,6 +33,8 @@ import {
|
|||||||
StatusBadge,
|
StatusBadge,
|
||||||
hasScope,
|
hasScope,
|
||||||
isApiError,
|
isApiError,
|
||||||
|
useUnsavedChanges,
|
||||||
|
useUnsavedDraftGuard,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo
|
type AuthInfo
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
@@ -54,6 +58,12 @@ import {
|
|||||||
type DatasourcePreview,
|
type DatasourcePreview,
|
||||||
type DatasourceStage
|
type DatasourceStage
|
||||||
} from "../../api/datasources";
|
} from "../../api/datasources";
|
||||||
|
import {
|
||||||
|
DATASOURCE_FIELDS_DOCUMENTATION,
|
||||||
|
DATASOURCE_GOVERNANCE_DOCUMENTATION,
|
||||||
|
DATASOURCES_DOCUMENTATION,
|
||||||
|
DATASOURCES_I18N
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
type CatalogueView = "catalogue" | "staging" | "origins";
|
type CatalogueView = "catalogue" | "staging" | "origins";
|
||||||
type AddKind = "upload" | "origin";
|
type AddKind = "upload" | "origin";
|
||||||
@@ -86,7 +96,9 @@ export default function DatasourcesPage({
|
|||||||
const [freezeOpen, setFreezeOpen] = useState(false);
|
const [freezeOpen, setFreezeOpen] = useState(false);
|
||||||
const [freezeLabel, setFreezeLabel] = useState("");
|
const [freezeLabel, setFreezeLabel] = useState("");
|
||||||
const [retireOpen, setRetireOpen] = useState(false);
|
const [retireOpen, setRetireOpen] = useState(false);
|
||||||
|
const [promoteOpen, setPromoteOpen] = useState(false);
|
||||||
const [governanceOpen, setGovernanceOpen] = useState(false);
|
const [governanceOpen, setGovernanceOpen] = useState(false);
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
|
|
||||||
const canManage = hasScope(auth, "datasources:source:write")
|
const canManage = hasScope(auth, "datasources:source:write")
|
||||||
|| hasScope(auth, "datasources:source:admin");
|
|| hasScope(auth, "datasources:source:admin");
|
||||||
@@ -211,8 +223,8 @@ export default function DatasourcesPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const freezeSelected = async () => {
|
const freezeSelected = async (): Promise<boolean> => {
|
||||||
if (!selectedDatasource) return;
|
if (!selectedDatasource) return false;
|
||||||
setWorking(true);
|
setWorking(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -225,8 +237,10 @@ export default function DatasourcesPage({
|
|||||||
setFreezeOpen(false);
|
setFreezeOpen(false);
|
||||||
setFreezeLabel("");
|
setFreezeLabel("");
|
||||||
await reload(selectedDatasource.ref);
|
await reload(selectedDatasource.ref);
|
||||||
|
return true;
|
||||||
} catch (operationError) {
|
} catch (operationError) {
|
||||||
setError(apiErrorMessage(operationError));
|
setError(apiErrorMessage(operationError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setWorking(false);
|
setWorking(false);
|
||||||
}
|
}
|
||||||
@@ -248,6 +262,23 @@ export default function DatasourcesPage({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty: Boolean(freezeOpen && freezeLabel.trim()),
|
||||||
|
onSave: freezeSelected,
|
||||||
|
onDiscard: () => {
|
||||||
|
setFreezeOpen(false);
|
||||||
|
setFreezeLabel("");
|
||||||
|
},
|
||||||
|
title: "i18n:govoplan-datasources.unsaved_freeze_title",
|
||||||
|
message: "i18n:govoplan-datasources.unsaved_freeze_message"
|
||||||
|
});
|
||||||
|
|
||||||
|
const closeFreeze = () => {
|
||||||
|
if (working) return;
|
||||||
|
if (freezeLabel.trim()) requestDiscard(() => setFreezeOpen(false));
|
||||||
|
else setFreezeOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="datasources-page">
|
<main className="datasources-page">
|
||||||
<div className="datasources-shell">
|
<div className="datasources-shell">
|
||||||
@@ -261,6 +292,7 @@ export default function DatasourcesPage({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => void reload(selectedDatasourceRef)}
|
onClick={() => void reload(selectedDatasourceRef)}
|
||||||
disabled={loading || working}
|
disabled={loading || working}
|
||||||
|
disabledReason={loading ? DATASOURCES_I18N.loading : working ? DATASOURCES_I18N.working : undefined}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
label="Add datasource or stage"
|
label="Add datasource or stage"
|
||||||
@@ -268,6 +300,7 @@ export default function DatasourcesPage({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => setAddOpen(true)}
|
onClick={() => setAddOpen(true)}
|
||||||
disabled={!canStage && !canManage}
|
disabled={!canStage && !canManage}
|
||||||
|
disabledReason={!canStage && !canManage ? DATASOURCES_I18N.manageReason : undefined}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -384,8 +417,9 @@ export default function DatasourcesPage({
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="datasources-toolbar-actions">
|
<span className="datasources-toolbar-actions">
|
||||||
|
<DocumentationHelpLink reference={DATASOURCES_DOCUMENTATION} />
|
||||||
{view === "catalogue" && selectedDatasource?.mode === "cached" ? (
|
{view === "catalogue" && selectedDatasource?.mode === "cached" ? (
|
||||||
<Button onClick={() => void refreshSelected()} disabled={!canManage || working}>
|
<Button onClick={() => void refreshSelected()} disabled={!canManage || working} disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}>
|
||||||
<Download size={16} /> Refresh
|
<Download size={16} /> Refresh
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -396,8 +430,9 @@ export default function DatasourcesPage({
|
|||||||
icon={<Pencil size={16} />}
|
icon={<Pencil size={16} />}
|
||||||
onClick={() => setGovernanceOpen(true)}
|
onClick={() => setGovernanceOpen(true)}
|
||||||
disabled={!canManage || working}
|
disabled={!canManage || working}
|
||||||
|
disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => setFreezeOpen(true)} disabled={!canManage || working}>
|
<Button onClick={() => setFreezeOpen(true)} disabled={!canManage || working} disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}>
|
||||||
<Snowflake size={16} /> Freeze
|
<Snowflake size={16} /> Freeze
|
||||||
</Button>
|
</Button>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -406,14 +441,16 @@ export default function DatasourcesPage({
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => setRetireOpen(true)}
|
onClick={() => setRetireOpen(true)}
|
||||||
disabled={!canManage || working}
|
disabled={!canManage || working}
|
||||||
|
disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{view === "staging" && selectedStage?.state === "ready" ? (
|
{view === "staging" && selectedStage?.state === "ready" ? (
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void promoteStage()}
|
onClick={() => setPromoteOpen(true)}
|
||||||
disabled={!canStage || working}
|
disabled={!canStage || working}
|
||||||
|
disabledReason={working ? DATASOURCES_I18N.working : !canStage ? DATASOURCES_I18N.stageReason : undefined}
|
||||||
>
|
>
|
||||||
<Upload size={16} /> Promote
|
<Upload size={16} /> Promote
|
||||||
</Button>
|
</Button>
|
||||||
@@ -423,6 +460,7 @@ export default function DatasourcesPage({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => setAddOpen(true)}
|
onClick={() => setAddOpen(true)}
|
||||||
disabled={!canManage || working}
|
disabled={!canManage || working}
|
||||||
|
disabledReason={working ? DATASOURCES_I18N.working : !canManage ? DATASOURCES_I18N.manageReason : undefined}
|
||||||
>
|
>
|
||||||
<Plus size={16} /> Register
|
<Plus size={16} /> Register
|
||||||
</Button>
|
</Button>
|
||||||
@@ -441,6 +479,18 @@ export default function DatasourcesPage({
|
|||||||
{success}
|
{success}
|
||||||
</DismissibleAlert>
|
</DismissibleAlert>
|
||||||
) : null}
|
) : null}
|
||||||
|
{!canManage && !canStage ? <ActionBlockerHint
|
||||||
|
tone="info"
|
||||||
|
reason={{
|
||||||
|
summary: "Datasources are read-only",
|
||||||
|
details: DATASOURCES_I18N.manageReason,
|
||||||
|
requiredAction: DATASOURCES_I18N.permissionAction,
|
||||||
|
actor: DATASOURCES_I18N.permissionActor,
|
||||||
|
target: DATASOURCES_I18N.permissionDestination
|
||||||
|
}}
|
||||||
|
labels={{ requiredAction: DATASOURCES_I18N.requiredAction, actor: DATASOURCES_I18N.actor, target: DATASOURCES_I18N.destination }}
|
||||||
|
documentation={DATASOURCES_DOCUMENTATION}
|
||||||
|
/> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="datasources-content">
|
<div className="datasources-content">
|
||||||
@@ -510,12 +560,10 @@ export default function DatasourcesPage({
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={freezeOpen}
|
open={freezeOpen}
|
||||||
title="Freeze datasource state"
|
title="Freeze datasource state"
|
||||||
onClose={() => {
|
onClose={closeFreeze}
|
||||||
if (!working) setFreezeOpen(false);
|
|
||||||
}}
|
|
||||||
footer={(
|
footer={(
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => setFreezeOpen(false)} disabled={working}>Cancel</Button>
|
<Button onClick={closeFreeze} disabled={working} disabledReason={working ? DATASOURCES_I18N.working : undefined}>Cancel</Button>
|
||||||
<Button variant="primary" onClick={() => void freezeSelected()} disabled={working}>
|
<Button variant="primary" onClick={() => void freezeSelected()} disabled={working}>
|
||||||
<Snowflake size={16} /> Freeze
|
<Snowflake size={16} /> Freeze
|
||||||
</Button>
|
</Button>
|
||||||
@@ -525,7 +573,7 @@ export default function DatasourcesPage({
|
|||||||
<p className="datasources-dialog-copy">
|
<p className="datasources-dialog-copy">
|
||||||
Create an immutable, addressable state for reproducible runs and evidence.
|
Create an immutable, addressable state for reproducible runs and evidence.
|
||||||
</p>
|
</p>
|
||||||
<FormField label="Label">
|
<FormField label="Label" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={freezeLabel}
|
value={freezeLabel}
|
||||||
onChange={(event) => setFreezeLabel(event.target.value)}
|
onChange={(event) => setFreezeLabel(event.target.value)}
|
||||||
@@ -533,6 +581,18 @@ export default function DatasourcesPage({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={promoteOpen}
|
||||||
|
title="i18n:govoplan-datasources.promote_title"
|
||||||
|
message="i18n:govoplan-datasources.promote_message"
|
||||||
|
confirmLabel="Promote"
|
||||||
|
busy={working}
|
||||||
|
onCancel={() => setPromoteOpen(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
setPromoteOpen(false);
|
||||||
|
void promoteStage();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={retireOpen}
|
open={retireOpen}
|
||||||
title="Retire datasource"
|
title="Retire datasource"
|
||||||
@@ -742,19 +802,27 @@ function GovernanceDialog({
|
|||||||
const [draft, setDraft] = useState<DatasourceGovernance | null>(null);
|
const [draft, setDraft] = useState<DatasourceGovernance | null>(null);
|
||||||
const [freshness, setFreshness] = useState("{}");
|
const [freshness, setFreshness] = useState("{}");
|
||||||
const [quality, setQuality] = useState("{}");
|
const [quality, setQuality] = useState("{}");
|
||||||
|
const [baselineKey, setBaselineKey] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !datasource) return;
|
if (!open || !datasource) return;
|
||||||
setDraft(structuredClone(datasource.governance));
|
const nextDraft = structuredClone(datasource.governance);
|
||||||
setFreshness(JSON.stringify(datasource.governance.freshness_policy, null, 2));
|
const nextFreshness = JSON.stringify(datasource.governance.freshness_policy, null, 2);
|
||||||
setQuality(JSON.stringify(datasource.governance.quality_policy, null, 2));
|
const nextQuality = JSON.stringify(datasource.governance.quality_policy, null, 2);
|
||||||
|
setDraft(nextDraft);
|
||||||
|
setFreshness(nextFreshness);
|
||||||
|
setQuality(nextQuality);
|
||||||
|
setBaselineKey(JSON.stringify({ draft: nextDraft, freshness: nextFreshness, quality: nextQuality }));
|
||||||
setError("");
|
setError("");
|
||||||
}, [datasource, open]);
|
}, [datasource, open]);
|
||||||
|
|
||||||
const save = async () => {
|
const dirty = Boolean(open && draft && JSON.stringify({ draft, freshness, quality }) !== baselineKey);
|
||||||
if (!datasource || !draft) return;
|
|
||||||
|
const save = async (): Promise<boolean> => {
|
||||||
|
if (!datasource || !draft) return false;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -764,13 +832,29 @@ function GovernanceDialog({
|
|||||||
quality_policy: parseObject(quality, "Quality policy")
|
quality_policy: parseObject(quality, "Quality policy")
|
||||||
});
|
});
|
||||||
await onSaved(updated);
|
await onSaved(updated);
|
||||||
|
return true;
|
||||||
} catch (saveError) {
|
} catch (saveError) {
|
||||||
setError(apiErrorMessage(saveError));
|
setError(apiErrorMessage(saveError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty,
|
||||||
|
onSave: save,
|
||||||
|
onDiscard: onClose,
|
||||||
|
title: "i18n:govoplan-datasources.unsaved_governance_title",
|
||||||
|
message: "i18n:govoplan-datasources.unsaved_governance_message"
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (busy) return;
|
||||||
|
if (dirty) requestDiscard(onClose);
|
||||||
|
else onClose();
|
||||||
|
};
|
||||||
|
|
||||||
const setValue = <K extends keyof DatasourceGovernance>(
|
const setValue = <K extends keyof DatasourceGovernance>(
|
||||||
key: K,
|
key: K,
|
||||||
value: DatasourceGovernance[K]
|
value: DatasourceGovernance[K]
|
||||||
@@ -781,11 +865,11 @@ function GovernanceDialog({
|
|||||||
open={open}
|
open={open}
|
||||||
title="Datasource governance"
|
title="Datasource governance"
|
||||||
className="datasources-governance-dialog"
|
className="datasources-governance-dialog"
|
||||||
onClose={() => { if (!busy) onClose(); }}
|
onClose={close}
|
||||||
footer={(
|
footer={(
|
||||||
<>
|
<>
|
||||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
<Button onClick={close} disabled={busy} disabledReason={busy ? DATASOURCES_I18N.working : undefined}>Cancel</Button>
|
||||||
<Button variant="primary" onClick={() => void save()} disabled={!draft || busy}>
|
<Button variant="primary" onClick={() => void save()} disabled={!draft || busy} disabledReason={busy ? DATASOURCES_I18N.working : !draft ? DATASOURCES_I18N.incomplete : undefined}>
|
||||||
Save governance
|
Save governance
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -795,7 +879,7 @@ function GovernanceDialog({
|
|||||||
{draft ? (
|
{draft ? (
|
||||||
<div className="datasources-dialog-fields">
|
<div className="datasources-dialog-fields">
|
||||||
<div className="datasources-dialog-grid">
|
<div className="datasources-dialog-grid">
|
||||||
<FormField label="Authority mode">
|
<FormField label="Authority mode" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={draft.authority_mode}
|
value={draft.authority_mode}
|
||||||
onChange={(event) => setValue("authority_mode", event.target.value as DatasourceGovernance["authority_mode"])}
|
onChange={(event) => setValue("authority_mode", event.target.value as DatasourceGovernance["authority_mode"])}
|
||||||
@@ -810,13 +894,13 @@ function GovernanceDialog({
|
|||||||
].map((value) => <option key={value} value={value}>{readableToken(value)}</option>)}
|
].map((value) => <option key={value} value={value}>{readableToken(value)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Authoritative source">
|
<FormField label="Authoritative source" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||||
<input value={draft.authoritative_source_ref ?? ""} onChange={(event) => setValue("authoritative_source_ref", event.target.value || null)} />
|
<input value={draft.authoritative_source_ref ?? ""} onChange={(event) => setValue("authoritative_source_ref", event.target.value || null)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Classification">
|
<FormField label="Classification" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||||
<input value={draft.classification} onChange={(event) => setValue("classification", event.target.value)} />
|
<input value={draft.classification} onChange={(event) => setValue("classification", event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Publication state">
|
<FormField label="Publication state" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||||
<input value={draft.publication_state} onChange={(event) => setValue("publication_state", event.target.value)} />
|
<input value={draft.publication_state} onChange={(event) => setValue("publication_state", event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Owner reference">
|
<FormField label="Owner reference">
|
||||||
@@ -847,7 +931,7 @@ function GovernanceDialog({
|
|||||||
<input value={draft.correction_procedure_ref ?? ""} onChange={(event) => setValue("correction_procedure_ref", event.target.value || null)} />
|
<input value={draft.correction_procedure_ref ?? ""} onChange={(event) => setValue("correction_procedure_ref", event.target.value || null)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<FormField label="Semantic definition">
|
<FormField label="Semantic definition" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||||
<textarea value={draft.semantic_definition ?? ""} onChange={(event) => setValue("semantic_definition", event.target.value || null)} />
|
<textarea value={draft.semantic_definition ?? ""} onChange={(event) => setValue("semantic_definition", event.target.value || null)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="datasources-dialog-grid">
|
<div className="datasources-dialog-grid">
|
||||||
@@ -860,10 +944,10 @@ function GovernanceDialog({
|
|||||||
<GovernanceListField label="Known limits" values={draft.known_limits} onChange={(values) => setValue("known_limits", values)} />
|
<GovernanceListField label="Known limits" values={draft.known_limits} onChange={(values) => setValue("known_limits", values)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="datasources-dialog-grid">
|
<div className="datasources-dialog-grid">
|
||||||
<FormField label="Freshness policy (JSON)">
|
<FormField label="Freshness policy (JSON)" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||||
<textarea value={freshness} onChange={(event) => setFreshness(event.target.value)} spellCheck={false} />
|
<textarea value={freshness} onChange={(event) => setFreshness(event.target.value)} spellCheck={false} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Quality policy (JSON)">
|
<FormField label="Quality policy (JSON)" documentation={DATASOURCE_GOVERNANCE_DOCUMENTATION}>
|
||||||
<textarea value={quality} onChange={(event) => setQuality(event.target.value)} spellCheck={false} />
|
<textarea value={quality} onChange={(event) => setQuality(event.target.value)} spellCheck={false} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
@@ -931,22 +1015,54 @@ function AddDatasourceDialog({
|
|||||||
const [rowsText, setRowsText] = useState('[\n { "id": 1 }\n]');
|
const [rowsText, setRowsText] = useState('[\n { "id": 1 }\n]');
|
||||||
const [csvText, setCsvText] = useState("");
|
const [csvText, setCsvText] = useState("");
|
||||||
const [delimiter, setDelimiter] = useState(";");
|
const [delimiter, setDelimiter] = useState(";");
|
||||||
|
const [baselineKey, setBaselineKey] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
setKind(initialKind);
|
setKind(initialKind);
|
||||||
|
setFormat("csv");
|
||||||
setOriginRef(initialOrigin?.ref ?? "");
|
setOriginRef(initialOrigin?.ref ?? "");
|
||||||
setName(initialOrigin?.name ?? "");
|
setName(initialOrigin?.name ?? "");
|
||||||
setSourceName(initialOrigin?.source_name ?? "");
|
setSourceName(initialOrigin?.source_name ?? "");
|
||||||
setDescription(initialOrigin?.description ?? "");
|
setDescription(initialOrigin?.description ?? "");
|
||||||
setMode(initialKind === "origin" ? "live" : "static");
|
setMode(initialKind === "origin" ? "live" : "static");
|
||||||
setTargetRef("");
|
setTargetRef("");
|
||||||
|
setRowsText('[\n { "id": 1 }\n]');
|
||||||
|
setCsvText("");
|
||||||
|
setDelimiter(";");
|
||||||
|
setBaselineKey(addDatasourceDraftKey({
|
||||||
|
kind: initialKind,
|
||||||
|
format: "csv",
|
||||||
|
mode: initialKind === "origin" ? "live" : "static",
|
||||||
|
originRef: initialOrigin?.ref ?? "",
|
||||||
|
targetRef: "",
|
||||||
|
name: initialOrigin?.name ?? "",
|
||||||
|
sourceName: initialOrigin?.source_name ?? "",
|
||||||
|
description: initialOrigin?.description ?? "",
|
||||||
|
rowsText: '[\n { "id": 1 }\n]',
|
||||||
|
csvText: "",
|
||||||
|
delimiter: ";"
|
||||||
|
}));
|
||||||
setError("");
|
setError("");
|
||||||
}, [initialKind, initialOrigin, open]);
|
}, [initialKind, initialOrigin, open]);
|
||||||
|
|
||||||
const selectedOrigin = origins.find((item) => item.ref === originRef) ?? null;
|
const selectedOrigin = origins.find((item) => item.ref === originRef) ?? null;
|
||||||
|
const dirty = Boolean(open && addDatasourceDraftKey({
|
||||||
|
kind,
|
||||||
|
format,
|
||||||
|
mode,
|
||||||
|
originRef,
|
||||||
|
targetRef,
|
||||||
|
name,
|
||||||
|
sourceName,
|
||||||
|
description,
|
||||||
|
rowsText,
|
||||||
|
csvText,
|
||||||
|
delimiter
|
||||||
|
}) !== baselineKey);
|
||||||
|
|
||||||
const chooseOrigin = (ref: string) => {
|
const chooseOrigin = (ref: string) => {
|
||||||
const origin = origins.find((item) => item.ref === ref);
|
const origin = origins.find((item) => item.ref === ref);
|
||||||
@@ -968,7 +1084,7 @@ function AddDatasourceDialog({
|
|||||||
setMode(target.mode === "cached" ? "cached" : "static");
|
setMode(target.mode === "cached" ? "cached" : "static");
|
||||||
};
|
};
|
||||||
|
|
||||||
const create = async () => {
|
const create = async (): Promise<boolean> => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -1006,13 +1122,29 @@ function AddDatasourceDialog({
|
|||||||
});
|
});
|
||||||
await onCreated(stage);
|
await onCreated(stage);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
} catch (createError) {
|
} catch (createError) {
|
||||||
setError(apiErrorMessage(createError));
|
setError(apiErrorMessage(createError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty,
|
||||||
|
onSave: create,
|
||||||
|
onDiscard: onClose,
|
||||||
|
title: "i18n:govoplan-datasources.unsaved_add_title",
|
||||||
|
message: "i18n:govoplan-datasources.unsaved_add_message"
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (busy) return;
|
||||||
|
if (dirty) requestDiscard(onClose);
|
||||||
|
else onClose();
|
||||||
|
};
|
||||||
|
|
||||||
const loadFile = async (file: File | undefined) => {
|
const loadFile = async (file: File | undefined) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
try {
|
try {
|
||||||
@@ -1034,12 +1166,10 @@ function AddDatasourceDialog({
|
|||||||
open={open}
|
open={open}
|
||||||
title="Add datasource"
|
title="Add datasource"
|
||||||
className="datasources-add-dialog"
|
className="datasources-add-dialog"
|
||||||
onClose={() => {
|
onClose={close}
|
||||||
if (!busy) onClose();
|
|
||||||
}}
|
|
||||||
footer={(
|
footer={(
|
||||||
<>
|
<>
|
||||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
<Button onClick={close} disabled={busy} disabledReason={busy ? DATASOURCES_I18N.working : undefined}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void create()}
|
onClick={() => void create()}
|
||||||
@@ -1050,6 +1180,7 @@ function AddDatasourceDialog({
|
|||||||
|| (kind === "upload" && !canStage)
|
|| (kind === "upload" && !canStage)
|
||||||
|| (kind === "origin" && (!canManage || !originRef))
|
|| (kind === "origin" && (!canManage || !originRef))
|
||||||
}
|
}
|
||||||
|
disabledReason={busy ? DATASOURCES_I18N.working : !name.trim() || !sourceName.trim() ? DATASOURCES_I18N.incomplete : kind === "upload" && !canStage ? DATASOURCES_I18N.stageReason : kind === "origin" && !canManage ? DATASOURCES_I18N.manageReason : kind === "origin" && !originRef ? DATASOURCES_I18N.incomplete : undefined}
|
||||||
>
|
>
|
||||||
{kind === "upload" ? <Upload size={16} /> : <Database size={16} />}
|
{kind === "upload" ? <Upload size={16} /> : <Database size={16} />}
|
||||||
{kind === "upload" ? "Create stage" : "Register"}
|
{kind === "upload" ? "Create stage" : "Register"}
|
||||||
@@ -1081,7 +1212,7 @@ function AddDatasourceDialog({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{kind === "origin" ? (
|
{kind === "origin" ? (
|
||||||
<FormField label="Connector origin">
|
<FormField label="Connector origin" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||||
<select value={originRef} onChange={(event) => chooseOrigin(event.target.value)}>
|
<select value={originRef} onChange={(event) => chooseOrigin(event.target.value)}>
|
||||||
<option value="">Choose an origin</option>
|
<option value="">Choose an origin</option>
|
||||||
{origins.map((origin) => (
|
{origins.map((origin) => (
|
||||||
@@ -1093,7 +1224,7 @@ function AddDatasourceDialog({
|
|||||||
</FormField>
|
</FormField>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<FormField label="Update existing datasource">
|
<FormField label="Update existing datasource" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||||
<select value={targetRef} onChange={(event) => chooseTarget(event.target.value)}>
|
<select value={targetRef} onChange={(event) => chooseTarget(event.target.value)}>
|
||||||
<option value="">Create a new datasource</option>
|
<option value="">Create a new datasource</option>
|
||||||
{datasources.filter((item) => item.mode !== "live" && item.shape === "tabular").map((item) => (
|
{datasources.filter((item) => item.mode !== "live" && item.shape === "tabular").map((item) => (
|
||||||
@@ -1111,10 +1242,10 @@ function AddDatasourceDialog({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="datasources-dialog-grid">
|
<div className="datasources-dialog-grid">
|
||||||
<FormField label="Name">
|
<FormField label="Name" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||||
<input value={name} onChange={(event) => setName(event.target.value)} />
|
<input value={name} onChange={(event) => setName(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Datasource key">
|
<FormField label="Datasource key" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={sourceName}
|
value={sourceName}
|
||||||
onChange={(event) => setSourceName(event.target.value)}
|
onChange={(event) => setSourceName(event.target.value)}
|
||||||
@@ -1127,7 +1258,7 @@ function AddDatasourceDialog({
|
|||||||
<FormField label="Description">
|
<FormField label="Description">
|
||||||
<input value={description} onChange={(event) => setDescription(event.target.value)} />
|
<input value={description} onChange={(event) => setDescription(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Mode">
|
<FormField label="Mode" documentation={DATASOURCE_FIELDS_DOCUMENTATION}>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
ariaLabel="Datasource mode"
|
ariaLabel="Datasource mode"
|
||||||
width="fill"
|
width="fill"
|
||||||
@@ -1285,6 +1416,22 @@ function parseRows(text: string): Record<string, unknown>[] {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addDatasourceDraftKey(value: {
|
||||||
|
kind: AddKind;
|
||||||
|
format: UploadFormat;
|
||||||
|
mode: "live" | "cached" | "static";
|
||||||
|
originRef: string;
|
||||||
|
targetRef: string;
|
||||||
|
name: string;
|
||||||
|
sourceName: string;
|
||||||
|
description: string;
|
||||||
|
rowsText: string;
|
||||||
|
csvText: string;
|
||||||
|
delimiter: string;
|
||||||
|
}): string {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export const DATASOURCES_DOCUMENTATION = {
|
||||||
|
topicId: "datasources.lifecycle",
|
||||||
|
documentationType: "user"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const DATASOURCE_FIELDS_DOCUMENTATION = {
|
||||||
|
topicId: "datasources.reference.fields-and-consequences",
|
||||||
|
documentationType: "admin"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const DATASOURCE_GOVERNANCE_DOCUMENTATION = {
|
||||||
|
topicId: "datasources.governance",
|
||||||
|
documentationType: "admin"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const DATASOURCES_I18N = {
|
||||||
|
loading: "i18n:govoplan-datasources.loading_reason",
|
||||||
|
working: "i18n:govoplan-datasources.working_reason",
|
||||||
|
manageReason: "i18n:govoplan-datasources.manage_permission_reason",
|
||||||
|
stageReason: "i18n:govoplan-datasources.stage_permission_reason",
|
||||||
|
noSelection: "i18n:govoplan-datasources.no_selection_reason",
|
||||||
|
incomplete: "i18n:govoplan-datasources.incomplete_reason",
|
||||||
|
requiredAction: "i18n:govoplan-datasources.required_action",
|
||||||
|
actor: "i18n:govoplan-datasources.actor",
|
||||||
|
destination: "i18n:govoplan-datasources.destination",
|
||||||
|
permissionAction: "i18n:govoplan-datasources.permission_action",
|
||||||
|
permissionActor: "i18n:govoplan-datasources.permission_actor",
|
||||||
|
permissionDestination: "i18n:govoplan-datasources.permission_destination"
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
const en = {
|
||||||
|
"i18n:govoplan-datasources.datasources": "Datasources",
|
||||||
|
"i18n:govoplan-datasources.catalogue": "Datasource catalogue",
|
||||||
|
"i18n:govoplan-datasources.staging": "Datasource staging",
|
||||||
|
"i18n:govoplan-datasources.origins": "Datasource origins",
|
||||||
|
"i18n:govoplan-datasources.governance": "Datasource governance",
|
||||||
|
"i18n:govoplan-datasources.preview": "Datasource preview and materializations",
|
||||||
|
"i18n:govoplan-datasources.loading_reason": "The Datasource catalogue is still loading.",
|
||||||
|
"i18n:govoplan-datasources.working_reason": "Another Datasource operation is still running.",
|
||||||
|
"i18n:govoplan-datasources.manage_permission_reason": "Your account may not register, refresh, freeze, govern, or retire Datasources.",
|
||||||
|
"i18n:govoplan-datasources.stage_permission_reason": "Your account may not create or promote Datasource stages.",
|
||||||
|
"i18n:govoplan-datasources.no_selection_reason": "Select the relevant Datasource, stage, or origin first.",
|
||||||
|
"i18n:govoplan-datasources.incomplete_reason": "Complete the required name, key, source, mode, and content fields first.",
|
||||||
|
"i18n:govoplan-datasources.required_action": "Required action",
|
||||||
|
"i18n:govoplan-datasources.actor": "Responsible actor",
|
||||||
|
"i18n:govoplan-datasources.destination": "Where to continue",
|
||||||
|
"i18n:govoplan-datasources.permission_action": "Ask for Datasource management or staging permission for the intended operation.",
|
||||||
|
"i18n:govoplan-datasources.permission_actor": "A tenant administrator or Datasource manager",
|
||||||
|
"i18n:govoplan-datasources.permission_destination": "Access administration for Datasources",
|
||||||
|
"i18n:govoplan-datasources.unsaved_add_title": "Uncreated Datasource or stage",
|
||||||
|
"i18n:govoplan-datasources.unsaved_add_message": "Create this Datasource or stage, or discard its draft before leaving.",
|
||||||
|
"i18n:govoplan-datasources.unsaved_governance_title": "Unsaved Datasource governance",
|
||||||
|
"i18n:govoplan-datasources.unsaved_governance_message": "Save or discard the governance draft before leaving.",
|
||||||
|
"i18n:govoplan-datasources.unsaved_freeze_title": "Unfrozen Datasource state",
|
||||||
|
"i18n:govoplan-datasources.unsaved_freeze_message": "Freeze this labelled state or discard the label before leaving.",
|
||||||
|
"i18n:govoplan-datasources.promote_title": "Promote Datasource stage",
|
||||||
|
"i18n:govoplan-datasources.promote_message": "Promote this reviewed stage? It will create or update the governed Datasource and append an immutable materialization revision.",
|
||||||
|
"Data": "Data",
|
||||||
|
"Catalogue": "Catalogue",
|
||||||
|
"Staging": "Staging",
|
||||||
|
"Origins": "Origins",
|
||||||
|
"No datasources": "No datasources",
|
||||||
|
"No staged data": "No staged data",
|
||||||
|
"No connector origins": "No connector origins",
|
||||||
|
"Connectors unavailable": "Connectors unavailable",
|
||||||
|
"Governed data": "Governed data",
|
||||||
|
"Inspect before promotion": "Inspect before promotion",
|
||||||
|
"External acquisition": "External acquisition",
|
||||||
|
"Edit datasource governance": "Edit Datasource governance",
|
||||||
|
"Freeze": "Freeze",
|
||||||
|
"Retire datasource": "Retire Datasource",
|
||||||
|
"Promote": "Promote",
|
||||||
|
"Register": "Register",
|
||||||
|
"Working...": "Working...",
|
||||||
|
"Freeze datasource state": "Freeze Datasource state",
|
||||||
|
"Datasource governance": "Datasource governance",
|
||||||
|
"Add datasource": "Add Datasource",
|
||||||
|
"Connector origin": "Connector origin",
|
||||||
|
"Update existing datasource": "Update existing Datasource",
|
||||||
|
"Datasource key": "Datasource key",
|
||||||
|
"Mode": "Mode",
|
||||||
|
"Static": "Static",
|
||||||
|
"Cached": "Cached",
|
||||||
|
"Live": "Live",
|
||||||
|
"Create stage": "Create stage",
|
||||||
|
"Save governance": "Save governance",
|
||||||
|
"Authority mode": "Authority mode",
|
||||||
|
"Authoritative source": "Authoritative source",
|
||||||
|
"Classification": "Classification",
|
||||||
|
"Publication state": "Publication state",
|
||||||
|
"Semantic definition": "Semantic definition",
|
||||||
|
"Freshness policy (JSON)": "Freshness policy (JSON)",
|
||||||
|
"Quality policy (JSON)": "Quality policy (JSON)"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const de: Record<keyof typeof en, string> = {
|
||||||
|
"i18n:govoplan-datasources.datasources": "Datenquellen",
|
||||||
|
"i18n:govoplan-datasources.catalogue": "Datenquellenkatalog",
|
||||||
|
"i18n:govoplan-datasources.staging": "Datenquellen-Staging",
|
||||||
|
"i18n:govoplan-datasources.origins": "Datenquellenursprünge",
|
||||||
|
"i18n:govoplan-datasources.governance": "Datenquellen-Governance",
|
||||||
|
"i18n:govoplan-datasources.preview": "Datenquellenvorschau und Materialisierungen",
|
||||||
|
"i18n:govoplan-datasources.loading_reason": "Der Datenquellenkatalog wird noch geladen.",
|
||||||
|
"i18n:govoplan-datasources.working_reason": "Eine andere Datenquellenoperation läuft noch.",
|
||||||
|
"i18n:govoplan-datasources.manage_permission_reason": "Ihr Konto darf Datenquellen nicht registrieren, aktualisieren, einfrieren, steuern oder stilllegen.",
|
||||||
|
"i18n:govoplan-datasources.stage_permission_reason": "Ihr Konto darf keine Datenquellen-Stages erstellen oder übernehmen.",
|
||||||
|
"i18n:govoplan-datasources.no_selection_reason": "Wählen Sie zuerst die betreffende Datenquelle, Stage oder den Ursprung.",
|
||||||
|
"i18n:govoplan-datasources.incomplete_reason": "Füllen Sie zuerst Name, Schlüssel, Quelle, Modus und Inhaltsfelder aus.",
|
||||||
|
"i18n:govoplan-datasources.required_action": "Erforderliche Aktion",
|
||||||
|
"i18n:govoplan-datasources.actor": "Verantwortliche Stelle",
|
||||||
|
"i18n:govoplan-datasources.destination": "Fortsetzung",
|
||||||
|
"i18n:govoplan-datasources.permission_action": "Fordern Sie die für die Operation erforderliche Datenquellen- oder Staging-Berechtigung an.",
|
||||||
|
"i18n:govoplan-datasources.permission_actor": "Mandantenadministration oder Datenquellenverwaltung",
|
||||||
|
"i18n:govoplan-datasources.permission_destination": "Zugriffsverwaltung für Datenquellen",
|
||||||
|
"i18n:govoplan-datasources.unsaved_add_title": "Nicht erstellte Datenquelle oder Stage",
|
||||||
|
"i18n:govoplan-datasources.unsaved_add_message": "Erstellen Sie diese Datenquelle oder Stage oder verwerfen Sie den Entwurf, bevor Sie fortfahren.",
|
||||||
|
"i18n:govoplan-datasources.unsaved_governance_title": "Ungespeicherte Datenquellen-Governance",
|
||||||
|
"i18n:govoplan-datasources.unsaved_governance_message": "Speichern oder verwerfen Sie den Governance-Entwurf, bevor Sie fortfahren.",
|
||||||
|
"i18n:govoplan-datasources.unsaved_freeze_title": "Nicht eingefrorener Datenquellenstand",
|
||||||
|
"i18n:govoplan-datasources.unsaved_freeze_message": "Frieren Sie diesen bezeichneten Stand ein oder verwerfen Sie die Bezeichnung.",
|
||||||
|
"i18n:govoplan-datasources.promote_title": "Datenquellen-Stage übernehmen",
|
||||||
|
"i18n:govoplan-datasources.promote_message": "Diese geprüfte Stage übernehmen? Die verwaltete Datenquelle wird erstellt oder aktualisiert und eine unveränderliche Materialisierungsrevision angefügt.",
|
||||||
|
"Data": "Daten",
|
||||||
|
"Catalogue": "Katalog",
|
||||||
|
"Staging": "Staging",
|
||||||
|
"Origins": "Ursprünge",
|
||||||
|
"No datasources": "Keine Datenquellen",
|
||||||
|
"No staged data": "Keine bereitgestellten Daten",
|
||||||
|
"No connector origins": "Keine Konnektorursprünge",
|
||||||
|
"Connectors unavailable": "Konnektoren nicht verfügbar",
|
||||||
|
"Governed data": "Verwaltete Daten",
|
||||||
|
"Inspect before promotion": "Vor Übernahme prüfen",
|
||||||
|
"External acquisition": "Externe Übernahme",
|
||||||
|
"Edit datasource governance": "Datenquellen-Governance bearbeiten",
|
||||||
|
"Freeze": "Einfrieren",
|
||||||
|
"Retire datasource": "Datenquelle stilllegen",
|
||||||
|
"Promote": "Übernehmen",
|
||||||
|
"Register": "Registrieren",
|
||||||
|
"Working...": "Vorgang läuft...",
|
||||||
|
"Freeze datasource state": "Datenquellenstand einfrieren",
|
||||||
|
"Datasource governance": "Datenquellen-Governance",
|
||||||
|
"Add datasource": "Datenquelle hinzufügen",
|
||||||
|
"Connector origin": "Konnektorursprung",
|
||||||
|
"Update existing datasource": "Bestehende Datenquelle aktualisieren",
|
||||||
|
"Datasource key": "Datenquellenschlüssel",
|
||||||
|
"Mode": "Modus",
|
||||||
|
"Static": "Statisch",
|
||||||
|
"Cached": "Zwischengespeichert",
|
||||||
|
"Live": "Live",
|
||||||
|
"Create stage": "Stage erstellen",
|
||||||
|
"Save governance": "Governance speichern",
|
||||||
|
"Authority mode": "Autoritätsmodus",
|
||||||
|
"Authoritative source": "Maßgebliche Quelle",
|
||||||
|
"Classification": "Klassifizierung",
|
||||||
|
"Publication state": "Veröffentlichungsstatus",
|
||||||
|
"Semantic definition": "Semantische Definition",
|
||||||
|
"Freshness policy (JSON)": "Aktualitätsrichtlinie (JSON)",
|
||||||
|
"Quality policy (JSON)": "Qualitätsrichtlinie (JSON)"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generatedTranslations: PlatformTranslations = { en, de };
|
||||||
+13
-2
@@ -1,5 +1,6 @@
|
|||||||
import { createElement, lazy } from "react";
|
import { createElement, lazy } from "react";
|
||||||
import type { PlatformWebModule } from "@govoplan/core-webui";
|
import type { PlatformWebModule } from "@govoplan/core-webui";
|
||||||
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
import "./styles/datasources.css";
|
import "./styles/datasources.css";
|
||||||
|
|
||||||
const DatasourcesPage = lazy(() => import("./features/datasources/DatasourcesPage"));
|
const DatasourcesPage = lazy(() => import("./features/datasources/DatasourcesPage"));
|
||||||
@@ -8,7 +9,7 @@ const readScopes = ["datasources:catalogue:read", "datasources:source:admin"];
|
|||||||
|
|
||||||
export const datasourcesModule: PlatformWebModule = {
|
export const datasourcesModule: PlatformWebModule = {
|
||||||
id: "datasources",
|
id: "datasources",
|
||||||
label: "Datasources",
|
label: "i18n:govoplan-datasources.datasources",
|
||||||
version: "0.1.14",
|
version: "0.1.14",
|
||||||
optionalDependencies: [
|
optionalDependencies: [
|
||||||
"access",
|
"access",
|
||||||
@@ -18,10 +19,19 @@ export const datasourcesModule: PlatformWebModule = {
|
|||||||
"notifications",
|
"notifications",
|
||||||
"policy"
|
"policy"
|
||||||
],
|
],
|
||||||
|
translations: generatedTranslations,
|
||||||
|
viewSurfaces: [
|
||||||
|
{ id: "datasources.page", moduleId: "datasources", kind: "route", label: "i18n:govoplan-datasources.datasources", order: 70 },
|
||||||
|
{ id: "datasources.catalogue", moduleId: "datasources", kind: "section", label: "i18n:govoplan-datasources.catalogue", parentId: "datasources.page", order: 10 },
|
||||||
|
{ id: "datasources.staging", moduleId: "datasources", kind: "section", label: "i18n:govoplan-datasources.staging", parentId: "datasources.page", order: 20 },
|
||||||
|
{ id: "datasources.origins", moduleId: "datasources", kind: "section", label: "i18n:govoplan-datasources.origins", parentId: "datasources.page", order: 30 },
|
||||||
|
{ id: "datasources.governance", moduleId: "datasources", kind: "action", label: "i18n:govoplan-datasources.governance", parentId: "datasources.catalogue", order: 40 },
|
||||||
|
{ id: "datasources.preview", moduleId: "datasources", kind: "section", label: "i18n:govoplan-datasources.preview", parentId: "datasources.catalogue", order: 50 }
|
||||||
|
],
|
||||||
navItems: [
|
navItems: [
|
||||||
{
|
{
|
||||||
to: "/datasources",
|
to: "/datasources",
|
||||||
label: "Datasources",
|
label: "i18n:govoplan-datasources.datasources",
|
||||||
iconName: "database-zap",
|
iconName: "database-zap",
|
||||||
anyOf: readScopes,
|
anyOf: readScopes,
|
||||||
order: 70
|
order: 70
|
||||||
@@ -32,6 +42,7 @@ export const datasourcesModule: PlatformWebModule = {
|
|||||||
path: "/datasources",
|
path: "/datasources",
|
||||||
anyOf: readScopes,
|
anyOf: readScopes,
|
||||||
order: 70,
|
order: 70,
|
||||||
|
surfaceId: "datasources.page",
|
||||||
render: ({ settings, auth }) => createElement(DatasourcesPage, { settings, auth })
|
render: ({ settings, auth }) => createElement(DatasourcesPage, { settings, auth })
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user