UsersSearch.tsx 5.8 KB

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