Migrate Dataflow interface patterns
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
# Dataflow Interface Pattern Migration
|
||||||
|
|
||||||
|
This migration applies the GovOPlaN interface pattern language to the pipeline
|
||||||
|
library, graphical and constrained-SQL editors, node inspector, intermediate
|
||||||
|
preview, automation triggers, and durable run/deployment surfaces.
|
||||||
|
|
||||||
|
## Surface Inventory
|
||||||
|
|
||||||
|
| Surface | Archetype | Consequence class | Contract |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `/dataflow` library | Governed directory | Select, create, reuse, or retire a pipeline | Stable loading, empty, permission, read-only, disabled-reason, and help states |
|
||||||
|
| Graph/SQL workspace | Consequential definition editor | Append immutable revision | One canonical graph, guarded draft, constrained SQL compilation, diagnostics, and explicit save/discard |
|
||||||
|
| Node inspector | Typed configuration editor | Change transform semantics | Contextual node/expression help, typed validation, bounded intermediate preview, and read-only state |
|
||||||
|
| Trigger editor | Governed automation editor | Create, enable, change, or delete trigger | Guarded nested draft, pinned revision/grant, authorization recheck, and destructive confirmation |
|
||||||
|
| Preview/results | Bounded evidence preview | Read transient intermediate rows | Selected-node context, diagnostics, privacy boundary, row limit, and no retained preview contents |
|
||||||
|
| Runs/deployments | Asynchronous command register | Queue, publish, cancel, reconcile, or promote | Explicit consequence confirmation, durable command identity, progress, recovery state, and retained evidence |
|
||||||
|
|
||||||
|
## Consequence And Availability Rules
|
||||||
|
|
||||||
|
- Saving appends an immutable definition revision. Derivation creates a
|
||||||
|
separately governed copy pinned to the source revision and content hash.
|
||||||
|
- SQL is parsed and compiled into the canonical graph. It is never passed
|
||||||
|
directly to a database or execution provider.
|
||||||
|
- Preview is bounded and transient. A saved run is revision-pinned and records
|
||||||
|
actor, authority, idempotency, progress, output, and recovery evidence.
|
||||||
|
- Publishing appends a governed Datasource materialization. Unknown external
|
||||||
|
outcomes require reconciliation before retry.
|
||||||
|
- Staging and production promotion require confirmation and never rewrite a
|
||||||
|
revision. Cancellation cannot promise reversal of acknowledged external
|
||||||
|
effects.
|
||||||
|
- Missing optional Datasources or automation capabilities disable only their
|
||||||
|
associated actions; local graph, SQL, validation, and preview behavior stays
|
||||||
|
available where authorized.
|
||||||
|
|
||||||
|
Backend and WebUI manifests publish matching surface identifiers. English and
|
||||||
|
German catalogues cover module-owned navigation, states, and core commands.
|
||||||
|
Contextual help resolves from manifest documentation, main and nested drafts
|
||||||
|
are guarded, and unavailable actions carry stable reasons.
|
||||||
@@ -181,6 +181,113 @@ DOCUMENTATION = (
|
|||||||
"Output publication uses forward recovery and blocks blind retry "
|
"Output publication uses forward recovery and blocks blind retry "
|
||||||
"when provider acknowledgement is uncertain."
|
"when provider acknowledgement is uncertain."
|
||||||
),
|
),
|
||||||
|
"help_contexts": [
|
||||||
|
"dataflow.page",
|
||||||
|
"dataflow.library",
|
||||||
|
"dataflow.graph",
|
||||||
|
"dataflow.sql",
|
||||||
|
"dataflow.inspector",
|
||||||
|
"dataflow.results",
|
||||||
|
"dataflow.state.read-only",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="dataflow.reference.nodes-and-expressions",
|
||||||
|
title="Dataflow nodes and expressions",
|
||||||
|
summary="Typed node inputs, expressions, schema propagation, and bounded intermediate previews.",
|
||||||
|
body=(
|
||||||
|
"Every graph node declares typed inputs, configuration, output schema, and validation rules. "
|
||||||
|
"Source nodes pin inline content or governed Datasource references; combine, filter, transform, "
|
||||||
|
"quality, reconciliation, reusable-subflow, and output nodes remain explicit in the canonical graph. "
|
||||||
|
"Expressions use the typed Dataflow expression language and never execute arbitrary host or database "
|
||||||
|
"code. Selecting a node may request a bounded intermediate preview; preview rows are transient, "
|
||||||
|
"privacy-filtered for the actor, and are not retained as run output. SQL editing compiles into the same "
|
||||||
|
"canonical graph, so unsupported statements are diagnostics rather than pass-through SQL."
|
||||||
|
),
|
||||||
|
layer="available",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("operator", "module_admin", "power_user", "data_steward"),
|
||||||
|
order=76,
|
||||||
|
related_modules=("datasources", "connectors", "policy", "audit"),
|
||||||
|
metadata={
|
||||||
|
"help_contexts": [
|
||||||
|
"dataflow.field.node-name",
|
||||||
|
"dataflow.field.source",
|
||||||
|
"dataflow.field.expression",
|
||||||
|
"dataflow.field.schema",
|
||||||
|
"dataflow.action.preview-node",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="dataflow.reference.fields-and-consequences",
|
||||||
|
title="Dataflow fields and lifecycle consequences",
|
||||||
|
summary="Definition scope, revision, reuse, automation, execution, publication, promotion, and deletion semantics.",
|
||||||
|
body=(
|
||||||
|
"Scope determines ownership and Policy inheritance. Templates can be derived but not run; complete "
|
||||||
|
"flows may be previewed, revisioned, automated, and executed when effective Policy allows it. Saving "
|
||||||
|
"appends an immutable revision. A scoped copy pins its source revision and content hash. Triggers pin "
|
||||||
|
"the revision and authorization grant, then re-evaluate authority for every delivery. Runs create "
|
||||||
|
"durable command and recovery evidence. Publishing creates a governed Datasource materialization, and "
|
||||||
|
"environment promotion changes which immutable revision is eligible for staging or production runs. "
|
||||||
|
"Deletion prevents future use while retained run, deployment, lineage, audit, and recovery evidence "
|
||||||
|
"continues under its retention policy."
|
||||||
|
),
|
||||||
|
layer="available",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("operator", "module_admin", "power_user", "product_owner"),
|
||||||
|
order=77,
|
||||||
|
related_modules=("datasources", "workflow_engine", "notifications", "policy", "audit"),
|
||||||
|
metadata={
|
||||||
|
"help_contexts": [
|
||||||
|
"dataflow.field.scope",
|
||||||
|
"dataflow.field.definition-kind",
|
||||||
|
"dataflow.action.save",
|
||||||
|
"dataflow.action.derive",
|
||||||
|
"dataflow.action.trigger",
|
||||||
|
"dataflow.action.delete",
|
||||||
|
],
|
||||||
|
"consequence_classes": {
|
||||||
|
"save_revision": "Appends an immutable pipeline definition revision.",
|
||||||
|
"derive_copy": "Creates a separately governed copy pinned to the source revision and hash.",
|
||||||
|
"configure_trigger": "Creates or changes an automation command with revision and authorization evidence.",
|
||||||
|
"delete_pipeline": "Prevents future use while retained evidence remains governed.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="dataflow.execution-and-recovery",
|
||||||
|
title="Dataflow execution, publication, and recovery",
|
||||||
|
summary="Pinned runs, environment promotion, output publication, cancellation, reconciliation, and retained evidence.",
|
||||||
|
body=(
|
||||||
|
"Every run is pinned to an immutable revision and idempotency key. The queue records actor, authority, "
|
||||||
|
"environment, progress, cancellation, output, and recovery state. Database-only runs commit atomically. "
|
||||||
|
"Publication to a governed Datasource uses forward recovery: an unknown provider outcome is reconciled "
|
||||||
|
"before retry so output is not duplicated. Staging and production promotion is explicit and does not "
|
||||||
|
"rewrite a revision. Cancellation is best effort once external work has started; the final evidence "
|
||||||
|
"states whether work stopped, completed, failed, or requires operator reconciliation."
|
||||||
|
),
|
||||||
|
layer="available",
|
||||||
|
documentation_types=("admin", "user"),
|
||||||
|
audience=("operator", "module_admin", "power_user", "security_admin"),
|
||||||
|
order=78,
|
||||||
|
related_modules=("datasources", "notifications", "policy", "audit", "ops"),
|
||||||
|
metadata={
|
||||||
|
"help_contexts": [
|
||||||
|
"dataflow.runs",
|
||||||
|
"dataflow.action.queue-run",
|
||||||
|
"dataflow.action.publish",
|
||||||
|
"dataflow.action.promote-staging",
|
||||||
|
"dataflow.action.promote-production",
|
||||||
|
"dataflow.state.recovery-attention",
|
||||||
|
],
|
||||||
|
"consequence_classes": {
|
||||||
|
"queue_run": "Creates a durable asynchronous command and authorization evidence.",
|
||||||
|
"publish_output": "Creates or updates a governed Datasource and appends a materialization.",
|
||||||
|
"promote_revision": "Makes an immutable revision eligible in a higher execution environment.",
|
||||||
|
"cancel_run": "Requests cancellation; already acknowledged external effects may remain.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -374,6 +481,69 @@ manifest = ModuleManifest(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
view_surfaces=(
|
view_surfaces=(
|
||||||
|
ViewSurface(
|
||||||
|
id="dataflow.page",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="route",
|
||||||
|
label="Dataflow",
|
||||||
|
order=72,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dataflow.library",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="section",
|
||||||
|
label="Pipeline library",
|
||||||
|
parent_id="dataflow.page",
|
||||||
|
order=10,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dataflow.graph",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="section",
|
||||||
|
label="Graph editor",
|
||||||
|
parent_id="dataflow.page",
|
||||||
|
order=20,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dataflow.sql",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="section",
|
||||||
|
label="Constrained SQL editor",
|
||||||
|
parent_id="dataflow.page",
|
||||||
|
order=30,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dataflow.inspector",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="section",
|
||||||
|
label="Node inspector",
|
||||||
|
parent_id="dataflow.page",
|
||||||
|
order=40,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dataflow.results",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="section",
|
||||||
|
label="Preview and diagnostics",
|
||||||
|
parent_id="dataflow.page",
|
||||||
|
order=50,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dataflow.triggers",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="action",
|
||||||
|
label="Automation triggers",
|
||||||
|
parent_id="dataflow.page",
|
||||||
|
order=60,
|
||||||
|
),
|
||||||
|
ViewSurface(
|
||||||
|
id="dataflow.runs",
|
||||||
|
module_id=MODULE_ID,
|
||||||
|
kind="action",
|
||||||
|
label="Runs and deployments",
|
||||||
|
parent_id="dataflow.page",
|
||||||
|
order=70,
|
||||||
|
),
|
||||||
ViewSurface(
|
ViewSurface(
|
||||||
id="dataflow.widget.pipelines",
|
id="dataflow.widget.pipelines",
|
||||||
module_id=MODULE_ID,
|
module_id=MODULE_ID,
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_dataflow.backend.manifest import get_manifest
|
||||||
|
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class DataflowInterfaceDocumentationContractTests(unittest.TestCase):
|
||||||
|
def test_backend_surfaces_and_hierarchy_remain_declared(self) -> None:
|
||||||
|
frontend = get_manifest().frontend
|
||||||
|
self.assertIsNotNone(frontend)
|
||||||
|
surfaces = {item.id: item for item in frontend.view_surfaces} # type: ignore[union-attr]
|
||||||
|
self.assertEqual(
|
||||||
|
{
|
||||||
|
"dataflow.page",
|
||||||
|
"dataflow.library",
|
||||||
|
"dataflow.graph",
|
||||||
|
"dataflow.sql",
|
||||||
|
"dataflow.inspector",
|
||||||
|
"dataflow.results",
|
||||||
|
"dataflow.triggers",
|
||||||
|
"dataflow.runs",
|
||||||
|
"dataflow.widget.pipelines",
|
||||||
|
},
|
||||||
|
set(surfaces),
|
||||||
|
)
|
||||||
|
for surface_id in (
|
||||||
|
"dataflow.library",
|
||||||
|
"dataflow.graph",
|
||||||
|
"dataflow.sql",
|
||||||
|
"dataflow.inspector",
|
||||||
|
"dataflow.results",
|
||||||
|
"dataflow.triggers",
|
||||||
|
"dataflow.runs",
|
||||||
|
):
|
||||||
|
self.assertEqual("dataflow.page", surfaces[surface_id].parent_id)
|
||||||
|
|
||||||
|
def test_help_and_consequence_metadata_remain_published(self) -> None:
|
||||||
|
topics = {topic.id: topic for topic in get_manifest().documentation}
|
||||||
|
boundary = topics["dataflow.module-boundary"]
|
||||||
|
fields = topics["dataflow.reference.fields-and-consequences"]
|
||||||
|
nodes = topics["dataflow.reference.nodes-and-expressions"]
|
||||||
|
execution = topics["dataflow.execution-and-recovery"]
|
||||||
|
|
||||||
|
self.assertIn("dataflow.state.read-only", boundary.metadata["help_contexts"])
|
||||||
|
self.assertIn("dataflow.field.expression", nodes.metadata["help_contexts"])
|
||||||
|
self.assertIn("save_revision", fields.metadata["consequence_classes"])
|
||||||
|
self.assertIn("delete_pipeline", fields.metadata["consequence_classes"])
|
||||||
|
self.assertIn("publish_output", execution.metadata["consequence_classes"])
|
||||||
|
self.assertIn("promote_revision", execution.metadata["consequence_classes"])
|
||||||
|
|
||||||
|
def test_webui_uses_shared_help_guard_and_consequence_components(self) -> None:
|
||||||
|
page = (REPO_ROOT / "webui/src/features/dataflow/DataflowPage.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
inspector = (
|
||||||
|
REPO_ROOT / "webui/src/features/dataflow/NodeInspector.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
for component in (
|
||||||
|
"ActionBlockerHint",
|
||||||
|
"DocumentationHelpLink",
|
||||||
|
"useUnsavedDraftGuard",
|
||||||
|
"ConfirmDialog",
|
||||||
|
):
|
||||||
|
self.assertIn(component, page)
|
||||||
|
self.assertIn("DATAFLOW_NODE_DOCUMENTATION", inspector)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -26,10 +26,12 @@ import {
|
|||||||
Upload
|
Upload
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
Dialog,
|
Dialog,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
FormField,
|
FormField,
|
||||||
IconButton,
|
IconButton,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
@@ -97,6 +99,12 @@ import {
|
|||||||
type PipelineDraft
|
type PipelineDraft
|
||||||
} from "./model";
|
} from "./model";
|
||||||
import { dataflowNodeIcon } from "./nodeIcons";
|
import { dataflowNodeIcon } from "./nodeIcons";
|
||||||
|
import {
|
||||||
|
DATAFLOW_DOCUMENTATION,
|
||||||
|
DATAFLOW_FIELDS_DOCUMENTATION,
|
||||||
|
DATAFLOW_I18N,
|
||||||
|
DATAFLOW_RUN_DOCUMENTATION
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
type ResultTab = "preview" | "diagnostics";
|
type ResultTab = "preview" | "diagnostics";
|
||||||
type SnapshotFormat = "json" | "csv";
|
type SnapshotFormat = "json" | "csv";
|
||||||
@@ -145,6 +153,9 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
const canEdit = canWrite && (
|
const canEdit = canWrite && (
|
||||||
!draft?.id || draft.governance?.actions.edit?.allowed !== false
|
!draft?.id || draft.governance?.actions.edit?.allowed !== false
|
||||||
);
|
);
|
||||||
|
const editBlockedReason = !canWrite
|
||||||
|
? DATAFLOW_I18N.writeReason
|
||||||
|
: draft?.governance?.actions.edit?.reason ?? DATAFLOW_I18N.writeReason;
|
||||||
const canReuse = Boolean(
|
const canReuse = Boolean(
|
||||||
draft?.id
|
draft?.id
|
||||||
&& canWrite
|
&& canWrite
|
||||||
@@ -313,8 +324,8 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
|
|
||||||
useUnsavedDraftGuard({
|
useUnsavedDraftGuard({
|
||||||
dirty,
|
dirty,
|
||||||
title: "Unsaved pipeline",
|
title: "i18n:govoplan-dataflow.unsaved_title",
|
||||||
message: "Save or discard the pipeline changes before leaving this workspace.",
|
message: "i18n:govoplan-dataflow.unsaved_message",
|
||||||
onSave: saveDraft,
|
onSave: saveDraft,
|
||||||
onDiscard: discardDraft
|
onDiscard: discardDraft
|
||||||
});
|
});
|
||||||
@@ -577,6 +588,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => requestNavigation(() => void loadPipelines(draft?.id))}
|
onClick={() => requestNavigation(() => void loadPipelines(draft?.id))}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
disabledReason={loading ? DATAFLOW_I18N.loading : undefined}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
label="New pipeline"
|
label="New pipeline"
|
||||||
@@ -584,6 +596,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={createNew}
|
onClick={createNew}
|
||||||
disabled={!canWrite}
|
disabled={!canWrite}
|
||||||
|
disabledReason={!canWrite ? DATAFLOW_I18N.writeReason : undefined}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -658,6 +671,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="dataflow-command-bar">
|
<div className="dataflow-command-bar">
|
||||||
|
<DocumentationHelpLink reference={DATAFLOW_DOCUMENTATION} />
|
||||||
<SegmentedControl<EditorMode>
|
<SegmentedControl<EditorMode>
|
||||||
ariaLabel="Pipeline editor mode"
|
ariaLabel="Pipeline editor mode"
|
||||||
options={[
|
options={[
|
||||||
@@ -676,7 +690,11 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
onClick={() => void runPreview(selectedNodeId ?? undefined)}
|
onClick={() => void runPreview(selectedNodeId ?? undefined)}
|
||||||
disabled={working || !canPreview}
|
disabled={working || !canPreview}
|
||||||
disabledReason={
|
disabledReason={
|
||||||
draft.governance?.actions.run?.reason ?? undefined
|
working
|
||||||
|
? DATAFLOW_I18N.working
|
||||||
|
: !canRun
|
||||||
|
? DATAFLOW_I18N.runReason
|
||||||
|
: draft.governance?.actions.run?.reason ?? undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{preview ? <RefreshCw size={16} /> : <Play size={16} />}
|
{preview ? <RefreshCw size={16} /> : <Play size={16} />}
|
||||||
@@ -695,7 +713,11 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
disabled={working || !canRun || dirty || !canStartSavedRun || !draft.currentRevision}
|
disabled={working || !canRun || dirty || !canStartSavedRun || !draft.currentRevision}
|
||||||
disabledReason={
|
disabledReason={
|
||||||
dirty
|
dirty
|
||||||
? "Save the pipeline before starting a pinned run."
|
? DATAFLOW_I18N.saveFirst
|
||||||
|
: working
|
||||||
|
? DATAFLOW_I18N.working
|
||||||
|
: !canRun
|
||||||
|
? DATAFLOW_I18N.runReason
|
||||||
: draft.governance?.actions.run?.reason ?? undefined
|
: draft.governance?.actions.run?.reason ?? undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -707,6 +729,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setDefinitionSettingsOpen(true)}
|
onClick={() => setDefinitionSettingsOpen(true)}
|
||||||
disabled={!draft}
|
disabled={!draft}
|
||||||
|
disabledReason={!draft ? DATAFLOW_I18N.noSelection : undefined}
|
||||||
/>
|
/>
|
||||||
{draft.id ? (
|
{draft.id ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -715,6 +738,11 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setDeriveOpen(true)}
|
onClick={() => setDeriveOpen(true)}
|
||||||
disabled={!canReuse}
|
disabled={!canReuse}
|
||||||
|
disabledReason={
|
||||||
|
!canReuse
|
||||||
|
? draft.governance?.actions.derive?.reason ?? editBlockedReason
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{draft.id ? (
|
{draft.id ? (
|
||||||
@@ -724,6 +752,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setTriggersOpen(true)}
|
onClick={() => setTriggersOpen(true)}
|
||||||
disabled={!canViewTriggers}
|
disabled={!canViewTriggers}
|
||||||
|
disabledReason={!canViewTriggers ? DATAFLOW_I18N.writeReason : undefined}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -732,6 +761,13 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => requestDiscard(() => undefined)}
|
onClick={() => requestDiscard(() => undefined)}
|
||||||
disabled={!dirty || saving}
|
disabled={!dirty || saving}
|
||||||
|
disabledReason={
|
||||||
|
saving
|
||||||
|
? DATAFLOW_I18N.working
|
||||||
|
: !dirty
|
||||||
|
? DATAFLOW_I18N.noChanges
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{draft.id ? (
|
{draft.id ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -740,12 +776,24 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => setDeleteOpen(true)}
|
onClick={() => setDeleteOpen(true)}
|
||||||
disabled={!canEdit || saving}
|
disabled={!canEdit || saving}
|
||||||
|
disabledReason={
|
||||||
|
saving ? DATAFLOW_I18N.working : !canEdit ? editBlockedReason : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void saveDraft()}
|
onClick={() => void saveDraft()}
|
||||||
disabled={saving || working || !dirty || !canEdit}
|
disabled={saving || working || !dirty || !canEdit}
|
||||||
|
disabledReason={
|
||||||
|
saving || working
|
||||||
|
? DATAFLOW_I18N.working
|
||||||
|
: !canEdit
|
||||||
|
? editBlockedReason
|
||||||
|
: !dirty
|
||||||
|
? DATAFLOW_I18N.noChanges
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Save size={16} /> {saving ? "Saving..." : "Save"}
|
<Save size={16} /> {saving ? "Saving..." : "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -757,6 +805,26 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
{success ? (
|
{success ? (
|
||||||
<DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>
|
<DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>
|
||||||
) : null}
|
) : null}
|
||||||
|
{!canEdit ? (
|
||||||
|
<div className="dataflow-action-guidance">
|
||||||
|
<ActionBlockerHint
|
||||||
|
tone="info"
|
||||||
|
reason={{
|
||||||
|
summary: "Pipeline definition is read-only",
|
||||||
|
details: editBlockedReason,
|
||||||
|
requiredAction: DATAFLOW_I18N.permissionAction,
|
||||||
|
actor: DATAFLOW_I18N.permissionActor,
|
||||||
|
target: DATAFLOW_I18N.permissionDestination
|
||||||
|
}}
|
||||||
|
labels={{
|
||||||
|
requiredAction: DATAFLOW_I18N.requiredAction,
|
||||||
|
actor: DATAFLOW_I18N.actor,
|
||||||
|
target: DATAFLOW_I18N.destination
|
||||||
|
}}
|
||||||
|
documentation={DATAFLOW_DOCUMENTATION}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className={`dataflow-editor ${draft.editorMode === "sql" ? "is-sql" : ""}`}>
|
<div className={`dataflow-editor ${draft.editorMode === "sql" ? "is-sql" : ""}`}>
|
||||||
{draft.editorMode === "graph" ? (
|
{draft.editorMode === "graph" ? (
|
||||||
<aside className="dataflow-palette" aria-label="Transform palette">
|
<aside className="dataflow-palette" aria-label="Transform palette">
|
||||||
@@ -872,7 +940,12 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
|||||||
<div className="dataflow-workspace-empty">
|
<div className="dataflow-workspace-empty">
|
||||||
<Network size={30} />
|
<Network size={30} />
|
||||||
<strong>No pipeline selected</strong>
|
<strong>No pipeline selected</strong>
|
||||||
<Button variant="primary" onClick={createNew} disabled={!canWrite}>
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={createNew}
|
||||||
|
disabled={!canWrite}
|
||||||
|
disabledReason={!canWrite ? DATAFLOW_I18N.writeReason : undefined}
|
||||||
|
>
|
||||||
<Plus size={16} /> New pipeline
|
<Plus size={16} /> New pipeline
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1038,6 +1111,7 @@ function DefinitionSettingsDialog({
|
|||||||
<FormField
|
<FormField
|
||||||
label="Scope"
|
label="Scope"
|
||||||
help="The scope determines ownership, visibility, and where Policy inheritance is resolved."
|
help="The scope determines ownership, visibility, and where Policy inheritance is resolved."
|
||||||
|
documentation={DATAFLOW_FIELDS_DOCUMENTATION}
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
value={draft.scopeType}
|
value={draft.scopeType}
|
||||||
@@ -1061,6 +1135,7 @@ function DefinitionSettingsDialog({
|
|||||||
? "The stable account ID is stored; directory labels are presentation-only."
|
? "The stable account ID is stored; directory labels are presentation-only."
|
||||||
: "Only groups available in the active tenant can be selected."
|
: "Only groups available in the active tenant can be selected."
|
||||||
}
|
}
|
||||||
|
documentation={DATAFLOW_FIELDS_DOCUMENTATION}
|
||||||
>
|
>
|
||||||
<ReferenceSelect
|
<ReferenceSelect
|
||||||
value={draft.scopeId}
|
value={draft.scopeId}
|
||||||
@@ -1084,6 +1159,7 @@ function DefinitionSettingsDialog({
|
|||||||
<FormField
|
<FormField
|
||||||
label="Definition kind"
|
label="Definition kind"
|
||||||
help="Templates can be reused or derived, but cannot be run or automated directly."
|
help="Templates can be reused or derived, but cannot be run or automated directly."
|
||||||
|
documentation={DATAFLOW_FIELDS_DOCUMENTATION}
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
value={draft.definitionKind}
|
value={draft.definitionKind}
|
||||||
@@ -1166,12 +1242,23 @@ function DerivePipelineDialog({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onDerived: (pipeline: Pipeline) => void;
|
onDerived: (pipeline: Pipeline) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [kind, setKind] = useState<"flow" | "template">("flow");
|
const [kind, setKind] = useState<"flow" | "template">("flow");
|
||||||
const [scopeType, setScopeType] = useState<"tenant" | "group" | "user">("tenant");
|
const [scopeType, setScopeType] = useState<"tenant" | "group" | "user">("tenant");
|
||||||
const [scopeId, setScopeId] = useState("");
|
const [scopeId, setScopeId] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const defaultName = `${pipeline?.name ?? "Pipeline"} copy`;
|
||||||
|
const dirty = Boolean(
|
||||||
|
open
|
||||||
|
&& (
|
||||||
|
name !== defaultName
|
||||||
|
|| kind !== "flow"
|
||||||
|
|| scopeType !== "tenant"
|
||||||
|
|| scopeId !== ""
|
||||||
|
)
|
||||||
|
);
|
||||||
const scopeProvider = useMemo(
|
const scopeProvider = useMemo(
|
||||||
() =>
|
() =>
|
||||||
dataflowScopeReferenceProvider(
|
dataflowScopeReferenceProvider(
|
||||||
@@ -1188,15 +1275,23 @@ function DerivePipelineDialog({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
setName(`${pipeline?.name ?? "Pipeline"} copy`);
|
setName(defaultName);
|
||||||
setKind("flow");
|
setKind("flow");
|
||||||
setScopeType("tenant");
|
setScopeType("tenant");
|
||||||
setScopeId("");
|
setScopeId("");
|
||||||
setError("");
|
setError("");
|
||||||
}, [open, pipeline?.id]);
|
}, [defaultName, open, pipeline?.id]);
|
||||||
|
|
||||||
const derive = async () => {
|
const resetDraft = () => {
|
||||||
if (!pipeline || !name.trim()) return;
|
setName(defaultName);
|
||||||
|
setKind("flow");
|
||||||
|
setScopeType("tenant");
|
||||||
|
setScopeId("");
|
||||||
|
setError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const derive = async (): Promise<boolean> => {
|
||||||
|
if (!pipeline || !name.trim()) return false;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -1211,23 +1306,39 @@ function DerivePipelineDialog({
|
|||||||
allow_reuse: false,
|
allow_reuse: false,
|
||||||
allow_automation: false
|
allow_automation: false
|
||||||
}));
|
}));
|
||||||
|
return true;
|
||||||
} catch (deriveError) {
|
} catch (deriveError) {
|
||||||
setError(apiErrorMessage(deriveError));
|
setError(apiErrorMessage(deriveError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty,
|
||||||
|
title: "i18n:govoplan-dataflow.unsaved_copy_title",
|
||||||
|
message: "i18n:govoplan-dataflow.unsaved_copy_message",
|
||||||
|
onSave: derive,
|
||||||
|
onDiscard: resetDraft
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (busy) return;
|
||||||
|
if (dirty) requestDiscard(onClose);
|
||||||
|
else onClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
title="Reuse as scoped copy"
|
title="Reuse as scoped copy"
|
||||||
className="dataflow-definition-dialog"
|
className="dataflow-definition-dialog"
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
onClose={onClose}
|
onClose={close}
|
||||||
footer={(
|
footer={(
|
||||||
<>
|
<>
|
||||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
<Button onClick={close} disabled={busy}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void derive()}
|
onClick={() => void derive()}
|
||||||
@@ -1245,10 +1356,10 @@ function DerivePipelineDialog({
|
|||||||
>
|
>
|
||||||
<div className="dataflow-definition-fields">
|
<div className="dataflow-definition-fields">
|
||||||
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
||||||
<FormField label="Name">
|
<FormField label="Name" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input value={name} onChange={(event) => setName(event.target.value)} />
|
<input value={name} onChange={(event) => setName(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Target scope">
|
<FormField label="Target scope" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={scopeType}
|
value={scopeType}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
@@ -1262,7 +1373,10 @@ function DerivePipelineDialog({
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
{scopeType !== "tenant" ? (
|
{scopeType !== "tenant" ? (
|
||||||
<FormField label={scopeType === "user" ? "User" : "Group"}>
|
<FormField
|
||||||
|
label={scopeType === "user" ? "User" : "Group"}
|
||||||
|
documentation={DATAFLOW_FIELDS_DOCUMENTATION}
|
||||||
|
>
|
||||||
<ReferenceSelect
|
<ReferenceSelect
|
||||||
value={scopeId}
|
value={scopeId}
|
||||||
onChange={(value) => setScopeId(value)}
|
onChange={(value) => setScopeId(value)}
|
||||||
@@ -1278,7 +1392,7 @@ function DerivePipelineDialog({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
) : null}
|
) : null}
|
||||||
<FormField label="Copy kind">
|
<FormField label="Copy kind" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<select value={kind} onChange={(event) => setKind(event.target.value as typeof kind)}>
|
<select value={kind} onChange={(event) => setKind(event.target.value as typeof kind)}>
|
||||||
<option value="flow">Complete flow</option>
|
<option value="flow">Complete flow</option>
|
||||||
<option value="template">Template</option>
|
<option value="template">Template</option>
|
||||||
@@ -1306,6 +1420,7 @@ function DataflowTriggersDialog({
|
|||||||
editable: boolean;
|
editable: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { requestNavigation, requestDiscard } = useUnsavedChanges();
|
||||||
const [triggers, setTriggers] = useState<DataflowTrigger[]>([]);
|
const [triggers, setTriggers] = useState<DataflowTrigger[]>([]);
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
@@ -1320,7 +1435,22 @@ function DataflowTriggersDialog({
|
|||||||
const [eventFilters, setEventFilters] = useState("{}");
|
const [eventFilters, setEventFilters] = useState("{}");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [savedFingerprint, setSavedFingerprint] = useState("");
|
||||||
|
const [deleteCandidate, setDeleteCandidate] = useState<DataflowTrigger | null>(null);
|
||||||
const selected = triggers.find((item) => item.id === selectedId) ?? null;
|
const selected = triggers.find((item) => item.id === selectedId) ?? null;
|
||||||
|
const currentFingerprint = JSON.stringify([
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
enabled,
|
||||||
|
runAt,
|
||||||
|
intervalMinutes,
|
||||||
|
catchUpPolicy,
|
||||||
|
maxConcurrentRuns,
|
||||||
|
eventType,
|
||||||
|
eventModule,
|
||||||
|
eventFilters
|
||||||
|
]);
|
||||||
|
const dirty = Boolean(open && savedFingerprint && currentFingerprint !== savedFingerprint);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!pipeline) return;
|
if (!pipeline) return;
|
||||||
@@ -1348,27 +1478,58 @@ function DataflowTriggersDialog({
|
|||||||
setEventType("");
|
setEventType("");
|
||||||
setEventModule("");
|
setEventModule("");
|
||||||
setEventFilters("{}");
|
setEventFilters("{}");
|
||||||
|
setSavedFingerprint(JSON.stringify([
|
||||||
|
`${pipeline.name} schedule`,
|
||||||
|
"interval",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
60,
|
||||||
|
"coalesce",
|
||||||
|
1,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"{}"
|
||||||
|
]));
|
||||||
|
setDeleteCandidate(null);
|
||||||
void load();
|
void load();
|
||||||
}, [open, pipeline?.id, load]);
|
}, [open, pipeline?.id, load]);
|
||||||
|
|
||||||
const edit = (trigger: DataflowTrigger) => {
|
const edit = (trigger: DataflowTrigger) => {
|
||||||
|
const nextRunAt = toLocalDateTime(trigger.schedule?.run_at ?? "");
|
||||||
|
const nextInterval = Math.max(1, Math.round((trigger.schedule?.interval_seconds ?? 3600) / 60));
|
||||||
|
const nextEventType = trigger.event?.event_type ?? "";
|
||||||
|
const nextEventModule = trigger.event?.module_id ?? "";
|
||||||
|
const nextFilters = JSON.stringify(trigger.event?.filters ?? {}, null, 2);
|
||||||
setSelectedId(trigger.id);
|
setSelectedId(trigger.id);
|
||||||
setName(trigger.name);
|
setName(trigger.name);
|
||||||
setKind(trigger.kind);
|
setKind(trigger.kind);
|
||||||
setEnabled(trigger.status === "enabled");
|
setEnabled(trigger.status === "enabled");
|
||||||
setRunAt(toLocalDateTime(trigger.schedule?.run_at ?? ""));
|
setRunAt(nextRunAt);
|
||||||
setIntervalMinutes(Math.max(1, Math.round((trigger.schedule?.interval_seconds ?? 3600) / 60)));
|
setIntervalMinutes(nextInterval);
|
||||||
setCatchUpPolicy(trigger.catch_up_policy);
|
setCatchUpPolicy(trigger.catch_up_policy);
|
||||||
setMaxConcurrentRuns(trigger.max_concurrent_runs);
|
setMaxConcurrentRuns(trigger.max_concurrent_runs);
|
||||||
setEventType(trigger.event?.event_type ?? "");
|
setEventType(nextEventType);
|
||||||
setEventModule(trigger.event?.module_id ?? "");
|
setEventModule(nextEventModule);
|
||||||
setEventFilters(JSON.stringify(trigger.event?.filters ?? {}, null, 2));
|
setEventFilters(nextFilters);
|
||||||
|
setSavedFingerprint(JSON.stringify([
|
||||||
|
trigger.name,
|
||||||
|
trigger.kind,
|
||||||
|
trigger.status === "enabled",
|
||||||
|
nextRunAt,
|
||||||
|
nextInterval,
|
||||||
|
trigger.catch_up_policy,
|
||||||
|
trigger.max_concurrent_runs,
|
||||||
|
nextEventType,
|
||||||
|
nextEventModule,
|
||||||
|
nextFilters
|
||||||
|
]));
|
||||||
setError("");
|
setError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
|
const defaultName = `${pipeline?.name ?? "Pipeline"} schedule`;
|
||||||
setSelectedId(null);
|
setSelectedId(null);
|
||||||
setName(`${pipeline?.name ?? "Pipeline"} schedule`);
|
setName(defaultName);
|
||||||
setKind("interval");
|
setKind("interval");
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
setRunAt("");
|
setRunAt("");
|
||||||
@@ -1378,11 +1539,23 @@ function DataflowTriggersDialog({
|
|||||||
setEventType("");
|
setEventType("");
|
||||||
setEventModule("");
|
setEventModule("");
|
||||||
setEventFilters("{}");
|
setEventFilters("{}");
|
||||||
|
setSavedFingerprint(JSON.stringify([
|
||||||
|
defaultName,
|
||||||
|
"interval",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
60,
|
||||||
|
"coalesce",
|
||||||
|
1,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"{}"
|
||||||
|
]));
|
||||||
setError("");
|
setError("");
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = async () => {
|
const save = async (): Promise<boolean> => {
|
||||||
if (!pipeline || !name.trim()) return;
|
if (!pipeline || !name.trim()) return false;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -1420,12 +1593,14 @@ function DataflowTriggersDialog({
|
|||||||
...current.filter((item) => item.id !== saved.id)
|
...current.filter((item) => item.id !== saved.id)
|
||||||
]);
|
]);
|
||||||
edit(saved);
|
edit(saved);
|
||||||
|
return true;
|
||||||
} catch (saveError) {
|
} catch (saveError) {
|
||||||
setError(
|
setError(
|
||||||
saveError instanceof SyntaxError
|
saveError instanceof SyntaxError
|
||||||
? "Event filters must be a JSON object with scalar values."
|
? "Event filters must be a JSON object with scalar values."
|
||||||
: apiErrorMessage(saveError)
|
: apiErrorMessage(saveError)
|
||||||
);
|
);
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -1437,6 +1612,7 @@ function DataflowTriggersDialog({
|
|||||||
try {
|
try {
|
||||||
await deleteDataflowTrigger(settings, trigger.id);
|
await deleteDataflowTrigger(settings, trigger.id);
|
||||||
setTriggers((current) => current.filter((item) => item.id !== trigger.id));
|
setTriggers((current) => current.filter((item) => item.id !== trigger.id));
|
||||||
|
setDeleteCandidate(null);
|
||||||
reset();
|
reset();
|
||||||
} catch (deleteError) {
|
} catch (deleteError) {
|
||||||
setError(apiErrorMessage(deleteError));
|
setError(apiErrorMessage(deleteError));
|
||||||
@@ -1451,18 +1627,57 @@ function DataflowTriggersDialog({
|
|||||||
&& (kind !== "event" || eventType.trim())
|
&& (kind !== "event" || eventType.trim())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty,
|
||||||
|
title: "i18n:govoplan-dataflow.unsaved_trigger_title",
|
||||||
|
message: "i18n:govoplan-dataflow.unsaved_trigger_message",
|
||||||
|
onSave: save,
|
||||||
|
onDiscard: () => {
|
||||||
|
if (selected) edit(selected);
|
||||||
|
else reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (busy) return;
|
||||||
|
if (dirty) requestDiscard(onClose);
|
||||||
|
else onClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
title={`Automation · ${pipeline?.name ?? "pipeline"}`}
|
title={`Automation · ${pipeline?.name ?? "pipeline"}`}
|
||||||
className="dataflow-triggers-dialog"
|
className="dataflow-triggers-dialog"
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
onClose={onClose}
|
onClose={close}
|
||||||
footer={(
|
footer={(
|
||||||
<>
|
<>
|
||||||
<Button onClick={onClose} disabled={busy}>Close</Button>
|
<Button onClick={close} disabled={busy}>Close</Button>
|
||||||
<Button onClick={reset} disabled={busy || !editable}>New trigger</Button>
|
<Button
|
||||||
<Button variant="primary" onClick={() => void save()} disabled={busy || !editable || !complete}>
|
onClick={() => requestNavigation(reset)}
|
||||||
|
disabled={busy || !editable}
|
||||||
|
disabledReason={busy ? DATAFLOW_I18N.working : !editable ? DATAFLOW_I18N.writeReason : undefined}
|
||||||
|
>
|
||||||
|
New trigger
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => void save()}
|
||||||
|
disabled={busy || !editable || !complete || (Boolean(selected) && !dirty)}
|
||||||
|
disabledReason={
|
||||||
|
busy
|
||||||
|
? DATAFLOW_I18N.working
|
||||||
|
: !editable
|
||||||
|
? DATAFLOW_I18N.writeReason
|
||||||
|
: !complete
|
||||||
|
? "Complete the trigger fields first."
|
||||||
|
: selected && !dirty
|
||||||
|
? DATAFLOW_I18N.noChanges
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<Save size={16} /> Save trigger
|
<Save size={16} /> Save trigger
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -1475,7 +1690,7 @@ function DataflowTriggersDialog({
|
|||||||
key={trigger.id}
|
key={trigger.id}
|
||||||
type="button"
|
type="button"
|
||||||
className={trigger.id === selectedId ? "is-selected" : ""}
|
className={trigger.id === selectedId ? "is-selected" : ""}
|
||||||
onClick={() => edit(trigger)}
|
onClick={() => requestNavigation(() => edit(trigger))}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<strong>{trigger.name}</strong>
|
<strong>{trigger.name}</strong>
|
||||||
@@ -1490,11 +1705,12 @@ function DataflowTriggersDialog({
|
|||||||
{!busy && !triggers.length ? <small>No triggers configured</small> : null}
|
{!busy && !triggers.length ? <small>No triggers configured</small> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="dataflow-trigger-form">
|
<div className="dataflow-trigger-form">
|
||||||
|
<DocumentationHelpLink reference={DATAFLOW_FIELDS_DOCUMENTATION} />
|
||||||
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
||||||
<FormField label="Name">
|
<FormField label="Name" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input disabled={!editable} value={name} onChange={(event) => setName(event.target.value)} />
|
<input disabled={!editable} value={name} onChange={(event) => setName(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Trigger">
|
<FormField label="Trigger" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<select disabled={!editable} value={kind} onChange={(event) => setKind(event.target.value as DataflowTriggerKind)}>
|
<select disabled={!editable} value={kind} onChange={(event) => setKind(event.target.value as DataflowTriggerKind)}>
|
||||||
<option value="once">Given time</option>
|
<option value="once">Given time</option>
|
||||||
<option value="interval">Interval</option>
|
<option value="interval">Interval</option>
|
||||||
@@ -1502,13 +1718,13 @@ function DataflowTriggersDialog({
|
|||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
{kind === "once" ? (
|
{kind === "once" ? (
|
||||||
<FormField label="Run at">
|
<FormField label="Run at" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input disabled={!editable} type="datetime-local" value={runAt} onChange={(event) => setRunAt(event.target.value)} />
|
<input disabled={!editable} type="datetime-local" value={runAt} onChange={(event) => setRunAt(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
) : null}
|
) : null}
|
||||||
{kind === "interval" ? (
|
{kind === "interval" ? (
|
||||||
<>
|
<>
|
||||||
<FormField label="Interval (minutes)">
|
<FormField label="Interval (minutes)" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
@@ -1517,7 +1733,7 @@ function DataflowTriggersDialog({
|
|||||||
onChange={(event) => setIntervalMinutes(Math.max(1, Number(event.target.value)))}
|
onChange={(event) => setIntervalMinutes(Math.max(1, Number(event.target.value)))}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Missed runs">
|
<FormField label="Missed runs" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
disabled={!editable}
|
disabled={!editable}
|
||||||
value={catchUpPolicy}
|
value={catchUpPolicy}
|
||||||
@@ -1531,15 +1747,16 @@ function DataflowTriggersDialog({
|
|||||||
) : null}
|
) : null}
|
||||||
{kind === "event" ? (
|
{kind === "event" ? (
|
||||||
<>
|
<>
|
||||||
<FormField label="Event type">
|
<FormField label="Event type" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input disabled={!editable} value={eventType} onChange={(event) => setEventType(event.target.value)} />
|
<input disabled={!editable} value={eventType} onChange={(event) => setEventType(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Source module">
|
<FormField label="Source module" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input disabled={!editable} value={eventModule} onChange={(event) => setEventModule(event.target.value)} />
|
<input disabled={!editable} value={eventModule} onChange={(event) => setEventModule(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField
|
<FormField
|
||||||
label="Exact payload filters"
|
label="Exact payload filters"
|
||||||
help="Only direct scalar payload fields are matched. No expressions are executed."
|
help="Only direct scalar payload fields are matched. No expressions are executed."
|
||||||
|
documentation={DATAFLOW_FIELDS_DOCUMENTATION}
|
||||||
>
|
>
|
||||||
<textarea disabled={!editable} value={eventFilters} onChange={(event) => setEventFilters(event.target.value)} />
|
<textarea disabled={!editable} value={eventFilters} onChange={(event) => setEventFilters(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
@@ -1551,7 +1768,7 @@ function DataflowTriggersDialog({
|
|||||||
disabled={busy || !editable}
|
disabled={busy || !editable}
|
||||||
onChange={setEnabled}
|
onChange={setEnabled}
|
||||||
/>
|
/>
|
||||||
<FormField label="Concurrent runs">
|
<FormField label="Concurrent runs" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
@@ -1570,7 +1787,12 @@ function DataflowTriggersDialog({
|
|||||||
{selected.grant_scopes.map((scope) => <code key={scope}>{scope}</code>)}
|
{selected.grant_scopes.map((scope) => <code key={scope}>{scope}</code>)}
|
||||||
<small>Authorization is resolved again for every delivery.</small>
|
<small>Authorization is resolved again for every delivery.</small>
|
||||||
{selected.last_error ? <small className="is-error">{selected.last_error}</small> : null}
|
{selected.last_error ? <small className="is-error">{selected.last_error}</small> : null}
|
||||||
<Button variant="danger" onClick={() => void remove(selected)} disabled={busy || !editable}>
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => requestNavigation(() => setDeleteCandidate(selected))}
|
||||||
|
disabled={busy || !editable}
|
||||||
|
disabledReason={busy ? DATAFLOW_I18N.working : !editable ? DATAFLOW_I18N.writeReason : undefined}
|
||||||
|
>
|
||||||
<Trash2 size={16} /> Delete trigger
|
<Trash2 size={16} /> Delete trigger
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
@@ -1578,6 +1800,19 @@ function DataflowTriggersDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={deleteCandidate !== null}
|
||||||
|
title="Delete automation trigger"
|
||||||
|
message={`Delete ${deleteCandidate?.name ?? "this trigger"}? Future automatic runs will stop; retained run and audit evidence is not removed.`}
|
||||||
|
confirmLabel="Delete trigger"
|
||||||
|
tone="danger"
|
||||||
|
busy={busy}
|
||||||
|
onCancel={() => setDeleteCandidate(null)}
|
||||||
|
onConfirm={() => {
|
||||||
|
if (deleteCandidate) void remove(deleteCandidate);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1589,6 +1824,7 @@ function toLocalDateTime(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RunMode = "run" | "publish";
|
type RunMode = "run" | "publish";
|
||||||
|
type RunConfirmation = RunMode | "staging" | "production";
|
||||||
|
|
||||||
function RunPipelineDialog({
|
function RunPipelineDialog({
|
||||||
open,
|
open,
|
||||||
@@ -1621,6 +1857,7 @@ function RunPipelineDialog({
|
|||||||
const [loadingRuns, setLoadingRuns] = useState(false);
|
const [loadingRuns, setLoadingRuns] = useState(false);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [confirmation, setConfirmation] = useState<RunConfirmation | null>(null);
|
||||||
const publicationTargets = sources.filter((source) =>
|
const publicationTargets = sources.filter((source) =>
|
||||||
source.mode !== "live" && source.capabilities.includes("read")
|
source.mode !== "live" && source.capabilities.includes("read")
|
||||||
);
|
);
|
||||||
@@ -1637,6 +1874,7 @@ function RunPipelineDialog({
|
|||||||
setIdempotencyKey(crypto.randomUUID());
|
setIdempotencyKey(crypto.randomUUID());
|
||||||
setEnvironment("development");
|
setEnvironment("development");
|
||||||
setError("");
|
setError("");
|
||||||
|
setConfirmation(null);
|
||||||
setLoadingRuns(true);
|
setLoadingRuns(true);
|
||||||
void Promise.all([
|
void Promise.all([
|
||||||
listDataflowPipelineRuns(settings, pipeline.id),
|
listDataflowPipelineRuns(settings, pipeline.id),
|
||||||
@@ -1738,22 +1976,60 @@ function RunPipelineDialog({
|
|||||||
const targetComplete = mode === "run"
|
const targetComplete = mode === "run"
|
||||||
|| targetRef !== "new"
|
|| targetRef !== "new"
|
||||||
|| Boolean(name.trim() && sourceName.trim());
|
|| Boolean(name.trim() && sourceName.trim());
|
||||||
|
const confirmationTitle = confirmation === "production"
|
||||||
|
? "i18n:govoplan-dataflow.promote_production_title"
|
||||||
|
: confirmation === "staging"
|
||||||
|
? "i18n:govoplan-dataflow.promote_staging_title"
|
||||||
|
: confirmation === "publish"
|
||||||
|
? "i18n:govoplan-dataflow.publish_title"
|
||||||
|
: "i18n:govoplan-dataflow.queue_title";
|
||||||
|
const confirmationMessage = confirmation === "production"
|
||||||
|
? "i18n:govoplan-dataflow.promote_production_message"
|
||||||
|
: confirmation === "staging"
|
||||||
|
? "i18n:govoplan-dataflow.promote_staging_message"
|
||||||
|
: confirmation === "publish"
|
||||||
|
? "i18n:govoplan-dataflow.publish_message"
|
||||||
|
: "i18n:govoplan-dataflow.queue_message";
|
||||||
|
|
||||||
|
const confirmAction = async () => {
|
||||||
|
const action = confirmation;
|
||||||
|
if (!action) return;
|
||||||
|
if (action === "staging" || action === "production") {
|
||||||
|
await promote(action);
|
||||||
|
} else {
|
||||||
|
await start();
|
||||||
|
}
|
||||||
|
setConfirmation(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
title={`Run ${pipeline?.name ?? "pipeline"}`}
|
title={`Run ${pipeline?.name ?? "pipeline"}`}
|
||||||
className="dataflow-run-dialog"
|
className="dataflow-run-dialog"
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
if (!busy) onClose();
|
if (!busy) {
|
||||||
|
setConfirmation(null);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
footer={(
|
footer={(
|
||||||
<>
|
<>
|
||||||
<Button onClick={onClose} disabled={busy}>Close</Button>
|
<Button onClick={onClose} disabled={busy}>Close</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void start()}
|
onClick={() => setConfirmation(mode)}
|
||||||
disabled={busy || !pipeline || !targetComplete}
|
disabled={busy || !pipeline || !targetComplete}
|
||||||
|
disabledReason={
|
||||||
|
busy
|
||||||
|
? DATAFLOW_I18N.working
|
||||||
|
: !pipeline
|
||||||
|
? DATAFLOW_I18N.noSelection
|
||||||
|
: !targetComplete
|
||||||
|
? "Complete the publication target fields first."
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Play size={16} /> {busy ? "Queueing..." : "Queue run"}
|
<Play size={16} /> {busy ? "Queueing..." : "Queue run"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1767,6 +2043,7 @@ function RunPipelineDialog({
|
|||||||
</DismissibleAlert>
|
</DismissibleAlert>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="dataflow-run-controls">
|
<div className="dataflow-run-controls">
|
||||||
|
<DocumentationHelpLink reference={DATAFLOW_RUN_DOCUMENTATION} />
|
||||||
<SegmentedControl<RunMode>
|
<SegmentedControl<RunMode>
|
||||||
ariaLabel="Run output"
|
ariaLabel="Run output"
|
||||||
options={[
|
options={[
|
||||||
@@ -1781,7 +2058,7 @@ function RunPipelineDialog({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="dataflow-run-publication-fields">
|
<div className="dataflow-run-publication-fields">
|
||||||
<FormField label="Environment">
|
<FormField label="Environment" documentation={DATAFLOW_RUN_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={environment}
|
value={environment}
|
||||||
onChange={(event) => setEnvironment(
|
onChange={(event) => setEnvironment(
|
||||||
@@ -1796,7 +2073,7 @@ function RunPipelineDialog({
|
|||||||
{canPromote ? (
|
{canPromote ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void promote("staging")}
|
onClick={() => setConfirmation("staging")}
|
||||||
disabled={busy || deployments.some((item) =>
|
disabled={busy || deployments.some((item) =>
|
||||||
item.environment === "staging" && item.revision === pipeline?.revision
|
item.environment === "staging" && item.revision === pipeline?.revision
|
||||||
)}
|
)}
|
||||||
@@ -1804,7 +2081,7 @@ function RunPipelineDialog({
|
|||||||
<Rocket size={16} /> Promote to staging
|
<Rocket size={16} /> Promote to staging
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => void promote("production")}
|
onClick={() => setConfirmation("production")}
|
||||||
disabled={busy
|
disabled={busy
|
||||||
|| !deployments.some((item) =>
|
|| !deployments.some((item) =>
|
||||||
item.environment === "staging" && item.revision === pipeline?.revision
|
item.environment === "staging" && item.revision === pipeline?.revision
|
||||||
@@ -1820,7 +2097,7 @@ function RunPipelineDialog({
|
|||||||
</div>
|
</div>
|
||||||
{mode === "publish" ? (
|
{mode === "publish" ? (
|
||||||
<div className="dataflow-run-publication-fields">
|
<div className="dataflow-run-publication-fields">
|
||||||
<FormField label="Target">
|
<FormField label="Target" documentation={DATAFLOW_RUN_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={targetRef}
|
value={targetRef}
|
||||||
onChange={(event) => setTargetRef(event.target.value)}
|
onChange={(event) => setTargetRef(event.target.value)}
|
||||||
@@ -1835,20 +2112,20 @@ function RunPipelineDialog({
|
|||||||
</FormField>
|
</FormField>
|
||||||
{targetRef === "new" ? (
|
{targetRef === "new" ? (
|
||||||
<>
|
<>
|
||||||
<FormField label="Name">
|
<FormField label="Name" documentation={DATAFLOW_RUN_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(event) => setName(event.target.value)}
|
onChange={(event) => setName(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Logical source name">
|
<FormField label="Logical source name" documentation={DATAFLOW_RUN_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={sourceName}
|
value={sourceName}
|
||||||
onChange={(event) => setSourceName(event.target.value)}
|
onChange={(event) => setSourceName(event.target.value)}
|
||||||
pattern="[A-Za-z_][A-Za-z0-9_]*"
|
pattern="[A-Za-z_][A-Za-z0-9_]*"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Description">
|
<FormField label="Description" documentation={DATAFLOW_RUN_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(event) => setDescription(event.target.value)}
|
onChange={(event) => setDescription(event.target.value)}
|
||||||
@@ -1864,7 +2141,7 @@ function RunPipelineDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{freeze ? (
|
{freeze ? (
|
||||||
<FormField label="Frozen-state label">
|
<FormField label="Frozen-state label" documentation={DATAFLOW_RUN_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={frozenLabel}
|
value={frozenLabel}
|
||||||
onChange={(event) => setFrozenLabel(event.target.value)}
|
onChange={(event) => setFrozenLabel(event.target.value)}
|
||||||
@@ -1948,6 +2225,19 @@ function RunPipelineDialog({
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<ConfirmDialog
|
||||||
|
open={confirmation !== null}
|
||||||
|
title={confirmationTitle}
|
||||||
|
message={confirmationMessage}
|
||||||
|
confirmLabel={confirmation === "production" ? "Promote to production" : confirmation === "staging" ? "Promote to staging" : "Queue run"}
|
||||||
|
tone={confirmation === "production" ? "danger" : "default"}
|
||||||
|
busy={busy}
|
||||||
|
onCancel={() => {
|
||||||
|
if (!busy) setConfirmation(null);
|
||||||
|
}}
|
||||||
|
onConfirm={() => void confirmAction()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1962,6 +2252,7 @@ function SourceSnapshotDialog({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCreated: (source: TabularSource) => void;
|
onCreated: (source: TabularSource) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [sourceName, setSourceName] = useState("");
|
const [sourceName, setSourceName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
@@ -1972,9 +2263,20 @@ function SourceSnapshotDialog({
|
|||||||
const [fileInputKey, setFileInputKey] = useState(0);
|
const [fileInputKey, setFileInputKey] = useState(0);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const dirty = Boolean(
|
||||||
|
open
|
||||||
|
&& (
|
||||||
|
name !== ""
|
||||||
|
|| sourceName !== ""
|
||||||
|
|| description !== ""
|
||||||
|
|| format !== "json"
|
||||||
|
|| rowsText !== "[]"
|
||||||
|
|| csvText !== ""
|
||||||
|
|| delimiter !== ","
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const resetDraft = () => {
|
||||||
if (!open) return;
|
|
||||||
setName("");
|
setName("");
|
||||||
setSourceName("");
|
setSourceName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
@@ -1984,9 +2286,14 @@ function SourceSnapshotDialog({
|
|||||||
setDelimiter(",");
|
setDelimiter(",");
|
||||||
setFileInputKey((current) => current + 1);
|
setFileInputKey((current) => current + 1);
|
||||||
setError("");
|
setError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
resetDraft();
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const create = async () => {
|
const create = async (): Promise<boolean> => {
|
||||||
setError("");
|
setError("");
|
||||||
let rows: Record<string, unknown>[] = [];
|
let rows: Record<string, unknown>[] = [];
|
||||||
if (format === "json") {
|
if (format === "json") {
|
||||||
@@ -1998,11 +2305,11 @@ function SourceSnapshotDialog({
|
|||||||
rows = parsed;
|
rows = parsed;
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
setError(parseError instanceof Error ? parseError.message : "Rows could not be parsed.");
|
setError(parseError instanceof Error ? parseError.message : "Rows could not be parsed.");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (!csvText.trim()) {
|
} else if (!csvText.trim()) {
|
||||||
setError("Choose a CSV file or paste CSV data.");
|
setError("Choose a CSV file or paste CSV data.");
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
@@ -2015,13 +2322,29 @@ function SourceSnapshotDialog({
|
|||||||
: { format, csv_text: csvText, delimiter })
|
: { format, csv_text: csvText, delimiter })
|
||||||
});
|
});
|
||||||
onCreated(source);
|
onCreated(source);
|
||||||
|
return true;
|
||||||
} catch (createError) {
|
} catch (createError) {
|
||||||
setError(apiErrorMessage(createError));
|
setError(apiErrorMessage(createError));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useUnsavedDraftGuard({
|
||||||
|
dirty,
|
||||||
|
title: "i18n:govoplan-dataflow.unsaved_source_title",
|
||||||
|
message: "i18n:govoplan-dataflow.unsaved_source_message",
|
||||||
|
onSave: create,
|
||||||
|
onDiscard: resetDraft
|
||||||
|
});
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
if (busy) return;
|
||||||
|
if (dirty) requestDiscard(onClose);
|
||||||
|
else onClose();
|
||||||
|
};
|
||||||
|
|
||||||
const loadCsvFile = async (file: File | undefined) => {
|
const loadCsvFile = async (file: File | undefined) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
setError("");
|
setError("");
|
||||||
@@ -2040,12 +2363,10 @@ function SourceSnapshotDialog({
|
|||||||
open={open}
|
open={open}
|
||||||
title="Stage datasource"
|
title="Stage datasource"
|
||||||
className="dataflow-source-dialog"
|
className="dataflow-source-dialog"
|
||||||
onClose={() => {
|
onClose={close}
|
||||||
if (!busy) onClose();
|
|
||||||
}}
|
|
||||||
footer={(
|
footer={(
|
||||||
<>
|
<>
|
||||||
<Button onClick={onClose} disabled={busy}>Cancel</Button>
|
<Button onClick={close} disabled={busy}>Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={() => void create()}
|
onClick={() => void create()}
|
||||||
@@ -2071,10 +2392,10 @@ function SourceSnapshotDialog({
|
|||||||
onChange={setFormat}
|
onChange={setFormat}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<FormField label="Name">
|
<FormField label="Name" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input value={name} onChange={(event) => setName(event.target.value)} />
|
<input value={name} onChange={(event) => setName(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Logical source name">
|
<FormField label="Logical source name" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
value={sourceName}
|
value={sourceName}
|
||||||
onChange={(event) => setSourceName(event.target.value)}
|
onChange={(event) => setSourceName(event.target.value)}
|
||||||
@@ -2082,12 +2403,12 @@ function SourceSnapshotDialog({
|
|||||||
placeholder="monthly_input"
|
placeholder="monthly_input"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Description">
|
<FormField label="Description" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input value={description} onChange={(event) => setDescription(event.target.value)} />
|
<input value={description} onChange={(event) => setDescription(event.target.value)} />
|
||||||
</FormField>
|
</FormField>
|
||||||
{format === "csv" ? (
|
{format === "csv" ? (
|
||||||
<>
|
<>
|
||||||
<FormField label="CSV file">
|
<FormField label="CSV file" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
key={fileInputKey}
|
key={fileInputKey}
|
||||||
type="file"
|
type="file"
|
||||||
@@ -2095,7 +2416,7 @@ function SourceSnapshotDialog({
|
|||||||
onChange={(event) => void loadCsvFile(event.target.files?.[0])}
|
onChange={(event) => void loadCsvFile(event.target.files?.[0])}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Delimiter">
|
<FormField label="Delimiter" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<select value={delimiter} onChange={(event) => setDelimiter(event.target.value)}>
|
<select value={delimiter} onChange={(event) => setDelimiter(event.target.value)}>
|
||||||
<option value=",">Comma</option>
|
<option value=",">Comma</option>
|
||||||
<option value=";">Semicolon</option>
|
<option value=";">Semicolon</option>
|
||||||
@@ -2103,7 +2424,7 @@ function SourceSnapshotDialog({
|
|||||||
<option value="|">Pipe</option>
|
<option value="|">Pipe</option>
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="CSV data">
|
<FormField label="CSV data" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<textarea
|
<textarea
|
||||||
className="dataflow-json-editor"
|
className="dataflow-json-editor"
|
||||||
value={csvText}
|
value={csvText}
|
||||||
@@ -2113,7 +2434,7 @@ function SourceSnapshotDialog({
|
|||||||
</FormField>
|
</FormField>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<FormField label="Rows">
|
<FormField label="Rows" documentation={DATAFLOW_FIELDS_DOCUMENTATION}>
|
||||||
<textarea
|
<textarea
|
||||||
className="dataflow-json-editor"
|
className="dataflow-json-editor"
|
||||||
value={rowsText}
|
value={rowsText}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, type ComponentProps } from "react";
|
||||||
import { Play, Trash2 } from "lucide-react";
|
import { Play, Trash2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
FormField,
|
FormField as CoreFormField,
|
||||||
IconButton
|
IconButton
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import type {
|
import type {
|
||||||
@@ -10,6 +10,18 @@ import type {
|
|||||||
PipelineGraphNode,
|
PipelineGraphNode,
|
||||||
TabularSource
|
TabularSource
|
||||||
} from "../../api/dataflow";
|
} from "../../api/dataflow";
|
||||||
|
import { DATAFLOW_NODE_DOCUMENTATION } from "./interfacePatterns";
|
||||||
|
|
||||||
|
type NodeFormFieldProps = ComponentProps<typeof CoreFormField>;
|
||||||
|
|
||||||
|
function FormField({ documentation, ...props }: NodeFormFieldProps) {
|
||||||
|
return (
|
||||||
|
<CoreFormField
|
||||||
|
{...props}
|
||||||
|
documentation={documentation ?? DATAFLOW_NODE_DOCUMENTATION}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type NodeInspectorProps = {
|
type NodeInspectorProps = {
|
||||||
node: PipelineGraphNode | null;
|
node: PipelineGraphNode | null;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { DocumentationHelpReference } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
export const DATAFLOW_DOCUMENTATION = {
|
||||||
|
topicId: "dataflow.module-boundary",
|
||||||
|
documentationType: "user"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const DATAFLOW_FIELDS_DOCUMENTATION = {
|
||||||
|
topicId: "dataflow.reference.fields-and-consequences",
|
||||||
|
documentationType: "admin"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const DATAFLOW_NODE_DOCUMENTATION = {
|
||||||
|
topicId: "dataflow.reference.nodes-and-expressions",
|
||||||
|
documentationType: "user"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const DATAFLOW_RUN_DOCUMENTATION = {
|
||||||
|
topicId: "dataflow.execution-and-recovery",
|
||||||
|
documentationType: "admin"
|
||||||
|
} satisfies DocumentationHelpReference;
|
||||||
|
|
||||||
|
export const DATAFLOW_I18N = {
|
||||||
|
loading: "i18n:govoplan-dataflow.loading_reason",
|
||||||
|
working: "i18n:govoplan-dataflow.working_reason",
|
||||||
|
writeReason: "i18n:govoplan-dataflow.write_reason",
|
||||||
|
runReason: "i18n:govoplan-dataflow.run_reason",
|
||||||
|
noSelection: "i18n:govoplan-dataflow.no_selection_reason",
|
||||||
|
noChanges: "i18n:govoplan-dataflow.no_changes_reason",
|
||||||
|
saveFirst: "i18n:govoplan-dataflow.save_first_reason",
|
||||||
|
requiredAction: "i18n:govoplan-dataflow.required_action",
|
||||||
|
actor: "i18n:govoplan-dataflow.actor",
|
||||||
|
destination: "i18n:govoplan-dataflow.destination",
|
||||||
|
permissionAction: "i18n:govoplan-dataflow.permission_action",
|
||||||
|
permissionActor: "i18n:govoplan-dataflow.permission_actor",
|
||||||
|
permissionDestination: "i18n:govoplan-dataflow.permission_destination"
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||||
|
|
||||||
|
const en = {
|
||||||
|
"i18n:govoplan-dataflow.dataflow": "Dataflow",
|
||||||
|
"i18n:govoplan-dataflow.library": "Pipeline library",
|
||||||
|
"i18n:govoplan-dataflow.graph": "Graph editor",
|
||||||
|
"i18n:govoplan-dataflow.sql": "Constrained SQL editor",
|
||||||
|
"i18n:govoplan-dataflow.inspector": "Node inspector",
|
||||||
|
"i18n:govoplan-dataflow.results": "Preview and diagnostics",
|
||||||
|
"i18n:govoplan-dataflow.triggers": "Automation triggers",
|
||||||
|
"i18n:govoplan-dataflow.runs": "Runs and deployments",
|
||||||
|
"i18n:govoplan-dataflow.widget": "Dataflows widget",
|
||||||
|
"i18n:govoplan-dataflow.loading_reason": "The pipeline library is still loading.",
|
||||||
|
"i18n:govoplan-dataflow.working_reason": "Another Dataflow operation is still running.",
|
||||||
|
"i18n:govoplan-dataflow.write_reason": "Your account may not create or change pipeline definitions.",
|
||||||
|
"i18n:govoplan-dataflow.run_reason": "Your account may not preview or run pipelines.",
|
||||||
|
"i18n:govoplan-dataflow.no_selection_reason": "Select or create a pipeline first.",
|
||||||
|
"i18n:govoplan-dataflow.no_changes_reason": "There are no unsaved pipeline changes.",
|
||||||
|
"i18n:govoplan-dataflow.save_first_reason": "Save the pipeline before starting a revision-pinned run.",
|
||||||
|
"i18n:govoplan-dataflow.required_action": "Required action",
|
||||||
|
"i18n:govoplan-dataflow.actor": "Responsible actor",
|
||||||
|
"i18n:govoplan-dataflow.destination": "Where to continue",
|
||||||
|
"i18n:govoplan-dataflow.permission_action": "Ask for Dataflow definition or run permission for the intended operation.",
|
||||||
|
"i18n:govoplan-dataflow.permission_actor": "A tenant administrator or Dataflow manager",
|
||||||
|
"i18n:govoplan-dataflow.permission_destination": "Access administration for Dataflow",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_title": "Unsaved pipeline",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_message": "Save or discard the pipeline changes before leaving this workspace.",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_copy_title": "Uncreated pipeline copy",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_copy_message": "Create the scoped copy or discard its changed settings before leaving.",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_source_title": "Unstaged datasource",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_source_message": "Import the datasource snapshot or discard its staged content before leaving.",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_trigger_title": "Unsaved automation trigger",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_trigger_message": "Save or discard the trigger changes before leaving this editor.",
|
||||||
|
"i18n:govoplan-dataflow.queue_title": "Queue pipeline run",
|
||||||
|
"i18n:govoplan-dataflow.queue_message": "Queue this pinned pipeline revision? The run and its authorization evidence will be retained.",
|
||||||
|
"i18n:govoplan-dataflow.publish_title": "Queue and publish pipeline output",
|
||||||
|
"i18n:govoplan-dataflow.publish_message": "Queue this pinned revision and publish its output to the selected governed Datasource target?",
|
||||||
|
"i18n:govoplan-dataflow.promote_staging_title": "Promote revision to staging",
|
||||||
|
"i18n:govoplan-dataflow.promote_staging_message": "Promote this immutable pipeline revision to the staging execution environment?",
|
||||||
|
"i18n:govoplan-dataflow.promote_production_title": "Promote revision to production",
|
||||||
|
"i18n:govoplan-dataflow.promote_production_message": "Promote this immutable pipeline revision to production? Future production runs may use it.",
|
||||||
|
"Pipelines": "Pipelines",
|
||||||
|
"Search pipelines": "Search pipelines",
|
||||||
|
"No matching pipelines": "No matching pipelines",
|
||||||
|
"No pipelines yet": "No pipelines yet",
|
||||||
|
"Graph": "Graph",
|
||||||
|
"SQL": "SQL",
|
||||||
|
"Validate": "Validate",
|
||||||
|
"Preview": "Preview",
|
||||||
|
"Refresh": "Refresh",
|
||||||
|
"Run": "Run",
|
||||||
|
"Save": "Save",
|
||||||
|
"Saving...": "Saving...",
|
||||||
|
"Nodes": "Nodes",
|
||||||
|
"No pipeline selected": "No pipeline selected",
|
||||||
|
"New pipeline": "New pipeline",
|
||||||
|
"Definition settings": "Definition settings",
|
||||||
|
"Reuse as scoped copy": "Reuse as scoped copy",
|
||||||
|
"Automation triggers": "Automation triggers",
|
||||||
|
"Discard changes": "Discard changes",
|
||||||
|
"Delete pipeline": "Delete pipeline",
|
||||||
|
"Constrained Dataflow SQL": "Constrained Dataflow SQL",
|
||||||
|
"Apply SQL": "Apply SQL",
|
||||||
|
"Stage datasource": "Stage datasource",
|
||||||
|
"Run only": "Run only",
|
||||||
|
"Publish result": "Publish result",
|
||||||
|
"Environment": "Environment",
|
||||||
|
"Development": "Development",
|
||||||
|
"Staging": "Staging",
|
||||||
|
"Production": "Production",
|
||||||
|
"Promote to staging": "Promote to staging",
|
||||||
|
"Promote to production": "Promote to production",
|
||||||
|
"Recent runs": "Recent runs",
|
||||||
|
"Queue run": "Queue run",
|
||||||
|
"New trigger": "New trigger",
|
||||||
|
"Save trigger": "Save trigger",
|
||||||
|
"Delete trigger": "Delete trigger",
|
||||||
|
"Delete automation trigger": "Delete automation trigger",
|
||||||
|
"Pipeline definition is read-only": "Pipeline definition is read-only",
|
||||||
|
"Working...": "Working..."
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const de: Record<keyof typeof en, string> = {
|
||||||
|
"i18n:govoplan-dataflow.dataflow": "Datenfluss",
|
||||||
|
"i18n:govoplan-dataflow.library": "Datenflussbibliothek",
|
||||||
|
"i18n:govoplan-dataflow.graph": "Graph-Editor",
|
||||||
|
"i18n:govoplan-dataflow.sql": "Eingeschränkter SQL-Editor",
|
||||||
|
"i18n:govoplan-dataflow.inspector": "Knoteninspektor",
|
||||||
|
"i18n:govoplan-dataflow.results": "Vorschau und Diagnosen",
|
||||||
|
"i18n:govoplan-dataflow.triggers": "Automatisierungsauslöser",
|
||||||
|
"i18n:govoplan-dataflow.runs": "Ausführungen und Bereitstellungen",
|
||||||
|
"i18n:govoplan-dataflow.widget": "Datenfluss-Widget",
|
||||||
|
"i18n:govoplan-dataflow.loading_reason": "Die Datenflussbibliothek wird noch geladen.",
|
||||||
|
"i18n:govoplan-dataflow.working_reason": "Eine andere Datenflussoperation läuft noch.",
|
||||||
|
"i18n:govoplan-dataflow.write_reason": "Ihr Konto darf keine Datenflussdefinitionen erstellen oder ändern.",
|
||||||
|
"i18n:govoplan-dataflow.run_reason": "Ihr Konto darf Datenflüsse nicht prüfen oder ausführen.",
|
||||||
|
"i18n:govoplan-dataflow.no_selection_reason": "Wählen oder erstellen Sie zuerst einen Datenfluss.",
|
||||||
|
"i18n:govoplan-dataflow.no_changes_reason": "Es gibt keine ungespeicherten Datenflussänderungen.",
|
||||||
|
"i18n:govoplan-dataflow.save_first_reason": "Speichern Sie den Datenfluss vor einer revisionsgebundenen Ausführung.",
|
||||||
|
"i18n:govoplan-dataflow.required_action": "Erforderliche Aktion",
|
||||||
|
"i18n:govoplan-dataflow.actor": "Verantwortliche Stelle",
|
||||||
|
"i18n:govoplan-dataflow.destination": "Fortsetzung",
|
||||||
|
"i18n:govoplan-dataflow.permission_action": "Fordern Sie die für den Vorgang erforderliche Datenfluss- oder Ausführungsberechtigung an.",
|
||||||
|
"i18n:govoplan-dataflow.permission_actor": "Mandantenadministration oder Datenflussverwaltung",
|
||||||
|
"i18n:govoplan-dataflow.permission_destination": "Zugriffsverwaltung für Datenflüsse",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_title": "Ungespeicherter Datenfluss",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_message": "Speichern oder verwerfen Sie die Datenflussänderungen, bevor Sie diesen Arbeitsbereich verlassen.",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_copy_title": "Nicht erstellte Datenflusskopie",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_copy_message": "Erstellen Sie die eingegrenzte Kopie oder verwerfen Sie ihre geänderten Einstellungen.",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_source_title": "Nicht bereitgestellte Datenquelle",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_source_message": "Importieren Sie den Datenquellenstand oder verwerfen Sie dessen bereitgestellten Inhalt.",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_trigger_title": "Ungespeicherter Automatisierungsauslöser",
|
||||||
|
"i18n:govoplan-dataflow.unsaved_trigger_message": "Speichern oder verwerfen Sie die Änderungen am Auslöser, bevor Sie diesen Editor verlassen.",
|
||||||
|
"i18n:govoplan-dataflow.queue_title": "Datenflussausführung einreihen",
|
||||||
|
"i18n:govoplan-dataflow.queue_message": "Diese gebundene Datenflussrevision einreihen? Ausführung und Berechtigungsnachweis werden aufbewahrt.",
|
||||||
|
"i18n:govoplan-dataflow.publish_title": "Datenflussausgabe einreihen und veröffentlichen",
|
||||||
|
"i18n:govoplan-dataflow.publish_message": "Diese gebundene Revision einreihen und ihre Ausgabe in der ausgewählten verwalteten Datenquelle veröffentlichen?",
|
||||||
|
"i18n:govoplan-dataflow.promote_staging_title": "Revision nach Staging übernehmen",
|
||||||
|
"i18n:govoplan-dataflow.promote_staging_message": "Diese unveränderliche Datenflussrevision in die Staging-Ausführungsumgebung übernehmen?",
|
||||||
|
"i18n:govoplan-dataflow.promote_production_title": "Revision in Produktion übernehmen",
|
||||||
|
"i18n:govoplan-dataflow.promote_production_message": "Diese unveränderliche Datenflussrevision in Produktion übernehmen? Künftige Produktionsausführungen können sie verwenden.",
|
||||||
|
"Pipelines": "Datenflüsse",
|
||||||
|
"Search pipelines": "Datenflüsse suchen",
|
||||||
|
"No matching pipelines": "Keine passenden Datenflüsse",
|
||||||
|
"No pipelines yet": "Noch keine Datenflüsse",
|
||||||
|
"Graph": "Graph",
|
||||||
|
"SQL": "SQL",
|
||||||
|
"Validate": "Validieren",
|
||||||
|
"Preview": "Vorschau",
|
||||||
|
"Refresh": "Aktualisieren",
|
||||||
|
"Run": "Ausführen",
|
||||||
|
"Save": "Speichern",
|
||||||
|
"Saving...": "Speichert...",
|
||||||
|
"Nodes": "Knoten",
|
||||||
|
"No pipeline selected": "Kein Datenfluss ausgewählt",
|
||||||
|
"New pipeline": "Neuer Datenfluss",
|
||||||
|
"Definition settings": "Definitionseinstellungen",
|
||||||
|
"Reuse as scoped copy": "Als eingegrenzte Kopie verwenden",
|
||||||
|
"Automation triggers": "Automatisierungsauslöser",
|
||||||
|
"Discard changes": "Änderungen verwerfen",
|
||||||
|
"Delete pipeline": "Datenfluss löschen",
|
||||||
|
"Constrained Dataflow SQL": "Eingeschränktes Datenfluss-SQL",
|
||||||
|
"Apply SQL": "SQL anwenden",
|
||||||
|
"Stage datasource": "Datenquelle bereitstellen",
|
||||||
|
"Run only": "Nur ausführen",
|
||||||
|
"Publish result": "Ergebnis veröffentlichen",
|
||||||
|
"Environment": "Umgebung",
|
||||||
|
"Development": "Entwicklung",
|
||||||
|
"Staging": "Staging",
|
||||||
|
"Production": "Produktion",
|
||||||
|
"Promote to staging": "Nach Staging übernehmen",
|
||||||
|
"Promote to production": "In Produktion übernehmen",
|
||||||
|
"Recent runs": "Letzte Ausführungen",
|
||||||
|
"Queue run": "Ausführung einreihen",
|
||||||
|
"New trigger": "Neuer Auslöser",
|
||||||
|
"Save trigger": "Auslöser speichern",
|
||||||
|
"Delete trigger": "Auslöser löschen",
|
||||||
|
"Delete automation trigger": "Automatisierungsauslöser löschen",
|
||||||
|
"Pipeline definition is read-only": "Datenflussdefinition ist schreibgeschützt",
|
||||||
|
"Working...": "Vorgang läuft..."
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generatedTranslations: PlatformTranslations = { en, de };
|
||||||
+75
-3
@@ -4,6 +4,7 @@ import type {
|
|||||||
PlatformWebModule
|
PlatformWebModule
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import DataflowPipelinesWidget from "./features/dataflow/DataflowPipelinesWidget";
|
import DataflowPipelinesWidget from "./features/dataflow/DataflowPipelinesWidget";
|
||||||
|
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||||
import "@xyflow/react/dist/style.css";
|
import "@xyflow/react/dist/style.css";
|
||||||
import "./styles/dataflow.css";
|
import "./styles/dataflow.css";
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ const dataflowDashboardWidgets: DashboardWidgetsUiCapability = {
|
|||||||
|
|
||||||
export const dataflowModule: PlatformWebModule = {
|
export const dataflowModule: PlatformWebModule = {
|
||||||
id: "dataflow",
|
id: "dataflow",
|
||||||
label: "Dataflow",
|
label: "i18n:govoplan-dataflow.dataflow",
|
||||||
version: "0.1.14",
|
version: "0.1.14",
|
||||||
optionalDependencies: [
|
optionalDependencies: [
|
||||||
"access",
|
"access",
|
||||||
@@ -70,23 +71,94 @@ export const dataflowModule: PlatformWebModule = {
|
|||||||
"risk_compliance",
|
"risk_compliance",
|
||||||
"workflow"
|
"workflow"
|
||||||
],
|
],
|
||||||
|
translations: generatedTranslations,
|
||||||
viewSurfaces: [
|
viewSurfaces: [
|
||||||
|
{
|
||||||
|
id: "dataflow.page",
|
||||||
|
moduleId: "dataflow",
|
||||||
|
kind: "route",
|
||||||
|
label: "i18n:govoplan-dataflow.dataflow",
|
||||||
|
order: 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dataflow.library",
|
||||||
|
moduleId: "dataflow",
|
||||||
|
kind: "section",
|
||||||
|
label: "i18n:govoplan-dataflow.library",
|
||||||
|
parentId: "dataflow.page",
|
||||||
|
order: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dataflow.graph",
|
||||||
|
moduleId: "dataflow",
|
||||||
|
kind: "section",
|
||||||
|
label: "i18n:govoplan-dataflow.graph",
|
||||||
|
parentId: "dataflow.page",
|
||||||
|
order: 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dataflow.sql",
|
||||||
|
moduleId: "dataflow",
|
||||||
|
kind: "section",
|
||||||
|
label: "i18n:govoplan-dataflow.sql",
|
||||||
|
parentId: "dataflow.page",
|
||||||
|
order: 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dataflow.inspector",
|
||||||
|
moduleId: "dataflow",
|
||||||
|
kind: "section",
|
||||||
|
label: "i18n:govoplan-dataflow.inspector",
|
||||||
|
parentId: "dataflow.page",
|
||||||
|
order: 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dataflow.results",
|
||||||
|
moduleId: "dataflow",
|
||||||
|
kind: "section",
|
||||||
|
label: "i18n:govoplan-dataflow.results",
|
||||||
|
parentId: "dataflow.page",
|
||||||
|
order: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dataflow.triggers",
|
||||||
|
moduleId: "dataflow",
|
||||||
|
kind: "action",
|
||||||
|
label: "i18n:govoplan-dataflow.triggers",
|
||||||
|
parentId: "dataflow.page",
|
||||||
|
order: 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dataflow.runs",
|
||||||
|
moduleId: "dataflow",
|
||||||
|
kind: "action",
|
||||||
|
label: "i18n:govoplan-dataflow.runs",
|
||||||
|
parentId: "dataflow.page",
|
||||||
|
order: 70
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "dataflow.widget.pipelines",
|
id: "dataflow.widget.pipelines",
|
||||||
moduleId: "dataflow",
|
moduleId: "dataflow",
|
||||||
kind: "section",
|
kind: "section",
|
||||||
label: "Dataflows widget",
|
label: "i18n:govoplan-dataflow.widget",
|
||||||
order: 70
|
order: 70
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
navItems: [
|
navItems: [
|
||||||
{ to: "/dataflow", label: "Dataflow", iconName: "waypoints", anyOf: readScopes, order: 72 }
|
{
|
||||||
|
to: "/dataflow",
|
||||||
|
label: "i18n:govoplan-dataflow.dataflow",
|
||||||
|
iconName: "waypoints",
|
||||||
|
anyOf: readScopes,
|
||||||
|
order: 72
|
||||||
|
}
|
||||||
],
|
],
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/dataflow",
|
path: "/dataflow",
|
||||||
anyOf: readScopes,
|
anyOf: readScopes,
|
||||||
order: 72,
|
order: 72,
|
||||||
|
surfaceId: "dataflow.page",
|
||||||
render: ({ settings, auth }) => createElement(DataflowPage, { settings, auth })
|
render: ({ settings, auth }) => createElement(DataflowPage, { settings, auth })
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -290,6 +290,10 @@
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dataflow-action-guidance {
|
||||||
|
padding: 0 10px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.dataflow-workspace-toolbar {
|
.dataflow-workspace-toolbar {
|
||||||
min-height: 58px;
|
min-height: 58px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user