UsersSearch.tsx204 lines · main
1// @ts-nocheck
2import { AuthUsersSearchSubmittedEvent } from 'common/telemetry-constants'
3import { Search, X } from 'lucide-react'
4import { parseAsString, parseAsStringEnum, useQueryState } from 'nuqs'
5import { Dispatch, forwardRef, SetStateAction } from 'react'
6import {
7 Button,
8 cn,
9 Select,
10 SelectContent,
11 SelectGroup,
12 SelectItem,
13 SelectSeparator,
14 SelectTrigger,
15 SelectValue,
16 Tooltip,
17 TooltipContent,
18 TooltipTrigger,
19} from 'ui'
20import { Input } from 'ui-patterns/DataInputs/Input'
21
22import {
23 PHONE_NUMBER_LEFT_PREFIX_REGEX,
24 SpecificFilterColumn,
25 UUIDV4_LEFT_PREFIX_REGEX,
26} from './Users.constants'
27import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
28import { onSearchInputEscape } from '@/lib/keyboard'
29
30const getSearchPlaceholder = (column: SpecificFilterColumn): string => {
31 switch (column) {
32 case 'id':
33 return 'Search by user ID'
34 case 'email':
35 return 'Search by email'
36 case 'name':
37 return 'Search by name'
38 case 'phone':
39 return 'Search by phone'
40 case 'freeform':
41 return 'Search by user ID, email, phone or name'
42 default:
43 return 'Search users...'
44 }
45}
46
47interface UsersSearchProps {
48 search: string
49 setSearch: Dispatch<SetStateAction<string>>
50 improvedSearchEnabled?: boolean
51 telemetryProps: Omit<AuthUsersSearchSubmittedEvent['properties'], 'trigger'>
52 telemetryGroups: AuthUsersSearchSubmittedEvent['groups']
53 onSelectFilterColumn: (value: SpecificFilterColumn) => void
54}
55
56export const UsersSearch = forwardRef<HTMLInputElement, UsersSearchProps>(function UsersSearch(
57 {
58 search,
59 setSearch,
60 improvedSearchEnabled = false,
61 telemetryProps,
62 telemetryGroups,
63 onSelectFilterColumn,
64 },
65 ref
66) {
67 const [, setSelectedId] = useQueryState(
68 'show',
69 parseAsString.withOptions({ history: 'push', clearOnDefault: true })
70 )
71 const [, setFilterKeywords] = useQueryState('keywords', { defaultValue: '' })
72 const [specificFilterColumn] = useQueryState<SpecificFilterColumn>(
73 'filter',
74 parseAsStringEnum<SpecificFilterColumn>([
75 'id',
76 'email',
77 'phone',
78 'name',
79 'freeform',
80 ]).withDefault('email')
81 )
82
83 const { mutate: sendEvent } = useSendEventMutation()
84
85 const searchInvalid =
86 !search ||
87 specificFilterColumn === 'freeform' ||
88 specificFilterColumn === 'email' ||
89 specificFilterColumn === 'name'
90 ? false
91 : specificFilterColumn === 'id'
92 ? !search.match(UUIDV4_LEFT_PREFIX_REGEX)
93 : !search.match(PHONE_NUMBER_LEFT_PREFIX_REGEX)
94
95 const onSubmitSearch = () => {
96 const s = search.trim().toLocaleLowerCase()
97 setFilterKeywords(s)
98 setSelectedId(null)
99 sendEvent({
100 action: 'auth_users_search_submitted',
101 properties: {
102 trigger: 'search_input',
103 ...telemetryProps,
104 keywords: s,
105 },
106 groups: telemetryGroups,
107 })
108 }
109
110 return (
111 <div className="flex items-center">
112 <div className="text-xs h-[26px] flex items-center px-1.5 border border-strong rounded-l-md bg-surface-300">
113 <Search size={14} />
114 </div>
115
116 <Select
117 value={specificFilterColumn}
118 onValueChange={(v) => onSelectFilterColumn(v as typeof specificFilterColumn)}
119 >
120 <SelectTrigger
121 size="tiny"
122 className={cn(
123 'w-[130px] bg-transparent! rounded-none -ml-px',
124 specificFilterColumn === 'freeform' && 'text-warning'
125 )}
126 >
127 <SelectValue />
128 </SelectTrigger>
129 <SelectContent>
130 <SelectGroup>
131 <SelectItem value="id" className="text-xs">
132 User ID
133 </SelectItem>
134 <SelectItem value="email" className="text-xs">
135 Email address
136 </SelectItem>
137 {improvedSearchEnabled && (
138 <SelectItem value="name" className="text-xs">
139 Name
140 </SelectItem>
141 )}
142 <SelectItem value="phone" className="text-xs">
143 Phone number
144 </SelectItem>
145 {!improvedSearchEnabled && (
146 <>
147 <SelectSeparator />
148 <Tooltip>
149 <TooltipTrigger>
150 <SelectItem value="freeform" className="text-xs">
151 Unified search
152 </SelectItem>
153 </TooltipTrigger>
154 <TooltipContent side="right" className="w-64 text-center">
155 Search by all columns at once, including mid-string search. May impact database
156 performance if you have many users.
157 </TooltipContent>
158 </Tooltip>
159 </>
160 )}
161 </SelectGroup>
162 </SelectContent>
163 </Select>
164
165 <Input
166 ref={ref}
167 size="tiny"
168 containerClassName="w-[245px] rounded-l-none -ml-px"
169 className={cn(
170 'bg-transparent',
171 searchInvalid ? 'text-red-900 dark:border-red-900' : '',
172 search.length > 1 && 'pr-6'
173 )}
174 placeholder={getSearchPlaceholder(specificFilterColumn)}
175 value={search}
176 onChange={(e) => setSearch(e.target.value)}
177 onKeyDown={(e) => {
178 if (e.code === 'Enter' || e.code === 'NumpadEnter') {
179 if (!searchInvalid) onSubmitSearch()
180 return
181 }
182 onSearchInputEscape(search, () => {
183 setSearch('')
184 setFilterKeywords('')
185 })(e)
186 }}
187 actions={
188 search ? (
189 <Button
190 size="tiny"
191 type="text"
192 className="p-0 h-5 w-5"
193 icon={<X className={cn(searchInvalid ? 'text-red-900' : '')} />}
194 onClick={() => {
195 setSearch('')
196 setFilterKeywords('')
197 }}
198 />
199 ) : null
200 }
201 />
202 </div>
203 )
204})