11"use client" ;
22
3- import { useCallback } from "react" ;
4- import { Copy , Trash2 } from "lucide-react" ;
3+ import { useCallback , useState , useEffect } from "react" ;
4+ import { Copy , Trash2 , ChevronRight } from "lucide-react" ;
55import { useFlowStore } from "@/stores/flow-store" ;
66import { SchemaForm } from "@/components/config-forms/schema-form" ;
77import { VrlEditor } from "@/components/vrl-editor/vrl-editor" ;
@@ -12,6 +12,18 @@ import { Button } from "@/components/ui/button";
1212import { Separator } from "@/components/ui/separator" ;
1313import { Switch } from "@/components/ui/switch" ;
1414import { Badge } from "@/components/ui/badge" ;
15+ import {
16+ Select ,
17+ SelectContent ,
18+ SelectItem ,
19+ SelectTrigger ,
20+ SelectValue ,
21+ } from "@/components/ui/select" ;
22+ import {
23+ Collapsible ,
24+ CollapsibleContent ,
25+ CollapsibleTrigger ,
26+ } from "@/components/ui/collapsible" ;
1527import type { VectorComponentDef } from "@/lib/vector/types" ;
1628
1729/* ------------------------------------------------------------------ */
@@ -59,6 +71,127 @@ function filterSchema(
5971 } ;
6072}
6173
74+ /* ------------------------------------------------------------------ */
75+ /* Pipeline Settings (shown when no node is selected) */
76+ /* ------------------------------------------------------------------ */
77+
78+ function PipelineSettings ( ) {
79+ const globalConfig = useFlowStore ( ( s ) => s . globalConfig ) ;
80+ const updateGlobalConfig = useFlowStore ( ( s ) => s . updateGlobalConfig ) ;
81+ const setGlobalConfig = useFlowStore ( ( s ) => s . setGlobalConfig ) ;
82+ const currentLogLevel = ( globalConfig ?. log_level as string ) || "info" ;
83+
84+ const [ jsonOpen , setJsonOpen ] = useState ( false ) ;
85+ const [ jsonText , setJsonText ] = useState ( "" ) ;
86+ const [ jsonError , setJsonError ] = useState < string | null > ( null ) ;
87+
88+ // Derive the config object minus log_level for the JSON editor
89+ useEffect ( ( ) => {
90+ const { log_level, ...rest } = globalConfig ?? { } ;
91+ setJsonText (
92+ Object . keys ( rest ) . length > 0 ? JSON . stringify ( rest , null , 2 ) : "" ,
93+ ) ;
94+ setJsonError ( null ) ;
95+ } , [ globalConfig ] ) ;
96+
97+ const handleApply = ( ) => {
98+ const trimmed = jsonText . trim ( ) ;
99+ if ( trimmed === "" ) {
100+ // Clear everything except log_level
101+ if ( currentLogLevel !== "info" ) {
102+ setGlobalConfig ( { log_level : currentLogLevel } ) ;
103+ } else {
104+ setGlobalConfig ( null ) ;
105+ }
106+ setJsonError ( null ) ;
107+ return ;
108+ }
109+ try {
110+ const parsed = JSON . parse ( trimmed ) ;
111+ if ( typeof parsed !== "object" || parsed === null || Array . isArray ( parsed ) ) {
112+ setJsonError ( "Must be a JSON object" ) ;
113+ return ;
114+ }
115+ // Merge back log_level if set
116+ const merged : Record < string , unknown > = { ...parsed } ;
117+ if ( currentLogLevel !== "info" ) {
118+ merged . log_level = currentLogLevel ;
119+ }
120+ setGlobalConfig ( merged ) ;
121+ setJsonError ( null ) ;
122+ } catch ( e ) {
123+ setJsonError ( e instanceof Error ? e . message : "Invalid JSON" ) ;
124+ }
125+ } ;
126+
127+ const hasJsonContent = jsonText . trim ( ) . length > 0 ;
128+
129+ return (
130+ < div className = "space-y-6 p-4" >
131+ < h3 className = "text-sm font-semibold" > Pipeline Settings</ h3 >
132+
133+ { /* Log Level */ }
134+ < div className = "space-y-2" >
135+ < Label htmlFor = "log-level" > Log Level</ Label >
136+ < Select
137+ value = { currentLogLevel }
138+ onValueChange = { ( value ) =>
139+ updateGlobalConfig ( "log_level" , value === "info" ? undefined : value )
140+ }
141+ >
142+ < SelectTrigger id = "log-level" className = "w-full" >
143+ < SelectValue />
144+ </ SelectTrigger >
145+ < SelectContent >
146+ { ( [ "trace" , "debug" , "info" , "warn" , "error" ] as const ) . map (
147+ ( level ) => (
148+ < SelectItem key = { level } value = { level } >
149+ { level }
150+ </ SelectItem >
151+ ) ,
152+ ) }
153+ </ SelectContent >
154+ </ Select >
155+ </ div >
156+
157+ < Separator />
158+
159+ { /* Global Configuration JSON */ }
160+ < Collapsible open = { jsonOpen } onOpenChange = { setJsonOpen } >
161+ < CollapsibleTrigger className = "flex w-full items-center gap-2 text-sm font-semibold" >
162+ < ChevronRight
163+ className = { `h-4 w-4 transition-transform ${ jsonOpen ? "rotate-90" : "" } ` }
164+ />
165+ Global Configuration (JSON)
166+ { hasJsonContent && (
167+ < Badge variant = "secondary" className = "ml-auto text-[10px]" >
168+ configured
169+ </ Badge >
170+ ) }
171+ </ CollapsibleTrigger >
172+ < CollapsibleContent className = "mt-3 space-y-3" >
173+ < textarea
174+ className = "min-h-[120px] w-full rounded-md border bg-muted/50 p-3 font-mono text-xs focus:outline-none focus:ring-2 focus:ring-ring"
175+ value = { jsonText }
176+ onChange = { ( e ) => {
177+ setJsonText ( e . target . value ) ;
178+ setJsonError ( null ) ;
179+ } }
180+ placeholder = '{ "enrichment_tables": { ... } }'
181+ spellCheck = { false }
182+ />
183+ { jsonError && (
184+ < p className = "text-xs text-destructive" > { jsonError } </ p >
185+ ) }
186+ < Button size = "sm" onClick = { handleApply } >
187+ Apply
188+ </ Button >
189+ </ CollapsibleContent >
190+ </ Collapsible >
191+ </ div >
192+ ) ;
193+ }
194+
62195/* ------------------------------------------------------------------ */
63196/* Component */
64197/* ------------------------------------------------------------------ */
@@ -101,14 +234,12 @@ export function DetailPanel() {
101234 }
102235 } , [ selectedNodeId , removeNode ] ) ;
103236
104- // ---- Empty state ----
237+ // ---- Empty state → Pipeline Settings ----
105238 if ( ! selectedNode ) {
106239 return (
107240 < div className = "flex h-full w-80 shrink-0 flex-col border-l bg-muted/30" >
108- < div className = "flex flex-1 items-center justify-center p-6" >
109- < p className = "text-sm text-muted-foreground" >
110- Select a node to edit its configuration
111- </ p >
241+ < div className = "min-h-0 flex-1 overflow-y-auto" >
242+ < PipelineSettings />
112243 </ div >
113244 </ div >
114245 ) ;
0 commit comments