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