ConnectConfigSection.tsx 9.5 KB

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