feat(dataflow): govern reusable definition updates
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/dataflow-webui",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.20",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -95,6 +95,11 @@ export type PipelineGovernance = {
|
||||
derived_from_pipeline_id?: string | null;
|
||||
derived_from_revision?: number | null;
|
||||
derived_from_hash?: string | null;
|
||||
source_available: boolean;
|
||||
source_name?: string | null;
|
||||
source_current_revision?: number | null;
|
||||
source_current_hash?: string | null;
|
||||
update_available: boolean;
|
||||
derivation_provenance: Record<string, unknown>;
|
||||
actions: Record<string, DefinitionActionDecision>;
|
||||
};
|
||||
@@ -505,6 +510,23 @@ export function deriveDataflowPipeline(
|
||||
);
|
||||
}
|
||||
|
||||
export function rebaseDataflowPipeline(
|
||||
settings: ApiSettings,
|
||||
pipelineId: string,
|
||||
payload: {
|
||||
expected_revision: number;
|
||||
source_revision: number;
|
||||
source_hash: string;
|
||||
reason: string;
|
||||
}
|
||||
): Promise<Pipeline> {
|
||||
return apiFetch(
|
||||
settings,
|
||||
`/api/v1/dataflow/pipelines/${encodeURIComponent(pipelineId)}/rebase`,
|
||||
{ method: "POST", body: JSON.stringify(payload) }
|
||||
);
|
||||
}
|
||||
|
||||
export function dataflowScopeReferenceProvider(
|
||||
settings: ApiSettings,
|
||||
scopeType: "user" | "group"
|
||||
@@ -564,7 +586,7 @@ export function deleteDataflowTrigger(
|
||||
|
||||
export function validateDataflowPipeline(
|
||||
settings: ApiSettings,
|
||||
payload: { graph?: PipelineGraph; sql_text?: string; source_nodes?: PipelineGraphNode[] }
|
||||
payload: { pipeline_id?: string | null; graph?: PipelineGraph; sql_text?: string; source_nodes?: PipelineGraphNode[] }
|
||||
): Promise<PipelineValidation> {
|
||||
return apiFetch<PipelineValidation>(settings, "/api/v1/dataflow/validate", {
|
||||
method: "POST",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Code2,
|
||||
CopyPlus,
|
||||
DatabaseZap,
|
||||
GitCompareArrows,
|
||||
ListChecks,
|
||||
Network,
|
||||
Play,
|
||||
@@ -84,6 +85,7 @@ import {
|
||||
listDataflowTriggers,
|
||||
previewDataflowPipeline,
|
||||
promoteDataflowPipeline,
|
||||
rebaseDataflowPipeline,
|
||||
recordDataflowDecision,
|
||||
runDataflowPipeline,
|
||||
renderDataflowSql,
|
||||
@@ -161,6 +163,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
||||
const [runOpen, setRunOpen] = useState(false);
|
||||
const [definitionSettingsOpen, setDefinitionSettingsOpen] = useState(false);
|
||||
const [deriveOpen, setDeriveOpen] = useState(false);
|
||||
const [rebaseOpen, setRebaseOpen] = useState(false);
|
||||
const [triggersOpen, setTriggersOpen] = useState(false);
|
||||
const [decisionReviewOpen, setDecisionReviewOpen] = useState(false);
|
||||
const [nodeLibrary, setNodeLibrary] = useState<NodeTypeDefinition[]>(FALLBACK_NODE_LIBRARY);
|
||||
@@ -183,6 +186,14 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
||||
&& canWrite
|
||||
&& draft.governance?.actions.derive?.allowed
|
||||
);
|
||||
const canRebase = Boolean(
|
||||
draft?.id
|
||||
&& draft.governance?.update_available
|
||||
&& draft.governance.source_current_revision
|
||||
&& draft.governance.source_current_hash
|
||||
&& canEdit
|
||||
&& !dirty
|
||||
);
|
||||
const canStartSavedRun = Boolean(
|
||||
draft?.id
|
||||
&& draft.definitionKind === "flow"
|
||||
@@ -411,10 +422,15 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
||||
setSuccess("");
|
||||
try {
|
||||
const response = await validateDataflowPipeline(settings, draft.editorMode === "sql"
|
||||
? { graph: draft.graph, sql_text: draft.sqlText, source_nodes: sourceNodes(draft.graph) }
|
||||
: { graph: draft.graph });
|
||||
? { pipeline_id: draft.id, graph: draft.graph, sql_text: draft.sqlText, source_nodes: sourceNodes(draft.graph) }
|
||||
: { pipeline_id: draft.id, graph: draft.graph });
|
||||
setDiagnostics(response.diagnostics);
|
||||
if (response.valid) setSuccess("Pipeline definition is valid.");
|
||||
if (response.valid) {
|
||||
if (response.graph && draft.editorMode === "graph") {
|
||||
updateDraft({ graph: response.graph });
|
||||
}
|
||||
setSuccess("Pipeline definition is valid.");
|
||||
}
|
||||
setResultOpen(true);
|
||||
setResultTab("diagnostics");
|
||||
} catch (validationError) {
|
||||
@@ -766,6 +782,26 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{draft.governance?.derived_from_pipeline_id ? (
|
||||
<IconButton
|
||||
label="Review source update"
|
||||
icon={<GitCompareArrows size={16} />}
|
||||
variant="ghost"
|
||||
onClick={() => setRebaseOpen(true)}
|
||||
disabled={!canRebase}
|
||||
disabledReason={
|
||||
dirty
|
||||
? DATAFLOW_I18N.saveFirst
|
||||
: !canEdit
|
||||
? editBlockedReason
|
||||
: !draft.governance.source_available
|
||||
? "The source definition is no longer available."
|
||||
: !draft.governance.update_available
|
||||
? "This copy already pins the current source revision."
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{draft.id ? (
|
||||
<IconButton
|
||||
label="Automation triggers"
|
||||
@@ -899,6 +935,10 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
||||
<NodeInspector
|
||||
node={selectedNode}
|
||||
nodeLibrary={nodeLibrary}
|
||||
reusablePipelines={pipelines.filter((pipeline) => (
|
||||
pipeline.id !== draft.id
|
||||
&& pipeline.governance.actions.reuse?.allowed
|
||||
))}
|
||||
sources={sources}
|
||||
sourceCatalogueAvailable={sourceCatalogueAvailable}
|
||||
readOnly={!canEdit}
|
||||
@@ -1060,6 +1100,32 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
|
||||
setSuccess("Created a pinned scoped copy.");
|
||||
}}
|
||||
/>
|
||||
<RebasePipelineDialog
|
||||
open={rebaseOpen}
|
||||
settings={settings}
|
||||
pipeline={draft?.id && draft.currentRevision && draft.governance ? {
|
||||
id: draft.id,
|
||||
name: draft.name,
|
||||
currentRevision: draft.currentRevision,
|
||||
governance: draft.governance
|
||||
} : null}
|
||||
onClose={() => setRebaseOpen(false)}
|
||||
onRebased={(pipeline) => {
|
||||
const next = draftFromPipeline(pipeline);
|
||||
setPipelines((current) => [
|
||||
pipeline,
|
||||
...current.filter((item) => item.id !== pipeline.id)
|
||||
]);
|
||||
setDraft(next);
|
||||
setSavedDraft(structuredClone(next));
|
||||
setSelectedNodeId(next.graph.nodes[0]?.id ?? null);
|
||||
setRebaseOpen(false);
|
||||
setPreview(null);
|
||||
setDiagnostics([]);
|
||||
setNodeDiagnostics([]);
|
||||
setSuccess(`Adopted source revision ${pipeline.governance.derived_from_revision} as draft revision ${pipeline.current_revision}.`);
|
||||
}}
|
||||
/>
|
||||
<DataflowTriggersDialog
|
||||
open={triggersOpen}
|
||||
settings={settings}
|
||||
@@ -1231,6 +1297,20 @@ function DefinitionSettingsDialog({
|
||||
{draft.governance.derived_from_revision}
|
||||
</span>
|
||||
<code>{draft.governance.derived_from_hash}</code>
|
||||
{!draft.governance.source_available ? (
|
||||
<StatusBadge status="warning" label="Source unavailable" />
|
||||
) : draft.governance.update_available ? (
|
||||
<>
|
||||
<StatusBadge status="warning" label="Source update available" />
|
||||
<span>
|
||||
{draft.governance.source_name ?? "Source definition"}
|
||||
{" · revision "}
|
||||
{draft.governance.source_current_revision}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<StatusBadge status="success" label="Source revision current" />
|
||||
)}
|
||||
</ContentSection>
|
||||
) : null}
|
||||
{provenance.length ? (
|
||||
@@ -1430,6 +1510,153 @@ function DerivePipelineDialog({
|
||||
);
|
||||
}
|
||||
|
||||
function RebasePipelineDialog({
|
||||
open,
|
||||
settings,
|
||||
pipeline,
|
||||
onClose,
|
||||
onRebased
|
||||
}: {
|
||||
open: boolean;
|
||||
settings: ApiSettings;
|
||||
pipeline: {
|
||||
id: string;
|
||||
name: string;
|
||||
currentRevision: number;
|
||||
governance: Pipeline["governance"];
|
||||
} | null;
|
||||
onClose: () => void;
|
||||
onRebased: (pipeline: Pipeline) => void;
|
||||
}) {
|
||||
const { requestDiscard } = useUnsavedChanges();
|
||||
const [reason, setReason] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const governance = pipeline?.governance;
|
||||
const sourceRevision = governance?.source_current_revision ?? null;
|
||||
const sourceHash = governance?.source_current_hash ?? null;
|
||||
const dirty = Boolean(open && reason);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setReason("");
|
||||
setError("");
|
||||
}, [open, pipeline?.id, sourceRevision]);
|
||||
|
||||
const resetDraft = () => {
|
||||
setReason("");
|
||||
setError("");
|
||||
};
|
||||
|
||||
const rebase = async (): Promise<boolean> => {
|
||||
if (
|
||||
!pipeline
|
||||
|| !sourceRevision
|
||||
|| !sourceHash
|
||||
|| !reason.trim()
|
||||
|| !governance?.update_available
|
||||
) return false;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
try {
|
||||
onRebased(await rebaseDataflowPipeline(settings, pipeline.id, {
|
||||
expected_revision: pipeline.currentRevision,
|
||||
source_revision: sourceRevision,
|
||||
source_hash: sourceHash,
|
||||
reason: reason.trim()
|
||||
}));
|
||||
return true;
|
||||
} catch (rebaseError) {
|
||||
setError(apiErrorMessage(rebaseError));
|
||||
return false;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
title: "Unapplied source update",
|
||||
message: "Apply the reviewed source update or discard the review reason before leaving.",
|
||||
onSave: rebase,
|
||||
onDiscard: resetDraft
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
if (busy) return;
|
||||
if (dirty) requestDiscard(onClose);
|
||||
else onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title="Review source update"
|
||||
className="dataflow-definition-dialog"
|
||||
closeDisabled={busy}
|
||||
onClose={close}
|
||||
footer={(
|
||||
<>
|
||||
<Button onClick={close} disabled={busy}>Cancel</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => void rebase()}
|
||||
disabled={
|
||||
busy
|
||||
|| !pipeline
|
||||
|| !sourceRevision
|
||||
|| !sourceHash
|
||||
|| !reason.trim()
|
||||
|| !governance?.update_available
|
||||
}
|
||||
>
|
||||
<GitCompareArrows size={16} /> Adopt source revision
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<div className="dataflow-definition-fields">
|
||||
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
|
||||
<ContentSection spacing="none" surface="subtle" density="compact" layout="stack">
|
||||
<strong>{governance?.source_name ?? "Source definition"}</strong>
|
||||
<span>
|
||||
Pinned revision {governance?.derived_from_revision ?? "—"}
|
||||
{" → source revision "}
|
||||
{sourceRevision ?? "—"}
|
||||
</span>
|
||||
{sourceHash ? <code>{sourceHash}</code> : null}
|
||||
</ContentSection>
|
||||
<DismissibleAlert
|
||||
tone="warning"
|
||||
resetKey={`${pipeline?.id ?? "none"}:${sourceRevision ?? "none"}`}
|
||||
>
|
||||
Adopting the update replaces the copy's current graph with the exact
|
||||
reviewed source revision and returns the copy to draft. Existing
|
||||
revisions, run evidence and rebase provenance remain immutable.
|
||||
</DismissibleAlert>
|
||||
<FormField
|
||||
label="Review reason"
|
||||
help="Record what was reviewed and why this source revision is appropriate for the scoped copy."
|
||||
interfaceId="dataflow.field.rebase-reason"
|
||||
helpContextId="dataflow.field.rebase-reason"
|
||||
helpModuleId="dataflow"
|
||||
helpTopicId="dataflow.reference.fields-and-consequences"
|
||||
documentation={DATAFLOW_FIELDS_DOCUMENTATION}
|
||||
>
|
||||
<textarea
|
||||
value={reason}
|
||||
onChange={(event) => setReason(event.target.value)}
|
||||
rows={4}
|
||||
maxLength={4000}
|
||||
disabled={busy}
|
||||
required
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function DataflowTriggersDialog({
|
||||
open,
|
||||
settings,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@govoplan/core-webui";
|
||||
import type {
|
||||
NodeTypeDefinition,
|
||||
Pipeline,
|
||||
PipelineGraphNode,
|
||||
TabularSource
|
||||
} from "../../api/dataflow";
|
||||
@@ -28,6 +29,7 @@ function NodeFormField({ documentation, ...props }: NodeFormFieldProps) {
|
||||
type NodeInspectorProps = {
|
||||
node: PipelineGraphNode | null;
|
||||
nodeLibrary: NodeTypeDefinition[];
|
||||
reusablePipelines: Pipeline[];
|
||||
sources: TabularSource[];
|
||||
sourceCatalogueAvailable: boolean;
|
||||
readOnly: boolean;
|
||||
@@ -41,6 +43,7 @@ type NodeInspectorProps = {
|
||||
export default function NodeInspector({
|
||||
node,
|
||||
nodeLibrary,
|
||||
reusablePipelines,
|
||||
sources,
|
||||
sourceCatalogueAvailable,
|
||||
readOnly,
|
||||
@@ -57,7 +60,6 @@ export default function NodeInspector({
|
||||
const [rankSortText, setRankSortText] = useState("");
|
||||
const [rulesText, setRulesText] = useState("");
|
||||
const [parametersText, setParametersText] = useState("");
|
||||
const [subflowGraphText, setSubflowGraphText] = useState("");
|
||||
const [localError, setLocalError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,7 +70,6 @@ export default function NodeInspector({
|
||||
setRankSortText(node ? sortFieldsToText(node.config.order_by) : "");
|
||||
setRulesText(node ? JSON.stringify(node.config.rules ?? [], null, 2) : "");
|
||||
setParametersText(node ? JSON.stringify(node.config.parameters ?? {}, null, 2) : "");
|
||||
setSubflowGraphText(node ? JSON.stringify(node.config.graph ?? {}, null, 2) : "");
|
||||
setLocalError("");
|
||||
}, [node?.id]);
|
||||
|
||||
@@ -84,6 +85,9 @@ export default function NodeInspector({
|
||||
}
|
||||
|
||||
const definition = nodeLibrary.find((item) => item.type === node.type);
|
||||
const selectedReusable = reusablePipelines.find(
|
||||
(item) => `pipeline:${item.id}` === textValue(node.config.template_ref)
|
||||
);
|
||||
const updateConfig = (patch: Record<string, unknown>) => {
|
||||
onChange({ ...node, config: { ...node.config, ...patch } });
|
||||
};
|
||||
@@ -263,16 +267,33 @@ export default function NodeInspector({
|
||||
</>
|
||||
) : null}
|
||||
{node.type === "source.inline" ? (
|
||||
<NodeFormField label="Rows">
|
||||
<textarea
|
||||
className="dataflow-json-editor"
|
||||
value={rowsText}
|
||||
onChange={(event) => setRowsText(event.target.value)}
|
||||
onBlur={commitRows}
|
||||
spellCheck={false}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</NodeFormField>
|
||||
<>
|
||||
<NodeFormField label="Rows">
|
||||
<textarea
|
||||
className="dataflow-json-editor"
|
||||
value={rowsText}
|
||||
onChange={(event) => setRowsText(event.target.value)}
|
||||
onBlur={commitRows}
|
||||
spellCheck={false}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</NodeFormField>
|
||||
<NodeFormField
|
||||
label="Reusable input binding"
|
||||
help="A reusable definition must mark exactly one typed inline source as the rows supplied by its caller."
|
||||
interfaceId="dataflow.field.reusable-input-binding"
|
||||
helpContextId="dataflow.field.reusable-input-binding"
|
||||
helpModuleId="dataflow"
|
||||
helpTopicId="dataflow.reference.nodes-and-expressions"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={node.config.input_binding === true}
|
||||
onChange={(event) => updateConfig({ input_binding: event.target.checked })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</NodeFormField>
|
||||
</>
|
||||
) : null}
|
||||
{node.type === "filter" ? (
|
||||
<>
|
||||
@@ -783,19 +804,62 @@ export default function NodeInspector({
|
||||
) : null}
|
||||
{node.type === "subflow" ? (
|
||||
<>
|
||||
<NodeFormField label="Template reference">
|
||||
<input
|
||||
<NodeFormField
|
||||
label="Reusable definition"
|
||||
interfaceId="dataflow.field.subflow-reference"
|
||||
helpContextId="dataflow.field.subflow-reference"
|
||||
helpModuleId="dataflow"
|
||||
helpTopicId="dataflow.reference.nodes-and-expressions"
|
||||
>
|
||||
<select
|
||||
value={textValue(node.config.template_ref)}
|
||||
onChange={(event) => updateConfig({ template_ref: event.target.value })}
|
||||
onChange={(event) => {
|
||||
const selected = reusablePipelines.find(
|
||||
(item) => `pipeline:${item.id}` === event.target.value
|
||||
);
|
||||
updateConfig({
|
||||
template_ref: event.target.value,
|
||||
template_version: selected ? String(selected.current_revision) : "",
|
||||
template_hash: "",
|
||||
graph: { schema_version: 1, nodes: [], edges: [] },
|
||||
input_schema: [],
|
||||
output_schema: []
|
||||
});
|
||||
}}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
>
|
||||
<option value="">Choose a reusable definition</option>
|
||||
{reusablePipelines.map((pipeline) => (
|
||||
<option key={pipeline.id} value={`pipeline:${pipeline.id}`}>
|
||||
{pipeline.name} · revision {pipeline.current_revision}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</NodeFormField>
|
||||
<NodeFormField label="Template version">
|
||||
<input
|
||||
<NodeFormField
|
||||
label="Template version"
|
||||
interfaceId="dataflow.field.subflow-revision"
|
||||
helpContextId="dataflow.field.subflow-revision"
|
||||
helpModuleId="dataflow"
|
||||
helpTopicId="dataflow.reference.nodes-and-expressions"
|
||||
>
|
||||
<select
|
||||
value={textValue(node.config.template_version)}
|
||||
onChange={(event) => updateConfig({ template_version: event.target.value })}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
>
|
||||
{textValue(node.config.template_version)
|
||||
&& textValue(node.config.template_version) !== String(selectedReusable?.current_revision ?? "") ? (
|
||||
<option value={textValue(node.config.template_version)}>
|
||||
Pinned revision {textValue(node.config.template_version)}
|
||||
</option>
|
||||
) : null}
|
||||
{selectedReusable ? (
|
||||
<option value={String(selectedReusable.current_revision)}>
|
||||
Current revision {selectedReusable.current_revision}
|
||||
</option>
|
||||
) : null}
|
||||
</select>
|
||||
</NodeFormField>
|
||||
<NodeFormField label="Parameters">
|
||||
<textarea
|
||||
@@ -807,16 +871,13 @@ export default function NodeInspector({
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</NodeFormField>
|
||||
<NodeFormField label="Pinned graph">
|
||||
<textarea
|
||||
className="dataflow-json-editor"
|
||||
value={subflowGraphText}
|
||||
onChange={(event) => setSubflowGraphText(event.target.value)}
|
||||
onBlur={() => commitJsonConfig("graph", subflowGraphText, "object")}
|
||||
spellCheck={false}
|
||||
disabled={readOnly}
|
||||
/>
|
||||
</NodeFormField>
|
||||
{Array.isArray(node.config.input_schema) && Array.isArray(node.config.output_schema) ? (
|
||||
<NodeFormField label="Pinned contracts">
|
||||
<code>
|
||||
{node.config.input_schema.length} input · {node.config.output_schema.length} output fields
|
||||
</code>
|
||||
</NodeFormField>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -272,7 +272,7 @@ export const FALLBACK_NODE_LIBRARY: NodeTypeDefinition[] = [
|
||||
"transform",
|
||||
"Transform",
|
||||
"Reusable subflow",
|
||||
"Run a pinned parameterized template snapshot.",
|
||||
"Run a Policy-authorized, server-resolved immutable definition revision.",
|
||||
"boxes",
|
||||
input,
|
||||
output,
|
||||
|
||||
@@ -56,6 +56,25 @@ const en = {
|
||||
"New pipeline": "New pipeline",
|
||||
"Definition settings": "Definition settings",
|
||||
"Reuse as scoped copy": "Reuse as scoped copy",
|
||||
"Review source update": "Review source update",
|
||||
"Adopt source revision": "Adopt source revision",
|
||||
"Source unavailable": "Source unavailable",
|
||||
"Source update available": "Source update available",
|
||||
"Source definition": "Source definition",
|
||||
"Source revision current": "Source revision current",
|
||||
"Unapplied source update": "Unapplied source update",
|
||||
"Apply the reviewed source update or discard the review reason before leaving.": "Apply the reviewed source update or discard the review reason before leaving.",
|
||||
"The source definition is no longer available.": "The source definition is no longer available.",
|
||||
"This copy already pins the current source revision.": "This copy already pins the current source revision.",
|
||||
"Adopting the update replaces the copy's current graph with the exact reviewed source revision and returns the copy to draft. Existing revisions, run evidence and rebase provenance remain immutable.": "Adopting the update replaces the copy's current graph with the exact reviewed source revision and returns the copy to draft. Existing revisions, run evidence and rebase provenance remain immutable.",
|
||||
"Review reason": "Review reason",
|
||||
"Record what was reviewed and why this source revision is appropriate for the scoped copy.": "Record what was reviewed and why this source revision is appropriate for the scoped copy.",
|
||||
"Reusable input binding": "Reusable input binding",
|
||||
"A reusable definition must mark exactly one typed inline source as the rows supplied by its caller.": "A reusable definition must mark exactly one typed inline source as the rows supplied by its caller.",
|
||||
"Reusable definition": "Reusable definition",
|
||||
"Choose a reusable definition": "Choose a reusable definition",
|
||||
"Template version": "Template version",
|
||||
"Pinned contracts": "Pinned contracts",
|
||||
"Automation triggers": "Automation triggers",
|
||||
"Discard changes": "Discard changes",
|
||||
"Delete pipeline": "Delete pipeline",
|
||||
@@ -136,6 +155,25 @@ const de: Record<keyof typeof en, string> = {
|
||||
"New pipeline": "Neuer Datenfluss",
|
||||
"Definition settings": "Definitionseinstellungen",
|
||||
"Reuse as scoped copy": "Als eingegrenzte Kopie verwenden",
|
||||
"Review source update": "Aktualisierung der Quelle prüfen",
|
||||
"Adopt source revision": "Quellrevision übernehmen",
|
||||
"Source unavailable": "Quelle nicht verfügbar",
|
||||
"Source update available": "Aktualisierung der Quelle verfügbar",
|
||||
"Source definition": "Quelldefinition",
|
||||
"Source revision current": "Quellrevision aktuell",
|
||||
"Unapplied source update": "Nicht übernommene Quellenaktualisierung",
|
||||
"Apply the reviewed source update or discard the review reason before leaving.": "Übernehmen Sie die geprüfte Quellenaktualisierung oder verwerfen Sie die Prüfbegründung, bevor Sie den Dialog verlassen.",
|
||||
"The source definition is no longer available.": "Die Quelldefinition ist nicht mehr verfügbar.",
|
||||
"This copy already pins the current source revision.": "Diese Kopie ist bereits an die aktuelle Quellrevision gebunden.",
|
||||
"Adopting the update replaces the copy's current graph with the exact reviewed source revision and returns the copy to draft. Existing revisions, run evidence and rebase provenance remain immutable.": "Die Übernahme ersetzt den aktuellen Graphen der Kopie durch die exakt geprüfte Quellrevision und setzt die Kopie auf Entwurf zurück. Bestehende Revisionen, Ausführungsnachweise und die Herkunft der Übernahme bleiben unveränderlich.",
|
||||
"Review reason": "Prüfbegründung",
|
||||
"Record what was reviewed and why this source revision is appropriate for the scoped copy.": "Dokumentieren Sie, was geprüft wurde und warum diese Quellrevision für die eingegrenzte Kopie geeignet ist.",
|
||||
"Reusable input binding": "Wiederverwendbare Eingabebindung",
|
||||
"A reusable definition must mark exactly one typed inline source as the rows supplied by its caller.": "Eine wiederverwendbare Definition muss genau eine typisierte Inline-Quelle als die vom Aufrufer gelieferten Zeilen kennzeichnen.",
|
||||
"Reusable definition": "Wiederverwendbare Definition",
|
||||
"Choose a reusable definition": "Wiederverwendbare Definition auswählen",
|
||||
"Template version": "Vorlagenversion",
|
||||
"Pinned contracts": "Gebundene Verträge",
|
||||
"Automation triggers": "Automatisierungsauslöser",
|
||||
"Discard changes": "Änderungen verwerfen",
|
||||
"Delete pipeline": "Datenfluss löschen",
|
||||
|
||||
Reference in New Issue
Block a user