Skip to content

Commit 9e0e4f6

Browse files
authored
Support downloading storage objects in project paused state (#29339)
* Support downloading storage objects in project paused state * Update * Revert hardcode * Feature flag storage object downlolad
1 parent d04ff41 commit 9e0e4f6

File tree

10 files changed

+329
-158
lines changed

10 files changed

+329
-158
lines changed

apps/studio/components/interfaces/Organization/NewProject/FreeProjectLimitWarning.tsx

Lines changed: 30 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Link from 'next/link'
2-
import { Button } from 'ui'
3-
import { Admonition } from 'ui-patterns/admonition'
42

53
import type { MemberWithFreeProjectLimit } from 'data/organizations/free-project-limit-check-query'
4+
import { Button } from 'ui'
5+
import { Admonition } from 'ui-patterns/admonition'
66

77
interface FreeProjectLimitWarningProps {
88
membersExceededLimit: MemberWithFreeProjectLimit[]
@@ -14,70 +14,36 @@ const FreeProjectLimitWarning = ({
1414
orgSlug,
1515
}: FreeProjectLimitWarningProps) => {
1616
return (
17-
<>
18-
<Admonition
19-
type={'default'}
20-
title={'The organization has members who have exceeded their free project limits'}
21-
description={
22-
<div className="space-y-3">
23-
<p className="text-sm leading-normal">
24-
The following members have reached their maximum limits for the number of active free
25-
plan projects within organizations where they are an administrator or owner:
26-
</p>
27-
<ul className="pl-5 list-disc">
28-
{membersExceededLimit.map((member, idx: number) => (
29-
<li key={`member-${idx}`}>
30-
{member.username || member.primary_email} (Limit: {member.free_project_limit} free
31-
projects)
32-
</li>
33-
))}
34-
</ul>
35-
<p className="text-sm leading-normal">
36-
These members will need to either delete, pause, or upgrade one or more of these
37-
projects before you're able to create a free project within this organization.
38-
</p>
39-
40-
<div>
41-
<Button asChild type="secondary">
42-
<Link href={`/org/${orgSlug}/billing?panel=subscriptionPlan`}>Upgrade plan</Link>
43-
</Button>
44-
</div>
45-
</div>
46-
}
47-
></Admonition>
48-
{/* <InformationBox
49-
icon={<AlertCircle className="text-foreground" size="large" strokeWidth={1.5} />}
50-
defaultVisibility={true}
51-
hideCollapse
52-
title="The organization has members who have exceeded their free project limits"
53-
description={
54-
<div className="space-y-3">
55-
<p className="text-sm leading-normal">
56-
The following members have reached their maximum limits for the number of active free
57-
plan projects within organizations where they are an administrator or owner:
58-
</p>
59-
<ul className="pl-5 list-disc">
60-
{membersExceededLimit.map((member, idx: number) => (
61-
<li key={`member-${idx}`}>
62-
{member.username || member.primary_email} (Limit: {member.free_project_limit} free
63-
projects)
64-
</li>
65-
))}
66-
</ul>
67-
<p className="text-sm leading-normal">
68-
These members will need to either delete, pause, or upgrade one or more of these
69-
projects before you're able to create a free project within this organization.
70-
</p>
17+
<Admonition
18+
type={'default'}
19+
title={'The organization has members who have exceeded their free project limits'}
20+
description={
21+
<div className="space-y-3">
22+
<p className="text-sm leading-normal">
23+
The following members have reached their maximum limits for the number of active free
24+
plan projects within organizations where they are an administrator or owner:
25+
</p>
26+
<ul className="pl-5 list-disc">
27+
{membersExceededLimit.map((member, idx: number) => (
28+
<li key={`member-${idx}`}>
29+
{member.username || member.primary_email} (Limit: {member.free_project_limit} free
30+
projects)
31+
</li>
32+
))}
33+
</ul>
34+
<p className="text-sm leading-normal">
35+
These members will need to either delete, pause, or upgrade one or more of these
36+
projects before you're able to create a free project within this organization.
37+
</p>
7138

72-
<div>
73-
<Button asChild type="primary">
74-
<Link href={`/org/${orgSlug}/billing?panel=subscriptionPlan`}>Upgrade plan</Link>
75-
</Button>
76-
</div>
39+
<div>
40+
<Button asChild type="secondary">
41+
<Link href={`/org/${orgSlug}/billing?panel=subscriptionPlan`}>Upgrade plan</Link>
42+
</Button>
7743
</div>
78-
}
79-
/> */}
80-
</>
44+
</div>
45+
}
46+
/>
8147
)
8248
}
8349

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { ChevronDown, Download } from 'lucide-react'
2+
import { useState } from 'react'
3+
import { toast } from 'sonner'
4+
5+
import { useParams } from 'common'
6+
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
7+
import { useBackupDownloadMutation } from 'data/database/backup-download-mutation'
8+
import { useProjectPauseStatusQuery } from 'data/projects/project-pause-status-query'
9+
import { useStorageArchiveCreateMutation } from 'data/storage/storage-archive-create-mutation'
10+
import { useStorageArchiveQuery } from 'data/storage/storage-archive-query'
11+
import { useFlag } from 'hooks/ui/useFlag'
12+
import { Database, Storage } from 'icons'
13+
import { PROJECT_STATUS } from 'lib/constants'
14+
import {
15+
Alert_Shadcn_,
16+
AlertDescription_Shadcn_,
17+
AlertTitle_Shadcn_,
18+
Button,
19+
DropdownMenu,
20+
DropdownMenuContent,
21+
DropdownMenuItem,
22+
DropdownMenuTrigger,
23+
WarningIcon,
24+
} from 'ui'
25+
import { useProjectContext } from '../ProjectContext'
26+
27+
export const PauseDisabledState = () => {
28+
const { ref } = useParams()
29+
const { project } = useProjectContext()
30+
const [toastId, setToastId] = useState<string | number>()
31+
const [refetchInterval, setRefetchInterval] = useState<number | false>(false)
32+
33+
const enforceNinetyDayUnpauseExpiry = useFlag('enforceNinetyDayUnpauseExpiry')
34+
const allowStorageObjectsDownload = useFlag('enableNinetyDayStorageDownload')
35+
36+
const { data: pauseStatus } = useProjectPauseStatusQuery(
37+
{ ref },
38+
{
39+
enabled: project?.status === PROJECT_STATUS.INACTIVE && enforceNinetyDayUnpauseExpiry,
40+
}
41+
)
42+
const latestBackup = pauseStatus?.latest_downloadable_backup_id
43+
44+
const { data: storageArchive } = useStorageArchiveQuery(
45+
{ projectRef: ref },
46+
{
47+
refetchInterval,
48+
refetchOnWindowFocus: false,
49+
onSuccess: (data) => {
50+
if (data.fileUrl && refetchInterval !== false) {
51+
toast.success('Downloading storage objects', { id: toastId })
52+
setToastId(undefined)
53+
setRefetchInterval(false)
54+
downloadStorageArchive(data.fileUrl)
55+
}
56+
},
57+
}
58+
)
59+
const storageArchiveUrl = storageArchive?.fileUrl
60+
61+
const { mutate: downloadBackup } = useBackupDownloadMutation({
62+
onSuccess: (res) => {
63+
const { fileUrl } = res
64+
65+
// Trigger browser download by create,trigger and remove tempLink
66+
const tempLink = document.createElement('a')
67+
tempLink.href = fileUrl
68+
document.body.appendChild(tempLink)
69+
tempLink.click()
70+
document.body.removeChild(tempLink)
71+
},
72+
})
73+
74+
const { mutate: createStorageArchive } = useStorageArchiveCreateMutation({
75+
onSuccess: () => {
76+
const toastId = toast.loading(
77+
'Retrieving storage archive. This may take a few minutes depending on the size of your storage objects.'
78+
)
79+
setToastId(toastId)
80+
setRefetchInterval(5000)
81+
},
82+
})
83+
84+
const onSelectDownloadBackup = () => {
85+
if (ref === undefined) return console.error('Project ref is required')
86+
if (!latestBackup) return toast.error('No backups available for download')
87+
88+
const toastId = toast.loading('Fetching database backup')
89+
90+
downloadBackup(
91+
{
92+
ref,
93+
backup: {
94+
id: latestBackup,
95+
// [Joshen] Just FYI these params aren't required for the download backup request
96+
// API types need to be updated
97+
project_id: -1,
98+
inserted_at: '',
99+
isPhysicalBackup: false,
100+
status: {},
101+
},
102+
},
103+
{
104+
onSuccess: () => {
105+
toast.success('Downloading database backup', { id: toastId })
106+
},
107+
}
108+
)
109+
}
110+
111+
const downloadStorageArchive = (url: string) => {
112+
const tempLink = document.createElement('a')
113+
tempLink.href = url
114+
document.body.appendChild(tempLink)
115+
tempLink.click()
116+
document.body.removeChild(tempLink)
117+
}
118+
119+
const onSelectDownloadStorageArchive = () => {
120+
if (!storageArchiveUrl) {
121+
createStorageArchive({ projectRef: ref })
122+
} else {
123+
toast.success('Downloading storage objects')
124+
downloadStorageArchive(storageArchiveUrl)
125+
}
126+
}
127+
128+
return (
129+
<Alert_Shadcn_ variant="warning">
130+
<WarningIcon />
131+
<AlertTitle_Shadcn_>Project cannot be restored through the dashboard</AlertTitle_Shadcn_>
132+
<AlertDescription_Shadcn_>
133+
This project has been paused for over{' '}
134+
<span className="text-foreground">
135+
{pauseStatus?.max_days_till_restore_disabled ?? 90} days
136+
</span>{' '}
137+
and cannot be restored through the dashboard. However, your data remains intact and can be
138+
downloaded as a backup.
139+
</AlertDescription_Shadcn_>
140+
<AlertDescription_Shadcn_ className="flex items-center gap-x-2 mt-3">
141+
<DropdownMenu>
142+
<DropdownMenuTrigger asChild>
143+
<Button type="default" icon={<Download />} iconRight={<ChevronDown />}>
144+
Download backup
145+
</Button>
146+
</DropdownMenuTrigger>
147+
<DropdownMenuContent className="w-56" align="start">
148+
<DropdownMenuItemTooltip
149+
className="gap-x-2"
150+
disabled={!latestBackup}
151+
onClick={() => onSelectDownloadBackup()}
152+
tooltip={{
153+
content: {
154+
side: 'right',
155+
text: 'No backups available, please reach out via support for assistance',
156+
},
157+
}}
158+
>
159+
<Database size={16} />
160+
Download database backup
161+
</DropdownMenuItemTooltip>
162+
<DropdownMenuItemTooltip
163+
className="gap-x-2"
164+
disabled={!allowStorageObjectsDownload}
165+
onClick={() => onSelectDownloadStorageArchive()}
166+
tooltip={{
167+
content: {
168+
side: 'right',
169+
text: 'This feature is not available yet, please reach out to support for assistance',
170+
},
171+
}}
172+
>
173+
<Storage size={16} />
174+
Download storage objects
175+
</DropdownMenuItemTooltip>
176+
{/* [Joshen] Once storage object download is supported, can just use the below component */}
177+
{/* <DropdownMenuItem className="gap-x-2" onClick={() => onSelectDownloadStorageArchive()}>
178+
<Storage size={16} />
179+
Download storage objects
180+
</DropdownMenuItem> */}
181+
</DropdownMenuContent>
182+
</DropdownMenu>
183+
</AlertDescription_Shadcn_>
184+
</Alert_Shadcn_>
185+
)
186+
}

0 commit comments

Comments
 (0)