ClientSelectDropdown.tsx130 lines · main
1'use client'
2
3import { Bot, Check, ChevronDown } from 'lucide-react'
4import { useState } from 'react'
5import {
6 Button,
7 cn,
8 Command,
9 CommandEmpty,
10 CommandGroup,
11 CommandInput,
12 CommandItem,
13 CommandList,
14 Popover,
15 PopoverContent,
16 PopoverTrigger,
17} from 'ui'
18
19import type { McpClient } from '../types'
20import { ConnectionIcon } from './ConnectionIcon'
21
22export interface ClientGroup {
23 heading: string
24 clients: McpClient[]
25}
26
27interface ClientSelectDropdownProps {
28 theme?: 'light' | 'dark'
29 label?: string
30 clients: McpClient[]
31 groups?: ClientGroup[]
32 selectedClient: McpClient
33 onClientChange: (clientKey: string) => void
34}
35
36export const ClientSelectDropdown = ({
37 theme = 'light',
38 label = 'Client',
39 clients,
40 groups,
41 selectedClient,
42 onClientChange,
43}: ClientSelectDropdownProps) => {
44 const [open, setOpen] = useState(false)
45
46 function onSelectClient(key: string) {
47 onClientChange(key)
48 setOpen(false)
49 }
50
51 function renderClient(client: McpClient) {
52 return (
53 <CommandItem
54 key={client.key}
55 value={client.key}
56 onSelect={() => onSelectClient(client.key)}
57 className="flex gap-2 items-center"
58 >
59 {client.icon ? (
60 <ConnectionIcon
61 connection={client.icon}
62 theme={theme}
63 hasDistinctDarkIcon={client.hasDistinctDarkIcon}
64 />
65 ) : (
66 <Bot size={12} aria-hidden={true} />
67 )}
68 {client.label}
69 <Check
70 aria-label={client.key === selectedClient.key ? 'selected' : undefined}
71 size={15}
72 className={cn('ml-auto', client.key === selectedClient.key ? 'opacity-100' : 'opacity-0')}
73 />
74 </CommandItem>
75 )
76 }
77
78 return (
79 <Popover open={open} onOpenChange={setOpen} modal={false}>
80 <div className="flex">
81 <span className="flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
82 {label}
83 </span>
84 <PopoverTrigger asChild>
85 <Button
86 size="small"
87 type="default"
88 className="gap-0 rounded-l-none"
89 iconRight={
90 <ChevronDown
91 strokeWidth={1.5}
92 className={cn('transition-transform duration-200', open && 'rotate-180')}
93 />
94 }
95 >
96 <div className="flex items-center gap-2">
97 {selectedClient?.icon ? (
98 <ConnectionIcon
99 connection={selectedClient.icon}
100 theme={theme}
101 hasDistinctDarkIcon={selectedClient.hasDistinctDarkIcon}
102 />
103 ) : (
104 <Bot size={12} aria-hidden={true} />
105 )}
106 {selectedClient?.label}
107 </div>
108 </Button>
109 </PopoverTrigger>
110 </div>
111 <PopoverContent className="mt-0 p-0 max-w-48" side="bottom" align="start">
112 <Command>
113 <CommandInput placeholder="Search..." />
114 <CommandList>
115 <CommandEmpty>No results found.</CommandEmpty>
116 {groups ? (
117 groups.map((group) => (
118 <CommandGroup key={group.heading} heading={group.heading}>
119 {group.clients.map(renderClient)}
120 </CommandGroup>
121 ))
122 ) : (
123 <CommandGroup>{clients.map(renderClient)}</CommandGroup>
124 )}
125 </CommandList>
126 </Command>
127 </PopoverContent>
128 </Popover>
129 )
130}