DatabaseSelector.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import { useParams } from 'common'
  2. import { noop } from 'lodash'
  3. import { Check, ChevronDown, Loader2, Plus } from 'lucide-react'
  4. import Link from 'next/link'
  5. import { useRouter } from 'next/router'
  6. import { parseAsBoolean, useQueryState } from 'nuqs'
  7. import { useEffect, useState } from 'react'
  8. import {
  9. Button,
  10. ButtonProps,
  11. cn,
  12. Command,
  13. CommandGroup,
  14. CommandItem,
  15. CommandList,
  16. Popover,
  17. PopoverContent,
  18. PopoverTrigger,
  19. ScrollArea,
  20. Tooltip,
  21. TooltipContent,
  22. TooltipTrigger,
  23. } from 'ui'
  24. import { Markdown } from '@/components/interfaces/Markdown'
  25. import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
  26. import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
  27. import { formatDatabaseID, formatDatabaseRegion } from '@/data/read-replicas/replicas.utils'
  28. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  29. import { IS_PLATFORM } from '@/lib/constants'
  30. import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
  31. interface DatabaseSelectorProps {
  32. selectedDatabaseId?: string // To override initial state
  33. variant?: 'regular' | 'connected-on-right' | 'connected-on-left' | 'connected-on-both'
  34. additionalOptions?: { id: string; name: string }[]
  35. buttonProps?: ButtonProps
  36. onSelectId?: (id: string) => void // Optional callback
  37. className?: string
  38. align?: 'start' | 'end'
  39. isForm?: boolean
  40. }
  41. export const DatabaseSelector = ({
  42. selectedDatabaseId: _selectedDatabaseId,
  43. variant = 'regular',
  44. additionalOptions = [],
  45. onSelectId = noop,
  46. buttonProps,
  47. align = 'end',
  48. className,
  49. isForm = false,
  50. }: DatabaseSelectorProps) => {
  51. const router = useRouter()
  52. const { ref: projectRef } = useParams()
  53. const [open, setOpen] = useState(false)
  54. const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
  55. const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
  56. const state = useDatabaseSelectorStateSnapshot()
  57. const selectedDatabaseId = _selectedDatabaseId ?? state.selectedDatabaseId
  58. const { data, isPending: isLoading, isSuccess } = useReadReplicasQuery({ projectRef })
  59. const databases = data ?? []
  60. const sortedDatabases = databases
  61. .sort((a, b) => (a.inserted_at > b.inserted_at ? 1 : 0))
  62. .sort((database) => (database.identifier === projectRef ? -1 : 0))
  63. const selectedDatabase = databases.find((db) => db.identifier === selectedDatabaseId)
  64. const selectedDatabaseRegion = formatDatabaseRegion(selectedDatabase?.region ?? '')
  65. const formattedDatabaseId = formatDatabaseID(selectedDatabaseId ?? '')
  66. const selectedAdditionalOption = additionalOptions.find((x) => x.id === selectedDatabaseId)
  67. const newReplicaURL = `/project/${projectRef}/database/replication?type=Read+Replica`
  68. useEffect(() => {
  69. if (_selectedDatabaseId && !isForm) state.setSelectedDatabaseId(_selectedDatabaseId)
  70. // eslint-disable-next-line react-hooks/exhaustive-deps
  71. }, [_selectedDatabaseId])
  72. return (
  73. <Popover open={open} onOpenChange={setOpen} modal={false}>
  74. <PopoverTrigger asChild>
  75. <div className={cn('flex cursor-pointer', className)}>
  76. {!isForm && (
  77. <span className="flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
  78. Source
  79. </span>
  80. )}
  81. <Button
  82. type="default"
  83. icon={isLoading && <Loader2 className="animate-spin" />}
  84. iconRight={<ChevronDown strokeWidth={1.5} size={12} />}
  85. {...buttonProps}
  86. className={cn(
  87. 'justify-start',
  88. !isForm && 'rounded-l-none',
  89. variant === 'connected-on-right' && 'rounded-r-none',
  90. variant === 'connected-on-left' && 'rounded-l-none border-l-0',
  91. variant === 'connected-on-both' && 'rounded-none border-x-0',
  92. buttonProps?.className
  93. )}
  94. >
  95. {selectedAdditionalOption ? (
  96. <span>{selectedAdditionalOption.name}</span>
  97. ) : (
  98. <>
  99. <span className="capitalize">
  100. {isLoading || selectedDatabase?.identifier === projectRef
  101. ? 'Primary database'
  102. : 'Read replica'}
  103. </span>{' '}
  104. {isSuccess && selectedDatabase?.identifier !== projectRef && (
  105. <span>
  106. ({selectedDatabaseRegion} - {formattedDatabaseId})
  107. </span>
  108. )}
  109. </>
  110. )}
  111. </Button>
  112. </div>
  113. </PopoverTrigger>
  114. <PopoverContent className="p-0 w-64" side="bottom" align={align}>
  115. <Command>
  116. <CommandList>
  117. {additionalOptions.length > 0 && (
  118. <CommandGroup className="border-b">
  119. {additionalOptions.map((option) => (
  120. <CommandItem
  121. key={option.id}
  122. value={option.id}
  123. className="cursor-pointer w-full"
  124. onSelect={() => {
  125. if (!isForm) state.setSelectedDatabaseId(option.id)
  126. setOpen(false)
  127. onSelectId(option.id)
  128. }}
  129. onClick={() => {
  130. if (!isForm) state.setSelectedDatabaseId(option.id)
  131. setOpen(false)
  132. onSelectId(option.id)
  133. }}
  134. >
  135. <div className="w-full flex items-center justify-between">
  136. <p>{option.name}</p>
  137. {option.id === selectedDatabaseId && <Check size={14} />}
  138. </div>
  139. </CommandItem>
  140. ))}
  141. </CommandGroup>
  142. )}
  143. <CommandGroup>
  144. <ScrollArea className={(databases || []).length > 7 ? 'h-[210px]' : ''}>
  145. {sortedDatabases?.map((database) => {
  146. const region = formatDatabaseRegion(database.region)
  147. const id = formatDatabaseID(database.identifier)
  148. if (database.status !== 'ACTIVE_HEALTHY') {
  149. const status = [
  150. REPLICA_STATUS.INIT_READ_REPLICA,
  151. REPLICA_STATUS.COMING_UP,
  152. ].includes(database.status)
  153. ? 'coming up'
  154. : 'not healthy'
  155. return (
  156. <Tooltip key={database.identifier}>
  157. <TooltipTrigger asChild>
  158. <div className="px-2 py-1.5 w-full flex items-center justify-between">
  159. <p className="text-xs text-foreground-lighter">
  160. Read replica ({region} - {id})
  161. </p>
  162. </div>
  163. </TooltipTrigger>
  164. <TooltipContent side="right" className="w-80">
  165. <Markdown
  166. className="text-xs text-foreground"
  167. content={`Replica unable to accept requests as its ${status}. [View infrastructure settings](/project/${projectRef}/settings/infrastructure) for more information.`}
  168. />
  169. </TooltipContent>
  170. </Tooltip>
  171. )
  172. }
  173. return (
  174. <CommandItem
  175. key={database.identifier}
  176. value={database.identifier}
  177. className="cursor-pointer w-full"
  178. onSelect={() => {
  179. if (!isForm) state.setSelectedDatabaseId(database.identifier)
  180. setOpen(false)
  181. onSelectId(database.identifier)
  182. }}
  183. onClick={() => {
  184. if (!isForm) state.setSelectedDatabaseId(database.identifier)
  185. setOpen(false)
  186. onSelectId(database.identifier)
  187. }}
  188. >
  189. <div className="w-full flex items-center justify-between">
  190. <p>
  191. {database.identifier === projectRef
  192. ? 'Primary database'
  193. : `Read replica (${region} - ${id})`}
  194. </p>
  195. {database.identifier === selectedDatabaseId && <Check size={16} />}
  196. </div>
  197. </CommandItem>
  198. )
  199. })}
  200. </ScrollArea>
  201. </CommandGroup>
  202. {IS_PLATFORM && infrastructureReadReplicas && (
  203. <CommandGroup className="border-t">
  204. <CommandItem
  205. className="cursor-pointer w-full"
  206. onSelect={() => {
  207. setOpen(false)
  208. router.push(newReplicaURL)
  209. }}
  210. onClick={() => setOpen(false)}
  211. >
  212. <Link
  213. href={newReplicaURL}
  214. onClick={async () => {
  215. setOpen(false)
  216. // [Joshen] This is used in the Connect UI which is available across all pages
  217. setShowConnect(false)
  218. }}
  219. className="w-full flex items-center gap-2"
  220. >
  221. <Plus size={14} strokeWidth={1.5} />
  222. <p>Create a new read replica</p>
  223. </Link>
  224. </CommandItem>
  225. </CommandGroup>
  226. )}
  227. </CommandList>
  228. </Command>
  229. </PopoverContent>
  230. </Popover>
  231. )
  232. }