diff --git a/studio/components/grid/components/common/ConfirmAlert.tsx b/studio/components/grid/components/common/ConfirmAlert.tsx deleted file mode 100644 index 294e3916cc0..00000000000 --- a/studio/components/grid/components/common/ConfirmAlert.tsx +++ /dev/null @@ -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 - variant?: 'danger' | 'warning' | 'success' | undefined -} -const ConfirmModal: React.FC = ({ - 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 ( - -
- - -
- - } - > - -

{message}

-
-
- ) -} - -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(, divTarget) - } else { - divTarget = document.createElement('div') - divTarget.id = 'supabase-ui-confirm-alert' - document.body.appendChild(divTarget) - render(, divTarget) - } -} - -export function showConfirmAlert(props: ConfirmModalProps) { - createElement(props) -} diff --git a/studio/components/grid/components/common/index.ts b/studio/components/grid/components/common/index.ts index ad34f221222..9ac92bc11a2 100644 --- a/studio/components/grid/components/common/index.ts +++ b/studio/components/grid/components/common/index.ts @@ -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' diff --git a/studio/components/grid/components/footer/pagination/Pagination.tsx b/studio/components/grid/components/footer/pagination/Pagination.tsx index 1c3ef9f5847..8bc26536eed 100644 --- a/studio/components/grid/components/footer/pagination/Pagination.tsx +++ b/studio/components/grid/components/footer/pagination/Pagination.tsx @@ -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 = () => { 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 = () => { 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: () => { diff --git a/studio/components/grid/components/header/Header.tsx b/studio/components/grid/components/header/Header.tsx index 502a6b2c2d7..2ad4c625b2c 100644 --- a/studio/components/grid/components/header/Header.tsx +++ b/studio/components/grid/components/header/Header.tsx @@ -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 = ({}) => { 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 () => { diff --git a/studio/components/grid/components/menu/RowContextMenu.tsx b/studio/components/grid/components/menu/RowContextMenu.tsx index 0db4c8749c0..2ae413641fc 100644 --- a/studio/components/grid/components/menu/RowContextMenu.tsx +++ b/studio/components/grid/components/menu/RowContextMenu.tsx @@ -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 = ({}) => { - 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 = ({}) => { - ); -}; -export default RowContextMenu; + ) +} +export default RowContextMenu diff --git a/studio/components/grid/components/menu/RowMenu.tsx b/studio/components/grid/components/menu/RowMenu.tsx index 36437a43fd8..bff5b03eaab 100644 --- a/studio/components/grid/components/menu/RowMenu.tsx +++ b/studio/components/grid/components/menu/RowMenu.tsx @@ -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 = ({}) => { 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 () => { diff --git a/studio/components/interfaces/Database/Backups/BackupItem.tsx b/studio/components/interfaces/Database/Backups/BackupItem.tsx index a2f1a7ff794..ae530eb0972 100644 --- a/studio/components/interfaces/Database/Backups/BackupItem.tsx +++ b/studio/components/interfaces/Database/Backups/BackupItem.tsx @@ -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 = ({ projectRef, backup, index }) => { const router = useRouter() - const { ui } = useStore() + const { app, ui } = useStore() const [isDownloading, setDownloading] = useState(false) const [isRestoring, setRestoring] = useState(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 = ({ 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 = ({ projectRef, backup, index }) => { } return ( - <> -
-

- {dayjs(backup.inserted_at).format('DD MMM YYYY HH:mm:ss')} -

-
{generateSideButtons(backup)}
-
- +
+

+ {dayjs(backup.inserted_at).format('DD MMM YYYY HH:mm:ss')} +

+
{generateSideButtons(backup)}
+
) } diff --git a/studio/components/interfaces/Database/Backups/BackupsEmpty.tsx b/studio/components/interfaces/Database/Backups/BackupsEmpty.tsx new file mode 100644 index 00000000000..81f1fdb97f9 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/BackupsEmpty.tsx @@ -0,0 +1,14 @@ +import { IconInfo } from '@supabase/ui' + +const BackupsEmpty = () => { + return ( +
+
+ +

No backups created yet - check again tomorrow.

+
+
+ ) +} + +export default BackupsEmpty diff --git a/studio/components/interfaces/Database/Backups/BackupsError.tsx b/studio/components/interfaces/Database/Backups/BackupsError.tsx new file mode 100644 index 00000000000..bc39eee1628 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/BackupsError.tsx @@ -0,0 +1,14 @@ +import { IconInfo } from '@supabase/ui' + +const BackupsError = () => { + return ( +
+
+ +

Failed to retrieve backups for project. Please contact support.

+
+
+ ) +} + +export default BackupsError diff --git a/studio/components/interfaces/Database/Backups/BackupsList.tsx b/studio/components/interfaces/Database/Backups/BackupsList.tsx index 339f0cf3fc1..fad655435fc 100644 --- a/studio/components/interfaces/Database/Backups/BackupsList.tsx +++ b/studio/components/interfaces/Database/Backups/BackupsList.tsx @@ -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 = ({}) => { - const { ui, app } = useStore() + const { ui, backups } = useStore() const projectRef = ui.selectedProject?.ref || 'default' - const [isLoading, setIsLoading] = useState(true) - const [projectData, setProjectData] = useState([]) + if (backups.isLoading) return + if (backups.error) return - 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 - if (projectData.error) { - return ( -
- Error loading backups -
- ) - } - - // 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 ( = ({}) => { return (
- {!sortedBackups?.length && tierId !== STRIPE_PRODUCT_IDS.FREE ? ( -
-
-
- -
-

No backups created yet. Check again tomorrow.

-
-
+ {!sortedBackups?.length && tierKey !== 'FREE' ? ( + ) : ( {sortedBackups?.map((x: any, i: number) => { diff --git a/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.constants.ts b/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.constants.ts new file mode 100644 index 00000000000..c0f1f870270 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.constants.ts @@ -0,0 +1,1204 @@ +// https://github.com/dmfilipenko/timezones.json/blob/master/timezones.json +export const ALL_TIMEZONES = [ + { + value: 'Dateline Standard Time', + abbr: 'DST', + offset: -12, + isdst: false, + text: '(UTC-12:00) International Date Line West', + utc: ['Etc/GMT+12'], + }, + { + value: 'UTC-11', + abbr: 'U', + offset: -11, + isdst: false, + text: '(UTC-11:00) Coordinated Universal Time-11', + utc: ['Etc/GMT+11', 'Pacific/Midway', 'Pacific/Niue', 'Pacific/Pago_Pago'], + }, + { + value: 'Hawaiian Standard Time', + abbr: 'HST', + offset: -10, + isdst: false, + text: '(UTC-10:00) Hawaii', + utc: [ + 'Etc/GMT+10', + 'Pacific/Honolulu', + 'Pacific/Johnston', + 'Pacific/Rarotonga', + 'Pacific/Tahiti', + ], + }, + { + value: 'Alaskan Standard Time', + abbr: 'AKDT', + offset: -8, + isdst: true, + text: '(UTC-09:00) Alaska', + utc: [ + 'America/Anchorage', + 'America/Juneau', + 'America/Nome', + 'America/Sitka', + 'America/Yakutat', + ], + }, + { + value: 'Pacific Standard Time (Mexico)', + abbr: 'PDT', + offset: -7, + isdst: true, + text: '(UTC-08:00) Baja California', + utc: ['America/Santa_Isabel'], + }, + { + value: 'Pacific Daylight Time', + abbr: 'PDT', + offset: -7, + isdst: true, + text: '(UTC-07:00) Pacific Daylight Time (US & Canada)', + utc: ['America/Los_Angeles', 'America/Tijuana', 'America/Vancouver'], + }, + { + value: 'Pacific Standard Time', + abbr: 'PST', + offset: -8, + isdst: false, + text: '(UTC-08:00) Pacific Standard Time (US & Canada)', + utc: ['America/Los_Angeles', 'America/Tijuana', 'America/Vancouver', 'PST8PDT'], + }, + { + value: 'US Mountain Standard Time', + abbr: 'UMST', + offset: -7, + isdst: false, + text: '(UTC-07:00) Arizona', + utc: [ + 'America/Creston', + 'America/Dawson', + 'America/Dawson_Creek', + 'America/Hermosillo', + 'America/Phoenix', + 'America/Whitehorse', + 'Etc/GMT+7', + ], + }, + { + value: 'Mountain Standard Time (Mexico)', + abbr: 'MDT', + offset: -6, + isdst: true, + text: '(UTC-07:00) Chihuahua, La Paz, Mazatlan', + utc: ['America/Chihuahua', 'America/Mazatlan'], + }, + { + value: 'Mountain Standard Time', + abbr: 'MDT', + offset: -6, + isdst: true, + text: '(UTC-07:00) Mountain Time (US & Canada)', + utc: [ + 'America/Boise', + 'America/Cambridge_Bay', + 'America/Denver', + 'America/Edmonton', + 'America/Inuvik', + 'America/Ojinaga', + 'America/Yellowknife', + 'MST7MDT', + ], + }, + { + value: 'Central America Standard Time', + abbr: 'CAST', + offset: -6, + isdst: false, + text: '(UTC-06:00) Central America', + utc: [ + 'America/Belize', + 'America/Costa_Rica', + 'America/El_Salvador', + 'America/Guatemala', + 'America/Managua', + 'America/Tegucigalpa', + 'Etc/GMT+6', + 'Pacific/Galapagos', + ], + }, + { + value: 'Central Standard Time', + abbr: 'CDT', + offset: -5, + isdst: true, + text: '(UTC-06:00) Central Time (US & Canada)', + utc: [ + 'America/Chicago', + 'America/Indiana/Knox', + 'America/Indiana/Tell_City', + 'America/Matamoros', + 'America/Menominee', + 'America/North_Dakota/Beulah', + 'America/North_Dakota/Center', + 'America/North_Dakota/New_Salem', + 'America/Rainy_River', + 'America/Rankin_Inlet', + 'America/Resolute', + 'America/Winnipeg', + 'CST6CDT', + ], + }, + { + value: 'Central Standard Time (Mexico)', + abbr: 'CDT', + offset: -5, + isdst: true, + text: '(UTC-06:00) Guadalajara, Mexico City, Monterrey', + utc: [ + 'America/Bahia_Banderas', + 'America/Cancun', + 'America/Merida', + 'America/Mexico_City', + 'America/Monterrey', + ], + }, + { + value: 'Canada Central Standard Time', + abbr: 'CCST', + offset: -6, + isdst: false, + text: '(UTC-06:00) Saskatchewan', + utc: ['America/Regina', 'America/Swift_Current'], + }, + { + value: 'SA Pacific Standard Time', + abbr: 'SPST', + offset: -5, + isdst: false, + text: '(UTC-05:00) Bogota, Lima, Quito', + utc: [ + 'America/Bogota', + 'America/Cayman', + 'America/Coral_Harbour', + 'America/Eirunepe', + 'America/Guayaquil', + 'America/Jamaica', + 'America/Lima', + 'America/Panama', + 'America/Rio_Branco', + 'Etc/GMT+5', + ], + }, + { + value: 'Eastern Standard Time', + abbr: 'EST', + offset: -5, + isdst: false, + text: '(UTC-05:00) Eastern Time (US & Canada)', + utc: [ + 'America/Detroit', + 'America/Havana', + 'America/Indiana/Petersburg', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Iqaluit', + 'America/Kentucky/Monticello', + 'America/Louisville', + 'America/Montreal', + 'America/Nassau', + 'America/New_York', + 'America/Nipigon', + 'America/Pangnirtung', + 'America/Port-au-Prince', + 'America/Thunder_Bay', + 'America/Toronto', + ], + }, + { + value: 'Eastern Daylight Time', + abbr: 'EDT', + offset: -4, + isdst: true, + text: '(UTC-04:00) Eastern Daylight Time (US & Canada)', + utc: [ + 'America/Detroit', + 'America/Havana', + 'America/Indiana/Petersburg', + 'America/Indiana/Vincennes', + 'America/Indiana/Winamac', + 'America/Iqaluit', + 'America/Kentucky/Monticello', + 'America/Louisville', + 'America/Montreal', + 'America/Nassau', + 'America/New_York', + 'America/Nipigon', + 'America/Pangnirtung', + 'America/Port-au-Prince', + 'America/Thunder_Bay', + 'America/Toronto', + ], + }, + { + value: 'US Eastern Standard Time', + abbr: 'UEDT', + offset: -5, + isdst: false, + text: '(UTC-05:00) Indiana (East)', + utc: ['America/Indiana/Marengo', 'America/Indiana/Vevay', 'America/Indianapolis'], + }, + { + value: 'Venezuela Standard Time', + abbr: 'VST', + offset: -4.5, + isdst: false, + text: '(UTC-04:30) Caracas', + utc: ['America/Caracas'], + }, + { + value: 'Paraguay Standard Time', + abbr: 'PYT', + offset: -4, + isdst: false, + text: '(UTC-04:00) Asuncion', + utc: ['America/Asuncion'], + }, + { + value: 'Atlantic Standard Time', + abbr: 'ADT', + offset: -3, + isdst: true, + text: '(UTC-04:00) Atlantic Time (Canada)', + utc: [ + 'America/Glace_Bay', + 'America/Goose_Bay', + 'America/Halifax', + 'America/Moncton', + 'America/Thule', + 'Atlantic/Bermuda', + ], + }, + { + value: 'Central Brazilian Standard Time', + abbr: 'CBST', + offset: -4, + isdst: false, + text: '(UTC-04:00) Cuiaba', + utc: ['America/Campo_Grande', 'America/Cuiaba'], + }, + { + value: 'SA Western Standard Time', + abbr: 'SWST', + offset: -4, + isdst: false, + text: '(UTC-04:00) Georgetown, La Paz, Manaus, San Juan', + utc: [ + 'America/Anguilla', + 'America/Antigua', + 'America/Aruba', + 'America/Barbados', + 'America/Blanc-Sablon', + 'America/Boa_Vista', + 'America/Curacao', + 'America/Dominica', + 'America/Grand_Turk', + 'America/Grenada', + 'America/Guadeloupe', + 'America/Guyana', + 'America/Kralendijk', + 'America/La_Paz', + 'America/Lower_Princes', + 'America/Manaus', + 'America/Marigot', + 'America/Martinique', + 'America/Montserrat', + 'America/Port_of_Spain', + 'America/Porto_Velho', + 'America/Puerto_Rico', + 'America/Santo_Domingo', + 'America/St_Barthelemy', + 'America/St_Kitts', + 'America/St_Lucia', + 'America/St_Thomas', + 'America/St_Vincent', + 'America/Tortola', + 'Etc/GMT+4', + ], + }, + { + value: 'Pacific SA Standard Time', + abbr: 'PSST', + offset: -4, + isdst: false, + text: '(UTC-04:00) Santiago', + utc: ['America/Santiago', 'Antarctica/Palmer'], + }, + { + value: 'Newfoundland Standard Time', + abbr: 'NDT', + offset: -2.5, + isdst: true, + text: '(UTC-03:30) Newfoundland', + utc: ['America/St_Johns'], + }, + { + value: 'E. South America Standard Time', + abbr: 'ESAST', + offset: -3, + isdst: false, + text: '(UTC-03:00) Brasilia', + utc: ['America/Sao_Paulo'], + }, + { + value: 'Argentina Standard Time', + abbr: 'AST', + offset: -3, + isdst: false, + text: '(UTC-03:00) Buenos Aires', + utc: [ + 'America/Argentina/La_Rioja', + 'America/Argentina/Rio_Gallegos', + 'America/Argentina/Salta', + 'America/Argentina/San_Juan', + 'America/Argentina/San_Luis', + 'America/Argentina/Tucuman', + 'America/Argentina/Ushuaia', + 'America/Buenos_Aires', + 'America/Catamarca', + 'America/Cordoba', + 'America/Jujuy', + 'America/Mendoza', + ], + }, + { + value: 'SA Eastern Standard Time', + abbr: 'SEST', + offset: -3, + isdst: false, + text: '(UTC-03:00) Cayenne, Fortaleza', + utc: [ + 'America/Araguaina', + 'America/Belem', + 'America/Cayenne', + 'America/Fortaleza', + 'America/Maceio', + 'America/Paramaribo', + 'America/Recife', + 'America/Santarem', + 'Antarctica/Rothera', + 'Atlantic/Stanley', + 'Etc/GMT+3', + ], + }, + { + value: 'Greenland Standard Time', + abbr: 'GDT', + offset: -3, + isdst: true, + text: '(UTC-03:00) Greenland', + utc: ['America/Godthab'], + }, + { + value: 'Montevideo Standard Time', + abbr: 'MST', + offset: -3, + isdst: false, + text: '(UTC-03:00) Montevideo', + utc: ['America/Montevideo'], + }, + { + value: 'Bahia Standard Time', + abbr: 'BST', + offset: -3, + isdst: false, + text: '(UTC-03:00) Salvador', + utc: ['America/Bahia'], + }, + { + value: 'UTC-02', + abbr: 'U', + offset: -2, + isdst: false, + text: '(UTC-02:00) Coordinated Universal Time-02', + utc: ['America/Noronha', 'Atlantic/South_Georgia', 'Etc/GMT+2'], + }, + { + value: 'Mid-Atlantic Standard Time', + abbr: 'MDT', + offset: -1, + isdst: true, + text: '(UTC-02:00) Mid-Atlantic - Old', + utc: [], + }, + { + value: 'Azores Standard Time', + abbr: 'ADT', + offset: 0, + isdst: true, + text: '(UTC-01:00) Azores', + utc: ['America/Scoresbysund', 'Atlantic/Azores'], + }, + { + value: 'Cape Verde Standard Time', + abbr: 'CVST', + offset: -1, + isdst: false, + text: '(UTC-01:00) Cape Verde Is.', + utc: ['Atlantic/Cape_Verde', 'Etc/GMT+1'], + }, + { + value: 'Morocco Standard Time', + abbr: 'MDT', + offset: 1, + isdst: true, + text: '(UTC) Casablanca', + utc: ['Africa/Casablanca', 'Africa/El_Aaiun'], + }, + { + value: 'UTC', + abbr: 'UTC', + offset: 0, + isdst: false, + text: '(UTC) Coordinated Universal Time', + utc: ['America/Danmarkshavn', 'Etc/GMT'], + }, + { + value: 'GMT Standard Time', + abbr: 'GMT', + offset: 0, + isdst: false, + text: '(UTC) Edinburgh, London', + utc: ['Europe/Isle_of_Man', 'Europe/Guernsey', 'Europe/Jersey', 'Europe/London'], + }, + { + value: 'British Summer Time', + abbr: 'BST', + offset: 1, + isdst: true, + text: '(UTC+01:00) Edinburgh, London', + utc: ['Europe/Isle_of_Man', 'Europe/Guernsey', 'Europe/Jersey', 'Europe/London'], + }, + { + value: 'GMT Standard Time', + abbr: 'GDT', + offset: 1, + isdst: true, + text: '(UTC) Dublin, Lisbon', + utc: [ + 'Atlantic/Canary', + 'Atlantic/Faeroe', + 'Atlantic/Madeira', + 'Europe/Dublin', + 'Europe/Lisbon', + ], + }, + { + value: 'Greenwich Standard Time', + abbr: 'GST', + offset: 0, + isdst: false, + text: '(UTC) Monrovia, Reykjavik', + utc: [ + 'Africa/Abidjan', + 'Africa/Accra', + 'Africa/Bamako', + 'Africa/Banjul', + 'Africa/Bissau', + 'Africa/Conakry', + 'Africa/Dakar', + 'Africa/Freetown', + 'Africa/Lome', + 'Africa/Monrovia', + 'Africa/Nouakchott', + 'Africa/Ouagadougou', + 'Africa/Sao_Tome', + 'Atlantic/Reykjavik', + 'Atlantic/St_Helena', + ], + }, + { + value: 'W. Europe Standard Time', + abbr: 'WEDT', + offset: 2, + isdst: true, + text: '(UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna', + utc: [ + 'Arctic/Longyearbyen', + 'Europe/Amsterdam', + 'Europe/Andorra', + 'Europe/Berlin', + 'Europe/Busingen', + 'Europe/Gibraltar', + 'Europe/Luxembourg', + 'Europe/Malta', + 'Europe/Monaco', + 'Europe/Oslo', + 'Europe/Rome', + 'Europe/San_Marino', + 'Europe/Stockholm', + 'Europe/Vaduz', + 'Europe/Vatican', + 'Europe/Vienna', + 'Europe/Zurich', + ], + }, + { + value: 'Central Europe Standard Time', + abbr: 'CEDT', + offset: 2, + isdst: true, + text: '(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague', + utc: [ + 'Europe/Belgrade', + 'Europe/Bratislava', + 'Europe/Budapest', + 'Europe/Ljubljana', + 'Europe/Podgorica', + 'Europe/Prague', + 'Europe/Tirane', + ], + }, + { + value: 'Romance Standard Time', + abbr: 'RDT', + offset: 2, + isdst: true, + text: '(UTC+01:00) Brussels, Copenhagen, Madrid, Paris', + utc: ['Africa/Ceuta', 'Europe/Brussels', 'Europe/Copenhagen', 'Europe/Madrid', 'Europe/Paris'], + }, + { + value: 'Central European Standard Time', + abbr: 'CEDT', + offset: 2, + isdst: true, + text: '(UTC+01:00) Sarajevo, Skopje, Warsaw, Zagreb', + utc: ['Europe/Sarajevo', 'Europe/Skopje', 'Europe/Warsaw', 'Europe/Zagreb'], + }, + { + value: 'W. Central Africa Standard Time', + abbr: 'WCAST', + offset: 1, + isdst: false, + text: '(UTC+01:00) West Central Africa', + utc: [ + 'Africa/Algiers', + 'Africa/Bangui', + 'Africa/Brazzaville', + 'Africa/Douala', + 'Africa/Kinshasa', + 'Africa/Lagos', + 'Africa/Libreville', + 'Africa/Luanda', + 'Africa/Malabo', + 'Africa/Ndjamena', + 'Africa/Niamey', + 'Africa/Porto-Novo', + 'Africa/Tunis', + 'Etc/GMT-1', + ], + }, + { + value: 'Namibia Standard Time', + abbr: 'NST', + offset: 1, + isdst: false, + text: '(UTC+01:00) Windhoek', + utc: ['Africa/Windhoek'], + }, + { + value: 'GTB Standard Time', + abbr: 'GDT', + offset: 3, + isdst: true, + text: '(UTC+02:00) Athens, Bucharest', + utc: ['Asia/Nicosia', 'Europe/Athens', 'Europe/Bucharest', 'Europe/Chisinau'], + }, + { + value: 'Middle East Standard Time', + abbr: 'MEDT', + offset: 3, + isdst: true, + text: '(UTC+02:00) Beirut', + utc: ['Asia/Beirut'], + }, + { + value: 'Egypt Standard Time', + abbr: 'EST', + offset: 2, + isdst: false, + text: '(UTC+02:00) Cairo', + utc: ['Africa/Cairo'], + }, + { + value: 'Syria Standard Time', + abbr: 'SDT', + offset: 3, + isdst: true, + text: '(UTC+02:00) Damascus', + utc: ['Asia/Damascus'], + }, + { + value: 'E. Europe Standard Time', + abbr: 'EEDT', + offset: 3, + isdst: true, + text: '(UTC+02:00) E. Europe', + utc: [ + 'Asia/Nicosia', + 'Europe/Athens', + 'Europe/Bucharest', + 'Europe/Chisinau', + 'Europe/Helsinki', + 'Europe/Kiev', + 'Europe/Mariehamn', + 'Europe/Nicosia', + 'Europe/Riga', + 'Europe/Sofia', + 'Europe/Tallinn', + 'Europe/Uzhgorod', + 'Europe/Vilnius', + 'Europe/Zaporozhye', + ], + }, + { + value: 'South Africa Standard Time', + abbr: 'SAST', + offset: 2, + isdst: false, + text: '(UTC+02:00) Harare, Pretoria', + utc: [ + 'Africa/Blantyre', + 'Africa/Bujumbura', + 'Africa/Gaborone', + 'Africa/Harare', + 'Africa/Johannesburg', + 'Africa/Kigali', + 'Africa/Lubumbashi', + 'Africa/Lusaka', + 'Africa/Maputo', + 'Africa/Maseru', + 'Africa/Mbabane', + 'Etc/GMT-2', + ], + }, + { + value: 'FLE Standard Time', + abbr: 'FDT', + offset: 3, + isdst: true, + text: '(UTC+02:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius', + utc: [ + 'Europe/Helsinki', + 'Europe/Kiev', + 'Europe/Mariehamn', + 'Europe/Riga', + 'Europe/Sofia', + 'Europe/Tallinn', + 'Europe/Uzhgorod', + 'Europe/Vilnius', + 'Europe/Zaporozhye', + ], + }, + { + value: 'Turkey Standard Time', + abbr: 'TDT', + offset: 3, + isdst: false, + text: '(UTC+03:00) Istanbul', + utc: ['Europe/Istanbul'], + }, + { + value: 'Israel Standard Time', + abbr: 'JDT', + offset: 3, + isdst: true, + text: '(UTC+02:00) Jerusalem', + utc: ['Asia/Jerusalem'], + }, + { + value: 'Libya Standard Time', + abbr: 'LST', + offset: 2, + isdst: false, + text: '(UTC+02:00) Tripoli', + utc: ['Africa/Tripoli'], + }, + { + value: 'Jordan Standard Time', + abbr: 'JST', + offset: 3, + isdst: false, + text: '(UTC+03:00) Amman', + utc: ['Asia/Amman'], + }, + { + value: 'Arabic Standard Time', + abbr: 'AST', + offset: 3, + isdst: false, + text: '(UTC+03:00) Baghdad', + utc: ['Asia/Baghdad'], + }, + { + value: 'Kaliningrad Standard Time', + abbr: 'KST', + offset: 3, + isdst: false, + text: '(UTC+02:00) Kaliningrad', + utc: ['Europe/Kaliningrad'], + }, + { + value: 'Arab Standard Time', + abbr: 'AST', + offset: 3, + isdst: false, + text: '(UTC+03:00) Kuwait, Riyadh', + utc: ['Asia/Aden', 'Asia/Bahrain', 'Asia/Kuwait', 'Asia/Qatar', 'Asia/Riyadh'], + }, + { + value: 'E. Africa Standard Time', + abbr: 'EAST', + offset: 3, + isdst: false, + text: '(UTC+03:00) Nairobi', + utc: [ + 'Africa/Addis_Ababa', + 'Africa/Asmera', + 'Africa/Dar_es_Salaam', + 'Africa/Djibouti', + 'Africa/Juba', + 'Africa/Kampala', + 'Africa/Khartoum', + 'Africa/Mogadishu', + 'Africa/Nairobi', + 'Antarctica/Syowa', + 'Etc/GMT-3', + 'Indian/Antananarivo', + 'Indian/Comoro', + 'Indian/Mayotte', + ], + }, + { + value: 'Moscow Standard Time', + abbr: 'MSK', + offset: 3, + isdst: false, + text: '(UTC+03:00) Moscow, St. Petersburg, Volgograd, Minsk', + utc: ['Europe/Kirov', 'Europe/Moscow', 'Europe/Simferopol', 'Europe/Volgograd', 'Europe/Minsk'], + }, + { + value: 'Samara Time', + abbr: 'SAMT', + offset: 4, + isdst: false, + text: '(UTC+04:00) Samara, Ulyanovsk, Saratov', + utc: ['Europe/Astrakhan', 'Europe/Samara', 'Europe/Ulyanovsk'], + }, + { + value: 'Iran Standard Time', + abbr: 'IDT', + offset: 4.5, + isdst: true, + text: '(UTC+03:30) Tehran', + utc: ['Asia/Tehran'], + }, + { + value: 'Arabian Standard Time', + abbr: 'AST', + offset: 4, + isdst: false, + text: '(UTC+04:00) Abu Dhabi, Muscat', + utc: ['Asia/Dubai', 'Asia/Muscat', 'Etc/GMT-4'], + }, + { + value: 'Azerbaijan Standard Time', + abbr: 'ADT', + offset: 5, + isdst: true, + text: '(UTC+04:00) Baku', + utc: ['Asia/Baku'], + }, + { + value: 'Mauritius Standard Time', + abbr: 'MST', + offset: 4, + isdst: false, + text: '(UTC+04:00) Port Louis', + utc: ['Indian/Mahe', 'Indian/Mauritius', 'Indian/Reunion'], + }, + { + value: 'Georgian Standard Time', + abbr: 'GET', + offset: 4, + isdst: false, + text: '(UTC+04:00) Tbilisi', + utc: ['Asia/Tbilisi'], + }, + { + value: 'Caucasus Standard Time', + abbr: 'CST', + offset: 4, + isdst: false, + text: '(UTC+04:00) Yerevan', + utc: ['Asia/Yerevan'], + }, + { + value: 'Afghanistan Standard Time', + abbr: 'AST', + offset: 4.5, + isdst: false, + text: '(UTC+04:30) Kabul', + utc: ['Asia/Kabul'], + }, + { + value: 'West Asia Standard Time', + abbr: 'WAST', + offset: 5, + isdst: false, + text: '(UTC+05:00) Ashgabat, Tashkent', + utc: [ + 'Antarctica/Mawson', + 'Asia/Aqtau', + 'Asia/Aqtobe', + 'Asia/Ashgabat', + 'Asia/Dushanbe', + 'Asia/Oral', + 'Asia/Samarkand', + 'Asia/Tashkent', + 'Etc/GMT-5', + 'Indian/Kerguelen', + 'Indian/Maldives', + ], + }, + { + value: 'Yekaterinburg Time', + abbr: 'YEKT', + offset: 5, + isdst: false, + text: '(UTC+05:00) Yekaterinburg', + utc: ['Asia/Yekaterinburg'], + }, + { + value: 'Pakistan Standard Time', + abbr: 'PKT', + offset: 5, + isdst: false, + text: '(UTC+05:00) Islamabad, Karachi', + utc: ['Asia/Karachi'], + }, + { + value: 'India Standard Time', + abbr: 'IST', + offset: 5.5, + isdst: false, + text: '(UTC+05:30) Chennai, Kolkata, Mumbai, New Delhi', + utc: ['Asia/Kolkata', 'Asia/Calcutta'], + }, + { + value: 'Sri Lanka Standard Time', + abbr: 'SLST', + offset: 5.5, + isdst: false, + text: '(UTC+05:30) Sri Jayawardenepura', + utc: ['Asia/Colombo'], + }, + { + value: 'Nepal Standard Time', + abbr: 'NST', + offset: 5.75, + isdst: false, + text: '(UTC+05:45) Kathmandu', + utc: ['Asia/Kathmandu'], + }, + { + value: 'Central Asia Standard Time', + abbr: 'CAST', + offset: 6, + isdst: false, + text: '(UTC+06:00) Nur-Sultan (Astana)', + utc: [ + 'Antarctica/Vostok', + 'Asia/Almaty', + 'Asia/Bishkek', + 'Asia/Qyzylorda', + 'Asia/Urumqi', + 'Etc/GMT-6', + 'Indian/Chagos', + ], + }, + { + value: 'Bangladesh Standard Time', + abbr: 'BST', + offset: 6, + isdst: false, + text: '(UTC+06:00) Dhaka', + utc: ['Asia/Dhaka', 'Asia/Thimphu'], + }, + { + value: 'Myanmar Standard Time', + abbr: 'MST', + offset: 6.5, + isdst: false, + text: '(UTC+06:30) Yangon (Rangoon)', + utc: ['Asia/Rangoon', 'Indian/Cocos'], + }, + { + value: 'SE Asia Standard Time', + abbr: 'SAST', + offset: 7, + isdst: false, + text: '(UTC+07:00) Bangkok, Hanoi, Jakarta', + utc: [ + 'Antarctica/Davis', + 'Asia/Bangkok', + 'Asia/Hovd', + 'Asia/Jakarta', + 'Asia/Phnom_Penh', + 'Asia/Pontianak', + 'Asia/Saigon', + 'Asia/Vientiane', + 'Etc/GMT-7', + 'Indian/Christmas', + ], + }, + { + value: 'N. Central Asia Standard Time', + abbr: 'NCAST', + offset: 7, + isdst: false, + text: '(UTC+07:00) Novosibirsk', + utc: ['Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk'], + }, + { + value: 'China Standard Time', + abbr: 'CST', + offset: 8, + isdst: false, + text: '(UTC+08:00) Beijing, Chongqing, Hong Kong, Urumqi', + utc: ['Asia/Hong_Kong', 'Asia/Macau', 'Asia/Shanghai'], + }, + { + value: 'North Asia Standard Time', + abbr: 'NAST', + offset: 8, + isdst: false, + text: '(UTC+08:00) Krasnoyarsk', + utc: ['Asia/Krasnoyarsk'], + }, + { + value: 'Singapore Standard Time', + abbr: 'MPST', + offset: 8, + isdst: false, + text: '(UTC+08:00) Kuala Lumpur, Singapore', + utc: [ + 'Asia/Brunei', + 'Asia/Kuala_Lumpur', + 'Asia/Kuching', + 'Asia/Makassar', + 'Asia/Manila', + 'Asia/Singapore', + 'Etc/GMT-8', + ], + }, + { + value: 'W. Australia Standard Time', + abbr: 'WAST', + offset: 8, + isdst: false, + text: '(UTC+08:00) Perth', + utc: ['Antarctica/Casey', 'Australia/Perth'], + }, + { + value: 'Taipei Standard Time', + abbr: 'TST', + offset: 8, + isdst: false, + text: '(UTC+08:00) Taipei', + utc: ['Asia/Taipei'], + }, + { + value: 'Ulaanbaatar Standard Time', + abbr: 'UST', + offset: 8, + isdst: false, + text: '(UTC+08:00) Ulaanbaatar', + utc: ['Asia/Choibalsan', 'Asia/Ulaanbaatar'], + }, + { + value: 'North Asia East Standard Time', + abbr: 'NAEST', + offset: 8, + isdst: false, + text: '(UTC+08:00) Irkutsk', + utc: ['Asia/Irkutsk'], + }, + { + value: 'Japan Standard Time', + abbr: 'JST', + offset: 9, + isdst: false, + text: '(UTC+09:00) Osaka, Sapporo, Tokyo', + utc: ['Asia/Dili', 'Asia/Jayapura', 'Asia/Tokyo', 'Etc/GMT-9', 'Pacific/Palau'], + }, + { + value: 'Korea Standard Time', + abbr: 'KST', + offset: 9, + isdst: false, + text: '(UTC+09:00) Seoul', + utc: ['Asia/Pyongyang', 'Asia/Seoul'], + }, + { + value: 'Cen. Australia Standard Time', + abbr: 'CAST', + offset: 9.5, + isdst: false, + text: '(UTC+09:30) Adelaide', + utc: ['Australia/Adelaide', 'Australia/Broken_Hill'], + }, + { + value: 'AUS Central Standard Time', + abbr: 'ACST', + offset: 9.5, + isdst: false, + text: '(UTC+09:30) Darwin', + utc: ['Australia/Darwin'], + }, + { + value: 'E. Australia Standard Time', + abbr: 'EAST', + offset: 10, + isdst: false, + text: '(UTC+10:00) Brisbane', + utc: ['Australia/Brisbane', 'Australia/Lindeman'], + }, + { + value: 'AUS Eastern Standard Time', + abbr: 'AEST', + offset: 10, + isdst: false, + text: '(UTC+10:00) Canberra, Melbourne, Sydney', + utc: ['Australia/Melbourne', 'Australia/Sydney'], + }, + { + value: 'West Pacific Standard Time', + abbr: 'WPST', + offset: 10, + isdst: false, + text: '(UTC+10:00) Guam, Port Moresby', + utc: [ + 'Antarctica/DumontDUrville', + 'Etc/GMT-10', + 'Pacific/Guam', + 'Pacific/Port_Moresby', + 'Pacific/Saipan', + 'Pacific/Truk', + ], + }, + { + value: 'Tasmania Standard Time', + abbr: 'TST', + offset: 10, + isdst: false, + text: '(UTC+10:00) Hobart', + utc: ['Australia/Currie', 'Australia/Hobart'], + }, + { + value: 'Yakutsk Standard Time', + abbr: 'YST', + offset: 9, + isdst: false, + text: '(UTC+09:00) Yakutsk', + utc: ['Asia/Chita', 'Asia/Khandyga', 'Asia/Yakutsk'], + }, + { + value: 'Central Pacific Standard Time', + abbr: 'CPST', + offset: 11, + isdst: false, + text: '(UTC+11:00) Solomon Is., New Caledonia', + utc: [ + 'Antarctica/Macquarie', + 'Etc/GMT-11', + 'Pacific/Efate', + 'Pacific/Guadalcanal', + 'Pacific/Kosrae', + 'Pacific/Noumea', + 'Pacific/Ponape', + ], + }, + { + value: 'Vladivostok Standard Time', + abbr: 'VST', + offset: 11, + isdst: false, + text: '(UTC+11:00) Vladivostok', + utc: ['Asia/Sakhalin', 'Asia/Ust-Nera', 'Asia/Vladivostok'], + }, + { + value: 'New Zealand Standard Time', + abbr: 'NZST', + offset: 12, + isdst: false, + text: '(UTC+12:00) Auckland, Wellington', + utc: ['Antarctica/McMurdo', 'Pacific/Auckland'], + }, + { + value: 'UTC+12', + abbr: 'U', + offset: 12, + isdst: false, + text: '(UTC+12:00) Coordinated Universal Time+12', + utc: [ + 'Etc/GMT-12', + 'Pacific/Funafuti', + 'Pacific/Kwajalein', + 'Pacific/Majuro', + 'Pacific/Nauru', + 'Pacific/Tarawa', + 'Pacific/Wake', + 'Pacific/Wallis', + ], + }, + { + value: 'Fiji Standard Time', + abbr: 'FST', + offset: 12, + isdst: false, + text: '(UTC+12:00) Fiji', + utc: ['Pacific/Fiji'], + }, + { + value: 'Magadan Standard Time', + abbr: 'MST', + offset: 12, + isdst: false, + text: '(UTC+12:00) Magadan', + utc: ['Asia/Anadyr', 'Asia/Kamchatka', 'Asia/Magadan', 'Asia/Srednekolymsk'], + }, + { + value: 'Kamchatka Standard Time', + abbr: 'KDT', + offset: 13, + isdst: true, + text: '(UTC+12:00) Petropavlovsk-Kamchatsky - Old', + utc: ['Asia/Kamchatka'], + }, + { + value: 'Tonga Standard Time', + abbr: 'TST', + offset: 13, + isdst: false, + text: "(UTC+13:00) Nuku'alofa", + utc: ['Etc/GMT-13', 'Pacific/Enderbury', 'Pacific/Fakaofo', 'Pacific/Tongatapu'], + }, + { + value: 'Samoa Standard Time', + abbr: 'SST', + offset: 13, + isdst: false, + text: '(UTC+13:00) Samoa', + utc: ['Pacific/Apia'], + }, +] diff --git a/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.types.ts b/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.types.ts new file mode 100644 index 00000000000..cd86f6ea2fc --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.types.ts @@ -0,0 +1,8 @@ +export interface Timezone { + value: string + abbr: string + offset: number + isdst: boolean + text: string + utc: string[] +} diff --git a/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.utils.ts b/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.utils.ts new file mode 100644 index 00000000000..545340e5e5d --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITRBackupSelection/PITRBackupSelection.utils.ts @@ -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() +} diff --git a/studio/components/interfaces/Database/Backups/PITRBackupSelection/index.tsx b/studio/components/interfaces/Database/Backups/PITRBackupSelection/index.tsx new file mode 100644 index 00000000000..ead2eeca011 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITRBackupSelection/index.tsx @@ -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 = () => { + const router = useRouter() + const { app, ui, backups } = useStore() + const isAvailable = useFlag('pitrBackups') + + if (!isAvailable) { + return ( +
+

Coming soon

+
+ ) + } + + const [searchString, setSearchString] = useState('') + const [selectedTimezone, setSelectedTimezone] = useState(getClientTimezone()) + const [showConfirmation, setShowConfirmation] = useState(false) + + const [errors, setErrors] = useState() + const [recoveryPoint, setRecoveryPoint] = useState() + + const { configuration } = backups + const projectId = ui.selectedProject?.id ?? -1 + const projectRef = ui.selectedProject?.ref ?? 'default' + + if (backups.isLoading) { + return + } else if (backups.error) { + return + } + + 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 ( + + ) + } else if (!hasPhysicalBackups) { + return + } + + 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) => { + 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 ( + <> +
+
+
+
+
+

Select timezone

+
+ { + const selectedTimezone = ALL_TIMEZONES.find((option) => option.text === text) + setSelectedTimezone(selectedTimezone) + }} + onBlur={() => setSearchString('')} + > +
+ + ) => + setSearchString(e.currentTarget.value) + } + /> +
+ {/* Whitespace to shift listbox options down for searchfield */} +
+ {timezoneOptions.map((option) => { + return ( + +
{option}
+
+ ) + })} + {timezoneOptions.length === 0 && ( + + No timezones found + + )} + +
+
+
+
+

Earliest point of recovery

+
+
+ +
+
+
+
+

Latest point of recovery

+
+
+ +
+
+
+ +
+
+

Recovery point

+

+ Select a date and time that you would like to restore your project to +

+
+
+ { + setErrors(undefined) + setRecoveryPoint(e.target.value) + }} + /> +
+
+ +
+ +
+
+
+ + +
+
+

+ Are you sure you want to restore your database from: +

+

+ {dayjs(recoveryPoint).format('DD MMM YYYY, HH:mm:ss')} ( + {getTimezoneOffsetText(selectedTimezone)})? +

+
+

+ This will destroy any new data written since this backup was made. +

+
+ + } + buttonLabel="Restore" + buttonLoadingLabel="Restoring" + onSelectCancel={() => setShowConfirmation(false)} + onSelectConfirm={onConfirmRestore} + /> + + ) +} + +export default PITRBackupSelection diff --git a/studio/components/interfaces/Database/index.ts b/studio/components/interfaces/Database/index.ts index 60141982fab..2bd42e99171 100644 --- a/studio/components/interfaces/Database/index.ts +++ b/studio/components/interfaces/Database/index.ts @@ -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, diff --git a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx index 1725558e1ba..cf59cef8af7 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx +++ b/studio/components/layouts/DatabaseLayout/DatabaseLayout.tsx @@ -14,7 +14,7 @@ interface Props { } const DatabaseLayout: FC = ({ 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 = ({ 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) diff --git a/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts b/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts index 33e7ea21753..a37952f3a32 100644 --- a/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts +++ b/studio/components/layouts/DatabaseLayout/DatabaseMenu.utils.ts @@ -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: [], + }, + ] + : []), + ], + }, + ] : []), ] } diff --git a/studio/components/layouts/ProjectLayout/ConnectingState.tsx b/studio/components/layouts/ProjectLayout/ConnectingState.tsx index 88b670c7a85..9c1908e8d18 100644 --- a/studio/components/layouts/ProjectLayout/ConnectingState.tsx +++ b/studio/components/layouts/ProjectLayout/ConnectingState.tsx @@ -12,7 +12,7 @@ interface Props { project: Project } -const ProjectRestartingState: FC = ({ project }) => { +const ConnectingState: FC = ({ project }) => { const { app } = useStore() const checkProjectConnectionIntervalRef = useRef() @@ -146,4 +146,4 @@ const ProjectRestartingState: FC = ({ project }) => { ) } -export default observer(ProjectRestartingState) +export default observer(ConnectingState) diff --git a/studio/components/ui/Error.tsx b/studio/components/ui/Error.tsx index 3d650b6924a..575c1504196 100644 --- a/studio/components/ui/Error.tsx +++ b/studio/components/ui/Error.tsx @@ -8,13 +8,13 @@ export default function EmptyPageState({ error }: any) { }, []) return ( -
-
+
+
Something went wrong 🤕 - +

Sorry about that, please try again later or feel free to reach out to us if the problem persists. - +

@@ -28,9 +28,9 @@ export default function EmptyPageState({ error }: any) {
- +

Error: [{error?.code}] {error?.message} - +

) } diff --git a/studio/components/ui/ProductMenu/ProductMenuItem.tsx b/studio/components/ui/ProductMenu/ProductMenuItem.tsx index 4e5d5954af7..3a61170a46d 100644 --- a/studio/components/ui/ProductMenu/ProductMenuItem.tsx +++ b/studio/components/ui/ProductMenu/ProductMenuItem.tsx @@ -53,7 +53,7 @@ const ProductMenuItem: FC = ({ } return ( - + {menuItem} diff --git a/studio/components/ui/UpgradeToPro.tsx b/studio/components/ui/UpgradeToPro.tsx index 46bc5c0224b..a3f6151b067 100644 --- a/studio/components/ui/UpgradeToPro.tsx +++ b/studio/components/ui/UpgradeToPro.tsx @@ -11,20 +11,20 @@ interface Props { const UpgradeToPro: FC = ({ primaryText, projectRef, secondaryText }) => (
-
-
+
+

{primaryText}

-

{secondaryText}

+

{secondaryText}

diff --git a/studio/package-lock.json b/studio/package-lock.json index 5a72584b7e0..d912c0e883e 100644 --- a/studio/package-lock.json +++ b/studio/package-lock.json @@ -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", diff --git a/studio/package.json b/studio/package.json index 53f50d4ebea..74955c82559 100644 --- a/studio/package.json +++ b/studio/package.json @@ -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", diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx index 9420bc43e17..0d92bfdc20e 100644 --- a/studio/pages/_app.tsx +++ b/studio/pages/_app.tsx @@ -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()) diff --git a/studio/pages/api/database/[ref]/backups.ts b/studio/pages/api/database/[ref]/backups.ts index bc4d6b8db43..32e883b78d4 100644 --- a/studio/pages/api/database/[ref]/backups.ts +++ b/studio/pages/api/database/[ref]/backups.ts @@ -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: [] }) } diff --git a/studio/pages/project/[ref]/database/backups.tsx b/studio/pages/project/[ref]/database/backups.tsx deleted file mode 100644 index 3b46cc4d52a..00000000000 --- a/studio/pages/project/[ref]/database/backups.tsx +++ /dev/null @@ -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 ( -
-
-

Backups

-
-

- Projects are backed up daily and can be restored at any time. -

- -
-
-
- ) -} - -DatabaseBackups.getLayout = (page) => {page} - -export default observer(DatabaseBackups) diff --git a/studio/pages/project/[ref]/database/backups/pitr.tsx b/studio/pages/project/[ref]/database/backups/pitr.tsx new file mode 100644 index 00000000000..65985343f81 --- /dev/null +++ b/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -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 ( +
+

Backups

+ + { + if (id === 'scheduled') router.push(`/project/${ref}/database/backups/scheduled`) + }} + > + + + + +
+

+ Restore your project from a specific date and time. +

+ +
+
+ ) +} + +DatabaseScheduledBackups.getLayout = (page) => ( + {page} +) + +export default observer(DatabaseScheduledBackups) diff --git a/studio/pages/project/[ref]/database/backups/scheduled.tsx b/studio/pages/project/[ref]/database/backups/scheduled.tsx new file mode 100644 index 00000000000..2b8cfefd74d --- /dev/null +++ b/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -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 ( +
+

Backups

+ + { + if (id === 'pitr') router.push(`/project/${ref}/database/backups/pitr`) + }} + > + + + + +
+

+ Projects are backed up daily around midnight of your project's region and can be restored + at any time. +

+ +
+
+ ) +} + +DatabaseScheduledBackups.getLayout = (page) => ( + {page} +) + +export default observer(DatabaseScheduledBackups) diff --git a/studio/stores/RootStore.ts b/studio/stores/RootStore.ts index 729e7d3445d..cafed00111a 100644 --- a/studio/stores/RootStore.ts +++ b/studio/stores/RootStore.ts @@ -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) { diff --git a/studio/stores/app/DatabaseStore.ts b/studio/stores/app/DatabaseStore.ts index 22f5672e817..9204961d007 100644 --- a/studio/stores/app/DatabaseStore.ts +++ b/studio/stores/app/DatabaseStore.ts @@ -6,9 +6,6 @@ import { constructHeaders } from 'lib/api/apiHelpers' import PostgresMetaInterface, { IPostgresMetaInterface } from '../common/PostgresMetaInterface' export interface IDatabaseStore extends IPostgresMetaInterface { - getBackups: ( - projectRef: string - ) => Promise<{ tierId: any; backups: any[] } | { error: ResponseError }> getPoolingConfiguration: (projectRef: string) => Promise } @@ -24,18 +21,6 @@ export default class DatabaseStore extends PostgresMetaInterface { 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 diff --git a/studio/stores/project/ProjectBackupsStore.ts b/studio/stores/project/ProjectBackupsStore.ts new file mode 100644 index 00000000000..45f6802ac2a --- /dev/null +++ b/studio/stores/project/ProjectBackupsStore.ts @@ -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` + } + } +} diff --git a/studio/stores/content/ProjectContentStore.ts b/studio/stores/project/ProjectContentStore.ts similarity index 100% rename from studio/stores/content/ProjectContentStore.ts rename to studio/stores/project/ProjectContentStore.ts diff --git a/studio/stores/functions/ProjectFunctionsStore.ts b/studio/stores/project/ProjectFunctionsStore.ts similarity index 97% rename from studio/stores/functions/ProjectFunctionsStore.ts rename to studio/stores/project/ProjectFunctionsStore.ts index b8d9e564884..5e1ae7e4485 100644 --- a/studio/stores/functions/ProjectFunctionsStore.ts +++ b/studio/stores/project/ProjectFunctionsStore.ts @@ -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