index.tsx250 lines · main
1import { SupportCategories } from '@supabase/shared-types/out/constants'
2import { DatabaseUpgradeProgress, DatabaseUpgradeStatus } from '@supabase/shared-types/out/events'
3import { useParams } from 'common'
4import dayjs from 'dayjs'
5import {
6 AlertCircle,
7 Check,
8 CheckCircle,
9 Circle,
10 Loader,
11 Maximize2,
12 Minimize2,
13 Settings,
14} from 'lucide-react'
15import { useSearchParams } from 'next/navigation'
16import { useState } from 'react'
17import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
18
19import { DATABASE_UPGRADE_MESSAGES } from './UpgradingState.constants'
20import { SupportLink } from '@/components/interfaces/Support/SupportLink'
21import { useProjectUpgradingStatusQuery } from '@/data/config/project-upgrade-status-query'
22import { useInvalidateProjectDetailsQuery } from '@/data/projects/project-detail-query'
23import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
24import { IS_PLATFORM } from '@/lib/constants'
25
26export const UpgradingState = () => {
27 const { ref } = useParams()
28 const queryParams = useSearchParams()
29 const { data: project } = useSelectedProjectQuery()
30 const [loading, setLoading] = useState(false)
31 const [isExpanded, setIsExpanded] = useState(false)
32
33 const { invalidateProjectDetailsQuery } = useInvalidateProjectDetailsQuery()
34
35 const { data } = useProjectUpgradingStatusQuery(
36 {
37 projectRef: ref,
38 projectStatus: project?.status,
39 trackingId: queryParams?.get('trackingId'),
40 },
41 {
42 enabled: IS_PLATFORM,
43 }
44 )
45
46 const { initiated_at, status, progress, target_version, error } =
47 data?.databaseUpgradeStatus ?? {}
48 const progressStage = Number((progress || '').split('_')[0])
49
50 const isFailed = status === DatabaseUpgradeStatus.Failed
51 const isCompleted = status === DatabaseUpgradeStatus.Upgraded
52
53 const isPerformingFullPhysicalBackup =
54 status === DatabaseUpgradeStatus.Upgrading &&
55 progress === DatabaseUpgradeProgress.CompletedUpgrade
56
57 const initiatedAtUTC = dayjs.utc(initiated_at ?? 0).format('DD MMM YYYY HH:mm:ss')
58 const initiatedAt = dayjs
59 .utc(initiated_at ?? 0)
60 .local()
61 .format('DD MMM YYYY HH:mm:ss (ZZ)')
62
63 const refetchProjectDetails = async () => {
64 setLoading(true)
65
66 if (ref) await invalidateProjectDetailsQuery(ref)
67 }
68
69 const subject = 'Upgrade failed for project'
70 const message = `Upgrade information:\n• Initiated at: ${initiated_at}\n• Target Version: ${target_version}\n• Error: ${error}`
71
72 return (
73 <div className="w-full mx-auto my-16 space-y-16 max-w-7xl">
74 <div className="mx-6 space-y-16">
75 <div className="flex flex-col space-y-4 lg:flex-row lg:items-center lg:space-y-0 lg:space-x-6">
76 <h1 className="text-3xl">{project?.name}</h1>
77 </div>
78 <div className="w-full mx-auto mt-8 mb-16 max-w-7xl">
79 <div className="flex h-[500px] items-center justify-center rounded-sm border border-muted bg-surface-100 p-8">
80 {isCompleted ? (
81 <div className="grid gap-4">
82 <div className="relative mx-auto max-w-[300px]">
83 <CheckCircle className="text-brand" size={40} strokeWidth={1.5} />
84 </div>
85 <div className="space-y-2">
86 <p className="text-center">Upgrade completed!</p>
87 <p className="mt-4 text-center text-sm text-foreground-light w-[300px] mx-auto">
88 Your project has been successfully upgraded to Postgres {target_version} and is
89 now back online.
90 </p>
91 </div>
92 <div className="mx-auto">
93 <Button loading={loading} disabled={loading} onClick={refetchProjectDetails}>
94 Return to project
95 </Button>
96 </div>
97 </div>
98 ) : isFailed ? (
99 <div className="grid gap-4">
100 <div className="relative mx-auto max-w-[300px]">
101 <AlertCircle className="text-amber-900" size={40} strokeWidth={1.5} />
102 </div>
103 <div className="space-y-2">
104 <p className="text-center">We ran into an issue while upgrading your project</p>
105 <p className="mt-4 text-center text-sm text-foreground-light w-full md:w-[450px] mx-auto">
106 Your project is back online and its data is not affected. Please reach out to us
107 via our support form for assistance with the upgrade.
108 </p>
109 </div>
110 <div className="flex items-center mx-auto space-x-2">
111 <Button asChild type="default">
112 <SupportLink
113 queryParams={{
114 category: SupportCategories.DATABASE_UNRESPONSIVE,
115 projectRef: ref,
116 subject,
117 message,
118 }}
119 >
120 Contact support
121 </SupportLink>
122 </Button>
123 <Button loading={loading} disabled={loading} onClick={refetchProjectDetails}>
124 Return to project
125 </Button>
126 </div>
127 </div>
128 ) : (
129 <div className="grid w-[480px] gap-4">
130 <div className="relative mx-auto max-w-[300px]">
131 <div className="absolute flex items-center justify-center w-full h-full">
132 <Settings className="animate-spin" size={20} strokeWidth={2} />
133 </div>
134 <Circle className="text-foreground-lighter" size={50} strokeWidth={1.5} />
135 </div>
136 <div className="space-y-2">
137 {isPerformingFullPhysicalBackup ? (
138 <div>
139 <p className="text-center">Performing a full backup</p>
140 <p className="text-sm text-center text-foreground-light">
141 Upgrade is now complete, and your project is online. A full backup is now
142 being performed to ensure that there is a proper base backup available
143 post-upgrade. This can take from a few minutes up to several hours depending
144 on the size of your database.
145 </p>
146 </div>
147 ) : (
148 <div>
149 <p className="text-center">Upgrading in progress</p>
150 <p className="text-sm text-center text-foreground-light">
151 Upgrades can take from a few minutes up to several hours depending on the
152 size of your database. Your project will be offline while it is being
153 upgraded.
154 </p>
155 </div>
156 )}
157
158 <div
159 className="mt-4! mb-2! py-3 px-4 transition-all overflow-hidden border rounded-sm relative"
160 style={{ maxHeight: isExpanded ? '500px' : '110px' }}
161 >
162 {isExpanded ? (
163 <Minimize2
164 size={14}
165 strokeWidth={2}
166 className="absolute z-10 cursor-pointer top-3 right-3"
167 onClick={() => setIsExpanded(false)}
168 />
169 ) : (
170 <Maximize2
171 size={14}
172 strokeWidth={2}
173 className="absolute z-10 cursor-pointer top-3 right-3"
174 onClick={() => setIsExpanded(true)}
175 />
176 )}
177 <div
178 className="space-y-2 transition-all"
179 style={{
180 translate: isExpanded
181 ? '0px 0px'
182 : `0px ${
183 (progressStage - 2 <= 0
184 ? 0
185 : progressStage > 6
186 ? 5
187 : progressStage - 2) * -28
188 }px`,
189 }}
190 >
191 {DATABASE_UPGRADE_MESSAGES.map((message, idx: number) => {
192 const isCurrent = message.key === progress
193 const isCompleted = progressStage > idx
194 return (
195 <div key={message.key} className="flex items-center space-x-4">
196 {isCurrent ? (
197 <div className="flex items-center justify-center w-5 h-5 rounded-full">
198 <Loader
199 size={20}
200 className="animate-spin text-foreground-light"
201 strokeWidth={2}
202 />
203 </div>
204 ) : isCompleted ? (
205 <div className="flex items-center justify-center w-5 h-5 border rounded-full bg-brand border-brand">
206 <Check size={12} className="text-white" strokeWidth={3} />
207 </div>
208 ) : (
209 <div className="flex items-center justify-center w-5 h-5 border rounded-full bg-overlay-hover" />
210 )}
211 <p
212 className={`text-sm ${
213 isCurrent
214 ? 'text-foreground'
215 : isCompleted
216 ? 'text-foreground-light'
217 : 'text-foreground-lighter'
218 } hover:text-foreground transition`}
219 >
220 {isCurrent
221 ? message.progress
222 : isCompleted
223 ? message.completed
224 : message.initial}
225 </p>
226 </div>
227 )
228 })}
229 </div>
230 </div>
231
232 {initiated_at !== undefined && (
233 <Tooltip>
234 <TooltipTrigger>
235 <p className="text-sm text-center text-foreground-light">
236 Started on: {initiatedAtUTC} (UTC)
237 </p>
238 </TooltipTrigger>
239 <TooltipContent side="bottom">{initiatedAt}</TooltipContent>
240 </Tooltip>
241 )}
242 </div>
243 </div>
244 )}
245 </div>
246 </div>
247 </div>
248 </div>
249 )
250}