BackupsList.tsx142 lines · main
1import { useParams } from 'common'
2import dayjs from 'dayjs'
3import { Clock } from 'lucide-react'
4import { useRouter } from 'next/router'
5import { useState } from 'react'
6import { toast } from 'sonner'
7import { TimestampInfo } from 'ui-patterns'
8import { Admonition } from 'ui-patterns/admonition'
9import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
10
11import { BackupItem } from './BackupItem'
12import { BackupsEmpty } from './BackupsEmpty'
13import { BackupsStorageAlert } from './BackupsStorageAlert'
14import Panel from '@/components/ui/Panel'
15import { UpgradeToPro } from '@/components/ui/UpgradeToPro'
16import { useBackupRestoreMutation } from '@/data/database/backup-restore-mutation'
17import { DatabaseBackup, useBackupsQuery } from '@/data/database/backups-query'
18import { useSetProjectStatus } from '@/data/projects/project-detail-query'
19import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
20import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
21import { PROJECT_STATUS } from '@/lib/constants'
22
23export const BackupsList = () => {
24 const router = useRouter()
25 const { ref: projectRef } = useParams()
26 const [selectedBackup, setSelectedBackup] = useState<DatabaseBackup>()
27 const { hasAccess: hasAccessToBackups } = useCheckEntitlements('backup.retention_days')
28
29 const { setProjectStatus } = useSetProjectStatus()
30 const { data: selectedProject } = useSelectedProjectQuery()
31 const isHealthy = selectedProject?.status === PROJECT_STATUS.ACTIVE_HEALTHY
32
33 const { data: backups } = useBackupsQuery({ projectRef })
34 const {
35 mutate: restoreFromBackup,
36 isPending: isRestoring,
37 isSuccess: isSuccessBackup,
38 } = useBackupRestoreMutation({
39 onSuccess: () => {
40 if (projectRef) {
41 setTimeout(() => {
42 setProjectStatus({ ref: projectRef, status: PROJECT_STATUS.RESTORING })
43 toast.success(
44 `Restoring database back to ${dayjs(selectedBackup?.inserted_at).format(
45 'DD MMM YYYY HH:mm:ss'
46 )}`
47 )
48 router.push(`/project/${projectRef}`)
49 }, 3000)
50 }
51 },
52 })
53
54 const sortedBackups = (backups?.backups ?? []).sort(
55 (a, b) => new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf()
56 )
57 const isPitrEnabled = backups?.pitr_enabled
58
59 if (!hasAccessToBackups) {
60 return (
61 <UpgradeToPro
62 addon="pitr"
63 source="backups"
64 featureProposition="have up to 7 days of scheduled backups"
65 icon={<Clock size={20} />}
66 primaryText="Free Plan does not include project backups."
67 secondaryText="Upgrade to the Pro Plan for up to 7 days of scheduled backups."
68 buttonText="Upgrade"
69 />
70 )
71 }
72
73 if (isPitrEnabled) return null
74
75 return (
76 <>
77 <div className="space-y-6">
78 {sortedBackups.length === 0 ? (
79 <BackupsEmpty />
80 ) : (
81 <>
82 <BackupsStorageAlert />
83 <Panel>
84 {sortedBackups?.map((x, i: number) => {
85 return (
86 <BackupItem
87 key={x.id}
88 backup={x}
89 index={i}
90 isHealthy={isHealthy}
91 onSelectBackup={() => setSelectedBackup(x)}
92 />
93 )
94 })}
95 </Panel>
96 </>
97 )}
98 </div>
99 <ConfirmationModal
100 size="small"
101 confirmLabel="Restore"
102 confirmLabelLoading="Restoring..."
103 variant="warning"
104 visible={selectedBackup !== undefined}
105 title="Restore from backup"
106 loading={isRestoring || isSuccessBackup}
107 onCancel={() => setSelectedBackup(undefined)}
108 onConfirm={() => {
109 if (projectRef === undefined) return console.error('Project ref required')
110 if (selectedBackup === undefined) return console.error('Backup required')
111 restoreFromBackup({ ref: projectRef, backup: selectedBackup })
112 }}
113 >
114 <div className="space-y-3">
115 {!!selectedBackup && (
116 <p className="text-sm">
117 This will restore your database to the backup made on{' '}
118 <TimestampInfo
119 displayAs="utc"
120 utcTimestamp={selectedBackup.inserted_at}
121 labelFormat="DD MMM YYYY HH:mm:ss (ZZ)"
122 className="text-sm!"
123 />
124 </p>
125 )}
126
127 <Admonition
128 showIcon={false}
129 type="warning"
130 title="This action cannot be undone"
131 description={
132 <ul className="list-disc list-inside">
133 <li>Your project will be offline during restoration</li>
134 <li>Any new data since this backup will be lost</li>
135 </ul>
136 }
137 />
138 </div>
139 </ConfirmationModal>
140 </>
141 )
142}