Merge pull request #16557 from supabase/chore/convert-requests-to-RQ-4

Convert independent POST requests to RQ P2
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-08-21 13:38:59 +07:00
commit b0aedc4e46
10 files changed
+403 -141

No files matched your search

@@ -2,14 +2,14 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { Badge, Button, IconDownload } from 'ui'
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
import { useBackupDownloadMutation } from 'data/database/backup-download-mutation'
import { useBackupRestoreMutation } from 'data/database/backup-restore-mutation'
import { setProjectStatus } from 'data/projects/projects-query'
import { useCheckPermissions, useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { Badge, Button, IconDownload } from 'ui'
import { PROJECT_STATUS } from 'lib/constants'
interface BackupItemProps {
projectRef: string
@@ -18,48 +18,33 @@ interface BackupItemProps {
}
const BackupItem = ({ projectRef, backup, index }: BackupItemProps) => {
const queryClient = useQueryClient()
const router = useRouter()
const { ui } = useStore()
const [isDownloading, setDownloading] = useState<boolean>(false)
const [isRestoring, setRestoring] = useState<boolean>(false)
const queryClient = useQueryClient()
const canTriggerScheduledBackups = useCheckPermissions(
PermissionAction.INFRA_EXECUTE,
'queue_job.restore.prepare'
)
async function restore(backup: any) {
setRestoring(true)
try {
post(`${API_URL}/database/${projectRef}/backups/restore`, backup).then(() => {
setTimeout(() => {
setProjectStatus(queryClient, projectRef, PROJECT_STATUS.RESTORING)
ui.setNotification({
category: 'success',
message: `Restoring database back to ${dayjs(backup.inserted_at).format(
'DD MMM YYYY HH:mm:ss'
)}`,
})
router.push(`/project/${projectRef}`)
}, 3000)
})
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to restore from backup: ${error.message}`,
})
setRestoring(false)
}
}
const { mutate: restoreFromBackup, isLoading: isRestoring } = useBackupRestoreMutation({
onSuccess: () => {
setTimeout(() => {
setProjectStatus(queryClient, projectRef, PROJECT_STATUS.RESTORING)
ui.setNotification({
category: 'success',
message: `Restoring database back to ${dayjs(backup.inserted_at).format(
'DD MMM YYYY HH:mm:ss'
)}`,
})
router.push(`/project/${projectRef}`)
}, 3000)
},
})
async function download(backup: any) {
setDownloading(true)
try {
const res = await post(`${API_URL}/database/${projectRef}/backups/download`, backup)
const { fileUrl } = await res
const { mutate: downloadBackup, isLoading: isDownloading } = useBackupDownloadMutation({
onSuccess: (res) => {
const { fileUrl } = res
// Trigger browser download by create,trigger and remove tempLink
const tempLink = document.createElement('a')
@@ -67,27 +52,16 @@ const BackupItem = ({ projectRef, backup, index }: BackupItemProps) => {
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
},
})
setDownloading(false)
} catch (error: any) {
ui.setNotification({
error,
category: 'error',
message: `Failed to download backup: ${error.message}`,
})
setDownloading(false)
}
}
function onRestoreClick() {
const onRestoreClick = () => {
confirmAlert({
title: 'Confirm to restore',
message: `Are you sure you want to restore from ${dayjs(backup.inserted_at).format(
'DD MMM YYYY'
)}? This will destroy any new data written since this backup was made.`,
onAsyncConfirm: async () => {
await restore(backup)
},
onAsyncConfirm: async () => restoreFromBackup({ ref: projectRef, backup }),
})
}
@@ -108,7 +82,7 @@ const BackupItem = ({ projectRef, backup, index }: BackupItemProps) => {
<Button
type="default"
disabled={!canTriggerScheduledBackups || isRestoring || isDownloading}
onClick={() => download(backup)}
onClick={() => downloadBackup({ ref: projectRef, backup })}
loading={isDownloading}
icon={<IconDownload />}
>
@@ -1,5 +1,6 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { useQueryClient } from '@tanstack/react-query'
import { useParams } from 'common'
import { format } from 'date-fns'
import dayjs from 'dayjs'
import { useRouter } from 'next/router'
@@ -7,13 +8,12 @@ import { useState } from 'react'
import DatePicker from 'react-datepicker'
import { Alert, Button, IconChevronLeft, IconChevronRight, IconHelpCircle, Modal } from 'ui'
import { useParams } from 'common'
import { FormHeader, FormPanel } from 'components/ui/Forms'
import InformationBox from 'components/ui/InformationBox'
import { usePitrRestoreMutation } from 'data/database/pitr-restore-mutation'
import { setProjectStatus } from 'data/projects/projects-query'
import { useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { PROJECT_STATUS } from 'lib/constants'
import BackupsEmpty from '../BackupsEmpty'
import { Timezone } from './PITR.types'
import {
@@ -27,14 +27,25 @@ import TimeInput from './TimeInput'
import TimezoneSelection from './TimezoneSelection'
const PITRSelection = () => {
const queryClient = useQueryClient()
const router = useRouter()
const { ref } = useParams()
const { backups } = useStore()
const queryClient = useQueryClient()
const { ui, backups } = useStore()
const [showConfiguration, setShowConfiguration] = useState(false)
const [showConfirmation, setShowConfirmation] = useState(false)
const [selectedTimezone, setSelectedTimezone] = useState<Timezone>(getClientTimezone())
const { mutate: restoreFromPitr, isLoading: isRestoring } = usePitrRestoreMutation({
onSuccess: (res, variables) => {
setTimeout(() => {
setShowConfirmation(false)
setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.RESTORING)
router.push(`/project/${variables.ref}`)
}, 3000)
},
})
const { earliestPhysicalBackupDateUnix, latestPhysicalBackupDateUnix } =
backups?.configuration?.physicalBackupData ?? {}
const hasNoBackupsAvailable = !earliestPhysicalBackupDateUnix || !latestPhysicalBackupDateUnix
@@ -46,10 +57,6 @@ const PITRSelection = () => {
const [selectedDateRaw, setSelectedDateRaw] = useState<Date>(latestAvailableBackup.toDate())
const selectedDate = dayjs(selectedDateRaw).tz(selectedTimezone.utc[0], true) // true to keep local time and just change +whatever
const [showConfiguration, setShowConfiguration] = useState(false)
const [showConfirmation, setShowConfirmation] = useState(false)
const [isRestoring, setIsRestoring] = useState(false)
const isSelectedOnEarliestDay = selectedDate.isSame(earliestAvailableBackup, 'day')
const isSelectedOnLatestDay = selectedDate.isSame(latestAvailableBackup, 'day')
const availableDates = getDatesBetweenRange(earliestAvailableBackup, latestAvailableBackup)
@@ -98,28 +105,9 @@ const PITRSelection = () => {
}
const onConfirmRestore = async () => {
if (!recoveryTimeTargetUnix) return
setIsRestoring(true)
const projectRef = ref as string
const { error } = await post(`${API_URL}/database/${projectRef}/backups/pitr`, {
recovery_time_target_unix: recoveryTimeTargetUnix,
})
if (error) {
ui.setNotification({
error,
category: 'error',
message: `Failed to start restoration: ${error.message}`,
})
setIsRestoring(false)
} else {
setTimeout(() => {
setShowConfirmation(false)
setProjectStatus(queryClient, projectRef, PROJECT_STATUS.RESTORING)
router.push(`/project/${projectRef}`)
}, 3000)
}
if (!ref) return console.error('Project ref is required')
if (!recoveryTimeTargetUnix) return console.error('Recovery time target unix is required')
restoreFromPitr({ ref, recovery_time_target_unix: recoveryTimeTargetUnix })
}
return (
@@ -3,17 +3,17 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { Button, IconPause } from 'ui'
import {
useIsProjectActive,
useProjectContext,
} from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
import { useProjectPauseMutation } from 'data/projects/project-pause-mutation'
import { setProjectStatus } from 'data/projects/projects-query'
import { useCheckPermissions, useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL, PROJECT_STATUS } from 'lib/constants'
import { Button, IconPause } from 'ui'
import { PROJECT_STATUS } from 'lib/constants'
const PauseProjectButton = () => {
const { ui } = useStore()
@@ -21,12 +21,8 @@ const PauseProjectButton = () => {
const queryClient = useQueryClient()
const { project } = useProjectContext()
const isProjectActive = useIsProjectActive()
const [loading, setLoading] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
const openModal = () => setIsModalOpen(true)
const closeModal = () => setIsModalOpen(false)
const projectRef = project?.ref ?? ''
const isPaused = project?.status === PROJECT_STATUS.INACTIVE
const canPauseProject = useCheckPermissions(
@@ -34,31 +30,22 @@ const PauseProjectButton = () => {
'queue_jobs.projects.pause'
)
const requestPauseProject = async () => {
const { mutate: pauseProject, isLoading: isPausing } = useProjectPauseMutation({
onSuccess: (res, variables) => {
setProjectStatus(queryClient, variables.ref, PROJECT_STATUS.PAUSING)
ui.setNotification({ category: 'success', message: 'Pausing project...' })
router.push(`/project/${projectRef}`)
},
})
const requestPauseProject = () => {
if (!canPauseProject) {
return ui.setNotification({
category: 'error',
message: 'You do not have the required permissions to pause this project',
})
}
setLoading(true)
const res = await post(`${API_URL}/projects/${projectRef}/pause`, {})
if (res.error) {
ui.setNotification({
error: res.error,
category: 'error',
message: `Failed to pause project: ${res.error.message}`,
})
setLoading(false)
} else {
setProjectStatus(queryClient, projectRef, PROJECT_STATUS.PAUSING)
ui.setNotification({ category: 'success', message: 'Pausing project' })
router.push(`/project/${projectRef}`)
}
closeModal()
pauseProject({ ref: projectRef })
}
return (
@@ -68,8 +55,8 @@ const PauseProjectButton = () => {
<Button
type="default"
icon={<IconPause />}
onClick={openModal}
loading={loading}
onClick={() => setIsModalOpen(true)}
loading={isPausing}
disabled={project === undefined || isPaused || !canPauseProject || !isProjectActive}
>
Pause Project
@@ -106,7 +93,7 @@ const PauseProjectButton = () => {
description="Are you sure you want to pause this project? It will not be accessible until you unpause it."
buttonLabel="Pause project"
buttonLoadingLabel="Pausing project"
onSelectCancel={closeModal}
onSelectCancel={() => setIsModalOpen(false)}
onSelectConfirm={requestPauseProject}
/>
</>
@@ -6,41 +6,57 @@ import { useRouter } from 'next/router'
import { useState } from 'react'
import { Button, Dropdown, IconChevronDown } from 'ui'
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
import { setProjectPostgrestStatus } from 'data/projects/projects-query'
import { useCheckPermissions, useStore } from 'hooks'
import { post } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import {
useIsProjectActive,
useProjectContext,
} from 'components/layouts/ProjectLayout/ProjectContext'
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
import { useProjectRestartMutation } from 'data/projects/project-restart-mutation'
import { useProjectRestartServicesMutation } from 'data/projects/project-restart-services-mutation'
import { setProjectPostgrestStatus } from 'data/projects/projects-query'
import { useCheckPermissions, useStore } from 'hooks'
const RestartServerButton = () => {
const queryClient = useQueryClient()
const { project } = useProjectContext()
const { ui } = useStore()
const router = useRouter()
const queryClient = useQueryClient()
const { project } = useProjectContext()
const isProjectActive = useIsProjectActive()
const [loading, setLoading] = useState(false)
const [serviceToRestart, setServiceToRestart] = useState<'project' | 'database'>()
const projectRef = project?.ref ?? ''
const projectRegion = project?.region
const projectRegion = project?.region ?? ''
const canRestartProject = useCheckPermissions(PermissionAction.INFRA_EXECUTE, 'reboot')
const requestProjectRestart = async () => {
const { mutate: restartProject, isLoading: isRestartingProject } = useProjectRestartMutation({
onSuccess: () => {
onRestartSuccess()
},
onError: (error) => {
onRestartFailed(error, 'project')
},
})
const { mutate: restartProjectServices, isLoading: isRestartingServices } =
useProjectRestartServicesMutation({
onSuccess: () => {
onRestartSuccess()
},
onError: (error) => {
onRestartFailed(error, 'database')
},
})
const isLoading = isRestartingProject || isRestartingServices
const requestProjectRestart = () => {
if (!canRestartProject) {
return ui.setNotification({
category: 'error',
message: 'You do not have the required permissions to restart this project',
})
}
setLoading(true)
const res = await post(`${API_URL}/projects/${projectRef}/restart`, {})
if (res.error) onRestartFailed(res.error, 'project')
else onRestartSuccess()
restartProject({ ref: projectRef })
}
const requestDatabaseRestart = async () => {
@@ -50,15 +66,7 @@ const RestartServerButton = () => {
message: 'You do not have the required permissions to restart this project',
})
}
setLoading(true)
const res = await post(`${API_URL}/projects/${projectRef}/restart-services`, {
restartRequest: {
region: projectRegion,
services: ['postgresql'],
},
})
if (res.error) onRestartFailed(res.error, 'database')
else onRestartSuccess()
restartProjectServices({ ref: projectRef, region: projectRegion, services: ['postgresql'] })
}
const onRestartFailed = (error: any, type: string) => {
@@ -67,15 +75,13 @@ const RestartServerButton = () => {
category: 'error',
message: `Unable to restart ${type}: ${error.message}`,
})
setLoading(false)
setServiceToRestart(undefined)
}
const onRestartSuccess = () => {
setProjectPostgrestStatus(queryClient, projectRef, 'OFFLINE')
ui.setNotification({ category: 'success', message: 'Restarting server' })
ui.setNotification({ category: 'success', message: 'Restarting server...' })
router.push(`/project/${projectRef}`)
setLoading(false)
setServiceToRestart(undefined)
}
@@ -99,7 +105,7 @@ const RestartServerButton = () => {
overlay={[
<Dropdown.Item
key="database"
disabled={loading}
disabled={isLoading}
onClick={() => {
setServiceToRestart('database')
}}
@@ -0,0 +1,48 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import { ResponseError } from 'types'
import { Backup } from './backup-restore-mutation'
export type BackupDownloadVariables = {
ref: string
backup: Backup
}
export async function downloadBackup({ ref, backup }: BackupDownloadVariables) {
const { data, error } = await post('/platform/database/{ref}/backups/download', {
params: { path: { ref } },
body: { ...backup, status: backup.status as unknown as string },
})
if (error) throw error
return data
}
type BackupDownloadData = Awaited<ReturnType<typeof downloadBackup>>
export const useBackupDownloadMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<BackupDownloadData, ResponseError, BackupDownloadVariables>,
'mutationFn'
> = {}) => {
return useMutation<BackupDownloadData, ResponseError, BackupDownloadVariables>(
(vars) => downloadBackup(vars),
{
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to download backup: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -0,0 +1,51 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import { ResponseError } from 'types'
import { components } from 'data/api'
export type BackupRestoreVariables = {
ref: string
backup: Backup
}
// [Joshen] Shift to backups RQ query once created
export type Backup = components['schemas']['Backup']
export async function restoreFromBackup({ ref, backup }: BackupRestoreVariables) {
const { data, error } = await post('/platform/database/{ref}/backups/restore', {
params: { path: { ref } },
body: { ...backup, status: backup.status as unknown as string },
})
if (error) throw error
return data
}
type BackupRestoreData = Awaited<ReturnType<typeof restoreFromBackup>>
export const useBackupRestoreMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<BackupRestoreData, ResponseError, BackupRestoreVariables>,
'mutationFn'
> = {}) => {
return useMutation<BackupRestoreData, ResponseError, BackupRestoreVariables>(
(vars) => restoreFromBackup(vars),
{
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to restore from backup: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -0,0 +1,47 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import { ResponseError } from 'types'
export type PitrRestoreVariables = {
ref: string
recovery_time_target_unix: number
}
export async function restoreFromPitr({ ref, recovery_time_target_unix }: PitrRestoreVariables) {
const { data, error } = await post('/platform/database/{ref}/backups/pitr', {
params: { path: { ref } },
body: { recovery_time_target_unix },
})
if (error) throw error
return data
}
type PitrRestoreData = Awaited<ReturnType<typeof restoreFromPitr>>
export const usePitrRestoreMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<PitrRestoreData, ResponseError, PitrRestoreVariables>,
'mutationFn'
> = {}) => {
return useMutation<PitrRestoreData, ResponseError, PitrRestoreVariables>(
(vars) => restoreFromPitr(vars),
{
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to start PITR restoration: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -0,0 +1,45 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import { ResponseError } from 'types'
export type ProjectPauseVariables = {
ref: string
}
export async function pauseProject({ ref }: ProjectPauseVariables) {
const { data, error } = await post('/platform/projects/{ref}/pause', {
params: { path: { ref } },
})
if (error) throw error
return data
}
type ProjectPauseData = Awaited<ReturnType<typeof pauseProject>>
export const useProjectPauseMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<ProjectPauseData, ResponseError, ProjectPauseVariables>,
'mutationFn'
> = {}) => {
return useMutation<ProjectPauseData, ResponseError, ProjectPauseVariables>(
(vars) => pauseProject(vars),
{
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to pause project: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -0,0 +1,45 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import { ResponseError } from 'types'
export type ProjectRestartVariables = {
ref: string
}
export async function restartProject({ ref }: ProjectRestartVariables) {
const { data, error } = await post('/platform/projects/{ref}/restart', {
params: { path: { ref } },
})
if (error) throw error
return data
}
type ProjectRestartData = Awaited<ReturnType<typeof restartProject>>
export const useProjectRestartMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<ProjectRestartData, ResponseError, ProjectRestartVariables>,
'mutationFn'
> = {}) => {
return useMutation<ProjectRestartData, ResponseError, ProjectRestartVariables>(
(vars) => restartProject(vars),
{
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to restart project: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -0,0 +1,71 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { post } from 'data/fetchers'
import { ResponseError } from 'types'
export type ProjectRestartServicesVariables = {
ref: string
region: string
services: (
| 'postgresql'
| 'adminapi'
| 'api-gateway'
| 'functions'
| 'gotrue'
| 'kong'
| 'pgbouncer'
| 'pgsodium'
| 'postgrest'
| 'realtime'
| 'storage'
| 'walg'
| 'autoshutdown'
)[]
}
export async function restartProjectServices({
ref,
region,
services = ['postgresql'],
}: ProjectRestartServicesVariables) {
const { data, error } = await post('/platform/projects/{ref}/restart-services', {
params: { path: { ref } },
body: {
restartRequest: {
region,
services,
},
},
})
if (error) throw error
return data
}
type ProjectRestartServicesData = Awaited<ReturnType<typeof restartProjectServices>>
export const useProjectRestartServicesMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<ProjectRestartServicesData, ResponseError, ProjectRestartServicesVariables>,
'mutationFn'
> = {}) => {
return useMutation<ProjectRestartServicesData, ResponseError, ProjectRestartServicesVariables>(
(vars) => restartProjectServices(vars),
{
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to restart project: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}