From d56aeeb359b2408f814a7dacfea0d90e767a95db Mon Sep 17 00:00:00 2001 From: Jason Zi Feng Lei Date: Tue, 12 Apr 2022 10:12:07 +0800 Subject: [PATCH 01/71] Updated default values for "Default Organization" and "Default Project" to be able to read from a .env file. --- docker/.env.example | 2 ++ docker/docker-compose.yml | 3 +++ studio/.env | 3 +++ studio/README.md | 7 +++++++ studio/pages/api/organizations/index.ts | 5 +++-- studio/pages/api/profile/index.ts | 7 ++++--- studio/pages/api/projects/[ref]/index.ts | 5 +++-- studio/pages/api/props/project/[ref]/api.ts | 3 ++- studio/pages/api/props/project/[ref]/index.ts | 3 ++- studio/pages/api/props/project/[ref]/settings.ts | 3 ++- 10 files changed, 31 insertions(+), 10 deletions(-) diff --git a/docker/.env.example b/docker/.env.example index 37e4b9a21d0..8d1b25b9770 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -41,6 +41,8 @@ KONG_HTTPS_PORT=8443 POSTGRES_PORT=5432 # Studio +STUDIO_ORGANIZATION_NAME=Default Organization +STUDIO_PROJECT_NAME=Default Project ## Public REST endpoint - replace this if you intend to use Studio outside of localhost PUBLIC_REST_URL=http://localhost:8000/rest/v1/ diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index cd965f3457d..be10619c4e0 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -17,6 +17,9 @@ services: STUDIO_PG_META_URL: http://meta:8080 POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + DEFAULT_ORGANIZATION: ${STUDIO_DEFAULT_ORGANIZATION} + DEFAULT_PROJECT: ${STUDIO_DEFAULT_PROJECT} + SUPABASE_URL: http://kong:8000 SUPABASE_REST_URL: ${PUBLIC_REST_URL} SUPABASE_ANON_KEY: ${ANON_KEY} diff --git a/studio/.env b/studio/.env index db284f3411e..478baf5dc20 100644 --- a/studio/.env +++ b/studio/.env @@ -1,6 +1,9 @@ STUDIO_PG_META_URL=http://localhost:8000/pg POSTGRES_PASSWORD=your-super-secret-and-long-postgres-password +DEFAULT_ORGANIZATION_NAME=Default Organization +DEFAULT_PROJECT_NAME=Default Project + SUPABASE_URL=http://localhost:8000 SUPABASE_REST_URL=http://localhost:8000/rest/v1/ SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE diff --git a/studio/README.md b/studio/README.md index df0df945274..e493579522d 100644 --- a/studio/README.md +++ b/studio/README.md @@ -73,6 +73,13 @@ npm install npm run dev ``` +If you would like to configure different defaults for "Default Organization" and "Default Project", you will need to update the `.env` in the studio folder with the corresponding values. + +``` +DEFAULT_ORGANIZATION_NAME= +DEFAULT_PROJECT_NAME= +``` + ## UI Testing Notes ### `` vs `` diff --git a/studio/pages/api/organizations/index.ts b/studio/pages/api/organizations/index.ts index d347ad0c017..c3b41b29bbd 100644 --- a/studio/pages/api/organizations/index.ts +++ b/studio/pages/api/organizations/index.ts @@ -1,6 +1,7 @@ -import apiWrapper from 'lib/api/apiWrapper' import { NextApiRequest, NextApiResponse } from 'next' +import apiWrapper from 'lib/api/apiWrapper' + export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -20,7 +21,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { const response = [ { id: 1, - name: 'Default Organization', + name: 'Default Organization' || process.env.DEFAULT_ORGANIZATION_NAME, slug: 'default-org-slug', billing_email: 'billing@supabase.co', }, diff --git a/studio/pages/api/profile/index.ts b/studio/pages/api/profile/index.ts index c6945b970ef..a985c48479b 100644 --- a/studio/pages/api/profile/index.ts +++ b/studio/pages/api/profile/index.ts @@ -1,7 +1,8 @@ import { NextApiRequest, NextApiResponse } from 'next' + import { IS_PLATFORM } from 'lib/constants' -import { createEncryptedDbConnectionString } from 'lib/api/apiHelpers' import apiWrapper from 'lib/api/apiWrapper' +import { createEncryptedDbConnectionString } from 'lib/api/apiHelpers' export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) @@ -28,14 +29,14 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { organizations: [ { id: 1, - name: 'Default Organization', + name: 'Default Organization' || process.env.DEFAULT_ORGANIZATION_NAME, slug: 'default-org-slug', billing_email: 'billing@supabase.co', projects: [ { id: 1, ref: 'default', - name: 'Default Project', + name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, organization_id: 1, cloud_provider: 'localhost', status: 'ACTIVE_HEALTHY', diff --git a/studio/pages/api/projects/[ref]/index.ts b/studio/pages/api/projects/[ref]/index.ts index 4c525abd79f..6339dbe5543 100644 --- a/studio/pages/api/projects/[ref]/index.ts +++ b/studio/pages/api/projects/[ref]/index.ts @@ -1,6 +1,7 @@ -import apiWrapper from 'lib/api/apiWrapper' import { NextApiRequest, NextApiResponse } from 'next' +import apiWrapper from 'lib/api/apiWrapper' + export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -20,7 +21,7 @@ const handleGet = async (req: NextApiRequest, res: NextApiResponse) => { const response = { id: 1, ref: 'default', - name: 'Default Project', + name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, organization_id: 1, cloud_provider: 'localhost', status: 'ACTIVE_HEALTHY', diff --git a/studio/pages/api/props/project/[ref]/api.ts b/studio/pages/api/props/project/[ref]/api.ts index 0f0cd019722..10705a675e0 100644 --- a/studio/pages/api/props/project/[ref]/api.ts +++ b/studio/pages/api/props/project/[ref]/api.ts @@ -1,4 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next' + import apiWrapper from 'lib/api/apiWrapper' export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) @@ -27,7 +28,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { db_user: 'postgres', id: 1, inserted_at: undefined, - name: 'Default Project', + name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, ref: 'default', status: 'ACTIVE_HEALTHY', services: [ diff --git a/studio/pages/api/props/project/[ref]/index.ts b/studio/pages/api/props/project/[ref]/index.ts index 27de041115f..1d14d1bf382 100644 --- a/studio/pages/api/props/project/[ref]/index.ts +++ b/studio/pages/api/props/project/[ref]/index.ts @@ -1,4 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next' + import apiWrapper from 'lib/api/apiWrapper' export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) @@ -21,7 +22,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { project: { id: 1, ref: 'default', - name: 'Default Project', + name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, status: 'ACTIVE_HEALTHY', organization_id: 1, inserted_at: '2021-08-02T06:40:40.646362', diff --git a/studio/pages/api/props/project/[ref]/settings.ts b/studio/pages/api/props/project/[ref]/settings.ts index a5ed9be25e9..a6132cf3d0a 100644 --- a/studio/pages/api/props/project/[ref]/settings.ts +++ b/studio/pages/api/props/project/[ref]/settings.ts @@ -1,4 +1,5 @@ import { NextApiRequest, NextApiResponse } from 'next' + import apiWrapper from 'lib/api/apiWrapper' export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) @@ -29,7 +30,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { id: 1, inserted_at: undefined, jwt_secret: '-', - name: 'Default Project', + name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, ref: 'default', region: 'ap-southeast-1', status: 'ACTIVE_HEALTHY', From e61aedf8c00fe2042ee31142c0254aaeda572c55 Mon Sep 17 00:00:00 2001 From: Jason Zi Feng Lei Date: Tue, 12 Apr 2022 10:39:40 +0800 Subject: [PATCH 02/71] Fixed a silly mistake in order of definition for the `process.env` variables. --- studio/pages/api/organizations/index.ts | 2 +- studio/pages/api/profile/index.ts | 4 ++-- studio/pages/api/projects/[ref]/index.ts | 2 +- studio/pages/api/projects/index.ts | 5 +++-- studio/pages/api/props/project/[ref]/api.ts | 2 +- studio/pages/api/props/project/[ref]/index.ts | 2 +- studio/pages/api/props/project/[ref]/settings.ts | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/studio/pages/api/organizations/index.ts b/studio/pages/api/organizations/index.ts index c3b41b29bbd..48ac8439cfc 100644 --- a/studio/pages/api/organizations/index.ts +++ b/studio/pages/api/organizations/index.ts @@ -21,7 +21,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { const response = [ { id: 1, - name: 'Default Organization' || process.env.DEFAULT_ORGANIZATION_NAME, + name: process.env.DEFAULT_ORGANIZATION_NAME || 'Default Organization', slug: 'default-org-slug', billing_email: 'billing@supabase.co', }, diff --git a/studio/pages/api/profile/index.ts b/studio/pages/api/profile/index.ts index a985c48479b..6b25f3d3396 100644 --- a/studio/pages/api/profile/index.ts +++ b/studio/pages/api/profile/index.ts @@ -29,14 +29,14 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { organizations: [ { id: 1, - name: 'Default Organization' || process.env.DEFAULT_ORGANIZATION_NAME, + name: process.env.DEFAULT_ORGANIZATION_NAME || 'Default Organization', slug: 'default-org-slug', billing_email: 'billing@supabase.co', projects: [ { id: 1, ref: 'default', - name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, + name: process.env.DEFAULT_PROJECT_NAME || 'Default Project', organization_id: 1, cloud_provider: 'localhost', status: 'ACTIVE_HEALTHY', diff --git a/studio/pages/api/projects/[ref]/index.ts b/studio/pages/api/projects/[ref]/index.ts index 6339dbe5543..d8747eafb49 100644 --- a/studio/pages/api/projects/[ref]/index.ts +++ b/studio/pages/api/projects/[ref]/index.ts @@ -21,7 +21,7 @@ const handleGet = async (req: NextApiRequest, res: NextApiResponse) => { const response = { id: 1, ref: 'default', - name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, + name: process.env.DEFAULT_PROJECT_NAME || 'Default Project', organization_id: 1, cloud_provider: 'localhost', status: 'ACTIVE_HEALTHY', diff --git a/studio/pages/api/projects/index.ts b/studio/pages/api/projects/index.ts index f708142b571..a6a20f609b3 100644 --- a/studio/pages/api/projects/index.ts +++ b/studio/pages/api/projects/index.ts @@ -1,6 +1,7 @@ -import apiWrapper from 'lib/api/apiWrapper' import { NextApiRequest, NextApiResponse } from 'next' +import apiWrapper from 'lib/api/apiWrapper' + export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -21,7 +22,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { { id: 1, ref: 'default', - name: 'Default Project', + name: process.env.DEFAULT_PROJECT_NAME || 'Default Project', organization_id: 1, cloud_provider: 'localhost', status: 'ACTIVE_HEALTHY', diff --git a/studio/pages/api/props/project/[ref]/api.ts b/studio/pages/api/props/project/[ref]/api.ts index 10705a675e0..b9a8b68743b 100644 --- a/studio/pages/api/props/project/[ref]/api.ts +++ b/studio/pages/api/props/project/[ref]/api.ts @@ -28,7 +28,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { db_user: 'postgres', id: 1, inserted_at: undefined, - name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, + name: process.env.DEFAULT_PROJECT_NAME || 'Default Project', ref: 'default', status: 'ACTIVE_HEALTHY', services: [ diff --git a/studio/pages/api/props/project/[ref]/index.ts b/studio/pages/api/props/project/[ref]/index.ts index 1d14d1bf382..a197855ddaf 100644 --- a/studio/pages/api/props/project/[ref]/index.ts +++ b/studio/pages/api/props/project/[ref]/index.ts @@ -22,7 +22,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { project: { id: 1, ref: 'default', - name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, + name: process.env.DEFAULT_PROJECT_NAME || 'Default Project', status: 'ACTIVE_HEALTHY', organization_id: 1, inserted_at: '2021-08-02T06:40:40.646362', diff --git a/studio/pages/api/props/project/[ref]/settings.ts b/studio/pages/api/props/project/[ref]/settings.ts index a6132cf3d0a..5677414b23a 100644 --- a/studio/pages/api/props/project/[ref]/settings.ts +++ b/studio/pages/api/props/project/[ref]/settings.ts @@ -30,7 +30,7 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { id: 1, inserted_at: undefined, jwt_secret: '-', - name: 'Default Project' || process.env.DEFAULT_PROJECT_NAME, + name: process.env.DEFAULT_PROJECT_NAME || 'Default Project', ref: 'default', region: 'ap-southeast-1', status: 'ACTIVE_HEALTHY', From a1412f71bf5120ce638ae0eeac855ef2bc7290bf Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 5 Sep 2022 18:17:50 +0800 Subject: [PATCH 03/71] Implement updated UI for PITR backups, midway refining components, validation --- .../Database/Backups/PITR/PITR.constants.ts | 1196 +++++++++++++++++ .../Database/Backups/PITR/PITR.types.ts | 14 + .../Database/Backups/PITR/PITR.utils.ts | 31 + .../Database/Backups/PITR/PITRNotice.tsx | 35 + .../Database/Backups/PITR/PITRSelection.tsx | 326 +++++ .../Database/Backups/PITR/PITRStatus.tsx | 72 + .../Database/Backups/PITR/TimeInput.tsx | 185 +++ .../Backups/PITR/TimezoneSelection.tsx | 78 ++ .../interfaces/Database/Backups/PITR/index.ts | 4 + .../PITRBackupSelection.tsx | 263 ++-- studio/components/ui/InformationBox.tsx | 40 +- studio/components/ui/PasswordStrengthBar.tsx | 1 + studio/package-lock.json | 32 +- studio/package.json | 4 +- .../project/[ref]/database/backups/pitr.tsx | 14 +- .../[ref]/database/backups/scheduled.tsx | 2 +- studio/styles/date-picker.scss | 8 + studio/styles/main.scss | 4 + studio/styles/ui.scss | 4 + 19 files changed, 2142 insertions(+), 171 deletions(-) create mode 100644 studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts create mode 100644 studio/components/interfaces/Database/Backups/PITR/PITR.types.ts create mode 100644 studio/components/interfaces/Database/Backups/PITR/PITR.utils.ts create mode 100644 studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx create mode 100644 studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx create mode 100644 studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx create mode 100644 studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx create mode 100644 studio/components/interfaces/Database/Backups/PITR/TimezoneSelection.tsx create mode 100644 studio/components/interfaces/Database/Backups/PITR/index.ts diff --git a/studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts b/studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts new file mode 100644 index 00000000000..bf1877b267c --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITR.constants.ts @@ -0,0 +1,1196 @@ +// 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: '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/PITR/PITR.types.ts b/studio/components/interfaces/Database/Backups/PITR/PITR.types.ts new file mode 100644 index 00000000000..7b7d4955df9 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITR.types.ts @@ -0,0 +1,14 @@ +export interface Timezone { + value: string + abbr: string + offset: number + isdst: boolean + text: string + utc: string[] +} + +export interface Time { + h: string + m: string + s: string +} diff --git a/studio/components/interfaces/Database/Backups/PITR/PITR.utils.ts b/studio/components/interfaces/Database/Backups/PITR/PITR.utils.ts new file mode 100644 index 00000000000..57477369ac8 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITR.utils.ts @@ -0,0 +1,31 @@ +import dayjs from 'dayjs' +import { ALL_TIMEZONES } from './PITR.constants' +import { Timezone } from './PITR.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 ?? ALL_TIMEZONES[0] +} + +export const checkMatchingDates = (selectedDate: Date | undefined, targetDate: Date) => { + if (!selectedDate) return false + + const formattedSelectedDate = new Date(selectedDate.getTime()) + formattedSelectedDate.setHours(0, 0, 0, 0) + + const formattedTargetDate = new Date(targetDate.getTime()) + formattedTargetDate.setHours(0, 0, 0, 0) + + return Number(formattedSelectedDate) === Number(formattedTargetDate) +} + +export const formatNumberToTwoDigits = (number: Number) => { + return number.toLocaleString('en-US', { + minimumIntegerDigits: 2, + useGrouping: false, + }) +} diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx new file mode 100644 index 00000000000..0cc70ce4940 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITRNotice.tsx @@ -0,0 +1,35 @@ +import { Button, IconCalendar, IconClock } from '@supabase/ui' +import { FormPanel } from 'components/ui/Forms' + +const PITRNotice = ({}) => { + return ( + + + Recovery retention period can be increase by upgrading + + + + } + > +
+
+ +
+
+

Recovery retention period

+

+ Database changes are logged every 2 minutes, + with a total recovery period of up to 7 days. +

+
+
+
+ ) +} + +export default PITRNotice diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx new file mode 100644 index 00000000000..60a8a97bdb6 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITRSelection.tsx @@ -0,0 +1,326 @@ +import dayjs from 'dayjs' +import DatePicker from 'react-datepicker' +import { useState } from 'react' +import { format } from 'date-fns' +import { observer } from 'mobx-react-lite' +import { + Button, + Modal, + IconChevronLeft, + IconChevronRight, + IconHelpCircle, + IconDatabase, +} from '@supabase/ui' + +import { useStore } from 'hooks' + +import { Time } from './PITR.types' +import { getClientTimezone, checkMatchingDates, formatNumberToTwoDigits } from './PITR.utils' +import PITRStatus from './PITRStatus' +import TimeInput from './TimeInput' +import TimezoneSelection from './TimezoneSelection' +import { FormHeader, FormPanel } from 'components/ui/Forms' +import InformationBox from 'components/ui/InformationBox' + +// [Joshen TODO] Remaining todos: +// - Need to validate the selectedDate against earliest and latest here to disable the review restore details button +// - Refine messaging in confirmation modal +// - Actually hook up the BE API, please ensure that the unix timestamp getting sent is correct +// - Highlight available dates in the calendar and add a legend +// - Just need to check in thereafter with Jonny if the changes are okay +// - Deprecate old PITR components + +const PITRSelection = ({}) => { + const { backups } = useStore() + const { earliestPhysicalBackupDateUnix, latestPhysicalBackupDateUnix } = + backups?.configuration?.physicalBackupData ?? {} + + const [selectedDate, setSelectedDate] = useState() + const [selectedTimezone, setSelectedTimezone] = useState(getClientTimezone()) + const [showConfiguration, setShowConfiguration] = useState(false) + const [showConfirmation, setShowConfirmation] = useState(false) + + const formattedSelectedDate = dayjs.unix(Number(selectedDate) / 1000) + // Should only convert to timezone at when confirming + // const formattedSelectedDate = dayjs.unix(Number(selectedDate) / 1000).tz(selectedTimezone?.utc[0]) + + const earliestAvailableBackup = new Date(earliestPhysicalBackupDateUnix * 1000) + const earliestAvailableTiming = dayjs(earliestAvailableBackup).tz(selectedTimezone?.utc[0]) + + const latestAvailableBackup = new Date(latestPhysicalBackupDateUnix * 1000) + const latestAvailableTiming = dayjs(latestAvailableBackup).tz(selectedTimezone?.utc[0]) + + const isSelectedOnEarliest = checkMatchingDates(selectedDate, earliestAvailableBackup) + const isSelectedOnLatest = checkMatchingDates(selectedDate, latestAvailableBackup) + + const formattedSelectedTiming = + selectedDate !== undefined + ? { + h: formatNumberToTwoDigits(selectedDate.getHours()), + m: formatNumberToTwoDigits(selectedDate.getMinutes()), + s: formatNumberToTwoDigits(selectedDate.getSeconds()), + } + : undefined + + const formattedEarliestTiming = isSelectedOnEarliest + ? { + h: formatNumberToTwoDigits(earliestAvailableTiming.hour()), + m: formatNumberToTwoDigits(earliestAvailableTiming.minute()), + s: formatNumberToTwoDigits(earliestAvailableTiming.second()), + } + : undefined + + const formattedLatestTiming = isSelectedOnLatest + ? { + h: formatNumberToTwoDigits(latestAvailableTiming.hour()), + m: formatNumberToTwoDigits(latestAvailableTiming.minute()), + s: formatNumberToTwoDigits(latestAvailableTiming.second()), + } + : undefined + + const onCancel = () => { + setSelectedDate(undefined) + setSelectedTimezone(getClientTimezone()) + setShowConfiguration(false) + } + + const onUpdateDate = (date: Date) => { + const isSelectedOnEarliest = checkMatchingDates(date, earliestAvailableBackup) + const isSelectedOnLatest = checkMatchingDates(selectedDate, latestAvailableBackup) + + if (isSelectedOnEarliest) { + date.setHours( + earliestAvailableTiming.hour(), + earliestAvailableTiming.minute(), + earliestAvailableTiming.second(), + 0 + ) + } else if (isSelectedOnLatest) { + date.setHours( + latestAvailableTiming.hour(), + latestAvailableTiming.minute(), + latestAvailableTiming.second(), + 0 + ) + } else { + date.setHours(0, 0, 0, 0) + } + + setSelectedDate(date) + } + + const onUpdateTime = (time: Time) => { + if (!selectedDate) return + const updatedDate = new Date(Number(selectedDate)) + updatedDate.setHours(Number(time.h)) + updatedDate.setMinutes(Number(time.m)) + updatedDate.setSeconds(Number(time.s)) + setSelectedDate(updatedDate) + } + + return ( + <> + + {!showConfiguration ? ( + setShowConfiguration(true)} /> + ) : ( + + + + + } + > +
+
+ {/* Highlight the available dates better */} + 'cursor-pointer'} + minDate={earliestAvailableBackup} + maxDate={latestAvailableBackup} + // highlightDates={[new Date()]} + renderCustomHeader={({ + date, + decreaseMonth, + increaseMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + }) => ( +
+
+ + {format(date, 'MMMM yyyy')} + +
+
+ )} + /> +
+
+ {!selectedDate ? ( +
+
+ } + title="Select a date which you'd like to restore your database to" + /> +
+
+ ) : ( +
+
+

Restore database to

+

+ {formattedSelectedDate.format('DD MMM YYYY, HH:mm:ss')} +

+
+
+

Time of recovery

+
+ +
+ + + +
+ {isSelectedOnEarliest && ( +

+ Earliest available backup on this date is at{' '} + {earliestAvailableTiming.format('HH:mm:ss')} +

+ )} + {isSelectedOnLatest && ( +

+ Latest available backup on this date is at{' '} + {latestAvailableTiming.format('HH:mm:ss')} +

+ )} +
+
+
+ )} +
+
+
+ )} + setShowConfirmation(false)} + customFooter={ +
+ + +
+ } + > +
+ +

Point in time recovery review

+
+ + +
+

Your database will be restored to

+

{formattedSelectedDate.format('DD MMM YYYY, HH:mm:ss')}

+

{selectedTimezone?.text}

+
+
+ + +
+

+ Any changes made to your database up to this point of time will be lost. This + includes any changes involving storage and authentication. +

+

+ The restoration process will incur some downtime to your project and cannot be + undone. +

+ {/*
+
+ +
+
+

Potential loss of recent data

+

+ Any database changes being made currently could be lost. +

+
+
+
+
+
+

Potential loss of new users

+

+ Users that recently signed up to your project could be lost. +

+
+
+
+
+
+

Potential loss of storage files

+

+ Any files recently uploaded to your storage could be lost. +

+
+
*/} +
+ +
+ + + ) +} + +export default observer(PITRSelection) diff --git a/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx b/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx new file mode 100644 index 00000000000..593f062dd4b --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/PITRStatus.tsx @@ -0,0 +1,72 @@ +import dayjs from 'dayjs' +import { FC, useState } from 'react' +import { observer } from 'mobx-react-lite' +import { Button, IconAlertCircle } from '@supabase/ui' + +import { useStore } from 'hooks' +import { getClientTimezone } from './PITR.utils' +import { FormHeader, FormPanel } from 'components/ui/Forms' +import TimezoneSelection from './TimezoneSelection' + +interface Props { + onSetConfiguration: () => void +} + +const PITRStatus: FC = ({ onSetConfiguration }) => { + const { backups } = useStore() + const { earliestPhysicalBackupDateUnix, latestPhysicalBackupDateUnix } = + backups?.configuration?.physicalBackupData ?? {} + + const [selectedTimezone, setSelectedTimezone] = useState(getClientTimezone()) + + const earliestAvailableBackup = dayjs + .unix(earliestPhysicalBackupDateUnix) + .tz(selectedTimezone?.utc[0]) + .format('DD MMM YYYY, HH:mm:ss') + + const latestAvailableBackup = dayjs + .unix(latestPhysicalBackupDateUnix) + .tz(selectedTimezone?.utc[0]) + .format('DD MMM YYYY, HH:mm:ss') + + return ( + <> + +
+ + + You'll be able to pick the right date and time when you begin + +
+ +
+ } + > +
+
+ +
+
+
+

Database restore available from

+

{earliestAvailableBackup}

+
+
+

Latest restore available at

+

{latestAvailableBackup}

+
+
+
+ + + ) +} + +export default observer(PITRStatus) diff --git a/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx b/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx new file mode 100644 index 00000000000..236d4fbb354 --- /dev/null +++ b/studio/components/interfaces/Database/Backups/PITR/TimeInput.tsx @@ -0,0 +1,185 @@ +import dayjs from 'dayjs' +import { isNaN } from 'lodash' +import { ChangeEvent, FC, useEffect, useState } from 'react' +import * as Tooltip from '@radix-ui/react-tooltip' +import { IconClock } from '@supabase/ui' + +import { Time } from './PITR.types' +import { formatNumberToTwoDigits } from './PITR.utils' + +// [Joshen] This is trying to do the same thing as TimeSplitInput.tsx +// so can we please look to try to combine these 2 components together if possible +// The problem with TimeSplitInput is that it's tightly coupled to the date + its +// tightly coupled to the context of a range. Ideally it should be more modular +// which is what this component is trying to achieve + +// [Joshen] Potential extension, give option to toggle 24 hours or AM/PM + +interface Props { + defaultTime?: Time + minimumTime?: Time + maximumTime?: Time + onChange?: (time: Time) => void +} + +const TimeInput: FC = ({ defaultTime, minimumTime, maximumTime, onChange = () => {} }) => { + const [isFocused, setIsFocused] = useState(false) + const [error, setError] = useState() + const [time, setTime] = useState