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