ConnectConfigSection.tsx269 lines · main
| 1 | import { Box, Cable, Database, Sparkles } from 'lucide-react' |
| 2 | import { |
| 3 | cn, |
| 4 | RadioGroupStacked, |
| 5 | RadioGroupStackedItem, |
| 6 | Select, |
| 7 | SelectContent, |
| 8 | SelectItem, |
| 9 | SelectTrigger, |
| 10 | SelectValue, |
| 11 | Switch, |
| 12 | } from 'ui' |
| 13 | import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' |
| 14 | import { |
| 15 | MultiSelector, |
| 16 | MultiSelectorContent, |
| 17 | MultiSelectorItem, |
| 18 | MultiSelectorList, |
| 19 | MultiSelectorTrigger, |
| 20 | } from 'ui-patterns/multi-select' |
| 21 | |
| 22 | import type { ConnectMode, FieldOption, ResolvedField } from './Connect.types' |
| 23 | import { ConnectionIcon } from './ConnectionIcon' |
| 24 | |
| 25 | const 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 | |
| 32 | interface 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 | |
| 39 | export 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 | |
| 239 | interface ModeSelectorProps { |
| 240 | modes: Array<{ id: ConnectMode; label: string; description: string }> |
| 241 | selected: ConnectMode |
| 242 | onChange: (mode: ConnectMode) => void |
| 243 | } |
| 244 | |
| 245 | export 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 | } |