mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 03:45:06 +03:00
Merge pull request #7231 from supabase/feat/point-in-time-recovery
feat/point in time recovery
This commit is contained in:
33 files changed
+1940
-333
No files matched your search
@@ -1,94 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { Button, Modal } from '@supabase/ui'
|
||||
import { render, unmountComponentAtNode } from 'react-dom'
|
||||
|
||||
type ConfirmModalProps = {
|
||||
title: string
|
||||
message: string
|
||||
onConfirm?: () => void
|
||||
onAsyncConfirm?: () => Promise<void>
|
||||
variant?: 'danger' | 'warning' | 'success' | undefined
|
||||
}
|
||||
const ConfirmModal: React.FC<ConfirmModalProps> = ({
|
||||
title,
|
||||
message,
|
||||
onConfirm,
|
||||
onAsyncConfirm,
|
||||
variant = 'danger',
|
||||
}) => {
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
|
||||
function onCancelClick() {
|
||||
if (!loading) onClose()
|
||||
}
|
||||
|
||||
async function onConfirmClick() {
|
||||
if (onAsyncConfirm) {
|
||||
setLoading(true)
|
||||
await onAsyncConfirm()
|
||||
|
||||
onClose()
|
||||
} else if (onConfirm) {
|
||||
onConfirm()
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
removeElement()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={variant}
|
||||
visible={true}
|
||||
header={title}
|
||||
showIcon={false}
|
||||
size="small"
|
||||
confirmText="OK"
|
||||
cancelText="Cancel"
|
||||
onCancel={onCancelClick}
|
||||
loading={loading}
|
||||
customFooter={
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="default" onClick={() => onCancelClick()}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" loading={loading} onClick={() => onConfirmClick()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="text-scale-1100 py-4 text-sm">{message}</p>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
function removeElement() {
|
||||
const target = document.getElementById('supabase-ui-confirm-alert')
|
||||
if (target) {
|
||||
unmountComponentAtNode(target)
|
||||
target?.parentNode?.removeChild(target)
|
||||
}
|
||||
}
|
||||
|
||||
function createElement(props: ConfirmModalProps) {
|
||||
let divTarget = document.getElementById('supabase-ui-confirm-alert')
|
||||
if (divTarget) {
|
||||
render(<ConfirmModal {...props} />, divTarget)
|
||||
} else {
|
||||
divTarget = document.createElement('div')
|
||||
divTarget.id = 'supabase-ui-confirm-alert'
|
||||
document.body.appendChild(divTarget)
|
||||
render(<ConfirmModal {...props} />, divTarget)
|
||||
}
|
||||
}
|
||||
|
||||
export function showConfirmAlert(props: ConfirmModalProps) {
|
||||
createElement(props)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
export * from './BlockKeys';
|
||||
export * from './ConfirmAlert';
|
||||
export * from './DropdownControl';
|
||||
export * from './MonacoEditor';
|
||||
export * from './NullValue';
|
||||
export * from './Hooks';
|
||||
export * from './Shortcuts';
|
||||
export * from './EmptyValue';
|
||||
export * from './BlockKeys'
|
||||
export * from './DropdownControl'
|
||||
export * from './MonacoEditor'
|
||||
export * from './NullValue'
|
||||
export * from './Hooks'
|
||||
export * from './Shortcuts'
|
||||
export * from './EmptyValue'
|
||||
@@ -1,8 +1,9 @@
|
||||
import { FC, useState, useEffect } from 'react'
|
||||
import AwesomeDebouncePromise from 'awesome-debounce-promise'
|
||||
import { Button, InputNumber, IconArrowRight, IconArrowLeft, IconLoader } from '@supabase/ui'
|
||||
import { DropdownControl, showConfirmAlert } from '../../common'
|
||||
import { DropdownControl } from '../../common'
|
||||
import { useDispatch, useTrackedState } from '../../../store'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
|
||||
const updatePage = (payload: number, dispatch: (value: unknown) => void) => {
|
||||
dispatch({
|
||||
@@ -41,7 +42,7 @@ const Pagination: FC<PaginationProps> = () => {
|
||||
const onPreviousPage = () => {
|
||||
if (state.page > 1) {
|
||||
if (state.selectedRows.size >= 1) {
|
||||
showConfirmAlert({
|
||||
confirmAlert({
|
||||
title: 'Confirm moving to previous page',
|
||||
message: 'The currently selected lines will be deselected, do you want to proceed?',
|
||||
onConfirm: () => {
|
||||
@@ -61,7 +62,7 @@ const Pagination: FC<PaginationProps> = () => {
|
||||
const onNextPage = () => {
|
||||
if (state.page < maxPages) {
|
||||
if (state.selectedRows.size >= 1) {
|
||||
showConfirmAlert({
|
||||
confirmAlert({
|
||||
title: 'Confirm moving to next page',
|
||||
message: 'The currently selected lines will be deselected, do you want to proceed?',
|
||||
onConfirm: () => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as React from 'react'
|
||||
import { useDispatch, useTrackedState } from '../../store'
|
||||
import { Button, Divider, IconDownload, IconPlus, IconX, IconTrash, Typography } from '@supabase/ui'
|
||||
import { Button, Divider, IconDownload, IconPlus, IconX, IconTrash } from '@supabase/ui'
|
||||
import { saveAs } from 'file-saver'
|
||||
import FilterDropdown from './filter'
|
||||
import SortPopover from './sort'
|
||||
import StatusLabel from './StatusLabel'
|
||||
import RefreshButton from './RefreshButton'
|
||||
import { exportRowsToCsv } from '../../utils'
|
||||
import { showConfirmAlert } from '../common'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
|
||||
type HeaderProps = {
|
||||
onAddColumn?: () => void
|
||||
@@ -91,7 +91,7 @@ const RowHeader: React.FC<RowHeaderProps> = ({}) => {
|
||||
const { selectedRows, rows: allRows, editable } = state
|
||||
|
||||
const onRowsDelete = () => {
|
||||
showConfirmAlert({
|
||||
confirmAlert({
|
||||
title: 'Confirm to delete',
|
||||
message: 'Are you sure you want to delete the selected rows? This action cannot be undone.',
|
||||
onConfirm: async () => {
|
||||
|
||||
@@ -1,71 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import { Menu, Item, ItemParams, PredicateParams } from 'react-contexify';
|
||||
import { IconTrash, IconClipboard, IconEdit } from '@supabase/ui';
|
||||
import { useDispatch, useTrackedState } from '../../store';
|
||||
import { formatClipboardValue, copyToClipboard } from '../../utils';
|
||||
import { showConfirmAlert } from '../common';
|
||||
import * as React from 'react'
|
||||
import { Menu, Item, ItemParams, PredicateParams } from 'react-contexify'
|
||||
import { IconTrash, IconClipboard, IconEdit } from '@supabase/ui'
|
||||
import { useDispatch, useTrackedState } from '../../store'
|
||||
import { formatClipboardValue, copyToClipboard } from '../../utils'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
|
||||
export const ROW_CONTEXT_MENU_ID = 'row-context-menu-id';
|
||||
export const ROW_CONTEXT_MENU_ID = 'row-context-menu-id'
|
||||
|
||||
type RowContextMenuProps = {};
|
||||
type RowContextMenuProps = {}
|
||||
|
||||
const RowContextMenu: React.FC<RowContextMenuProps> = ({}) => {
|
||||
const state = useTrackedState();
|
||||
const dispatch = useDispatch();
|
||||
const state = useTrackedState()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
function onDeleteRow(p: ItemParams) {
|
||||
showConfirmAlert({
|
||||
confirmAlert({
|
||||
title: 'Confirm to delete',
|
||||
message:
|
||||
'Are you sure you want to delete this row? This action cannot be undone.',
|
||||
message: 'Are you sure you want to delete this row? This action cannot be undone.',
|
||||
onConfirm: async () => {
|
||||
const { props } = p;
|
||||
const { rowIdx } = props;
|
||||
const row = state.rows[rowIdx];
|
||||
const { error } = state.rowService!.delete([row]);
|
||||
const { props } = p
|
||||
const { rowIdx } = props
|
||||
const row = state.rows[rowIdx]
|
||||
const { error } = state.rowService!.delete([row])
|
||||
if (error) {
|
||||
if (state.onError) state.onError(error);
|
||||
if (state.onError) state.onError(error)
|
||||
} else {
|
||||
dispatch({ type: 'REMOVE_ROWS', payload: { rowIdxs: [row.idx] } });
|
||||
dispatch({ type: 'REMOVE_ROWS', payload: { rowIdxs: [row.idx] } })
|
||||
dispatch({
|
||||
type: 'SELECTED_ROWS_CHANGE',
|
||||
payload: { selectedRows: new Set() },
|
||||
});
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function onEditRowClick(p: ItemParams) {
|
||||
const { props } = p;
|
||||
const { rowIdx } = props;
|
||||
const row = state.rows[rowIdx];
|
||||
if (state.onEditRow) state.onEditRow(row);
|
||||
const { props } = p
|
||||
const { rowIdx } = props
|
||||
const row = state.rows[rowIdx]
|
||||
if (state.onEditRow) state.onEditRow(row)
|
||||
}
|
||||
|
||||
function isItemHidden({ data }: PredicateParams) {
|
||||
if (data === 'edit') return state.onEditRow == undefined;
|
||||
if (data === 'delete') return !state.editable;
|
||||
return false;
|
||||
if (data === 'edit') return state.onEditRow == undefined
|
||||
if (data === 'delete') return !state.editable
|
||||
return false
|
||||
}
|
||||
|
||||
function onCopyCellContent(p: ItemParams) {
|
||||
const { props } = p;
|
||||
const { props } = p
|
||||
|
||||
if (!state.selectedCellPosition || !props) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const { rowIdx } = props;
|
||||
const row = state.rows[rowIdx];
|
||||
const { rowIdx } = props
|
||||
const row = state.rows[rowIdx]
|
||||
|
||||
const columnKey =
|
||||
state.gridColumns[state.selectedCellPosition?.idx as number].key;
|
||||
const columnKey = state.gridColumns[state.selectedCellPosition?.idx as number].key
|
||||
|
||||
const value = row[columnKey];
|
||||
const text = formatClipboardValue(value);
|
||||
const value = row[columnKey]
|
||||
const text = formatClipboardValue(value)
|
||||
|
||||
copyToClipboard(text);
|
||||
copyToClipboard(text)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -85,6 +83,6 @@ const RowContextMenu: React.FC<RowContextMenuProps> = ({}) => {
|
||||
</Item>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default RowContextMenu;
|
||||
)
|
||||
}
|
||||
export default RowContextMenu
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react'
|
||||
import { useDispatch, useTrackedState } from '../../store'
|
||||
import { exportRowsToCsv } from '../../utils'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { showConfirmAlert } from '../common'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
|
||||
type RowMenuProps = {}
|
||||
|
||||
@@ -13,7 +13,7 @@ const RowMenu: React.FC<RowMenuProps> = ({}) => {
|
||||
const { selectedRows, rows: allRows, editable } = state
|
||||
|
||||
function onRowsDelete() {
|
||||
showConfirmAlert({
|
||||
confirmAlert({
|
||||
title: 'Confirm to delete',
|
||||
message: 'Are you sure you want to delete the selected rows? This action cannot be undone.',
|
||||
onConfirm: async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { Badge, Button, IconDownload } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { confirmAlert } from 'components/to-be-cleaned/ModalsDeprecated/ConfirmModal'
|
||||
|
||||
@@ -17,17 +17,26 @@ interface Props {
|
||||
|
||||
const BackupItem: FC<Props> = ({ projectRef, backup, index }) => {
|
||||
const router = useRouter()
|
||||
const { ui } = useStore()
|
||||
const { app, ui } = useStore()
|
||||
|
||||
const [isDownloading, setDownloading] = useState<boolean>(false)
|
||||
const [isRestoring, setRestoring] = useState<boolean>(false)
|
||||
|
||||
const projectId = ui.selectedProject?.id ?? -1
|
||||
|
||||
async function restore(backup: any) {
|
||||
setRestoring(true)
|
||||
try {
|
||||
post(`${API_URL}/database/${projectRef}/backups/restore`, backup).then(() => {
|
||||
setTimeout(() => {
|
||||
router.push('/project/[id]', `/project/${projectRef}`)
|
||||
app.onProjectStatusUpdated(projectId, 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) {
|
||||
@@ -46,7 +55,7 @@ const BackupItem: FC<Props> = ({ projectRef, backup, index }) => {
|
||||
const res = await post(`${API_URL}/database/${projectRef}/backups/download`, backup)
|
||||
const { fileUrl } = await res
|
||||
|
||||
// triger browser download by create,trigger and remove tempLink
|
||||
// Trigger browser download by create,trigger and remove tempLink
|
||||
const tempLink = document.createElement('a')
|
||||
tempLink.href = fileUrl
|
||||
document.body.appendChild(tempLink)
|
||||
@@ -101,18 +110,16 @@ const BackupItem: FC<Props> = ({ projectRef, backup, index }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`flex h-12 items-center justify-between px-6 ${
|
||||
index ? 'dark:border-dark border-t' : ''
|
||||
}`}
|
||||
>
|
||||
<p className="text-scale-1200 text-sm ">
|
||||
{dayjs(backup.inserted_at).format('DD MMM YYYY HH:mm:ss')}
|
||||
</p>
|
||||
<div className="">{generateSideButtons(backup)}</div>
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
className={`flex h-12 items-center justify-between px-6 ${
|
||||
index ? 'dark:border-dark border-t' : ''
|
||||
}`}
|
||||
>
|
||||
<p className="text-scale-1200 text-sm ">
|
||||
{dayjs(backup.inserted_at).format('DD MMM YYYY HH:mm:ss')}
|
||||
</p>
|
||||
<div className="">{generateSideButtons(backup)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { IconInfo } from '@supabase/ui'
|
||||
|
||||
const BackupsEmpty = () => {
|
||||
return (
|
||||
<div className="block w-full rounded border border-gray-400 border-opacity-50 bg-gray-300 p-3">
|
||||
<div className="flex space-x-3">
|
||||
<IconInfo size={20} strokeWidth={1.5} />
|
||||
<p className="text-sm">No backups created yet - check again tomorrow.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BackupsEmpty
|
||||
@@ -0,0 +1,14 @@
|
||||
import { IconInfo } from '@supabase/ui'
|
||||
|
||||
const BackupsError = () => {
|
||||
return (
|
||||
<div className="block w-full rounded border border-gray-400 border-opacity-50 bg-gray-300 p-3">
|
||||
<div className="flex space-x-3">
|
||||
<IconInfo size={20} strokeWidth={1.5} />
|
||||
<p className="text-sm">Failed to retrieve backups for project. Please contact support.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BackupsError
|
||||
@@ -1,48 +1,28 @@
|
||||
import React, { FC, useState, useEffect } from 'react'
|
||||
import { FC } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Typography, IconInfo } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { STRIPE_PRODUCT_IDS } from 'lib/constants'
|
||||
import BackupItem from './BackupItem'
|
||||
import Loading from 'components/ui/Loading'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import Panel from 'components/ui/Panel'
|
||||
|
||||
import BackupItem from './BackupItem'
|
||||
import BackupsError from './BackupsError'
|
||||
import BackupsEmpty from './BackupsEmpty'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const BackupsList: FC<Props> = ({}) => {
|
||||
const { ui, app } = useStore()
|
||||
const { ui, backups } = useStore()
|
||||
const projectRef = ui.selectedProject?.ref || 'default'
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const [projectData, setProjectData] = useState<any>([])
|
||||
if (backups.isLoading) return <Loading />
|
||||
if (backups.error) return <BackupsError />
|
||||
|
||||
useEffect(() => {
|
||||
retrieveBackups()
|
||||
}, [])
|
||||
const { tierKey } = backups.configuration
|
||||
const sortedBackups = backups.list()
|
||||
|
||||
const retrieveBackups = async () => {
|
||||
setIsLoading(true)
|
||||
const projectData = (await app.database.getBackups(projectRef)) as any
|
||||
setProjectData(projectData)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
if (projectData.error) {
|
||||
return (
|
||||
<div>
|
||||
<Typography.Text type="secondary">Error loading backups</Typography.Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Data Loaded
|
||||
const { backups, tierId } = projectData
|
||||
const sortedBackups = backups.sort((a: any, b: any) => b.id - a.id)
|
||||
|
||||
if (tierId === STRIPE_PRODUCT_IDS.FREE) {
|
||||
if (tierKey === 'FREE') {
|
||||
return (
|
||||
<UpgradeToPro
|
||||
primaryText="Free Plan does not include project backups."
|
||||
@@ -54,15 +34,8 @@ const BackupsList: FC<Props> = ({}) => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{!sortedBackups?.length && tierId !== STRIPE_PRODUCT_IDS.FREE ? (
|
||||
<div className="block w-full rounded border border-green-500 border-opacity-50 bg-green-500 bg-opacity-5 p-3">
|
||||
<div className="flex space-x-3">
|
||||
<div>
|
||||
<IconInfo className="text-green-500" size="large" />
|
||||
</div>
|
||||
<p>No backups created yet. Check again tomorrow.</p>
|
||||
</div>
|
||||
</div>
|
||||
{!sortedBackups?.length && tierKey !== 'FREE' ? (
|
||||
<BackupsEmpty />
|
||||
) : (
|
||||
<Panel>
|
||||
{sortedBackups?.map((x: any, i: number) => {
|
||||
|
||||
+1204
File diff suppressed because it is too large.
Load diff
+8
@@ -0,0 +1,8 @@
|
||||
export interface Timezone {
|
||||
value: string
|
||||
abbr: string
|
||||
offset: number
|
||||
isdst: boolean
|
||||
text: string
|
||||
utc: string[]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { ALL_TIMEZONES } from './PITRBackupSelection.constants'
|
||||
import { Timezone } from './PITRBackupSelection.types'
|
||||
|
||||
export const getClientTimezone = () => {
|
||||
const defaultTz = dayjs.tz.guess()
|
||||
const timezone = ALL_TIMEZONES.find((option) => {
|
||||
if (option.utc.includes(defaultTz)) return option
|
||||
else return undefined
|
||||
})
|
||||
return timezone
|
||||
}
|
||||
|
||||
export const getTimezoneOffsetText = (timezone: Timezone) => {
|
||||
return timezone.text.split(')')[0].slice(1)
|
||||
}
|
||||
|
||||
export const getTimezoneOffset = (timezone: Timezone) => {
|
||||
return timezone.text.split(')')[0].slice(4)
|
||||
}
|
||||
|
||||
export const convertTimeStringtoUnixS = (datetime: string, timezone: Timezone) => {
|
||||
// Datetime in the format of YYYY-MM-DDTHH:mm:ss (from datetime-local input)
|
||||
const timezoneOffset = getTimezoneOffset(timezone)
|
||||
const datetimestringWithTimezone = `${datetime}${timezoneOffset}`
|
||||
return dayjs(datetimestringWithTimezone).unix()
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, FormEvent, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { IconSearch, Input, Listbox, Popover, Button, Modal } from '@supabase/ui'
|
||||
|
||||
import { post } from 'lib/common/fetch'
|
||||
import { API_URL, PROJECT_STATUS } from 'lib/constants'
|
||||
import { useStore, useFlag } from 'hooks'
|
||||
import Loading from 'components/ui/Loading'
|
||||
import UpgradeToPro from 'components/ui/UpgradeToPro'
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
|
||||
import BackupsError from '../BackupsError'
|
||||
import BackupsEmpty from '../BackupsEmpty'
|
||||
import { ALL_TIMEZONES } from './PITRBackupSelection.constants'
|
||||
import {
|
||||
getClientTimezone,
|
||||
getTimezoneOffsetText,
|
||||
convertTimeStringtoUnixS,
|
||||
} from './PITRBackupSelection.utils'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const PITRBackupSelection: FC<Props> = () => {
|
||||
const router = useRouter()
|
||||
const { app, ui, backups } = useStore()
|
||||
const isAvailable = useFlag('pitrBackups')
|
||||
|
||||
if (!isAvailable) {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded border border-gray-500 bg-gray-300 py-8">
|
||||
<p className="text-scale-1000 text-sm">Coming soon</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const [searchString, setSearchString] = useState<string>('')
|
||||
const [selectedTimezone, setSelectedTimezone] = useState<any>(getClientTimezone())
|
||||
const [showConfirmation, setShowConfirmation] = useState<boolean>(false)
|
||||
|
||||
const [errors, setErrors] = useState<any>()
|
||||
const [recoveryPoint, setRecoveryPoint] = useState<string>()
|
||||
|
||||
const { configuration } = backups
|
||||
const projectId = ui.selectedProject?.id ?? -1
|
||||
const projectRef = ui.selectedProject?.ref ?? 'default'
|
||||
|
||||
if (backups.isLoading) {
|
||||
return <Loading />
|
||||
} else if (backups.error) {
|
||||
return <BackupsError />
|
||||
}
|
||||
|
||||
const hasPhysicalBackups =
|
||||
configuration.physicalBackupData.earliestPhysicalBackupDateUnix !== null &&
|
||||
configuration.physicalBackupData.latestPhysicalBackupDateUnix !== null &&
|
||||
configuration.physicalBackupData.earliestPhysicalBackupDateUnix !==
|
||||
configuration.physicalBackupData.latestPhysicalBackupDateUnix
|
||||
|
||||
if (!configuration.walg_enabled) {
|
||||
// Using this check as opposed to checking price tier to allow enabling PITR for our own internal projects
|
||||
return (
|
||||
<UpgradeToPro
|
||||
primaryText="Free Plan does not include project backups."
|
||||
projectRef={projectRef}
|
||||
secondaryText="Please upgrade to Pro plan for access to point in time recovery backups."
|
||||
/>
|
||||
)
|
||||
} else if (!hasPhysicalBackups) {
|
||||
return <BackupsEmpty />
|
||||
}
|
||||
|
||||
const timezoneOptions =
|
||||
searchString.length > 0
|
||||
? ALL_TIMEZONES.map((option) => option.text).filter((option) =>
|
||||
option.toLowerCase().includes(searchString.toLowerCase())
|
||||
)
|
||||
: ALL_TIMEZONES.map((option) => option.text)
|
||||
|
||||
const earliestAvailableBackup = dayjs
|
||||
.unix(configuration.physicalBackupData.earliestPhysicalBackupDateUnix)
|
||||
.tz(selectedTimezone.utc[0])
|
||||
.format('YYYY-MM-DDTHH:mm:ss')
|
||||
|
||||
const latestAvailableBackup = dayjs
|
||||
.unix(configuration.physicalBackupData.latestPhysicalBackupDateUnix)
|
||||
.tz(selectedTimezone.utc[0])
|
||||
.format('YYYY-MM-DDTHH:mm:ss')
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
// Validation
|
||||
if (!recoveryPoint) {
|
||||
setErrors({ recoveryPoint: 'Please enter a date to recover your project from' })
|
||||
} else {
|
||||
const recoveryTimeTargetUnix = convertTimeStringtoUnixS(recoveryPoint, selectedTimezone)
|
||||
const { earliestPhysicalBackupDateUnix, latestPhysicalBackupDateUnix } =
|
||||
configuration.physicalBackupData
|
||||
|
||||
const isOutOfRange =
|
||||
recoveryTimeTargetUnix < earliestPhysicalBackupDateUnix ||
|
||||
recoveryTimeTargetUnix > latestPhysicalBackupDateUnix
|
||||
|
||||
if (isOutOfRange) {
|
||||
setErrors({
|
||||
recoveryPoint: 'Selected date is out of range for available points of recovery',
|
||||
})
|
||||
} else {
|
||||
setErrors(undefined)
|
||||
setShowConfirmation(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirmRestore = async () => {
|
||||
if (!recoveryPoint) return
|
||||
|
||||
const recoveryTimeTargetUnix = convertTimeStringtoUnixS(recoveryPoint, selectedTimezone)
|
||||
const { error } = await post(`${API_URL}/database/${projectRef}/backups/pitr`, {
|
||||
recovery_time_target_unix: recoveryTimeTargetUnix,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Point in time recovery for project failed`,
|
||||
})
|
||||
setShowConfirmation(false)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setShowConfirmation(false)
|
||||
app.onProjectStatusUpdated(projectId, PROJECT_STATUS.RESTORING)
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Restoring database back to ${dayjs(recoveryPoint).format(
|
||||
'DD MMM YYYY, HH:mm:ss'
|
||||
)} (
|
||||
${getTimezoneOffsetText(selectedTimezone)})`,
|
||||
})
|
||||
router.push(`/project/${projectRef}`)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 rounded border border-gray-400 border-opacity-50 bg-gray-300 py-3">
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="flex justify-between space-x-8">
|
||||
<p className="w-2/5 pl-4 text-sm">Select timezone</p>
|
||||
<div className="w-3/5 px-4">
|
||||
<Listbox
|
||||
value={selectedTimezone.text}
|
||||
onChange={(text) => {
|
||||
const selectedTimezone = ALL_TIMEZONES.find((option) => option.text === text)
|
||||
setSelectedTimezone(selectedTimezone)
|
||||
}}
|
||||
onBlur={() => setSearchString('')}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'fixed top-0 flex w-full items-center',
|
||||
'rounded-t-md border-b border-gray-600 bg-gray-500',
|
||||
'mb-4 space-x-2 px-4 py-2',
|
||||
].join(' ')}
|
||||
style={{ zIndex: 1 }}
|
||||
>
|
||||
<IconSearch size={14} />
|
||||
<input
|
||||
autoFocus
|
||||
className="placeholder-scale-1000 w-72 bg-transparent text-sm outline-none"
|
||||
value={searchString}
|
||||
placeholder={''}
|
||||
onChange={(e: FormEvent<HTMLInputElement>) =>
|
||||
setSearchString(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{/* Whitespace to shift listbox options down for searchfield */}
|
||||
<div className="h-8" />
|
||||
{timezoneOptions.map((option) => {
|
||||
return (
|
||||
<Listbox.Option key={option} label={option} value={option}>
|
||||
<div>{option}</div>
|
||||
</Listbox.Option>
|
||||
)
|
||||
})}
|
||||
{timezoneOptions.length === 0 && (
|
||||
<Listbox.Option disabled key="no-results" label="" value="">
|
||||
No timezones found
|
||||
</Listbox.Option>
|
||||
)}
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between space-x-8">
|
||||
<div className="w-2/5 space-y-2 pl-4">
|
||||
<p className="text-sm">Earliest point of recovery</p>
|
||||
</div>
|
||||
<div className="w-3/5">
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
step={1}
|
||||
type="datetime-local"
|
||||
className="px-4"
|
||||
value={earliestAvailableBackup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between space-x-8">
|
||||
<div className="w-2/5 space-y-2 pl-4">
|
||||
<p className="text-sm">Latest point of recovery</p>
|
||||
</div>
|
||||
<div className="w-3/5">
|
||||
<Input
|
||||
readOnly
|
||||
disabled
|
||||
step={1}
|
||||
type="datetime-local"
|
||||
className="px-4"
|
||||
value={latestAvailableBackup}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Popover.Seperator />
|
||||
<div className="flex justify-between space-x-8 py-2">
|
||||
<div className="w-2/5 space-y-2 pl-4">
|
||||
<p className="text-sm">Recovery point</p>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Select a date and time that you would like to restore your project to
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-3/5">
|
||||
<Input
|
||||
step={1}
|
||||
type="datetime-local"
|
||||
error={errors?.recoveryPoint}
|
||||
className="px-4"
|
||||
onChange={(e) => {
|
||||
setErrors(undefined)
|
||||
setRecoveryPoint(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Popover.Seperator />
|
||||
<div className="flex items-center justify-end px-4">
|
||||
<Button type="default">Restore</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<ConfirmationModal
|
||||
visible={showConfirmation}
|
||||
size="medium"
|
||||
header="Confirm to restore"
|
||||
children={
|
||||
<Modal.Content>
|
||||
<div className="space-y-2 py-4">
|
||||
<div className="space-y-1">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Are you sure you want to restore your database from:
|
||||
</p>
|
||||
<p className="text-scale-1200 text-sm">
|
||||
{dayjs(recoveryPoint).format('DD MMM YYYY, HH:mm:ss')} (
|
||||
{getTimezoneOffsetText(selectedTimezone)})?
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
This will destroy any new data written since this backup was made.
|
||||
</p>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
}
|
||||
buttonLabel="Restore"
|
||||
buttonLoadingLabel="Restoring"
|
||||
onSelectCancel={() => setShowConfirmation(false)}
|
||||
onSelectConfirm={onConfirmRestore}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PITRBackupSelection
|
||||
@@ -10,6 +10,7 @@ import PublicationsList from './Publications/PublicationsList'
|
||||
import PublicationsTables from './Publications/PublicationsTables'
|
||||
|
||||
import BackupsList from './Backups/BackupsList'
|
||||
import PITRBackupSelection from './Backups/PITRBackupSelection'
|
||||
|
||||
import ConnectionPooling from './Pooling/ConnectionPooling'
|
||||
|
||||
@@ -28,6 +29,7 @@ export {
|
||||
PublicationsList,
|
||||
PublicationsTables,
|
||||
BackupsList,
|
||||
PITRBackupSelection,
|
||||
ConnectionPooling,
|
||||
CreateTrigger,
|
||||
DeleteTrigger,
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const DatabaseLayout: FC<Props> = ({ title, children }) => {
|
||||
const { meta, ui } = useStore()
|
||||
const { meta, ui, backups } = useStore()
|
||||
const { isInitialized, isLoading, error } = meta.tables
|
||||
const project = ui.selectedProject
|
||||
|
||||
@@ -33,9 +33,13 @@ const DatabaseLayout: FC<Props> = ({ title, children }) => {
|
||||
meta.triggers.load()
|
||||
meta.extensions.load()
|
||||
meta.publications.load()
|
||||
|
||||
backups.load()
|
||||
}
|
||||
}, [ui.selectedProject])
|
||||
|
||||
// Optimization required: load logic should be at the page level
|
||||
// e.g backups page is waiting for meta.tables to load finish when it doesnt even need that data
|
||||
useEffect(() => {
|
||||
if (!isLoading && !loaded) {
|
||||
setLoaded(true)
|
||||
|
||||
@@ -29,63 +29,63 @@ export const generateDatabaseMenu = (project?: Project): ProductMenuGroup[] => {
|
||||
{
|
||||
name: 'Backups',
|
||||
key: 'backups',
|
||||
url: `/project/${ref}/database/backups`,
|
||||
url: `/project/${ref}/database/backups/scheduled`,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
title: 'Logs',
|
||||
items: [
|
||||
{
|
||||
name: 'API logs',
|
||||
key: 'api-logs',
|
||||
url: `/project/${ref}/database/api-logs`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Postgres logs',
|
||||
key: 'postgres-logs',
|
||||
url: `/project/${ref}/database/postgres-logs`,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
title: 'Logs',
|
||||
items: [
|
||||
{
|
||||
name: 'API logs',
|
||||
key: 'api-logs',
|
||||
url: `/project/${ref}/database/api-logs`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Postgres logs',
|
||||
key: 'postgres-logs',
|
||||
url: `/project/${ref}/database/postgres-logs`,
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(IS_PLATFORM
|
||||
? [
|
||||
{
|
||||
title: 'Alpha Preview',
|
||||
isPreview: true,
|
||||
items: [
|
||||
{
|
||||
name: 'Triggers',
|
||||
key: 'triggers',
|
||||
url: `/project/${ref}/database/triggers`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Functions',
|
||||
key: 'functions',
|
||||
url: `/project/${ref}/database/functions`,
|
||||
items: [],
|
||||
},
|
||||
...(showHooksRoute
|
||||
? [
|
||||
{
|
||||
name: 'Function Hooks',
|
||||
key: 'hooks',
|
||||
url: `/project/${ref}/database/hooks`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
title: 'Alpha Preview',
|
||||
isPreview: true,
|
||||
items: [
|
||||
{
|
||||
name: 'Triggers',
|
||||
key: 'triggers',
|
||||
url: `/project/${ref}/database/triggers`,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
name: 'Functions',
|
||||
key: 'functions',
|
||||
url: `/project/${ref}/database/functions`,
|
||||
items: [],
|
||||
},
|
||||
...(showHooksRoute
|
||||
? [
|
||||
{
|
||||
name: 'Function Hooks',
|
||||
key: 'hooks',
|
||||
url: `/project/${ref}/database/hooks`,
|
||||
items: [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
project: Project
|
||||
}
|
||||
|
||||
const ProjectRestartingState: FC<Props> = ({ project }) => {
|
||||
const ConnectingState: FC<Props> = ({ project }) => {
|
||||
const { app } = useStore()
|
||||
const checkProjectConnectionIntervalRef = useRef<number>()
|
||||
|
||||
@@ -146,4 +146,4 @@ const ProjectRestartingState: FC<Props> = ({ project }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ProjectRestartingState)
|
||||
export default observer(ConnectingState)
|
||||
@@ -8,13 +8,13 @@ export default function EmptyPageState({ error }: any) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col space-y-6 items-center justify-center mx-auto">
|
||||
<div className="w-[320px] flex flex-col items-center justify-center space-y-3">
|
||||
<div className="mx-auto flex h-full w-full flex-col items-center justify-center space-y-6">
|
||||
<div className="flex w-[320px] flex-col items-center justify-center space-y-3">
|
||||
<Typography.Title level={3}>Something went wrong 🤕</Typography.Title>
|
||||
<Typography.Text className="text-center">
|
||||
<p className="text-scale-1100 text-center text-sm">
|
||||
Sorry about that, please try again later or feel free to reach out to us if the problem
|
||||
persists.
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/">
|
||||
@@ -28,9 +28,9 @@ export default function EmptyPageState({ error }: any) {
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<Typography.Text type="secondary" small>
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Error: [{error?.code}] {error?.message}
|
||||
</Typography.Text>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ const ProductMenuItem: FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={url}>
|
||||
<Link href={url} passHref>
|
||||
<a className="block" target={target}>
|
||||
{menuItem}
|
||||
</a>
|
||||
|
||||
@@ -11,20 +11,20 @@ interface Props {
|
||||
const UpgradeToPro: FC<Props> = ({ primaryText, projectRef, secondaryText }) => (
|
||||
<div
|
||||
className={[
|
||||
'block w-full py-4 px-6 border rounded border-opacity-20',
|
||||
'bg-gray-100 border-gray-600',
|
||||
'dark:bg-gray-400 dark:border-gray-300',
|
||||
'block w-full rounded border border-opacity-20 py-4 px-6',
|
||||
'border-gray-600 bg-gray-100',
|
||||
'dark:border-gray-300 dark:bg-gray-400',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="flex space-x-3">
|
||||
<div className="mt-1">
|
||||
<IconClock size="large" />
|
||||
</div>
|
||||
<div className="flex justify-between w-full items-center">
|
||||
<div>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p>{primaryText}</p>
|
||||
<div>
|
||||
<p className="text-sm text-scale-1100">{secondaryText}</p>
|
||||
<p className="text-scale-1100 text-sm">{secondaryText}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link href={`/project/${projectRef}/settings/billing`}>
|
||||
|
||||
Generated
+7
-7
@@ -22,7 +22,7 @@
|
||||
"@supabase/react-data-grid": "^7.1.0-beta.7",
|
||||
"@supabase/shared-types": "^0.1.24",
|
||||
"@supabase/supabase-js": "^1.35.3",
|
||||
"@supabase/ui": "0.37.0-alpha.58",
|
||||
"@supabase/ui": "0.37.0-alpha.65",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
@@ -8365,9 +8365,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/ui": {
|
||||
"version": "0.37.0-alpha.58",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.58.tgz",
|
||||
"integrity": "sha512-owL7tiqmNUtIRlv/dZVh29LvcgsftDciDjOjG/3bpZ59xgZl24MmznQ6EIaJaZ6dQzCKIX+LqMLM+QSo/StveA==",
|
||||
"version": "0.37.0-alpha.65",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.65.tgz",
|
||||
"integrity": "sha512-11Pm/Q2j7JnhHtZ8uZ/tXF7uHenT4tJU9LCpfiHFgiPI91C9Kt0s4QvLiYehNLr8TxJ/rETz5uM5eODY6t4wmw==",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.0.0",
|
||||
"@mertasan/tailwindcss-variables": "^2.0.1",
|
||||
@@ -35163,9 +35163,9 @@
|
||||
}
|
||||
},
|
||||
"@supabase/ui": {
|
||||
"version": "0.37.0-alpha.58",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.58.tgz",
|
||||
"integrity": "sha512-owL7tiqmNUtIRlv/dZVh29LvcgsftDciDjOjG/3bpZ59xgZl24MmznQ6EIaJaZ6dQzCKIX+LqMLM+QSo/StveA==",
|
||||
"version": "0.37.0-alpha.65",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ui/-/ui-0.37.0-alpha.65.tgz",
|
||||
"integrity": "sha512-11Pm/Q2j7JnhHtZ8uZ/tXF7uHenT4tJU9LCpfiHFgiPI91C9Kt0s4QvLiYehNLr8TxJ/rETz5uM5eODY6t4wmw==",
|
||||
"requires": {
|
||||
"@headlessui/react": "^1.0.0",
|
||||
"@mertasan/tailwindcss-variables": "^2.0.1",
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@
|
||||
"@supabase/react-data-grid": "^7.1.0-beta.7",
|
||||
"@supabase/shared-types": "^0.1.24",
|
||||
"@supabase/supabase-js": "^1.35.3",
|
||||
"@supabase/ui": "0.37.0-alpha.58",
|
||||
"@supabase/ui": "0.37.0-alpha.65",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
"awesome-debounce-promise": "^2.1.0",
|
||||
|
||||
@@ -36,8 +36,8 @@ import PageTelemetry from 'components/ui/PageTelemetry'
|
||||
import FlagProvider from 'components/ui/Flag/FlagProvider'
|
||||
|
||||
dayjs.extend(customParseFormat)
|
||||
dayjs.extend(timezone)
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const [rootStore] = useState(() => new RootStore())
|
||||
|
||||
@@ -16,5 +16,5 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
}
|
||||
|
||||
const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return res.status(200).json({ tierId: undefined, backups: [] })
|
||||
return res.status(200).json({ tierKey: 'FREE', backups: [] })
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
import { DatabaseLayout } from 'components/layouts'
|
||||
import { BackupsList } from 'components/interfaces/Database'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const DatabaseBackups: NextPageWithLayout = () => {
|
||||
return (
|
||||
<div className="flex">
|
||||
<div className="my-2 mx-auto w-full max-w-4xl space-y-8 p-4">
|
||||
<h3 className="text-2xl">Backups</h3>
|
||||
<div className="space-y-4">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Projects are backed up daily and can be restored at any time.
|
||||
</p>
|
||||
<BackupsList />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseBackups.getLayout = (page) => <DatabaseLayout title="Database">{page}</DatabaseLayout>
|
||||
|
||||
export default observer(DatabaseBackups)
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Tabs } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { DatabaseLayout } from 'components/layouts'
|
||||
import { PITRBackupSelection } from 'components/interfaces/Database'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const DatabaseScheduledBackups: NextPageWithLayout = () => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const ref = ui.selectedProject?.ref ?? 'default'
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl space-y-6 px-5 pt-12 pb-20">
|
||||
<h3 className="text-scale-1200 text-xl">Backups</h3>
|
||||
|
||||
<Tabs
|
||||
type="underlined"
|
||||
activeId="pitr"
|
||||
onChange={(id: any) => {
|
||||
if (id === 'scheduled') router.push(`/project/${ref}/database/backups/scheduled`)
|
||||
}}
|
||||
>
|
||||
<Tabs.Panel id="scheduled" label="Scheduled" />
|
||||
<Tabs.Panel id="pitr" label="Point in Time" />
|
||||
</Tabs>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Restore your project from a specific date and time.
|
||||
</p>
|
||||
<PITRBackupSelection />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseScheduledBackups.getLayout = (page) => (
|
||||
<DatabaseLayout title="Database">{page}</DatabaseLayout>
|
||||
)
|
||||
|
||||
export default observer(DatabaseScheduledBackups)
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Tabs } from '@supabase/ui'
|
||||
|
||||
import { useStore } from 'hooks'
|
||||
import { DatabaseLayout } from 'components/layouts'
|
||||
import { BackupsList } from 'components/interfaces/Database'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const DatabaseScheduledBackups: NextPageWithLayout = () => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
const ref = ui.selectedProject?.ref
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl space-y-6 px-5 pt-12 pb-20">
|
||||
<h3 className="text-scale-1200 text-xl">Backups</h3>
|
||||
|
||||
<Tabs
|
||||
type="underlined"
|
||||
activeId="scheduled"
|
||||
onChange={(id: any) => {
|
||||
if (id === 'pitr') router.push(`/project/${ref}/database/backups/pitr`)
|
||||
}}
|
||||
>
|
||||
<Tabs.Panel id="scheduled" label="Scheduled" />
|
||||
<Tabs.Panel id="pitr" label="Point in Time" />
|
||||
</Tabs>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-scale-1100 text-sm">
|
||||
Projects are backed up daily around midnight of your project's region and can be restored
|
||||
at any time.
|
||||
</p>
|
||||
<BackupsList />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseScheduledBackups.getLayout = (page) => (
|
||||
<DatabaseLayout title="Database">{page}</DatabaseLayout>
|
||||
)
|
||||
|
||||
export default observer(DatabaseScheduledBackups)
|
||||
+16
-10
@@ -3,8 +3,9 @@ import { Project } from 'types'
|
||||
import AppStore, { IAppStore } from './app/AppStore'
|
||||
import MetaStore, { IMetaStore } from './pgmeta/MetaStore'
|
||||
import UiStore, { IUiStore } from './UiStore'
|
||||
import ProjectContentStore, { IProjectContentStore } from './content/ProjectContentStore'
|
||||
import ProjectFunctionsStore, { IProjectFunctionsStore } from './functions/ProjectFunctionsStore'
|
||||
import ProjectContentStore, { IProjectContentStore } from './project/ProjectContentStore'
|
||||
import ProjectFunctionsStore, { IProjectFunctionsStore } from './project/ProjectFunctionsStore'
|
||||
import ProjectBackupsStore, { IProjectBackupsStore } from './project/ProjectBackupsStore'
|
||||
|
||||
// Temporary disable mobx warnings
|
||||
// TODO: need to remove this after refactoring old stores.
|
||||
@@ -13,31 +14,35 @@ configure({
|
||||
})
|
||||
|
||||
export interface IRootStore {
|
||||
app: IAppStore
|
||||
ui: IUiStore
|
||||
meta: IMetaStore
|
||||
content: IProjectContentStore
|
||||
functions: IProjectFunctionsStore
|
||||
meta: IMetaStore
|
||||
app: IAppStore
|
||||
backups: IProjectBackupsStore
|
||||
setProjectRef: (value?: string) => void
|
||||
setOrganizationSlug: (value?: string) => void
|
||||
}
|
||||
export class RootStore implements IRootStore {
|
||||
app: IAppStore
|
||||
ui: IUiStore
|
||||
meta: IMetaStore
|
||||
content: IProjectContentStore
|
||||
functions: IProjectFunctionsStore
|
||||
meta: IMetaStore
|
||||
app: IAppStore
|
||||
backups: IProjectBackupsStore
|
||||
|
||||
constructor() {
|
||||
this.app = new AppStore(this)
|
||||
this.ui = new UiStore(this)
|
||||
// @ts-ignore
|
||||
this.content = new ProjectContentStore(this, { projectRef: '' })
|
||||
this.functions = new ProjectFunctionsStore(this, { projectRef: '' })
|
||||
this.meta = new MetaStore(this, {
|
||||
projectRef: '',
|
||||
connectionString: '',
|
||||
})
|
||||
this.app = new AppStore(this)
|
||||
|
||||
// @ts-ignore
|
||||
this.content = new ProjectContentStore(this, { projectRef: '' })
|
||||
this.functions = new ProjectFunctionsStore(this, { projectRef: '' })
|
||||
this.backups = new ProjectBackupsStore(this, { projectRef: '' })
|
||||
|
||||
/**
|
||||
* TODO: meta and content are not observable
|
||||
@@ -87,6 +92,7 @@ export class RootStore implements IRootStore {
|
||||
this.ui.setProjectRef(value)
|
||||
this.functions.setProjectRef(value)
|
||||
this.content.setProjectRef(value)
|
||||
this.backups.setProjectRef(value)
|
||||
}
|
||||
|
||||
setOrganizationSlug(value?: string) {
|
||||
|
||||
@@ -6,9 +6,6 @@ import { constructHeaders } from 'lib/api/apiHelpers'
|
||||
import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/PostgresMetaInterface'
|
||||
|
||||
export interface IDatabaseStore extends IPostgresMetaInterface<any> {
|
||||
getBackups: (
|
||||
projectRef: string
|
||||
) => Promise<{ tierId: any; backups: any[] } | { error: ResponseError }>
|
||||
getPoolingConfiguration: (projectRef: string) => Promise<any | { error: ResponseError }>
|
||||
}
|
||||
|
||||
@@ -24,18 +21,6 @@ export default class DatabaseStore extends PostgresMetaInterface<any> {
|
||||
super(rootStore, dataUrl, headers, options)
|
||||
}
|
||||
|
||||
async getBackups(projectRef: string) {
|
||||
try {
|
||||
const url = `${this.url}/${projectRef}/backups`
|
||||
const headers = constructHeaders(this.headers)
|
||||
const response = await get(url, { headers })
|
||||
if (response.error) throw response.error
|
||||
return response
|
||||
} catch (error: any) {
|
||||
return { error }
|
||||
}
|
||||
}
|
||||
|
||||
async getPoolingConfiguration(projectRef: string) {
|
||||
try {
|
||||
// Need to use project ref -> shouldn;t need to pass it in via argument
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { makeAutoObservable } from 'mobx'
|
||||
import { keyBy } from 'lodash'
|
||||
|
||||
import { get } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { IRootStore } from '../RootStore'
|
||||
|
||||
export interface IProjectBackupsStore {
|
||||
isLoading: boolean
|
||||
isInitialized: boolean
|
||||
isLoaded: boolean
|
||||
error: any
|
||||
|
||||
baseUrl: string
|
||||
projectRef: string
|
||||
configuration: any
|
||||
|
||||
load: () => void
|
||||
list: () => any[]
|
||||
setProjectRef: (ref?: string) => void
|
||||
}
|
||||
|
||||
export default class ProjectBackupsStore implements IProjectBackupsStore {
|
||||
rootStore: IRootStore
|
||||
|
||||
STATES = {
|
||||
INITIAL: 'initial',
|
||||
LOADING: 'loading',
|
||||
ERROR: 'error',
|
||||
LOADED: 'loaded',
|
||||
}
|
||||
|
||||
baseUrl: string
|
||||
projectRef: string
|
||||
data: any = []
|
||||
configuration: any = {}
|
||||
|
||||
state = this.STATES.INITIAL
|
||||
error = null
|
||||
|
||||
constructor(rootStore: IRootStore, options: { projectRef: string }) {
|
||||
const { projectRef } = options
|
||||
this.projectRef = projectRef
|
||||
this.rootStore = rootStore
|
||||
this.baseUrl = ``
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
get isLoading() {
|
||||
return this.state === this.STATES.INITIAL || this.state === this.STATES.LOADING
|
||||
}
|
||||
|
||||
get isInitialized() {
|
||||
return this.state !== this.STATES.INITIAL
|
||||
}
|
||||
|
||||
get isLoaded() {
|
||||
return this.state === this.STATES.LOADED
|
||||
}
|
||||
|
||||
async fetchData() {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
const response = await get(this.baseUrl, { headers })
|
||||
if (response.error) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
const { backups, ...configuration } = response
|
||||
this.data = keyBy(backups, 'id')
|
||||
this.configuration = configuration
|
||||
|
||||
return backups
|
||||
}
|
||||
|
||||
async load() {
|
||||
let { LOADING, ERROR, LOADED } = this.STATES
|
||||
try {
|
||||
this.error = null
|
||||
this.state = LOADING
|
||||
await this.fetchData()
|
||||
this.state = LOADED
|
||||
return this.data
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load project backups', e.message)
|
||||
this.error = e
|
||||
this.state = ERROR
|
||||
}
|
||||
}
|
||||
|
||||
list(filter?: any) {
|
||||
const arr = Object.values(this.data)
|
||||
if (!!filter) {
|
||||
return arr.filter(filter).sort((a: any, b: any) => b.id - a.id)
|
||||
} else {
|
||||
return arr.sort((a: any, b: any) => b.id - a.id)
|
||||
}
|
||||
}
|
||||
|
||||
setProjectRef(ref?: string) {
|
||||
if (ref) {
|
||||
this.projectRef = ref
|
||||
this.baseUrl = `${API_URL}/database/${ref}/backups`
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
-3
@@ -5,9 +5,6 @@ import { get, post, patch, delete_ } from 'lib/common/fetch'
|
||||
import { UserContent, UserContentMap } from 'types'
|
||||
import { IRootStore } from '../RootStore'
|
||||
|
||||
// [Joshen] This will be the new ProjectContentStore
|
||||
// but use the one under the stores folder projectContentStore first while we transition
|
||||
|
||||
export interface IProjectFunctionsStore {
|
||||
isLoading: boolean
|
||||
isInitialized: boolean
|
||||
Reference in new issue
Block a user