ConnectConfigSection.tsx269 lines · main
1import { Box, Cable, Database, Sparkles } from 'lucide-react'
2import {
3 cn,
4 RadioGroupStacked,
5 RadioGroupStackedItem,
6 Select,
7 SelectContent,
8 SelectItem,
9 SelectTrigger,
10 SelectValue,
11 Switch,
12} from 'ui'
13import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
14import {
15 MultiSelector,
16 MultiSelectorContent,
17 MultiSelectorItem,
18 MultiSelectorList,
19 MultiSelectorTrigger,
20} from 'ui-patterns/multi-select'
21
22import type { ConnectMode, FieldOption, ResolvedField } from './Connect.types'
23import { ConnectionIcon } from './ConnectionIcon'
24
25const MODE_ICONS: Record<string, React.ReactNode> = {
26 framework: <Box size={16} strokeWidth={1.5} />,
27 direct: <Database size={16} strokeWidth={1.5} />,
28 orm: <Cable size={16} strokeWidth={1.5} />,
29 mcp: <Sparkles size={16} strokeWidth={1.5} />,
30}
31
32interface ConnectConfigSectionProps {
33 activeFields: ResolvedField[]
34 state: Record<string, string | boolean | string[]>
35 onFieldChange: (fieldId: string, value: string | boolean | string[]) => void
36 getFieldOptions: (fieldId: string) => FieldOption[]
37}
38
39export function ConnectConfigSection({
40 activeFields,
41 state,
42 onFieldChange,
43 getFieldOptions,
44}: ConnectConfigSectionProps) {
45 if (activeFields.length === 0) return null
46
47 return (
48 <div className="flex flex-col gap-y-4">
49 {activeFields.map((field) => {
50 const options = getFieldOptions(field.id)
51 const value = state[field.id]
52
53 // Skip fields with no options (or single option that's auto-selected)
54 // Exception: switch and multi-select fields don't require options
55 if (field.type !== 'switch' && field.type !== 'multi-select') {
56 if (options.length === 0) return null
57 if (options.length === 1) return null
58 }
59
60 switch (field.type) {
61 case 'radio-grid':
62 return (
63 <FormItemLayout
64 key={field.id}
65 isReactForm={false}
66 layout="horizontal"
67 label={field.label}
68 >
69 <RadioGroupStacked
70 value={String(value ?? '')}
71 onValueChange={(v) => onFieldChange(field.id, v)}
72 className="flex-row gap-3 space-y-0"
73 >
74 {options.map((option) => (
75 <RadioGroupStackedItem
76 key={option.value}
77 id={`connect-${field.id}-${option.value}`}
78 value={option.value}
79 label=""
80 className="flex-1 rounded-lg text-left"
81 >
82 <div className="flex items-center gap-2">
83 {option.icon && <ConnectionIcon supportsDarkMode icon={option.icon} />}
84 <span className="text-sm">{option.label}</span>
85 </div>
86 </RadioGroupStackedItem>
87 ))}
88 </RadioGroupStacked>
89 </FormItemLayout>
90 )
91
92 case 'radio-list':
93 return (
94 <FormItemLayout
95 key={field.id}
96 isReactForm={false}
97 layout="horizontal"
98 label={field.label}
99 >
100 <RadioGroupStacked
101 value={String(value ?? '')}
102 onValueChange={(v) => onFieldChange(field.id, v)}
103 >
104 {options.map((option) => (
105 <RadioGroupStackedItem
106 key={option.value}
107 id={`connect-${field.id}-${option.value}`}
108 value={option.value}
109 label=""
110 className="w-full text-left"
111 >
112 <div className="flex flex-col gap-0.5">
113 <div className="flex items-center gap-2">
114 {option.icon && <ConnectionIcon icon={option.icon} />}
115 <span className="text-sm">{option.label}</span>
116 </div>
117 {option.description && (
118 <span className="text-sm text-foreground-lighter">
119 {option.description}
120 </span>
121 )}
122 </div>
123 </RadioGroupStackedItem>
124 ))}
125 </RadioGroupStacked>
126 </FormItemLayout>
127 )
128
129 case 'select':
130 return (
131 <FormItemLayout
132 key={field.id}
133 isReactForm={false}
134 layout="horizontal"
135 label={field.label}
136 description={field.description}
137 >
138 <Select
139 value={String(value ?? '')}
140 onValueChange={(v) => onFieldChange(field.id, v)}
141 >
142 <SelectTrigger
143 size="small"
144 className="[&>span:first-child]:flex [&>span:first-child]:items-center [&>span:first-child]:gap-x-2"
145 >
146 <SelectValue />
147 </SelectTrigger>
148 <SelectContent>
149 {options.map((option) => (
150 <SelectItem
151 key={option.value}
152 value={option.value}
153 className="[&>span:last-child]:flex [&>span:last-child]:items-center [&>span:last-child]:gap-x-2"
154 >
155 {/*
156 [Joshen] Omitting MCP icons for now as the images are not optimized (large)
157 and is causing noticeably latency issues on the browser (even with the existing Connect UI)
158 */}
159 {field.id === 'framework' && option.icon && (
160 <ConnectionIcon icon={option.icon} />
161 )}
162 {option.label}
163 </SelectItem>
164 ))}
165 </SelectContent>
166 </Select>
167 </FormItemLayout>
168 )
169
170 case 'switch':
171 return (
172 <FormItemLayout
173 key={field.id}
174 isReactForm={false}
175 layout="horizontal"
176 label={field.label}
177 description={field.description}
178 className="[&>div>label>span]:break-keep! [&>div>label>span]:text-balance"
179 >
180 <Switch
181 id={field.id}
182 checked={Boolean(value)}
183 onCheckedChange={(v) => onFieldChange(field.id, v)}
184 />
185 </FormItemLayout>
186 )
187
188 case 'multi-select':
189 return (
190 <FormItemLayout
191 key={field.id}
192 isReactForm={false}
193 layout="horizontal"
194 label={field.label}
195 description={field.description}
196 >
197 <MultiSelector
198 values={Array.isArray(value) ? value : []}
199 onValuesChange={(v) => onFieldChange(field.id, v)}
200 >
201 <MultiSelectorTrigger
202 className="w-full"
203 label="All features except Storage enabled by default"
204 badgeLimit="wrap"
205 showIcon={true}
206 />
207 <MultiSelectorContent>
208 <MultiSelectorList>
209 {options.map((option) => (
210 <MultiSelectorItem
211 key={option.value}
212 value={option.value}
213 className="items-start"
214 >
215 <div className="flex flex-col ml-2 gap-y-0.5">
216 <span className="font-medium">{option.label}</span>
217 {option.description && (
218 <span className="text-xs text-foreground-light">
219 {option.description}
220 </span>
221 )}
222 </div>
223 </MultiSelectorItem>
224 ))}
225 </MultiSelectorList>
226 </MultiSelectorContent>
227 </MultiSelector>
228 </FormItemLayout>
229 )
230
231 default:
232 return null
233 }
234 })}
235 </div>
236 )
237}
238
239interface ModeSelectorProps {
240 modes: Array<{ id: ConnectMode; label: string; description: string }>
241 selected: ConnectMode
242 onChange: (mode: ConnectMode) => void
243}
244
245export function ModeSelector({ modes, selected, onChange }: ModeSelectorProps) {
246 return (
247 <div className="grid grid-cols-4 rounded-lg border overflow-hidden">
248 {modes.map((mode) => (
249 <button
250 key={mode.id}
251 type="button"
252 onClick={() => onChange(mode.id)}
253 className={cn(
254 'flex flex-col items-center gap-2 p-4 transition-colors border-r last:border-r-0',
255 selected === mode.id
256 ? 'bg-surface-200'
257 : 'border-default hover:border-strong hover:bg-surface-100 '
258 )}
259 >
260 <span className="text-foreground-light">{MODE_ICONS[mode.id]}</span>
261 <div>
262 <p className="heading-default text-center">{mode.label}</p>
263 <p className="text-sm text-foreground-lighter text-center">{mode.description}</p>
264 </div>
265 </button>
266 ))}
267 </div>
268 )
269}