Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:31 +02:00
parent f908ddbf9d
commit 20d134898a
6 changed files with 225 additions and 640 deletions
@@ -60,11 +60,14 @@ const checks = [
moduleEntry.indexOf("./styles/dataflow.css"), moduleEntry.indexOf("./styles/dataflow.css"),
"XyFlow base styles before GovOPlaN overrides" "XyFlow base styles before GovOPlaN overrides"
], ],
[css.includes("height: calc(100vh - 115px)"), "full-height workspace"], [
page.includes('<WorkspaceFrame as="main" height="viewport"'),
"central full-height workspace"
],
[css.includes(".dataflow-preview-table-wrap"), "bounded preview scrolling"], [css.includes(".dataflow-preview-table-wrap"), "bounded preview scrolling"],
[ [
/\.dataflow-palette-items\s*\{[^}]*grid-auto-rows:\s*max-content;[^}]*overflow-y:\s*auto;/s.test(css), page.includes("<DefinitionPalette") && !css.includes(".dataflow-palette-items"),
"bounded palette scrolling" "centralized bounded palette scrolling"
], ],
[ [
/\.dataflow-inspector-fields\s*\{[^}]*grid-auto-rows:\s*max-content;[^}]*overflow-y:\s*auto;/s.test(css), /\.dataflow-inspector-fields\s*\{[^}]*grid-auto-rows:\s*max-content;[^}]*overflow-y:\s*auto;/s.test(css),
@@ -14,6 +14,7 @@ import {
type Edge, type Edge,
type ReactFlowInstance type ReactFlowInstance
} from "@xyflow/react"; } from "@xyflow/react";
import { StatePanel } from "@govoplan/core-webui";
import { definitionConnectionError } from "@govoplan/core-webui/definition-graph"; import { definitionConnectionError } from "@govoplan/core-webui/definition-graph";
import type { import type {
DataflowDiagnostic, DataflowDiagnostic,
@@ -226,7 +227,7 @@ export default function DataflowCanvas({
return ( return (
<div <div
ref={canvasRef} ref={canvasRef}
className="dataflow-canvas" className="definition-graph-canvas dataflow-canvas"
tabIndex={0} tabIndex={0}
aria-label="Dataflow graph canvas" aria-label="Dataflow graph canvas"
onDragOver={(event) => { onDragOver={(event) => {
@@ -360,7 +361,7 @@ export default function DataflowCanvas({
<Controls showInteractive={false} /> <Controls showInteractive={false} />
</ReactFlow> </ReactFlow>
{!graph.nodes.length ? ( {!graph.nodes.length ? (
<div className="dataflow-canvas-empty">Drop a source here</div> <StatePanel className="definition-graph-canvas-empty" size="fill" description="Drop a source here" />
) : null} ) : null}
</div> </div>
); );
+6 -5
View File
@@ -1,4 +1,5 @@
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
import { CountBadge, DefinitionNodeIcon } from "@govoplan/core-webui";
import type { NodeTypeDefinition } from "../../api/dataflow"; import type { NodeTypeDefinition } from "../../api/dataflow";
import { dataflowNodeIcon } from "./nodeIcons"; import { dataflowNodeIcon } from "./nodeIcons";
@@ -31,7 +32,7 @@ export default function DataflowNode({ data, selected }: NodeProps<DataflowFlowN
id={port.id} id={port.id}
type="target" type="target"
position={Position.Left} position={Position.Left}
className="dataflow-node-handle dataflow-node-handle-input" className="definition-node-handle dataflow-node-handle-input"
style={{ top: portPosition(index, data.definition.input_ports.length) }} style={{ top: portPosition(index, data.definition.input_ports.length) }}
/> />
))} ))}
@@ -46,15 +47,15 @@ export default function DataflowNode({ data, selected }: NodeProps<DataflowFlowN
</span> </span>
)) ))
: null} : null}
<span className="dataflow-node-icon" aria-hidden="true"> <DefinitionNodeIcon aria-hidden="true">
<Icon size={17} strokeWidth={1.8} /> <Icon size={17} strokeWidth={1.8} />
</span> </DefinitionNodeIcon>
<span className="dataflow-node-copy"> <span className="dataflow-node-copy">
<strong>{data.label}</strong> <strong>{data.label}</strong>
<small>{data.definition.label}</small> <small>{data.definition.label}</small>
</span> </span>
{typeof data.outputRows === "number" ? ( {typeof data.outputRows === "number" ? (
<span className="dataflow-node-count">{data.outputRows}</span> <CountBadge tone="neutral" size="compact">{data.outputRows}</CountBadge>
) : null} ) : null}
{data.definition.output_ports.map((port, index) => ( {data.definition.output_ports.map((port, index) => (
<Handle <Handle
@@ -62,7 +63,7 @@ export default function DataflowNode({ data, selected }: NodeProps<DataflowFlowN
id={port.id} id={port.id}
type="source" type="source"
position={Position.Right} position={Position.Right}
className="dataflow-node-handle dataflow-node-handle-output" className="definition-node-handle dataflow-node-handle-output"
style={{ top: portPosition(index, data.definition.output_ports.length) }} style={{ top: portPosition(index, data.definition.output_ports.length) }}
/> />
))} ))}
+76 -72
View File
@@ -30,16 +30,29 @@ import { DialogSection, ToolbarGroup, ActionToolbar,
ActionBlockerHint, ActionBlockerHint,
Button, Button,
ConfirmDialog, ConfirmDialog,
ContentGrid,
ContentSection,
Dialog, Dialog,
DismissibleAlert, DismissibleAlert,
DefinitionPalette,
DefinitionPaletteGroup,
DefinitionPaletteItem,
DocumentationHelpLink, DocumentationHelpLink,
FilterBar,
FloatingStatus,
FormField, FormField,
IconButton, IconButton,
LoadingFrame, LoadingFrame,
ReferenceSelect, ReferenceSelect,
SegmentedControl, SegmentedControl,
SelectionList,
SelectionListItem,
SelectionListItemContent,
StatePanel,
StatusBadge, StatusBadge,
ToggleSwitch, ToggleSwitch,
WorkspaceFrame,
WorkspaceLayout,
hasScope, hasScope,
isApiError, isApiError,
useUnsavedChanges, useUnsavedChanges,
@@ -584,10 +597,18 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
}; };
return ( return (
<main className="dataflow-page"> <WorkspaceFrame as="main" height="viewport" surface="plain" className="dataflow-page" label="Dataflow workspace">
<div className="dataflow-shell"> <WorkspaceLayout
<aside className="dataflow-pipeline-panel" aria-label="Data pipelines"> variant="split"
<ActionToolbar className="dataflow-panel-toolbar"> primarySize="compact"
surface="contained"
primaryScrollable={false}
contentScrollable={false}
primaryLabel="Data pipelines"
contentLabel="Pipeline editor"
contentClassName="dataflow-workspace"
primary={<>
<ActionToolbar surface="panel-header">
<strong>Pipelines</strong> <strong>Pipelines</strong>
<ToolbarGroup align="end" className="dataflow-toolbar-actions"> <ToolbarGroup align="end" className="dataflow-toolbar-actions">
<IconButton <IconButton
@@ -608,7 +629,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
/> />
</ToolbarGroup> </ToolbarGroup>
</ActionToolbar> </ActionToolbar>
<div className="dataflow-pipeline-search"> <FilterBar surface="panel">
<input <input
type="search" type="search"
value={search} value={search}
@@ -616,39 +637,32 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
placeholder="Search pipelines" placeholder="Search pipelines"
aria-label="Search pipelines" aria-label="Search pipelines"
/> />
</div> </FilterBar>
<LoadingFrame loading={loading} label="Loading pipelines" className="dataflow-pipeline-list-frame"> <LoadingFrame loading={loading} label="Loading pipelines" className="dataflow-pipeline-list-frame">
<div className="dataflow-pipeline-list"> <SelectionList variant="navigation" label="Data pipelines">
{filteredPipelines.map((pipeline) => ( {filteredPipelines.map((pipeline) => (
<button <SelectionListItem
key={pipeline.id} key={pipeline.id}
type="button" selected={pipeline.id === draft?.id}
className={pipeline.id === draft?.id ? "is-selected" : ""}
onClick={() => selectPipeline(pipeline)} onClick={() => selectPipeline(pipeline)}
> >
<span> <SelectionListItemContent
<strong>{pipeline.name}</strong> title={pipeline.name}
<small>Revision {pipeline.current_revision}</small> description={`Revision ${pipeline.current_revision} · ${pipeline.governance.scope_type} · ${pipeline.governance.definition_kind}`}
<small> />
{pipeline.governance.scope_type} · {pipeline.governance.definition_kind}
</small>
</span>
<StatusBadge status={pipeline.status} /> <StatusBadge status={pipeline.status} />
</button> </SelectionListItem>
))} ))}
{!loading && !filteredPipelines.length ? ( {!loading && !filteredPipelines.length ? (
<div className="dataflow-pipeline-empty"> <StatePanel size="compact" description={search ? "No matching pipelines" : "No pipelines yet"} />
{search ? "No matching pipelines" : "No pipelines yet"}
</div>
) : null} ) : null}
</div> </SelectionList>
</LoadingFrame> </LoadingFrame>
</aside> </>}
>
<section className="dataflow-workspace">
{draft ? ( {draft ? (
<> <>
<ActionToolbar className="dataflow-workspace-toolbar"> <ActionToolbar surface="panel-header" className="dataflow-workspace-toolbar">
<div className="dataflow-identity-fields"> <div className="dataflow-identity-fields">
<input <input
className="dataflow-name-input" className="dataflow-name-input"
@@ -835,10 +849,10 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
) : null} ) : 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"> <DefinitionPalette
<div className="dataflow-panel-heading"> label="Nodes"
<strong>Nodes</strong> aria-label="Transform palette"
{canImportSources ? ( actions={canImportSources ? (
<IconButton <IconButton
label="Stage datasource" label="Stage datasource"
icon={<Upload size={15} />} icon={<Upload size={15} />}
@@ -846,36 +860,30 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
onClick={() => setSnapshotOpen(true)} onClick={() => setSnapshotOpen(true)}
/> />
) : null} ) : null}
</div> >
<div className="dataflow-palette-items">
{paletteGroups.map((group) => ( {paletteGroups.map((group) => (
<section key={group.category} className="dataflow-palette-group"> <DefinitionPaletteGroup key={group.category} label={group.label}>
<h3>{group.label}</h3>
{group.nodes.map((definition) => { {group.nodes.map((definition) => {
const Icon = dataflowNodeIcon(definition.icon); const Icon = dataflowNodeIcon(definition.icon);
const disabled = !canEdit || uniqueNodeExists(draft, definition.type); const disabled = !canEdit || uniqueNodeExists(draft, definition.type);
return ( return (
<button <DefinitionPaletteItem
key={definition.type} key={definition.type}
type="button"
title={definition.description} title={definition.description}
icon={<Icon size={16} />}
label={definition.label}
draggable={!disabled} draggable={!disabled}
disabled={disabled} disabled={disabled}
onDragStart={(event) => startPaletteDrag(event, definition.type)} onDragStart={(event) => startPaletteDrag(event, definition.type)}
onClick={() => addNode(definition.type)} onClick={() => addNode(definition.type)}
> />
<Icon size={16} />
<span>{definition.label}</span>
<Plus size={14} className="dataflow-palette-add" />
</button>
); );
})} })}
</section> </DefinitionPaletteGroup>
))} ))}
</div> </DefinitionPalette>
</aside>
) : null} ) : null}
<section className="dataflow-editor-surface"> <section className="definition-editor-surface dataflow-editor-surface">
{draft.editorMode === "graph" ? ( {draft.editorMode === "graph" ? (
<ReactFlowProvider> <ReactFlowProvider>
<DataflowCanvas <DataflowCanvas
@@ -891,7 +899,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
</ReactFlowProvider> </ReactFlowProvider>
) : ( ) : (
<div className="dataflow-sql-workbench"> <div className="dataflow-sql-workbench">
<ActionToolbar className="dataflow-sql-toolbar"> <ActionToolbar surface="section-header" className="dataflow-sql-toolbar">
<span>Constrained Dataflow SQL</span> <span>Constrained Dataflow SQL</span>
<Button <Button
variant="primary" variant="primary"
@@ -955,22 +963,22 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
) : null} ) : null}
</> </>
) : ( ) : (
<div className="dataflow-workspace-empty"> <StatePanel
<Network size={30} /> size="fill"
<strong>No pipeline selected</strong> icon={<Network size={30} />}
<Button title="No pipeline selected"
actions={<Button
variant="primary" variant="primary"
onClick={createNew} onClick={createNew}
disabled={!canWrite} disabled={!canWrite}
disabledReason={!canWrite ? DATAFLOW_I18N.writeReason : undefined} disabledReason={!canWrite ? DATAFLOW_I18N.writeReason : undefined}
> >
<Plus size={16} /> New pipeline <Plus size={16} /> New pipeline
</Button> </Button>}
</div> />
)} )}
{working ? <div className="dataflow-working-indicator" role="status">Working...</div> : null} {working ? <FloatingStatus>Working...</FloatingStatus> : null}
</section> </WorkspaceLayout>
</div>
<ConfirmDialog <ConfirmDialog
open={deleteOpen} open={deleteOpen}
title="Delete pipeline" title="Delete pipeline"
@@ -1102,7 +1110,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
}).catch((sourceError) => setError(apiErrorMessage(sourceError))); }).catch((sourceError) => setError(apiErrorMessage(sourceError)));
}} }}
/> />
</main> </WorkspaceFrame>
); );
} }
@@ -1207,7 +1215,7 @@ function DefinitionSettingsDialog({
<option value="template">Template</option> <option value="template">Template</option>
</select> </select>
</FormField> </FormField>
<div className="dataflow-definition-toggles"> <ContentGrid columns={2} gap="default" collapseAt="narrow">
<ToggleSwitch <ToggleSwitch
label="Visible to lower scopes" label="Visible to lower scopes"
checked={draft.inheritToLowerScopes} checked={draft.inheritToLowerScopes}
@@ -1232,9 +1240,9 @@ function DefinitionSettingsDialog({
disabled={!editable} disabled={!editable}
onChange={(value) => onChange({ allowAutomation: value })} onChange={(value) => onChange({ allowAutomation: value })}
/> />
</div> </ContentGrid>
{draft.governance?.derived_from_pipeline_id ? ( {draft.governance?.derived_from_pipeline_id ? (
<section className="dataflow-provenance"> <ContentSection spacing="none" surface="subtle" density="compact" layout="stack" className="dataflow-provenance">
<strong>Derived from</strong> <strong>Derived from</strong>
<span> <span>
{draft.governance.derived_from_pipeline_id} {draft.governance.derived_from_pipeline_id}
@@ -1242,10 +1250,10 @@ function DefinitionSettingsDialog({
{draft.governance.derived_from_revision} {draft.governance.derived_from_revision}
</span> </span>
<code>{draft.governance.derived_from_hash}</code> <code>{draft.governance.derived_from_hash}</code>
</section> </ContentSection>
) : null} ) : null}
{provenance.length ? ( {provenance.length ? (
<section className="dataflow-provenance"> <ContentSection spacing="none" surface="subtle" density="compact" layout="stack" className="dataflow-provenance">
<strong>Effective Policy path</strong> <strong>Effective Policy path</strong>
{provenance.map((item, index) => ( {provenance.map((item, index) => (
<span key={`${String(item.path ?? item.scope_type)}-${index}`}> <span key={`${String(item.path ?? item.scope_type)}-${index}`}>
@@ -1255,7 +1263,7 @@ function DefinitionSettingsDialog({
{!editable && draft.governance?.actions.edit?.reason ? ( {!editable && draft.governance?.actions.edit?.reason ? (
<small>{draft.governance.actions.edit.reason}</small> <small>{draft.governance.actions.edit.reason}</small>
) : null} ) : null}
</section> </ContentSection>
) : null} ) : null}
</div> </div>
) : null} ) : null}
@@ -2745,9 +2753,7 @@ function ReconciliationDecisionDialog({
</div> </div>
</ActionToolbar> </ActionToolbar>
{!rows.length ? ( {!rows.length ? (
<div className="dataflow-results-empty"> <StatePanel size="compact" description="Run a preview of a reconciliation comparison that emits stable key and input hashes." />
Run a preview of a reconciliation comparison that emits stable key and input hashes.
</div>
) : ( ) : (
<div className="dataflow-decision-layout"> <div className="dataflow-decision-layout">
<div className="dataflow-decision-rows" role="listbox" aria-label="Reconciliation rows"> <div className="dataflow-decision-rows" role="listbox" aria-label="Reconciliation rows">
@@ -2844,9 +2850,7 @@ function ReconciliationDecisionDialog({
</div> </div>
</> </>
) : ( ) : (
<div className="dataflow-results-empty"> <StatePanel size="compact" description={loading ? "Loading decision sets..." : "Create or select a decision set to review this row."} />
{loading ? "Loading decision sets..." : "Create or select a decision set to review this row."}
</div>
)} )}
</div> </div>
</div> </div>
@@ -2885,7 +2889,7 @@ function ResultPanel({
const previewRows = preview?.node_preview?.total_rows ?? preview?.total_rows; const previewRows = preview?.node_preview?.total_rows ?? preview?.total_rows;
return ( return (
<section className="dataflow-results" aria-label="Pipeline results"> <section className="dataflow-results" aria-label="Pipeline results">
<ActionToolbar className="dataflow-results-toolbar"> <ActionToolbar surface="section-header" className="dataflow-results-toolbar">
<div className="dataflow-results-view-controls"> <div className="dataflow-results-view-controls">
<SegmentedControl<ResultTab> <SegmentedControl<ResultTab>
ariaLabel="Result view" ariaLabel="Result view"
@@ -2926,9 +2930,9 @@ function ResultPanel({
} }
function PreviewTable({ preview }: { preview: PipelinePreview | null }) { function PreviewTable({ preview }: { preview: PipelinePreview | null }) {
if (!preview) return <div className="dataflow-results-empty">No preview has been run.</div>; if (!preview) return <StatePanel size="compact" description="No preview has been run." />;
if (preview.status === "failed" && !preview.node_preview) { if (preview.status === "failed" && !preview.node_preview) {
return <div className="dataflow-results-empty">Preview failed.</div>; return <StatePanel size="compact" tone="danger" description="Preview failed." />;
} }
const result = preview.node_preview ?? preview; const result = preview.node_preview ?? preview;
return ( return (
@@ -2978,7 +2982,7 @@ function DiagnosticsPanel({
nodeDiagnostics: NodePreviewDiagnostic[]; nodeDiagnostics: NodePreviewDiagnostic[];
}) { }) {
if (!diagnostics.length && !nodeDiagnostics.length) { if (!diagnostics.length && !nodeDiagnostics.length) {
return <div className="dataflow-results-empty">No diagnostics.</div>; return <StatePanel size="compact" description="No diagnostics." />;
} }
return ( return (
<div className="dataflow-diagnostics-list"> <div className="dataflow-diagnostics-list">
+131 -129
View File
@@ -1,9 +1,11 @@
import { useEffect, useState, type ComponentProps } from "react"; import { useEffect, useState, type ComponentProps } from "react";
import { Play, Trash2 } from "lucide-react"; import { Play, Trash2 } from "lucide-react";
import { import {
ActionToolbar,
DismissibleAlert, DismissibleAlert,
FormField as CoreFormField, FormField as CoreFormField,
IconButton IconButton,
StatePanel
} from "@govoplan/core-webui"; } from "@govoplan/core-webui";
import type { import type {
NodeTypeDefinition, NodeTypeDefinition,
@@ -14,7 +16,7 @@ import { DATAFLOW_NODE_DOCUMENTATION } from "./interfacePatterns";
type NodeFormFieldProps = ComponentProps<typeof CoreFormField>; type NodeFormFieldProps = ComponentProps<typeof CoreFormField>;
function FormField({ documentation, ...props }: NodeFormFieldProps) { function NodeFormField({ documentation, ...props }: NodeFormFieldProps) {
return ( return (
<CoreFormField <CoreFormField
{...props} {...props}
@@ -73,10 +75,10 @@ export default function NodeInspector({
if (!node) { if (!node) {
return ( return (
<aside className="dataflow-inspector" aria-label="Node inspector"> <aside className="dataflow-inspector" aria-label="Node inspector">
<div className="dataflow-panel-heading"> <ActionToolbar surface="section-header" className="dataflow-panel-heading">
<strong>Inspector</strong> <strong>Inspector</strong>
</div> </ActionToolbar>
<div className="dataflow-inspector-empty">No node selected</div> <StatePanel size="compact" description="No node selected" />
</aside> </aside>
); );
} }
@@ -161,7 +163,7 @@ export default function NodeInspector({
return ( return (
<aside className="dataflow-inspector" aria-label="Node inspector"> <aside className="dataflow-inspector" aria-label="Node inspector">
<div className="dataflow-panel-heading"> <ActionToolbar surface="section-header" className="dataflow-panel-heading">
<span> <span>
<strong>Inspector</strong> <strong>Inspector</strong>
<small>{definition?.label ?? node.type}</small> <small>{definition?.label ?? node.type}</small>
@@ -182,32 +184,32 @@ export default function NodeInspector({
disabled={readOnly} disabled={readOnly}
/> />
</span> </span>
</div> </ActionToolbar>
<div className="dataflow-inspector-fields"> <div className="dataflow-inspector-fields">
{localError ? ( {localError ? (
<DismissibleAlert tone="danger" resetKey={localError}> <DismissibleAlert tone="danger" resetKey={localError}>
{localError} {localError}
</DismissibleAlert> </DismissibleAlert>
) : null} ) : null}
<FormField label="Name"> <NodeFormField label="Name">
<input <input
value={node.label} value={node.label}
onChange={(event) => onChange({ ...node, label: event.target.value })} onChange={(event) => onChange({ ...node, label: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
{node.type.startsWith("source.") ? ( {node.type.startsWith("source.") ? (
<FormField label="Logical source name"> <NodeFormField label="Logical source name">
<input <input
value={textValue(node.config.source_name)} value={textValue(node.config.source_name)}
onChange={(event) => updateConfig({ source_name: event.target.value })} onChange={(event) => updateConfig({ source_name: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
{node.type === "source.reference" ? ( {node.type === "source.reference" ? (
<> <>
<FormField label="Source"> <NodeFormField label="Source">
<select <select
value={textValue(node.config.source_ref)} value={textValue(node.config.source_ref)}
onChange={(event) => { onChange={(event) => {
@@ -237,8 +239,8 @@ export default function NodeInspector({
</option> </option>
))} ))}
</select> </select>
</FormField> </NodeFormField>
<FormField label="State"> <NodeFormField label="State">
<select <select
value={textValue(node.config.consistency) || "current"} value={textValue(node.config.consistency) || "current"}
onChange={(event) => updateConfig({ consistency: event.target.value })} onChange={(event) => updateConfig({ consistency: event.target.value })}
@@ -248,20 +250,20 @@ export default function NodeInspector({
<option value="live">Live</option> <option value="live">Live</option>
<option value="frozen">Latest frozen</option> <option value="frozen">Latest frozen</option>
</select> </select>
</FormField> </NodeFormField>
{textValue(node.config.expected_fingerprint) ? ( {textValue(node.config.expected_fingerprint) ? (
<FormField label="Pinned fingerprint"> <NodeFormField label="Pinned fingerprint">
<input <input
value={textValue(node.config.expected_fingerprint)} value={textValue(node.config.expected_fingerprint)}
readOnly readOnly
title={textValue(node.config.expected_fingerprint)} title={textValue(node.config.expected_fingerprint)}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
</> </>
) : null} ) : null}
{node.type === "source.inline" ? ( {node.type === "source.inline" ? (
<FormField label="Rows"> <NodeFormField label="Rows">
<textarea <textarea
className="dataflow-json-editor" className="dataflow-json-editor"
value={rowsText} value={rowsText}
@@ -270,18 +272,18 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
{node.type === "filter" ? ( {node.type === "filter" ? (
<> <>
<FormField label="Column"> <NodeFormField label="Column">
<input <input
value={textValue(node.config.column)} value={textValue(node.config.column)}
onChange={(event) => updateConfig({ column: event.target.value })} onChange={(event) => updateConfig({ column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Operator"> <NodeFormField label="Operator">
<select <select
value={textValue(node.config.operator) || "eq"} value={textValue(node.config.operator) || "eq"}
onChange={(event) => updateConfig({ operator: event.target.value })} onChange={(event) => updateConfig({ operator: event.target.value })}
@@ -297,20 +299,20 @@ export default function NodeInspector({
<option value="is_null">is null</option> <option value="is_null">is null</option>
<option value="not_null">is not null</option> <option value="not_null">is not null</option>
</select> </select>
</FormField> </NodeFormField>
{!["is_null", "not_null"].includes(textValue(node.config.operator)) ? ( {!["is_null", "not_null"].includes(textValue(node.config.operator)) ? (
<FormField label="Value"> <NodeFormField label="Value">
<input <input
value={displayScalar(node.config.value)} value={displayScalar(node.config.value)}
onChange={(event) => updateConfig({ value: parseScalar(event.target.value) })} onChange={(event) => updateConfig({ value: parseScalar(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
</> </>
) : null} ) : null}
{node.type === "filter.expression" ? ( {node.type === "filter.expression" ? (
<FormField label="Expression"> <NodeFormField label="Expression">
<textarea <textarea
className="dataflow-expression-editor" className="dataflow-expression-editor"
value={textValue(node.config.expression)} value={textValue(node.config.expression)}
@@ -318,20 +320,20 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
{node.type === "distinct" ? ( {node.type === "distinct" ? (
<FormField label="Key columns"> <NodeFormField label="Key columns">
<input <input
value={stringList(node.config.columns).join(", ")} value={stringList(node.config.columns).join(", ")}
onChange={(event) => updateConfig({ columns: commaList(event.target.value) })} onChange={(event) => updateConfig({ columns: commaList(event.target.value) })}
placeholder="All columns" placeholder="All columns"
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
{node.type === "combine.union" ? ( {node.type === "combine.union" ? (
<FormField label="Duplicates"> <NodeFormField label="Duplicates">
<select <select
value={textValue(node.config.mode) || "all"} value={textValue(node.config.mode) || "all"}
onChange={(event) => updateConfig({ mode: event.target.value })} onChange={(event) => updateConfig({ mode: event.target.value })}
@@ -340,11 +342,11 @@ export default function NodeInspector({
<option value="all">Keep all rows</option> <option value="all">Keep all rows</option>
<option value="distinct">Remove duplicates</option> <option value="distinct">Remove duplicates</option>
</select> </select>
</FormField> </NodeFormField>
) : null} ) : null}
{node.type === "combine.join" ? ( {node.type === "combine.join" ? (
<> <>
<FormField label="Join type"> <NodeFormField label="Join type">
<select <select
value={textValue(node.config.join_type) || "inner"} value={textValue(node.config.join_type) || "inner"}
onChange={(event) => updateConfig({ join_type: event.target.value })} onChange={(event) => updateConfig({ join_type: event.target.value })}
@@ -357,51 +359,51 @@ export default function NodeInspector({
<option value="semi">Left rows with a match</option> <option value="semi">Left rows with a match</option>
<option value="anti">Left rows without a match</option> <option value="anti">Left rows without a match</option>
</select> </select>
</FormField> </NodeFormField>
<FormField label="Left keys"> <NodeFormField label="Left keys">
<input <input
value={stringList(node.config.left_keys).join(", ")} value={stringList(node.config.left_keys).join(", ")}
onChange={(event) => updateConfig({ left_keys: commaList(event.target.value) })} onChange={(event) => updateConfig({ left_keys: commaList(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Right keys"> <NodeFormField label="Right keys">
<input <input
value={stringList(node.config.right_keys).join(", ")} value={stringList(node.config.right_keys).join(", ")}
onChange={(event) => updateConfig({ right_keys: commaList(event.target.value) })} onChange={(event) => updateConfig({ right_keys: commaList(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
{!["semi", "anti"].includes(textValue(node.config.join_type)) ? ( {!["semi", "anti"].includes(textValue(node.config.join_type)) ? (
<FormField label="Right-column prefix"> <NodeFormField label="Right-column prefix">
<input <input
value={textValue(node.config.right_prefix)} value={textValue(node.config.right_prefix)}
onChange={(event) => updateConfig({ right_prefix: event.target.value })} onChange={(event) => updateConfig({ right_prefix: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
</> </>
) : null} ) : null}
{node.type === "select" ? ( {node.type === "select" ? (
<FormField label="Columns"> <NodeFormField label="Columns">
<input <input
value={selectFieldsToText(node.config.fields)} value={selectFieldsToText(node.config.fields)}
onChange={(event) => updateConfig({ fields: selectFieldsFromText(event.target.value) })} onChange={(event) => updateConfig({ fields: selectFieldsFromText(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
{node.type === "aggregate" ? ( {node.type === "aggregate" ? (
<> <>
<FormField label="Group by"> <NodeFormField label="Group by">
<input <input
value={stringList(node.config.group_by).join(", ")} value={stringList(node.config.group_by).join(", ")}
onChange={(event) => updateConfig({ group_by: commaList(event.target.value) })} onChange={(event) => updateConfig({ group_by: commaList(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Aggregates"> <NodeFormField label="Aggregates">
<textarea <textarea
className="dataflow-expression-editor" className="dataflow-expression-editor"
value={aggregateText} value={aggregateText}
@@ -410,19 +412,19 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
</> </>
) : null} ) : null}
{node.type === "derive" ? ( {node.type === "derive" ? (
<> <>
<FormField label="Output column"> <NodeFormField label="Output column">
<input <input
value={textValue(node.config.target_column)} value={textValue(node.config.target_column)}
onChange={(event) => updateConfig({ target_column: event.target.value })} onChange={(event) => updateConfig({ target_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Operation"> <NodeFormField label="Operation">
<select <select
value={textValue(node.config.operation) || "copy"} value={textValue(node.config.operation) || "copy"}
onChange={(event) => updateConfig({ operation: event.target.value })} onChange={(event) => updateConfig({ operation: event.target.value })}
@@ -439,35 +441,35 @@ export default function NodeInspector({
<option value="multiply">Multiply</option> <option value="multiply">Multiply</option>
<option value="divide">Divide</option> <option value="divide">Divide</option>
</select> </select>
</FormField> </NodeFormField>
<FormField label="Source columns"> <NodeFormField label="Source columns">
<input <input
value={stringList(node.config.source_columns).join(", ")} value={stringList(node.config.source_columns).join(", ")}
onChange={(event) => updateConfig({ source_columns: commaList(event.target.value) })} onChange={(event) => updateConfig({ source_columns: commaList(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
{textValue(node.config.operation) === "concat" ? ( {textValue(node.config.operation) === "concat" ? (
<FormField label="Separator"> <NodeFormField label="Separator">
<input <input
value={textValue(node.config.separator)} value={textValue(node.config.separator)}
onChange={(event) => updateConfig({ separator: event.target.value })} onChange={(event) => updateConfig({ separator: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
</> </>
) : null} ) : null}
{node.type === "expression" ? ( {node.type === "expression" ? (
<> <>
<FormField label="Output column"> <NodeFormField label="Output column">
<input <input
value={textValue(node.config.target_column)} value={textValue(node.config.target_column)}
onChange={(event) => updateConfig({ target_column: event.target.value })} onChange={(event) => updateConfig({ target_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Expression"> <NodeFormField label="Expression">
<textarea <textarea
className="dataflow-expression-editor" className="dataflow-expression-editor"
value={textValue(node.config.expression)} value={textValue(node.config.expression)}
@@ -475,8 +477,8 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Expected type"> <NodeFormField label="Expected type">
<select <select
value={textValue(node.config.result_type) || "unknown"} value={textValue(node.config.result_type) || "unknown"}
onChange={(event) => updateConfig({ result_type: event.target.value })} onChange={(event) => updateConfig({ result_type: event.target.value })}
@@ -490,11 +492,11 @@ export default function NodeInspector({
<option value="date">Date</option> <option value="date">Date</option>
<option value="datetime">Date and time</option> <option value="datetime">Date and time</option>
</select> </select>
</FormField> </NodeFormField>
</> </>
) : null} ) : null}
{node.type === "calculate" ? ( {node.type === "calculate" ? (
<FormField label="Calculated columns"> <NodeFormField label="Calculated columns">
<textarea <textarea
className="dataflow-expression-editor" className="dataflow-expression-editor"
value={calculationText} value={calculationText}
@@ -503,25 +505,25 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
{node.type === "convert" ? ( {node.type === "convert" ? (
<> <>
<FormField label="Source column"> <NodeFormField label="Source column">
<input <input
value={textValue(node.config.source_column)} value={textValue(node.config.source_column)}
onChange={(event) => updateConfig({ source_column: event.target.value })} onChange={(event) => updateConfig({ source_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Output column"> <NodeFormField label="Output column">
<input <input
value={textValue(node.config.target_column)} value={textValue(node.config.target_column)}
onChange={(event) => updateConfig({ target_column: event.target.value })} onChange={(event) => updateConfig({ target_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Data type"> <NodeFormField label="Data type">
<select <select
value={textValue(node.config.target_type) || "string"} value={textValue(node.config.target_type) || "string"}
onChange={(event) => updateConfig({ target_type: event.target.value })} onChange={(event) => updateConfig({ target_type: event.target.value })}
@@ -534,8 +536,8 @@ export default function NodeInspector({
<option value="date">Date</option> <option value="date">Date</option>
<option value="datetime">Date and time</option> <option value="datetime">Date and time</option>
</select> </select>
</FormField> </NodeFormField>
<FormField label="Conversion error"> <NodeFormField label="Conversion error">
<select <select
value={textValue(node.config.on_error) || "fail"} value={textValue(node.config.on_error) || "fail"}
onChange={(event) => updateConfig({ on_error: event.target.value })} onChange={(event) => updateConfig({ on_error: event.target.value })}
@@ -545,26 +547,26 @@ export default function NodeInspector({
<option value="null">Use null</option> <option value="null">Use null</option>
<option value="keep">Keep original</option> <option value="keep">Keep original</option>
</select> </select>
</FormField> </NodeFormField>
</> </>
) : null} ) : null}
{node.type === "replace" ? ( {node.type === "replace" ? (
<> <>
<FormField label="Source column"> <NodeFormField label="Source column">
<input <input
value={textValue(node.config.source_column)} value={textValue(node.config.source_column)}
onChange={(event) => updateConfig({ source_column: event.target.value })} onChange={(event) => updateConfig({ source_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Output column"> <NodeFormField label="Output column">
<input <input
value={textValue(node.config.target_column)} value={textValue(node.config.target_column)}
onChange={(event) => updateConfig({ target_column: event.target.value })} onChange={(event) => updateConfig({ target_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Mode"> <NodeFormField label="Mode">
<select <select
value={textValue(node.config.mode) || "exact"} value={textValue(node.config.mode) || "exact"}
onChange={(event) => updateConfig({ mode: event.target.value })} onChange={(event) => updateConfig({ mode: event.target.value })}
@@ -573,25 +575,25 @@ export default function NodeInspector({
<option value="exact">Exact value</option> <option value="exact">Exact value</option>
<option value="text">Text fragment</option> <option value="text">Text fragment</option>
</select> </select>
</FormField> </NodeFormField>
<FormField label="Find"> <NodeFormField label="Find">
<input <input
value={displayScalar(node.config.find)} value={displayScalar(node.config.find)}
onChange={(event) => updateConfig({ find: parseScalar(event.target.value) })} onChange={(event) => updateConfig({ find: parseScalar(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Replacement"> <NodeFormField label="Replacement">
<input <input
value={displayScalar(node.config.replacement)} value={displayScalar(node.config.replacement)}
onChange={(event) => updateConfig({ replacement: parseScalar(event.target.value) })} onChange={(event) => updateConfig({ replacement: parseScalar(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
</> </>
) : null} ) : null}
{node.type === "sort" ? ( {node.type === "sort" ? (
<FormField label="Sort fields"> <NodeFormField label="Sort fields">
<textarea <textarea
className="dataflow-expression-editor" className="dataflow-expression-editor"
value={sortText} value={sortText}
@@ -600,11 +602,11 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
{node.type === "window.rank" ? ( {node.type === "window.rank" ? (
<> <>
<FormField label="Method"> <NodeFormField label="Method">
<select <select
value={textValue(node.config.method) || "row_number"} value={textValue(node.config.method) || "row_number"}
onChange={(event) => updateConfig({ method: event.target.value })} onChange={(event) => updateConfig({ method: event.target.value })}
@@ -614,22 +616,22 @@ export default function NodeInspector({
<option value="rank">Rank with gaps</option> <option value="rank">Rank with gaps</option>
<option value="dense_rank">Dense rank</option> <option value="dense_rank">Dense rank</option>
</select> </select>
</FormField> </NodeFormField>
<FormField label="Output column"> <NodeFormField label="Output column">
<input <input
value={textValue(node.config.target_column)} value={textValue(node.config.target_column)}
onChange={(event) => updateConfig({ target_column: event.target.value })} onChange={(event) => updateConfig({ target_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Partition by"> <NodeFormField label="Partition by">
<input <input
value={stringList(node.config.partition_by).join(", ")} value={stringList(node.config.partition_by).join(", ")}
onChange={(event) => updateConfig({ partition_by: commaList(event.target.value) })} onChange={(event) => updateConfig({ partition_by: commaList(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Order by"> <NodeFormField label="Order by">
<textarea <textarea
className="dataflow-expression-editor" className="dataflow-expression-editor"
value={rankSortText} value={rankSortText}
@@ -638,11 +640,11 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
</> </>
) : null} ) : null}
{node.type === "limit" ? ( {node.type === "limit" ? (
<FormField label="Maximum rows"> <NodeFormField label="Maximum rows">
<input <input
type="number" type="number"
min={1} min={1}
@@ -651,11 +653,11 @@ export default function NodeInspector({
onChange={(event) => updateConfig({ count: Number(event.target.value) })} onChange={(event) => updateConfig({ count: Number(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
) : null} ) : null}
{node.type === "quality.rules" ? ( {node.type === "quality.rules" ? (
<> <>
<FormField label="Rules"> <NodeFormField label="Rules">
<textarea <textarea
className="dataflow-json-editor" className="dataflow-json-editor"
value={rulesText} value={rulesText}
@@ -664,8 +666,8 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Invalid rows"> <NodeFormField label="Invalid rows">
<select <select
value={textValue(node.config.action) || "annotate"} value={textValue(node.config.action) || "annotate"}
onChange={(event) => updateConfig({ action: event.target.value })} onChange={(event) => updateConfig({ action: event.target.value })}
@@ -675,26 +677,26 @@ export default function NodeInspector({
<option value="drop">Drop</option> <option value="drop">Drop</option>
<option value="fail">Stop</option> <option value="fail">Stop</option>
</select> </select>
</FormField> </NodeFormField>
</> </>
) : null} ) : null}
{node.type === "reconcile.compare" ? ( {node.type === "reconcile.compare" ? (
<> <>
<FormField label="Expected keys"> <NodeFormField label="Expected keys">
<input <input
value={stringList(node.config.left_keys).join(", ")} value={stringList(node.config.left_keys).join(", ")}
onChange={(event) => updateConfig({ left_keys: commaList(event.target.value) })} onChange={(event) => updateConfig({ left_keys: commaList(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Observed keys"> <NodeFormField label="Observed keys">
<input <input
value={stringList(node.config.right_keys).join(", ")} value={stringList(node.config.right_keys).join(", ")}
onChange={(event) => updateConfig({ right_keys: commaList(event.target.value) })} onChange={(event) => updateConfig({ right_keys: commaList(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Compared columns"> <NodeFormField label="Compared columns">
<input <input
value={comparisonFieldsToText(node.config.compare_columns)} value={comparisonFieldsToText(node.config.compare_columns)}
onChange={(event) => updateConfig({ onChange={(event) => updateConfig({
@@ -702,100 +704,100 @@ export default function NodeInspector({
})} })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Observed prefix"> <NodeFormField label="Observed prefix">
<input <input
value={textValue(node.config.right_prefix)} value={textValue(node.config.right_prefix)}
onChange={(event) => updateConfig({ right_prefix: event.target.value })} onChange={(event) => updateConfig({ right_prefix: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
</> </>
) : null} ) : null}
{node.type === "reconcile.decisions" ? ( {node.type === "reconcile.decisions" ? (
<> <>
<FormField label="Decision key hash column"> <NodeFormField label="Decision key hash column">
<input <input
value={textValue(node.config.decision_key_column)} value={textValue(node.config.decision_key_column)}
onChange={(event) => updateConfig({ decision_key_column: event.target.value })} onChange={(event) => updateConfig({ decision_key_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Decision input hash column"> <NodeFormField label="Decision input hash column">
<input <input
value={textValue(node.config.decision_input_column)} value={textValue(node.config.decision_input_column)}
onChange={(event) => updateConfig({ decision_input_column: event.target.value })} onChange={(event) => updateConfig({ decision_input_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Decision reference column"> <NodeFormField label="Decision reference column">
<input <input
value={textValue(node.config.decision_ref_column)} value={textValue(node.config.decision_ref_column)}
onChange={(event) => updateConfig({ decision_ref_column: event.target.value })} onChange={(event) => updateConfig({ decision_ref_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Action column"> <NodeFormField label="Action column">
<input <input
value={textValue(node.config.action_column)} value={textValue(node.config.action_column)}
onChange={(event) => updateConfig({ action_column: event.target.value })} onChange={(event) => updateConfig({ action_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Actor reference column"> <NodeFormField label="Actor reference column">
<input <input
value={textValue(node.config.actor_column)} value={textValue(node.config.actor_column)}
onChange={(event) => updateConfig({ actor_column: event.target.value })} onChange={(event) => updateConfig({ actor_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Decision time column"> <NodeFormField label="Decision time column">
<input <input
value={textValue(node.config.decided_at_column)} value={textValue(node.config.decided_at_column)}
onChange={(event) => updateConfig({ decided_at_column: event.target.value })} onChange={(event) => updateConfig({ decided_at_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Reason column"> <NodeFormField label="Reason column">
<input <input
value={textValue(node.config.reason_column)} value={textValue(node.config.reason_column)}
onChange={(event) => updateConfig({ reason_column: event.target.value })} onChange={(event) => updateConfig({ reason_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Correction column"> <NodeFormField label="Correction column">
<input <input
value={textValue(node.config.correction_column)} value={textValue(node.config.correction_column)}
onChange={(event) => updateConfig({ correction_column: event.target.value })} onChange={(event) => updateConfig({ correction_column: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Allowed actions"> <NodeFormField label="Allowed actions">
<input <input
value={stringList(node.config.allowed_actions).join(", ")} value={stringList(node.config.allowed_actions).join(", ")}
onChange={(event) => updateConfig({ allowed_actions: commaList(event.target.value) })} onChange={(event) => updateConfig({ allowed_actions: commaList(event.target.value) })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
</> </>
) : null} ) : null}
{node.type === "subflow" ? ( {node.type === "subflow" ? (
<> <>
<FormField label="Template reference"> <NodeFormField label="Template reference">
<input <input
value={textValue(node.config.template_ref)} value={textValue(node.config.template_ref)}
onChange={(event) => updateConfig({ template_ref: event.target.value })} onChange={(event) => updateConfig({ template_ref: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Template version"> <NodeFormField label="Template version">
<input <input
value={textValue(node.config.template_version)} value={textValue(node.config.template_version)}
onChange={(event) => updateConfig({ template_version: event.target.value })} onChange={(event) => updateConfig({ template_version: event.target.value })}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Parameters"> <NodeFormField label="Parameters">
<textarea <textarea
className="dataflow-json-editor" className="dataflow-json-editor"
value={parametersText} value={parametersText}
@@ -804,8 +806,8 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
<FormField label="Pinned graph"> <NodeFormField label="Pinned graph">
<textarea <textarea
className="dataflow-json-editor" className="dataflow-json-editor"
value={subflowGraphText} value={subflowGraphText}
@@ -814,7 +816,7 @@ export default function NodeInspector({
spellCheck={false} spellCheck={false}
disabled={readOnly} disabled={readOnly}
/> />
</FormField> </NodeFormField>
</> </>
) : null} ) : null}
</div> </div>
+3 -429
View File
@@ -1,69 +1,10 @@
.dataflow-page {
position: relative;
height: calc(100vh - 115px);
min-width: 0;
min-height: 0;
padding: 0;
overflow: hidden;
color: var(--text);
background: var(--bg);
}
.dataflow-page *,
.dataflow-page *::before,
.dataflow-page *::after {
box-sizing: border-box;
}
.dataflow-shell {
display: grid;
grid-template-columns: minmax(250px, 300px) minmax(0, 1fr);
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
border: var(--border-line);
background: var(--panel);
}
.dataflow-pipeline-panel,
.dataflow-workspace,
.dataflow-editor, .dataflow-editor,
.dataflow-editor-surface,
.dataflow-canvas, .dataflow-canvas,
.dataflow-pipeline-list-frame { .dataflow-pipeline-list-frame {
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
} }
.dataflow-pipeline-panel {
display: flex;
flex-direction: column;
overflow: hidden;
border-right: var(--border-line);
background: var(--panel-soft);
}
.dataflow-panel-toolbar,
.dataflow-workspace-toolbar,
.dataflow-results-toolbar,
.dataflow-sql-toolbar,
.dataflow-panel-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex: 0 0 auto;
border-bottom: var(--border-line);
background: var(--panel-header);
}
.dataflow-panel-toolbar {
min-height: 52px;
padding: 8px 10px 8px 14px;
}
.dataflow-toolbar-actions, .dataflow-toolbar-actions,
.dataflow-command-bar, .dataflow-command-bar,
.dataflow-identity-fields { .dataflow-identity-fields {
@@ -72,66 +13,11 @@
gap: 7px; gap: 7px;
} }
.dataflow-pipeline-search {
padding: 10px;
border-bottom: var(--border-line);
}
.dataflow-pipeline-search input {
min-height: 34px;
padding: 7px 9px;
}
.dataflow-pipeline-list-frame { .dataflow-pipeline-list-frame {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
} }
.dataflow-pipeline-list {
height: 100%;
overflow: auto;
padding: 6px;
}
.dataflow-pipeline-list > button {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
width: 100%;
min-height: 56px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
cursor: pointer;
padding: 8px 9px;
text-align: left;
}
.dataflow-pipeline-list > button:hover,
.dataflow-pipeline-list > button:focus-visible {
background: var(--primary-soft);
outline: none;
}
.dataflow-pipeline-list > button.is-selected {
background: var(--primary-soft-strong);
box-shadow: inset 3px 0 0 var(--accent);
}
.dataflow-pipeline-list > button > span:first-child {
min-width: 0;
}
.dataflow-pipeline-list strong,
.dataflow-pipeline-list small {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dataflow-definition-dialog { .dataflow-definition-dialog {
width: min(620px, calc(100vw - 32px)); width: min(620px, calc(100vw - 32px));
} }
@@ -150,13 +36,6 @@
width: 100%; width: 100%;
} }
.dataflow-definition-toggles {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 18px;
}
.dataflow-provenance,
.dataflow-trigger-security { .dataflow-trigger-security {
display: grid; display: grid;
gap: 6px; gap: 6px;
@@ -241,8 +120,7 @@
} }
@media (max-width: 760px) { @media (max-width: 760px) {
.dataflow-triggers-layout, .dataflow-triggers-layout {
.dataflow-definition-toggles {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -254,49 +132,12 @@
} }
} }
.dataflow-pipeline-list strong {
color: var(--text-strong);
font-size: 13px;
}
.dataflow-pipeline-list small {
margin-top: 4px;
color: var(--muted);
font-size: 11px;
}
.dataflow-pipeline-list .status-badge {
max-width: 74px;
overflow: hidden;
text-overflow: ellipsis;
}
.dataflow-pipeline-empty,
.dataflow-inspector-empty,
.dataflow-results-empty {
display: grid;
place-items: center;
min-height: 100px;
color: var(--muted);
font-size: 13px;
text-align: center;
}
.dataflow-workspace {
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
}
.dataflow-action-guidance { .dataflow-action-guidance {
padding: 0 10px 8px; padding: 0 10px 8px;
} }
.dataflow-workspace-toolbar { .dataflow-workspace-toolbar {
min-height: 58px; min-width: 0;
padding: 8px 10px;
} }
.dataflow-identity-fields { .dataflow-identity-fields {
@@ -368,7 +209,6 @@
outline-offset: -2px; outline-offset: -2px;
} }
.dataflow-palette,
.dataflow-inspector { .dataflow-inspector {
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
@@ -376,19 +216,10 @@
background: var(--panel-soft); background: var(--panel-soft);
} }
.dataflow-palette {
border-right: var(--border-line);
}
.dataflow-inspector { .dataflow-inspector {
border-left: var(--border-line); border-left: var(--border-line);
} }
.dataflow-panel-heading {
min-height: 44px;
padding: 8px 10px 8px 12px;
}
.dataflow-panel-heading > span { .dataflow-panel-heading > span {
min-width: 0; min-width: 0;
} }
@@ -413,127 +244,6 @@
white-space: nowrap; white-space: nowrap;
} }
.dataflow-palette-items {
display: grid;
grid-auto-rows: max-content;
align-content: start;
gap: 4px;
height: calc(100% - 44px);
padding: 8px;
overflow-x: hidden;
overflow-y: auto;
scrollbar-gutter: stable;
}
.dataflow-palette-group {
display: grid;
grid-auto-rows: max-content;
align-content: start;
gap: 2px;
}
.dataflow-palette-group + .dataflow-palette-group {
margin-top: 6px;
padding-top: 8px;
border-top: var(--border-line);
}
.dataflow-palette-group h3 {
margin: 0;
color: var(--muted);
font-size: 10px;
font-weight: 700;
letter-spacing: 0;
padding: 3px 8px;
text-transform: uppercase;
}
.dataflow-palette-items button {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) 14px;
align-items: center;
gap: 7px;
min-height: 38px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
cursor: grab;
font: inherit;
font-size: 12px;
padding: 7px 8px;
text-align: left;
}
.dataflow-palette-items button:hover:not(:disabled),
.dataflow-palette-items button:focus-visible:not(:disabled) {
background: var(--primary-soft);
color: var(--text-strong);
outline: none;
}
.dataflow-palette-items button:active:not(:disabled) {
cursor: grabbing;
}
.dataflow-palette-items button:disabled {
cursor: default;
opacity: .42;
}
.dataflow-palette-add {
color: var(--accent);
}
.dataflow-editor-surface {
position: relative;
overflow: hidden;
background: var(--bg);
}
.dataflow-canvas {
position: absolute;
inset: 0;
}
.dataflow-canvas .react-flow {
background: var(--bg);
}
.dataflow-canvas .react-flow__background {
color: var(--line-dark);
}
.dataflow-canvas .react-flow__controls,
.dataflow-canvas .react-flow__minimap {
overflow: hidden;
border: var(--border-line);
border-radius: var(--radius-sm);
background: var(--panel);
box-shadow: var(--shadow-xs);
}
.dataflow-canvas .react-flow__controls-button {
border-bottom: var(--border-line);
background: var(--panel);
color: var(--text);
}
.dataflow-canvas .react-flow__minimap-mask {
fill: color-mix(in srgb, var(--bg) 76%, transparent);
}
.dataflow-canvas .react-flow__edge-path {
stroke: var(--line-dark);
stroke-width: 2;
}
.dataflow-canvas .react-flow__edge.selected .react-flow__edge-path,
.dataflow-canvas .react-flow__edge:hover .react-flow__edge-path {
stroke: var(--accent);
stroke-width: 3;
}
.dataflow-canvas .react-flow__edge.dataflow-edge-proximity .react-flow__edge-path { .dataflow-canvas .react-flow__edge.dataflow-edge-proximity .react-flow__edge-path {
stroke: var(--accent); stroke: var(--accent);
stroke-width: 3; stroke-width: 3;
@@ -545,15 +255,6 @@
stroke-width: 3; stroke-width: 3;
} }
.dataflow-canvas-empty {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: var(--muted);
pointer-events: none;
}
.dataflow-node { .dataflow-node {
position: relative; position: relative;
display: flex; display: flex;
@@ -610,17 +311,6 @@
border-color: var(--danger-text); border-color: var(--danger-text);
} }
.dataflow-node-icon {
display: grid;
width: 28px;
height: 28px;
flex: 0 0 28px;
place-items: center;
border-radius: 5px;
background: var(--panel-soft);
color: var(--text-strong);
}
.dataflow-node-copy { .dataflow-node-copy {
min-width: 0; min-width: 0;
flex: 1 1 auto; flex: 1 1 auto;
@@ -645,32 +335,6 @@
font-size: 10px; font-size: 10px;
} }
.dataflow-node-count {
min-width: 22px;
border-radius: 999px;
background: var(--primary-soft);
color: var(--text-strong);
font-size: 10px;
font-variant-numeric: tabular-nums;
padding: 3px 5px;
text-align: center;
}
.dataflow-node-handle {
width: 13px;
height: 13px;
border: 3px solid var(--panel);
background: var(--line-dark);
}
.dataflow-node-handle:hover,
.dataflow-node-handle.connectingto,
.dataflow-node-handle.valid {
width: 17px;
height: 17px;
background: var(--accent);
}
.dataflow-node-port-label { .dataflow-node-port-label {
position: absolute; position: absolute;
left: 8px; left: 8px;
@@ -685,7 +349,7 @@
white-space: nowrap; white-space: nowrap;
} }
.dataflow-node-combine-join .dataflow-node-icon { .dataflow-node-combine-join .definition-node-icon {
margin-left: 31px; margin-left: 31px;
} }
@@ -1077,8 +741,6 @@
} }
.dataflow-sql-toolbar { .dataflow-sql-toolbar {
min-height: 44px;
padding: 7px 10px 7px 14px;
color: var(--text-strong); color: var(--text-strong);
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
@@ -1115,8 +777,6 @@
} }
.dataflow-results-toolbar { .dataflow-results-toolbar {
min-height: 42px;
padding: 5px 9px;
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -1255,44 +915,7 @@
white-space: nowrap; white-space: nowrap;
} }
.dataflow-workspace-empty {
display: grid;
place-content: center;
justify-items: center;
gap: 12px;
height: 100%;
color: var(--muted);
}
.dataflow-workspace-empty strong {
color: var(--text-strong);
}
.dataflow-workspace-empty .btn {
display: inline-flex;
align-items: center;
gap: 6px;
}
.dataflow-working-indicator {
position: absolute;
z-index: 20;
right: 12px;
bottom: 10px;
border: var(--border-line-dark);
border-radius: var(--radius-sm);
background: var(--panel);
box-shadow: var(--shadow-popover);
color: var(--text-strong);
font-size: 12px;
padding: 7px 10px;
}
@media (max-width: 1180px) { @media (max-width: 1180px) {
.dataflow-shell {
grid-template-columns: 240px minmax(0, 1fr);
}
.dataflow-workspace-toolbar { .dataflow-workspace-toolbar {
align-items: stretch; align-items: stretch;
flex-direction: column; flex-direction: column;
@@ -1312,10 +935,6 @@
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.dataflow-shell {
grid-template-columns: 210px minmax(0, 1fr);
}
.dataflow-editor { .dataflow-editor {
grid-template-columns: 135px minmax(0, 1fr); grid-template-columns: 135px minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) minmax(170px, 32%); grid-template-rows: minmax(0, 1fr) minmax(170px, 32%);
@@ -1348,16 +967,6 @@
height: calc(100dvh - 115px); height: calc(100dvh - 115px);
} }
.dataflow-shell {
grid-template-columns: 1fr;
grid-template-rows: minmax(150px, 24%) minmax(0, 1fr);
}
.dataflow-pipeline-panel {
border-right: 0;
border-bottom: var(--border-line);
}
.dataflow-workspace-toolbar { .dataflow-workspace-toolbar {
overflow-x: auto; overflow-x: auto;
align-items: flex-start; align-items: flex-start;
@@ -1383,41 +992,6 @@
grid-template-rows: auto minmax(0, 1fr) minmax(160px, 30%); grid-template-rows: auto minmax(0, 1fr) minmax(160px, 30%);
} }
.dataflow-palette {
border-right: 0;
border-bottom: var(--border-line);
}
.dataflow-palette .dataflow-panel-heading {
display: none;
}
.dataflow-palette-items {
display: flex;
overflow-x: auto;
}
.dataflow-palette-group {
display: flex;
flex: 0 0 auto;
}
.dataflow-palette-group + .dataflow-palette-group {
margin-top: 0;
padding-top: 0;
padding-left: 6px;
border-top: 0;
border-left: var(--border-line);
}
.dataflow-palette-group h3 {
display: none;
}
.dataflow-palette-items button {
min-width: 128px;
}
.dataflow-inspector-fields { .dataflow-inspector-fields {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }