From 0b38a70311940a36c05f58188cae7fc8665e0be5 Mon Sep 17 00:00:00 2001 From: shankar-gpio Date: Fri, 29 May 2026 11:17:51 +0530 Subject: [PATCH] Phase 3: structured config editors, guided import, rollout & version compare MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make authoring & governance first-class (proposal.md §5.3/5.4/5.5): - Shared primitives: accessible Dialog (focus-trap, Esc, scroll-lock), Slider, Switch, and config-field form helpers. - Channel config editor: structured sections for the known ChannelConfig shape (rate limit, backpressure, timeout, CORS, cache, dedup, tracing) with a read-only DataLogicEditor for validation_logic and a synced Advanced(JSON) fallback; replaces the raw textarea in channel-form. - Connector config editor: schema-agnostic field editor switched by type with "leave unchanged" masked-secret handling so a save never clobbers a stored secret; replaces the raw textarea + checkbox in connector-form. - Rollout control: 0-100% slider on active workflows wired to the existing useSetWorkflowRollout, plus one-click rollback. - Version compare: line-based diff of two workflow versions in the Versions tab. - Guided workflow import wizard: paste -> validate -> import-as-draft -> dry-run -> activate w/ rollout, hosted in the new Dialog; adds the missing Import button and useImportWorkflows/useValidateWorkflow hooks. - Refactor ImportDialog onto the accessible Dialog primitive. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../shared/channel-config-editor.tsx | 286 ++++++++++++ src/components/shared/config-field.tsx | 192 ++++++++ .../shared/connector-config-editor.tsx | 310 +++++++++++++ src/components/shared/import-dialog.tsx | 40 +- src/components/shared/rollout-control.tsx | 72 +++ src/components/shared/version-compare.tsx | 137 ++++++ .../shared/workflow-import-wizard.tsx | 429 ++++++++++++++++++ src/components/ui/dialog.tsx | 114 +++++ src/components/ui/slider.tsx | 50 ++ src/components/ui/switch.tsx | 49 ++ src/hooks/use-workflows.ts | 20 + src/pages/channel-form.tsx | 27 +- src/pages/connector-form.tsx | 54 +-- src/pages/workflow-detail.tsx | 26 +- src/pages/workflows.tsx | 13 +- 15 files changed, 1727 insertions(+), 92 deletions(-) create mode 100644 src/components/shared/channel-config-editor.tsx create mode 100644 src/components/shared/config-field.tsx create mode 100644 src/components/shared/connector-config-editor.tsx create mode 100644 src/components/shared/rollout-control.tsx create mode 100644 src/components/shared/version-compare.tsx create mode 100644 src/components/shared/workflow-import-wizard.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/slider.tsx create mode 100644 src/components/ui/switch.tsx diff --git a/src/components/shared/channel-config-editor.tsx b/src/components/shared/channel-config-editor.tsx new file mode 100644 index 0000000..2afe11b --- /dev/null +++ b/src/components/shared/channel-config-editor.tsx @@ -0,0 +1,286 @@ +import { useState } from "react" +import { DataLogicEditor } from "@goplasmatic/datalogic-ui" +import type { ChannelConfig } from "@/api/types" +import { useTheme } from "@/lib/use-theme" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { + ConfigSection, + NumberField, + TextField, + SelectField, + ToggleField, + StringListField, +} from "@/components/shared/config-field" +import { Braces, SlidersHorizontal } from "lucide-react" + +interface ChannelConfigEditorProps { + value: ChannelConfig + onChange: (next: ChannelConfig) => void +} + +const TRACING_MODES = [ + { value: "sync", label: "Sync" }, + { value: "async", label: "Async" }, + { value: "batch", label: "Batch" }, + { value: "off", label: "Off" }, +] + +/** + * Structured editor for the well-known ChannelConfig shape, with an "Advanced + * (JSON)" escape hatch. The ChannelConfig object held by the parent form is the + * single source of truth; the JSON view edits the same object and syncs back on + * every valid parse. Empty sub-objects are pruned so unset config keys stay unset. + */ +export function ChannelConfigEditor({ value, onChange }: ChannelConfigEditorProps) { + const { resolvedTheme } = useTheme() + const [mode, setMode] = useState<"form" | "json">("form") + const [jsonText, setJsonText] = useState("") + const [jsonError, setJsonError] = useState(null) + + const setTop = (key: K, val: ChannelConfig[K] | undefined) => { + const next = { ...value } + if (val === undefined) delete next[key] + else next[key] = val + onChange(next) + } + + const setSub = (key: K, field: string, val: unknown) => { + const current = (value[key] ?? {}) as Record + const sub: Record = { ...current } + if (val === undefined) delete sub[field] + else sub[field] = val + const next = { ...value } + if (Object.keys(sub).length === 0) delete next[key] + else next[key] = sub as ChannelConfig[K] + onChange(next) + } + + const openJson = () => { + setJsonText(JSON.stringify(value, null, 2)) + setJsonError(null) + setMode("json") + } + + const onJsonChange = (text: string) => { + setJsonText(text) + try { + const parsed = JSON.parse(text) + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + setJsonError("Config must be a JSON object") + return + } + setJsonError(null) + onChange(parsed as ChannelConfig) + } catch { + setJsonError("Invalid JSON") + } + } + + const rateLimit = value.rate_limit ?? {} + const backpressure = value.backpressure ?? {} + const cors = value.cors ?? {} + const cache = value.cache ?? {} + const dedup = value.deduplication ?? {} + const tracing = value.tracing ?? {} + + return ( +
+
+ +
+ + +
+
+ + {mode === "json" ? ( +
+