Migrations.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import { SupportCategories } from '@supabase/shared-types/out/constants'
  2. import { Search } from 'lucide-react'
  3. import { useRef, useState } from 'react'
  4. import {
  5. Button,
  6. Card,
  7. cn,
  8. SidePanel,
  9. Table,
  10. TableBody,
  11. TableCell,
  12. TableHead,
  13. TableHeader,
  14. TableRow,
  15. Tooltip,
  16. TooltipContent,
  17. TooltipTrigger,
  18. } from 'ui'
  19. import { Admonition, TimestampInfo } from 'ui-patterns'
  20. import { Input } from 'ui-patterns/DataInputs/Input'
  21. import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
  22. import { MigrationsEmptyState } from './MigrationsEmptyState'
  23. import { SupportLink } from '@/components/interfaces/Support/SupportLink'
  24. import CodeEditor from '@/components/ui/CodeEditor/CodeEditor'
  25. import { InlineLink } from '@/components/ui/InlineLink'
  26. import { DatabaseMigration, useMigrationsQuery } from '@/data/database/migrations-query'
  27. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  28. import { DOCS_URL } from '@/lib/constants'
  29. import { formatMigrationVersionLabel, parseMigrationVersion } from '@/lib/migration-utils'
  30. import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
  31. import { useShortcut } from '@/state/shortcuts/useShortcut'
  32. const Migrations = () => {
  33. const [search, setSearch] = useState('')
  34. const [selectedMigration, setSelectedMigration] = useState<DatabaseMigration>()
  35. const searchInputRef = useRef<HTMLInputElement>(null)
  36. useShortcut(
  37. SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
  38. () => {
  39. searchInputRef.current?.focus()
  40. searchInputRef.current?.select()
  41. },
  42. { label: 'Search migrations' }
  43. )
  44. useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => setSearch(''))
  45. const { data: project } = useSelectedProjectQuery()
  46. const {
  47. data = [],
  48. isPending: isLoading,
  49. isSuccess,
  50. isError,
  51. error,
  52. } = useMigrationsQuery({
  53. projectRef: project?.ref,
  54. connectionString: project?.connectionString,
  55. })
  56. const migrations =
  57. search.length === 0
  58. ? data
  59. : (data.filter(
  60. (migration) => migration.version.includes(search) || migration.name?.includes(search)
  61. ) ?? [])
  62. return (
  63. <>
  64. {isLoading && (
  65. <div className="space-y-2">
  66. <ShimmeringLoader />
  67. <ShimmeringLoader className="w-3/4" />
  68. <ShimmeringLoader className="w-1/2" />
  69. </div>
  70. )}
  71. <div>
  72. {isError && (
  73. <Admonition
  74. type="warning"
  75. title="Failed to retrieve migration history for database"
  76. description={
  77. <>
  78. <p className="mb-1">
  79. Try refreshing your browser, but if the issue persists for more than a few
  80. minutes, please reach out to us via support.
  81. </p>
  82. <p className="mb-4">Error: {error?.message ?? 'Unknown'}</p>
  83. </>
  84. }
  85. >
  86. <Button key="contact-support" asChild type="default">
  87. <SupportLink
  88. queryParams={{
  89. projectRef: project?.ref,
  90. category: SupportCategories.DASHBOARD_BUG,
  91. subject: 'Unable to view database migrations',
  92. }}
  93. >
  94. Contact support
  95. </SupportLink>
  96. </Button>
  97. </Admonition>
  98. )}
  99. {isSuccess && (
  100. <div>
  101. {data.length <= 0 && <MigrationsEmptyState />}
  102. {data.length > 0 && (
  103. <div className="flex flex-col gap-y-4">
  104. <Input
  105. ref={searchInputRef}
  106. size="tiny"
  107. placeholder="Search for a migration"
  108. value={search}
  109. className="w-full lg:w-52"
  110. onChange={(e: any) => setSearch(e.target.value)}
  111. icon={<Search />}
  112. />
  113. <Card>
  114. <Table>
  115. <TableHeader>
  116. <TableRow>
  117. <TableHead key="version" style={{ width: '180px' }}>
  118. Version
  119. </TableHead>
  120. <TableHead key="name">Name</TableHead>
  121. <TableHead key="insertedAt">Inserted at (UTC)</TableHead>
  122. <TableHead key="buttons" />
  123. </TableRow>
  124. </TableHeader>
  125. <TableBody>
  126. {migrations.length > 0 ? (
  127. migrations.map((migration) => {
  128. const versionDayjs = parseMigrationVersion(migration.version)
  129. const label = formatMigrationVersionLabel(migration.version)
  130. const insertedAt = versionDayjs ? versionDayjs.toISOString() : undefined
  131. return (
  132. <TableRow key={migration.version}>
  133. <TableCell>{migration.version}</TableCell>
  134. <TableCell
  135. className={cn(
  136. (migration?.name ?? '').length === 0 && 'text-foreground-lighter!'
  137. )}
  138. >
  139. {migration?.name ?? 'Name not available'}
  140. </TableCell>
  141. <TableCell>
  142. <Tooltip>
  143. <TooltipTrigger>
  144. {!!insertedAt ? (
  145. <TimestampInfo
  146. className="text-sm"
  147. label={label}
  148. utcTimestamp={insertedAt}
  149. />
  150. ) : (
  151. <p className="text-foreground-lighter">Unknown</p>
  152. )}
  153. </TooltipTrigger>
  154. {!insertedAt && (
  155. <TooltipContent side="right" className="w-64 text-center">
  156. This migration was not generated via the{' '}
  157. <InlineLink
  158. href={`${DOCS_URL}/guides/deployment/database-migrations`}
  159. >
  160. Briven CLI
  161. </InlineLink>{' '}
  162. and hence we're unable to parse when this migration was
  163. inserted at.
  164. </TooltipContent>
  165. )}
  166. </Tooltip>
  167. </TableCell>
  168. <TableCell align="right">
  169. <Button
  170. type="default"
  171. onClick={() => setSelectedMigration(migration)}
  172. >
  173. View migration SQL
  174. </Button>
  175. </TableCell>
  176. </TableRow>
  177. )
  178. })
  179. ) : (
  180. <TableRow>
  181. <TableCell colSpan={3}>
  182. <p className="text-sm text-foreground">No results found</p>
  183. <p className="text-sm text-foreground-light">
  184. Your search for "{search}" did not return any results
  185. </p>
  186. </TableCell>
  187. </TableRow>
  188. )}
  189. </TableBody>
  190. </Table>
  191. </Card>
  192. </div>
  193. )}
  194. </div>
  195. )}
  196. </div>
  197. <SidePanel
  198. size="large"
  199. visible={selectedMigration !== undefined}
  200. header={`Migration: ${selectedMigration?.version}`}
  201. onCancel={() => setSelectedMigration(undefined)}
  202. customFooter={
  203. <div className="flex items-center justify-end p-4 border-t border-overlay-border">
  204. <Button type="default" onClick={() => setSelectedMigration(undefined)}>
  205. Close
  206. </Button>
  207. </div>
  208. }
  209. >
  210. <div className="h-full">
  211. <div className="relative h-full">
  212. <CodeEditor
  213. isReadOnly
  214. id={selectedMigration?.version ?? ''}
  215. language="pgsql"
  216. defaultValue={
  217. selectedMigration?.statements?.join(';\n') +
  218. (selectedMigration?.statements?.length ? ';' : '')
  219. }
  220. />
  221. </div>
  222. </div>
  223. </SidePanel>
  224. </>
  225. )
  226. }
  227. export default Migrations