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" ? ( +
+