index.tsx250 lines · main
| 1 | import { SupportCategories } from '@supabase/shared-types/out/constants' |
| 2 | import { DatabaseUpgradeProgress, DatabaseUpgradeStatus } from '@supabase/shared-types/out/events' |
| 3 | import { useParams } from 'common' |
| 4 | import dayjs from 'dayjs' |
| 5 | import { |
| 6 | AlertCircle, |
| 7 | Check, |
| 8 | CheckCircle, |
| 9 | Circle, |
| 10 | Loader, |
| 11 | Maximize2, |
| 12 | Minimize2, |
| 13 | Settings, |
| 14 | } from 'lucide-react' |
| 15 | import { useSearchParams } from 'next/navigation' |
| 16 | import { useState } from 'react' |
| 17 | import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' |
| 18 | |
| 19 | import { DATABASE_UPGRADE_MESSAGES } from './UpgradingState.constants' |
| 20 | import { SupportLink } from '@/components/interfaces/Support/SupportLink' |
| 21 | import { useProjectUpgradingStatusQuery } from '@/data/config/project-upgrade-status-query' |
| 22 | import { useInvalidateProjectDetailsQuery } from '@/data/projects/project-detail-query' |
| 23 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 24 | import { IS_PLATFORM } from '@/lib/constants' |
| 25 | |
| 26 | export 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 | } |