From bc3eabd291ddb5488c1945e70fdea2f9c623b129 Mon Sep 17 00:00:00 2001 From: Supun Sudaraka Date: Fri, 21 Jun 2024 20:53:07 +0530 Subject: [PATCH 1/9] chore: update the docs for custom domain activation (#27437) update cli command docs for activating custom domain for supabase projects Co-authored-by: Rodrigo Mansueli --- apps/docs/spec/cli_v1_commands.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/docs/spec/cli_v1_commands.yaml b/apps/docs/spec/cli_v1_commands.yaml index 81bc3d8afe7..dec405023d4 100644 --- a/apps/docs/spec/cli_v1_commands.yaml +++ b/apps/docs/spec/cli_v1_commands.yaml @@ -2831,7 +2831,8 @@ commands: Activates the custom hostname configuration for a project. This reconfigures your Supabase project to respond to requests on your custom hostname. - After the custom hostname is activated, your project's auth services will no longer function on the Supabase-provisioned subdomain. + + After the custom hostname is activated, your project's third-party auth providers will no longer function on the Supabase-provisioned subdomain. Please refer to [Prepare to activate your domain](/docs/guides/platform/custom-domains#prepare-to-activate-your-domain) section in our documentation to learn more about the steps you need to follow. tags: [] links: [] usage: supabase domains activate From 253668fd93d119ea701e94891b472c8c00c89c15 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:22:42 -0400 Subject: [PATCH 2/9] fix: types (#27433) --- .../SpreadsheetImport/SpreadsheetImport.utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.ts index 0c3e0bd8ba3..49540d62a29 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadsheetImport.utils.ts @@ -7,7 +7,7 @@ import { UPLOAD_FILE_EXTENSIONS } from './SpreadsheetImport.constants' const CHUNK_SIZE = 1024 * 1024 * 0.25 // 0.25MB export const parseSpreadsheetText: any = (text: string) => { - const columnTypeMap: any = {} + const columnTypeMap: Record = {} let previewRows: any[] = [] return new Promise((resolve) => { Papa.parse(text, { @@ -49,7 +49,7 @@ export const parseSpreadsheet = ( let rowCount = 0 let previewRows: any[] = [] - const columnTypeMap: any = {} + const columnTypeMap: Record = {} const errors: any[] = [] return new Promise((resolve) => { From eecafc41e0384bd3c8e65c03922f667a9197ccef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20Gr=C3=BCneberg?= Date: Sat, 22 Jun 2024 17:19:53 +0800 Subject: [PATCH 3/9] fix: Updating EU tax ids (#27440) --- .../BillingSettings/TaxID/TaxID.constants.ts | 85 +++++++++++++++++++ .../BillingSettings/TaxID/TaxID.tsx | 8 +- .../BillingSettings/TaxID/TaxID.utils.ts | 4 +- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts index 08e48d3aaed..39a17cbf1bb 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts +++ b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.constants.ts @@ -7,6 +7,7 @@ export interface TaxId { name: string type: string country: string + countryIso2: string placeholder: string vatPrefix?: string } @@ -17,6 +18,7 @@ export const TAX_IDS: TaxId[] = [ type: 'ae_trn', country: 'United Arab Emirates', placeholder: '123456789012345', + countryIso2: 'AE', }, { name: 'AT VAT', @@ -24,30 +26,35 @@ export const TAX_IDS: TaxId[] = [ country: 'Austria', placeholder: 'ATU12345678', vatPrefix: 'ATU', + countryIso2: 'AT', }, { name: 'AD NRT', type: 'ad_nrt', country: 'Andorra', placeholder: 'A-123456-Z', + countryIso2: 'AD', }, { name: 'AR CUIT', type: 'ar_cuit', country: 'Argentina', placeholder: '12-3456789-01', + countryIso2: 'AR', }, { name: 'AU ABN', type: 'au_abn', country: 'Australia', placeholder: '12345678912', + countryIso2: 'AU', }, { name: 'AU ARN', type: 'au_arn', country: 'Australia', placeholder: '123456789123', + countryIso2: 'AU', }, { name: 'BE VAT', @@ -55,12 +62,14 @@ export const TAX_IDS: TaxId[] = [ country: 'Belgium', placeholder: 'BE0123456789', vatPrefix: 'BE', + countryIso2: 'BE', }, { name: 'BO TIN', type: 'bo_tin', country: 'Bolivia', placeholder: '123456789', + countryIso2: 'BO', }, { name: 'BG VAT', @@ -68,54 +77,63 @@ export const TAX_IDS: TaxId[] = [ country: 'Bulgaria', placeholder: 'BG0123456789', vatPrefix: 'BG', + countryIso2: 'BG', }, { name: 'BR CNPJ', type: 'br_cnpj', country: 'Brazil', placeholder: '01.234.456/5432-10', + countryIso2: 'BR', }, { name: 'BR CPF', type: 'br_cpf', country: 'Brazil', placeholder: '123.456.789-87', + countryIso2: 'BR', }, { name: 'CA BN', type: 'ca_bn', country: 'Canada', placeholder: '123456789', + countryIso2: 'CA', }, { name: 'CA GST/HST', type: 'ca_gst_hst', country: 'Canada', placeholder: '123456789RT0002', + countryIso2: 'CA', }, { name: 'CA PST-BC', type: 'ca_pst_bc', country: 'Canada', placeholder: 'PST-1234-5678', + countryIso2: 'CA', }, { name: 'CA PST-MB', type: 'ca_pst_mb', country: 'Canada', placeholder: '123456-7', + countryIso2: 'CA', }, { name: 'CA PST-SK', type: 'ca_pst_mb', country: 'Canada', placeholder: '1234567', + countryIso2: 'CA', }, { name: 'CA QST', type: 'ca_qst', country: 'Canada', placeholder: '1234567890TQ1234', + countryIso2: 'CA', }, { name: 'CH VAT', @@ -123,30 +141,35 @@ export const TAX_IDS: TaxId[] = [ country: 'Switzerland', placeholder: 'CHE-123.456.789 MWST', vatPrefix: 'CHE', + countryIso2: 'CH', }, { name: 'CL TIN', type: 'cl_tin', country: 'Chile', placeholder: '12.345.678-K', + countryIso2: 'CL', }, { name: 'CN TIN', type: 'cn_tin', country: 'China', placeholder: '123456789012345678', + countryIso2: 'CN', }, { name: 'CO NIT', type: 'co_nit', country: 'Colombia', placeholder: '123.456.789-0', + countryIso2: 'CO', }, { name: 'CR TIN', type: 'cr_tin', country: 'Costa Rica', placeholder: '1-234-567890', + countryIso2: 'CR', }, { name: 'CY VAT', @@ -154,6 +177,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Cyprus', placeholder: 'CY12345678Z', vatPrefix: 'CY', + countryIso2: 'CY', }, { name: 'CZ VAT', @@ -161,6 +185,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Czech Republic', placeholder: 'CZ1234567890', vatPrefix: 'CZ', + countryIso2: 'CZ', }, { name: 'DE VAT', @@ -168,6 +193,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Germany', placeholder: 'DE123456789', vatPrefix: 'DE', + countryIso2: 'DE', }, { name: 'DK VAT', @@ -175,12 +201,14 @@ export const TAX_IDS: TaxId[] = [ country: 'Denmark', placeholder: 'DK12345678', vatPrefix: 'DK', + countryIso2: 'DK', }, { name: 'DO RCN', type: 'do_rcn', country: 'Dominican Republic', placeholder: '123-4567890-1', + countryIso2: 'DO', }, { name: 'EE VAT', @@ -188,12 +216,14 @@ export const TAX_IDS: TaxId[] = [ country: 'Estonia', placeholder: 'EE123456789', vatPrefix: 'EE', + countryIso2: 'EE', }, { name: 'ES CIF', type: 'es_cif', country: 'Spain', placeholder: 'A12345678', + countryIso2: 'ES', }, { name: 'ES VAT', @@ -201,6 +231,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Spain', placeholder: 'ESX1234567Z', vatPrefix: 'ES', + countryIso2: 'ES', }, { name: 'FI VAT', @@ -208,6 +239,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Finland', placeholder: 'FI12345678', vatPrefix: 'FI', + countryIso2: 'FI', }, { name: 'FR VAT', @@ -215,6 +247,7 @@ export const TAX_IDS: TaxId[] = [ country: 'France', placeholder: 'FRAB123456789', vatPrefix: 'FR', + countryIso2: 'FR', }, { name: 'GB VAT', @@ -222,17 +255,20 @@ export const TAX_IDS: TaxId[] = [ country: 'United Kingdom', placeholder: 'GB123456789', vatPrefix: 'GB', + countryIso2: 'GB', }, { name: 'GE VAT', type: 'ge_vat', country: 'Georgia', placeholder: '123456789', + countryIso2: 'GE', }, { name: 'EL VAT', type: 'eu_vat', country: 'Greece', + countryIso2: 'GR', placeholder: 'EL123456789', vatPrefix: 'EL', }, @@ -241,6 +277,7 @@ export const TAX_IDS: TaxId[] = [ type: 'hk_br', country: 'Hong Kong SAR China', placeholder: '12345678', + countryIso2: 'HK', }, { name: 'HR VAT', @@ -248,6 +285,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Croatia', placeholder: 'HR12345678912', vatPrefix: 'HR', + countryIso2: 'HR', }, { name: 'HU VAT', @@ -255,12 +293,14 @@ export const TAX_IDS: TaxId[] = [ country: 'Hungary', placeholder: 'HU12345678912', vatPrefix: 'HU', + countryIso2: 'HU', }, { name: 'ID NPWP', type: 'id_npwp', country: 'Indonesia', placeholder: '12.345.678.9-012.345', + countryIso2: 'ID', }, { name: 'IE VAT', @@ -268,24 +308,28 @@ export const TAX_IDS: TaxId[] = [ country: 'Ireland', placeholder: 'IE1234567AB', vatPrefix: 'IE', + countryIso2: 'IE', }, { name: 'IL VAT', type: 'il_vat', country: 'Israel', placeholder: '000012345', + countryIso2: 'IL', }, { name: 'IN GST', type: 'in_gst', country: 'India', placeholder: '12ABCDE3456FGZH', + countryIso2: 'IN', }, { name: 'IS VAT', type: 'is_vat', country: 'Iceland', placeholder: '123456', + countryIso2: 'IS', }, { name: 'IT VAT', @@ -293,36 +337,42 @@ export const TAX_IDS: TaxId[] = [ country: 'Italy', placeholder: 'IT12345678912', vatPrefix: 'IT', + countryIso2: 'IT', }, { name: 'JP CN', type: 'jp_cn', country: 'Japan', placeholder: '1234567891234', + countryIso2: 'JP', }, { name: 'KE PIN', type: 'ke_pin', country: 'Kenya', placeholder: 'P000111111A', + countryIso2: 'KE', }, { name: 'JP RN', type: 'jp_rn', country: 'Japan', placeholder: '12345', + countryIso2: 'JP', }, { name: 'KR BRN', type: 'kr_brn', country: 'Korea', placeholder: '123-45-67890', + countryIso2: 'Kr', }, { name: 'LI UID', type: 'li_uid', country: 'Liechtenstein', placeholder: 'CHE123456789', + countryIso2: 'LI', }, { name: 'LT VAT', @@ -330,6 +380,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Lithuania', placeholder: 'LT123456789123', vatPrefix: 'LT', + countryIso2: 'LT', }, { name: 'LU VAT', @@ -337,6 +388,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Luxembourg', placeholder: 'LU12345678', vatPrefix: 'LU', + countryIso2: 'LU', }, { name: 'LV VAT', @@ -344,6 +396,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Latvia', placeholder: 'LV12345678912', vatPrefix: 'LV', + countryIso2: 'LV', }, { name: 'MT VAT', @@ -351,30 +404,35 @@ export const TAX_IDS: TaxId[] = [ country: 'Malta', placeholder: 'MT12345678', vatPrefix: 'MT', + countryIso2: 'MT', }, { name: 'MX RFC', type: 'mx_rfc', country: 'Mexico', placeholder: 'ABC010203AB9', + countryIso2: 'MX', }, { name: 'MY FRP', type: 'my_frp', country: 'Malaysia', placeholder: '12345678', + countryIso2: 'MY', }, { name: 'MY ITN', type: 'my_itn', country: 'Malaysia', placeholder: 'C 1234567890', + countryIso2: 'MY', }, { name: 'MY SST', type: 'my_sst', country: 'Malaysia', placeholder: 'A12-3456-78912345', + countryIso2: 'MY', }, { name: 'NL VAT', @@ -382,30 +440,35 @@ export const TAX_IDS: TaxId[] = [ country: 'Netherlands', placeholder: 'NL123456789B12', vatPrefix: 'NL', + countryIso2: 'NL', }, { name: 'NO VAT', type: 'no_vat', country: 'Norway', placeholder: '123456789MVA', + countryIso2: 'NO', }, { name: 'PE RUC', type: 'pe_ruc', country: 'Peru', placeholder: '12345678901', + countryIso2: 'PE', }, { name: 'PH TIN', type: 'ph_tin', country: 'Philippines', placeholder: '123456789012', + countryIso2: 'PH', }, { name: 'NZ GST', type: 'nz_gst', country: 'New Zealand', placeholder: '123456789', + countryIso2: 'NZ', }, { name: 'PL VAT', @@ -413,6 +476,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Poland', placeholder: 'PL1234567890', vatPrefix: 'PL', + countryIso2: 'PL', }, { name: 'PT VAT', @@ -420,6 +484,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Portugal', placeholder: 'PT123456789', vatPrefix: 'PT', + countryIso2: 'PT', }, { name: 'RO VAT', @@ -427,30 +492,35 @@ export const TAX_IDS: TaxId[] = [ country: 'Romania', placeholder: 'RO1234567891', vatPrefix: 'RO', + countryIso2: 'RO', }, { name: 'RU INN', type: 'ru_inn', country: 'Russia', placeholder: '1234567891', + countryIso2: 'RU', }, { name: 'RU KPP', type: 'ru_kpp', country: 'Russia', placeholder: '123456789', + countryIso2: 'RU', }, { name: 'SA VAT', type: 'sa_vat', country: 'Saudi Arabia', placeholder: '123456789012345', + countryIso2: 'SA', }, { name: 'RS PIB', type: 'rs_pib', country: 'Serbia', placeholder: '123456789', + countryIso2: 'RS', }, { name: 'SE VAT', @@ -458,18 +528,21 @@ export const TAX_IDS: TaxId[] = [ country: 'Sweden', placeholder: 'SE123456789123', vatPrefix: 'SE', + countryIso2: 'SE', }, { name: 'SG GST', type: 'sg_gst', country: 'Singapore', placeholder: 'M12345678X', + countryIso2: 'SG', }, { name: 'SG UEN', type: 'sg_uen', country: 'Singapore', placeholder: '123456789F', + countryIso2: 'SG', }, { name: 'SI VAT', @@ -477,6 +550,7 @@ export const TAX_IDS: TaxId[] = [ country: 'Slovenia', placeholder: 'SI12345678', vatPrefix: 'SI', + countryIso2: 'SI', }, { name: 'SK VAT', @@ -484,36 +558,42 @@ export const TAX_IDS: TaxId[] = [ country: 'Slovakia', placeholder: 'SK1234567891', vatPrefix: 'SK', + countryIso2: 'SK', }, { name: 'TH VAT', type: 'th_vat', country: 'Thailand', placeholder: '1234567891234', + countryIso2: 'TH', }, { name: 'TR TIN', type: 'tr_tin', country: 'Turkey', placeholder: '0123456789', + countryIso2: 'TR', }, { name: 'TW VAT', type: 'tw_vat', country: 'Taiwan', placeholder: '12345678', + countryIso2: 'TW', }, { name: 'UA VAT', type: 'ua_vat', country: 'Ukraine', placeholder: '123456789', + countryIso2: 'UA', }, { name: 'US EIN', type: 'us_ein', country: 'United States', placeholder: '12-3456789', + countryIso2: 'US', }, { name: 'XI VAT', @@ -521,29 +601,34 @@ export const TAX_IDS: TaxId[] = [ country: 'United Kingdom (Northern Ireland)', placeholder: 'XI123456789', vatPrefix: 'XI', + countryIso2: 'GB', }, { name: 'ZA VAT', type: 'za_vat', country: 'South Africa', placeholder: '4123456789', + countryIso2: 'ZA', }, { name: 'UY RUC', type: 'uy_ruc', country: 'Uruguay', placeholder: '123456789012', + countryIso2: 'UY', }, { name: 'VE RIF', type: 've_rif', country: 'Venezuela', placeholder: 'A-12345678-9', + countryIso2: 'VE', }, { name: 'VN TIN', type: 'vn_tin', country: 'Vietnam', placeholder: '1234567890', + countryIso2: 'VN', }, ] diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx index 97f2c03c6e7..a50b80737e2 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx +++ b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.tsx @@ -59,9 +59,9 @@ const TaxID = () => { type: taxId.type, value: taxId.value, name: - taxId.type === 'eu_vat' - ? `${taxId.country} VAT` - : TAX_IDS.find((option) => option.type === taxId.type)?.name ?? '', + TAX_IDS.find( + (option) => option.type === taxId.type && option.countryIso2 === taxId.country + )?.name ?? '', } : { type: '', value: '', name: '' } @@ -94,7 +94,7 @@ const TaxID = () => { type?.length && value?.length ? { type: type, - value: sanitizeTaxIdValue({ type, value }), + value: sanitizeTaxIdValue({ value, name: form.getValues().name }), } : null, }) diff --git a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts index ce69896aff4..b96a035b24a 100644 --- a/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts +++ b/apps/studio/components/interfaces/Organization/BillingSettings/TaxID/TaxID.utils.ts @@ -1,7 +1,7 @@ import { TAX_IDS } from './TaxID.constants' -export const sanitizeTaxIdValue = (taxId: { type: string; value: string }) => { - const selectedTaxId = TAX_IDS.find((option) => option.type === taxId.type) +export const sanitizeTaxIdValue = (taxId: { name: string; value: string }) => { + const selectedTaxId = TAX_IDS.find((option) => option.name === taxId.name) const vatIdPrefix = selectedTaxId?.vatPrefix From 0bf85c32d6f0b8c955a06b4587393e5aeb46d73b Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 24 Jun 2024 13:43:52 +0800 Subject: [PATCH 4/9] Update restore validity logic for paused projects behind feature flag (#27404) * Reinstate 90 day restore disable logic with feature flag * Update types * Fix * Fix conditionalg * Fix minor UI papercuts * Add exact date for pause disable * Error handler for pause status * Update flag nmame --- .../ProjectLayout/ProjectPausedState.tsx | 240 +++++++++++------- .../organization-payment-methods-query.ts | 9 +- packages/api-types/types/api.d.ts | 182 ++----------- 3 files changed, 169 insertions(+), 262 deletions(-) diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx index a2e9a794d40..4f17a56ecba 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx @@ -1,18 +1,18 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' -import { ExternalLink, PauseCircle } from 'lucide-react' +import { Download, ExternalLink, PauseCircle } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import toast from 'react-hot-toast' import { useParams } from 'common' -import { useBackupsQuery } from 'data/database/backups-query' +import { useBackupDownloadMutation } from 'data/database/backup-download-mutation' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { useProjectPauseStatusQuery } from 'data/projects/project-pause-status-query' import { useProjectRestoreMutation } from 'data/projects/project-restore-mutation' import { setProjectStatus } from 'data/projects/projects-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' -import { useCheckPermissions, useSelectedOrganization } from 'hooks' +import { useCheckPermissions, useFlag, useSelectedOrganization } from 'hooks' import { PROJECT_STATUS } from 'lib/constants' import { AlertDescription_Shadcn_, @@ -27,6 +27,9 @@ import { import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog' import { WarningIcon } from 'ui-patterns/Icons/StatusIcons' import { useProjectContext } from './ProjectContext' +import { GenericSkeletonLoader } from 'ui-patterns' +import dayjs from 'dayjs' +import AlertError from 'components/ui/AlertError' export interface ProjectPausedStateProps { product?: string @@ -37,26 +40,33 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { const queryClient = useQueryClient() const { project } = useProjectContext() const selectedOrganization = useSelectedOrganization() + const enableDisablingOfProjectRestores90DayLimit = useFlag('enforceNinetyDayUnpauseExpiry') const orgSlug = selectedOrganization?.slug const { data: subscription } = useOrgSubscriptionQuery({ orgSlug }) - // const { data: pauseStatus } = useProjectPauseStatusQuery( - // { ref }, - // { enabled: project?.status === PROJECT_STATUS.INACTIVE } - // ) + const { + data: pauseStatus, + error: pauseStatusError, + isError, + isSuccess, + isLoading, + } = useProjectPauseStatusQuery( + { ref }, + { + enabled: + project?.status === PROJECT_STATUS.INACTIVE && enableDisablingOfProjectRestores90DayLimit, + } + ) const isFreePlan = subscription?.plan?.id === 'free' - const isRestoreDisabled = false // !pauseStatus?.can_restore + const isRestoreDisabled = + enableDisablingOfProjectRestores90DayLimit && isSuccess && !pauseStatus.can_restore + const latestBackup = pauseStatus?.latest_downloadable_backup_id const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery( { slug: orgSlug }, { enabled: isFreePlan } ) - const { data: backups } = useBackupsQuery({ projectRef: ref }) - const sortedBackups = (backups?.backups ?? []).sort( - (a, b) => new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf() - ) - const latestBackup = sortedBackups[0] const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 const [showConfirmRestore, setShowConfirmRestore] = useState(false) @@ -69,6 +79,19 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { }, }) + const { mutate: downloadBackup, isLoading: isDownloading } = useBackupDownloadMutation({ + onSuccess: (res) => { + const { fileUrl } = res + + // Trigger browser download by create,trigger and remove tempLink + const tempLink = document.createElement('a') + tempLink.href = fileUrl + document.body.appendChild(tempLink) + tempLink.click() + document.body.removeChild(tempLink) + }, + }) + const canResumeProject = useCheckPermissions( PermissionAction.INFRA_EXECUTE, 'queue_jobs.projects.initialize_or_resume' @@ -88,6 +111,24 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { restoreProject({ ref: project.ref }) } + const onSelectDownloadBackup = () => { + if (ref === undefined) return console.error('Project ref is required') + if (!latestBackup) return toast.error('No backups available for download') + + downloadBackup({ + ref, + backup: { + id: latestBackup, + // [Joshen] Just FYI these params aren't required for the download backup request + // API types need to be updated + project_id: -1, + inserted_at: '', + isPhysicalBackup: false, + status: {}, + }, + }) + } + return ( <>
@@ -117,88 +158,107 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {

- {/* {isRestoreDisabled ? ( - - - - Project cannot be restored through the dashboard - - - This project has been paused for over{' '} - - {pauseStatus?.max_days_till_restore_disabled ?? 90} days - {' '} - and cannot be restored through the dashboard. However, your data remains - intact and can be downloaded as a backup. - - - - - - - {latestBackup === undefined && ( - - No backups available - - )} - - - - ) : isFreePlan ? ( + {isLoading && } + {isError && ( + + )} + {isSuccess && ( <> -

- To prevent future pauses, consider upgrading to Pro. -

- - - Project can be restored through the dashboard within the next{' '} - {pauseStatus.remaining_days_till_restore_disabled} day - {(pauseStatus?.remaining_days_till_restore_disabled ?? 0) > 1 ? 's' : ''} - - - Free projects cannot be restored through the dashboard if they are paused - for more than{' '} - - {pauseStatus.max_days_till_restore_disabled} days - - . However, your database backup will still be available for download. - - + {isRestoreDisabled ? ( + + + + Project cannot be restored through the dashboard + + + This project has been paused for over{' '} + + {pauseStatus?.max_days_till_restore_disabled ?? 90} days + {' '} + and cannot be restored through the dashboard. However, your data remains + intact and can be downloaded as a backup. + + + + + + + {!latestBackup && ( + + No backups available, please reach out via support for assistance + + )} + + + + ) : isFreePlan ? ( + <> +

+ To prevent future pauses, consider upgrading to Pro. +

+ + + Project can be restored through the dashboard within the next{' '} + {pauseStatus?.remaining_days_till_restore_disabled} day + {(pauseStatus?.remaining_days_till_restore_disabled ?? 0) > 1 + ? 's' + : ''} + + + Free projects cannot be restored through the dashboard if they are + paused for more than{' '} + + {pauseStatus?.max_days_till_restore_disabled} days + + . The latest that your project can be restored is by{' '} + + {dayjs() + .utc() + .add(pauseStatus.max_days_till_restore_disabled ?? 0, 'day') + .format('DD MMM YYYY')} + + . However, your database backup will still be available for download + thereafter. + + + + ) : ( + + + + Project will count towards compute usage once restored + + + For every hour your instance is active, we will bill you based on the + instance size of your project. + + + + + + )} - ) : null} */} - - {!isFreePlan && ( - - - - Project will count towards compute usage once restored - - - For every hour your instance is active, we will bill you based on the instance - size of your project. - - - - - )} - {!isRestoreDisabled && ( + {isSuccess && !isRestoreDisabled && (
diff --git a/apps/studio/data/organizations/organization-payment-methods-query.ts b/apps/studio/data/organizations/organization-payment-methods-query.ts index 39b278254e1..610142e4668 100644 --- a/apps/studio/data/organizations/organization-payment-methods-query.ts +++ b/apps/studio/data/organizations/organization-payment-methods-query.ts @@ -5,8 +5,7 @@ import { get, handleError } from 'data/fetchers' import { components } from 'api-types' export type OrganizationPaymentMethodsVariables = { slug?: string } -export type OrganizationPaymentMethodsResponse = components['schemas']['PaymentsResponseV2'] -export type OrganizationPaymentMethod = components['schemas']['PaymentV2'] +export type OrganizationPaymentMethod = components['schemas']['Payment'] export async function getOrganizationPaymentMethods( { slug }: OrganizationPaymentMethodsVariables, @@ -25,11 +24,9 @@ export async function getOrganizationPaymentMethods( }, signal, }) - if (error) handleError(error) - // Due to API versioning, this is not correctly recognized until the old endpoint is removed - // @ts-ignore - return data as OrganizationPaymentMethodsResponse + if (error) handleError(error) + return data } export type OrganizationPaymentMethodsData = Awaited< diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts index 1f00abc4776..9bbf3572900 100644 --- a/packages/api-types/types/api.d.ts +++ b/packages/api-types/types/api.d.ts @@ -157,8 +157,6 @@ export interface paths { get: operations['CustomerController_getCustomer'] /** Updates the billing customer */ put: operations['updateCustomerV2'] - /** Updates the Stripe customer */ - patch: operations['CustomerController_updateCustomer'] } '/platform/organizations/{slug}/roles': { /** Gets the given organization's roles */ @@ -267,7 +265,7 @@ export interface paths { } '/platform/organizations/{slug}/payments': { /** Gets Stripe payment methods for the given organization */ - get: operations['PaymentsController_getPaymentMethods'] + get: operations['getPaymentMethodsV2'] /** Detach payment method with the given card ID */ delete: operations['PaymentsController_detachPaymentMethod'] } @@ -2582,51 +2580,11 @@ export interface components { email: string address: components['schemas']['CustomerBillingAddress'] | null balance: number - invoice_settings: Record billing_via_partner: boolean } BillingCustomerUpdateBody: { address?: components['schemas']['CustomerBillingAddress'] } - CustomerUpdateResponse: { - id: string - object: string - address: Record - balance: number - cash_balance?: Record - created: number - currency: string - default_currency?: string - default_source: string - delinquent: boolean - description: string - discount: Record - email: string - invoice_credit_balance?: Record - invoice_prefix: string - invoice_settings: Record - livemode: boolean - metadata: Record - name: string - next_invoice_sequence?: number - phone: string - preferred_locales: string[] - shipping: Record - sources?: Record - subscriptions?: Record - tax?: Record - tax_exempt?: string - tax_ids?: Record - test_clock?: Record - lastResponse: { - headers?: Record - requestId?: string - statusCode?: number - apiVersion?: string - idempotencyKey?: string - stripeAccount?: string - } - } OrganizationRole: { id: number name: string @@ -2819,70 +2777,16 @@ export interface components { exp_year: number last4: string } - PaymentV2: { + Payment: { id: string card?: components['schemas']['PaymentMethodCard'] created: number type: string is_default: boolean } - PaymentsResponseV2: { - defaultPaymentMethodId: string | null - data: components['schemas']['PaymentV2'][] - } - Payment: { - id: string - object: string - acss_debit?: Record - affirm?: Record - afterpay_clearpay?: Record - alipay?: Record - au_becs_debit?: Record - bacs_debit?: Record - bancontact?: Record - billing_details: Record - blik?: Record - boleto?: Record - card?: Record - card_present?: Record - created: number - customer: Record - customer_balance?: Record - eps?: Record - fpx?: Record - giropay?: Record - grabpay?: Record - ideal?: Record - interac_present?: Record - klarna?: Record - konbini?: Record - link?: Record - livemode: boolean - metadata: Record - oxxo?: Record - p24?: Record - paynow?: Record - promptpay?: Record - radar_options?: Record - sepa_debit?: Record - sofort?: Record - type: string - us_bank_account?: Record - wechat_pay?: Record - } PaymentsResponse: { - object: string + defaultPaymentMethodId: string | null data: components['schemas']['Payment'][] - has_more: boolean - url: string - lastResponse: { - headers?: Record - requestId?: string - statusCode?: number - apiVersion?: string - idempotencyKey?: string - stripeAccount?: string - } } DetachPaymentMethodBody: { card_id: string @@ -2894,37 +2798,8 @@ export interface components { hcaptchaToken: string } SetupIntentResponse: { - id: string - object: string - application: Record - attach_to_self?: boolean - cancellation_reason: string client_secret: string - created: number - customer: Record - description: string - flow_directions: Record - last_setup_error: Record - latest_attempt: Record - livemode: boolean - mandate: Record - metadata: Record - next_action: Record - on_behalf_of: Record payment_method: Record - payment_method_options: Record - payment_method_types: string[] - single_use_mandate: Record - status: string - usage: string - lastResponse: { - headers?: Record - requestId?: string - statusCode?: number - apiVersion?: string - idempotencyKey?: string - stripeAccount?: string - } } /** @enum {string} */ BillingPlanId: 'free' | 'pro' | 'team' | 'enterprise' @@ -2978,12 +2853,6 @@ export interface components { name: string ref: string } - PaymentMethodCardDetails: { - last_4_digits: string - brand: string - expiry_month: number - expiry_year: number - } ScheduledPlanChange: { target_plan: components['schemas']['BillingPlanId'] /** Format: date-time */ @@ -3000,8 +2869,6 @@ export interface components { addons: components['schemas']['BillingSubscriptionAddon'][] project_addons: components['schemas']['BillingProjectAddonResponse'][] payment_method_type: string - payment_method_id?: string - payment_method_card_details?: components['schemas']['PaymentMethodCardDetails'] billing_via_partner: boolean /** @enum {string} */ billing_partner: 'fly' | 'aws' @@ -3947,9 +3814,10 @@ export interface components { } Buffer: Record PauseStatusResponse: { - max_days_till_restore_disabled: number | null + max_days_till_restore_disabled: number remaining_days_till_restore_disabled: number | null - can_restore: boolean | null + can_restore: boolean + latest_downloadable_backup_id: number | null } ResizeBody: { volume_size_gb: number @@ -4015,16 +3883,20 @@ export interface components { region: string /** @enum {string} */ status: - | 'INACTIVE' | 'ACTIVE_HEALTHY' | 'ACTIVE_UNHEALTHY' | 'COMING_UP' - | 'UNKNOWN' | 'GOING_DOWN' + | 'INACTIVE' | 'INIT_FAILED' | 'REMOVED' - | 'RESTORING' + | 'RESTARTING' + | 'UNKNOWN' | 'UPGRADING' + | 'PAUSING' + | 'RESTORING' + | 'RESTORE_FAILED' + | 'PAUSE_FAILED' subscription_id: string connectionString: string kpsVersion?: string @@ -5676,6 +5548,7 @@ export interface components { default?: Record | number | string | boolean name?: string names?: string[] + array?: boolean } AttributeMapping: { keys: { @@ -6904,7 +6777,7 @@ export interface operations { } } responses: { - 200: { + 204: { content: never } 403: { @@ -6916,29 +6789,6 @@ export interface operations { } } } - /** Updates the Stripe customer */ - CustomerController_updateCustomer: { - parameters: { - path: { - /** @description Organization slug */ - slug: string - } - } - responses: { - 200: { - content: { - 'application/json': components['schemas']['CustomerUpdateResponse'] - } - } - 403: { - content: never - } - /** @description Failed to update the Stripe customer */ - 500: { - content: never - } - } - } /** Gets the given organization's roles */ OrganizationRolesController_getAllRolesV2: { parameters: { @@ -7573,7 +7423,7 @@ export interface operations { } } /** Gets Stripe payment methods for the given organization */ - PaymentsController_getPaymentMethods: { + getPaymentMethodsV2: { parameters: { path: { /** @description Organization slug */ From 45066f6b6586d767b623ff858ec1728ccecbe353 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 24 Jun 2024 14:17:16 +0800 Subject: [PATCH 5/9] Handle if flag is false for restore disable check (#27483) --- .../ProjectLayout/ProjectPausedState.tsx | 193 +++++++++--------- .../RestorePaidPlanProjectNotice.tsx | 29 +++ 2 files changed, 120 insertions(+), 102 deletions(-) create mode 100644 apps/studio/components/layouts/ProjectLayout/RestorePaidPlanProjectNotice.tsx diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx index 4f17a56ecba..fc8fddea99a 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectPausedState.tsx @@ -1,17 +1,19 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useQueryClient } from '@tanstack/react-query' -import { Download, ExternalLink, PauseCircle } from 'lucide-react' +import { Download, PauseCircle } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import toast from 'react-hot-toast' import { useParams } from 'common' +import AlertError from 'components/ui/AlertError' import { useBackupDownloadMutation } from 'data/database/backup-download-mutation' import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { useProjectPauseStatusQuery } from 'data/projects/project-pause-status-query' import { useProjectRestoreMutation } from 'data/projects/project-restore-mutation' import { setProjectStatus } from 'data/projects/projects-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' +import dayjs from 'dayjs' import { useCheckPermissions, useFlag, useSelectedOrganization } from 'hooks' import { PROJECT_STATUS } from 'lib/constants' import { @@ -24,12 +26,11 @@ import { TooltipTrigger_Shadcn_, Tooltip_Shadcn_, } from 'ui' +import { GenericSkeletonLoader } from 'ui-patterns' import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog' import { WarningIcon } from 'ui-patterns/Icons/StatusIcons' import { useProjectContext } from './ProjectContext' -import { GenericSkeletonLoader } from 'ui-patterns' -import dayjs from 'dayjs' -import AlertError from 'components/ui/AlertError' +import { RestorePaidPlanProjectNotice } from './RestorePaidPlanProjectNotice' export interface ProjectPausedStateProps { product?: string @@ -40,7 +41,7 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { const queryClient = useQueryClient() const { project } = useProjectContext() const selectedOrganization = useSelectedOrganization() - const enableDisablingOfProjectRestores90DayLimit = useFlag('enforceNinetyDayUnpauseExpiry') + const enforceNinetyDayUnpauseExpiry = useFlag('enforceNinetyDayUnpauseExpiry') const orgSlug = selectedOrganization?.slug const { data: subscription } = useOrgSubscriptionQuery({ orgSlug }) @@ -53,14 +54,12 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => { } = useProjectPauseStatusQuery( { ref }, { - enabled: - project?.status === PROJECT_STATUS.INACTIVE && enableDisablingOfProjectRestores90DayLimit, + enabled: project?.status === PROJECT_STATUS.INACTIVE && enforceNinetyDayUnpauseExpiry, } ) const isFreePlan = subscription?.plan?.id === 'free' - const isRestoreDisabled = - enableDisablingOfProjectRestores90DayLimit && isSuccess && !pauseStatus.can_restore + const isRestoreDisabled = enforceNinetyDayUnpauseExpiry && isSuccess && !pauseStatus.can_restore const latestBackup = pauseStatus?.latest_downloadable_backup_id const { data: membersExceededLimit } = useFreeProjectLimitCheckQuery( @@ -158,107 +157,97 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {

- {isLoading && } - {isError && ( - - )} - {isSuccess && ( + {enforceNinetyDayUnpauseExpiry && ( <> - {isRestoreDisabled ? ( - - - - Project cannot be restored through the dashboard - - - This project has been paused for over{' '} - - {pauseStatus?.max_days_till_restore_disabled ?? 90} days - {' '} - and cannot be restored through the dashboard. However, your data remains - intact and can be downloaded as a backup. - - - - - - - {!latestBackup && ( - - No backups available, please reach out via support for assistance - - )} - - - - ) : isFreePlan ? ( + {isLoading && } + {isError && ( + + )} + {isSuccess && ( <> -

- To prevent future pauses, consider upgrading to Pro. -

- - - Project can be restored through the dashboard within the next{' '} - {pauseStatus?.remaining_days_till_restore_disabled} day - {(pauseStatus?.remaining_days_till_restore_disabled ?? 0) > 1 - ? 's' - : ''} - - - Free projects cannot be restored through the dashboard if they are - paused for more than{' '} - - {pauseStatus?.max_days_till_restore_disabled} days - - . The latest that your project can be restored is by{' '} - - {dayjs() - .utc() - .add(pauseStatus.max_days_till_restore_disabled ?? 0, 'day') - .format('DD MMM YYYY')} - - . However, your database backup will still be available for download - thereafter. - - + {isRestoreDisabled ? ( + + + + Project cannot be restored through the dashboard + + + This project has been paused for over{' '} + + {pauseStatus?.max_days_till_restore_disabled ?? 90} days + {' '} + and cannot be restored through the dashboard. However, your data + remains intact and can be downloaded as a backup. + + + + + + + {!latestBackup && ( + + No backups available, please reach out via support for + assistance + + )} + + + + ) : isFreePlan ? ( + <> +

+ To prevent future pauses, consider upgrading to Pro. +

+ + + Project can be restored through the dashboard within the next{' '} + {pauseStatus?.remaining_days_till_restore_disabled} day + {(pauseStatus?.remaining_days_till_restore_disabled ?? 0) > 1 + ? 's' + : ''} + + + Free projects cannot be restored through the dashboard if they are + paused for more than{' '} + + {pauseStatus?.max_days_till_restore_disabled} days + + . The latest that your project can be restored is by{' '} + + {dayjs() + .utc() + .add(pauseStatus.max_days_till_restore_disabled ?? 0, 'day') + .format('DD MMM YYYY')} + + . However, your database backup will still be available for download + thereafter. + + + + ) : ( + + )} - ) : ( - - - - Project will count towards compute usage once restored - - - For every hour your instance is active, we will bill you based on the - instance size of your project. - - - - - )} )} + + {!enforceNinetyDayUnpauseExpiry && !isFreePlan && } - {isSuccess && !isRestoreDisabled && ( + {(!enforceNinetyDayUnpauseExpiry || (isSuccess && !isRestoreDisabled)) && (
diff --git a/apps/studio/components/layouts/ProjectLayout/RestorePaidPlanProjectNotice.tsx b/apps/studio/components/layouts/ProjectLayout/RestorePaidPlanProjectNotice.tsx new file mode 100644 index 00000000000..e121d0efec6 --- /dev/null +++ b/apps/studio/components/layouts/ProjectLayout/RestorePaidPlanProjectNotice.tsx @@ -0,0 +1,29 @@ +import { ExternalLink } from 'lucide-react' +import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button } from 'ui' +import { WarningIcon } from 'ui-patterns/Icons/StatusIcons' + +export const RestorePaidPlanProjectNotice = () => { + return ( + + + + Project will count towards compute usage once restored + + + For every hour your instance is active, we will bill you based on the instance size of your + project. + + + + + + ) +} From d80befbae29c084de354ff3115b4e281249db5fb Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 24 Jun 2024 14:29:12 +0800 Subject: [PATCH 6/9] More auth related sentry whitelist errors (#27481) * More auth related sentry whitelist errors * Make pretty --- .../interfaces/SignIn/ForgotPasswordForm.tsx | 7 --- .../interfaces/SignIn/SignInMfaForm.tsx | 57 ++++++++----------- .../interfaces/SignIn/SignUpForm.tsx | 13 +---- .../data/misc/reset-password-mutation.ts | 6 ++ apps/studio/data/misc/signup-mutation.ts | 11 ++++ .../mfa-challenge-and-verify-mutation.ts | 18 +++++- 6 files changed, 61 insertions(+), 51 deletions(-) diff --git a/apps/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx b/apps/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx index cb88f8ccab0..0c5630b9007 100644 --- a/apps/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx +++ b/apps/studio/components/interfaces/SignIn/ForgotPasswordForm.tsx @@ -1,5 +1,4 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' -import * as Sentry from '@sentry/nextjs' import { useRouter } from 'next/router' import { useRef, useState } from 'react' import toast from 'react-hot-toast' @@ -9,8 +8,6 @@ import { useResetPasswordMutation } from 'data/misc/reset-password-mutation' import { BASE_PATH } from 'lib/constants' import { Button, Form, Input } from 'ui' -const WHITELIST_ERRORS = ['email must be an email'] - const forgotPasswordSchema = object({ email: string().email('Must be a valid email').required('Email is required'), }) @@ -31,10 +28,6 @@ const ForgotPasswordForm = () => { setCaptchaToken(null) captchaRef.current?.resetCaptcha() toast.error(`Failed to send reset email: ${error.message}`) - - if (!WHITELIST_ERRORS.some((e) => error.message.includes(e))) { - Sentry.captureMessage('[CRITICAL] Failed to send reset password email: ' + error.message) - } }, }) diff --git a/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx b/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx index 837a8204d1f..20c3c60ca06 100644 --- a/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx +++ b/apps/studio/components/interfaces/SignIn/SignInMfaForm.tsx @@ -1,12 +1,10 @@ -import type { AuthError } from '@supabase/auth-js' import type { Factor } from '@supabase/supabase-js' import { useQueryClient } from '@tanstack/react-query' +import { Lock } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useState } from 'react' -import toast from 'react-hot-toast' import { object, string } from 'yup' -import * as Sentry from '@sentry/nextjs' import { useTelemetryProps } from 'common' import AlertError from 'components/ui/AlertError' @@ -16,7 +14,7 @@ import { useMfaListFactorsQuery } from 'data/profile/mfa-list-factors-query' import { useSignOut } from 'lib/auth' import { getReturnToPath } from 'lib/gotrue' import Telemetry from 'lib/telemetry' -import { Button, Form, IconLock, Input } from 'ui' +import { Button, Form, Input } from 'ui' const signInSchema = object({ code: string().required('MFA Code is required'), @@ -36,7 +34,21 @@ const SignInMfaForm = () => { isSuccess: isSuccessFactors, isLoading: isLoadingFactors, } = useMfaListFactorsQuery() - const { mutate: mfaChallengeAndVerify, isLoading, isSuccess } = useMfaChallengeAndVerifyMutation() + const { + mutate: mfaChallengeAndVerify, + isLoading: isVerifying, + isSuccess, + } = useMfaChallengeAndVerifyMutation({ + onSuccess: async () => { + Telemetry.sendEvent( + { category: 'account', action: 'sign_in', label: '' }, + telemetryProps, + router + ) + await queryClient.resetQueries() + router.push(getReturnToPath()) + }, + }) const onClickLogout = async () => { await signOut() @@ -44,27 +56,8 @@ const SignInMfaForm = () => { } const onSignIn = async ({ code }: { code: string }) => { - const toastId = toast.loading('Signing in...') if (selectedFactor) { - mfaChallengeAndVerify( - { factorId: selectedFactor.id, code, refreshFactors: false }, - { - onSuccess: async () => { - toast.success('Signed in successfully!', { id: toastId }) - Telemetry.sendEvent( - { category: 'account', action: 'sign_in', label: '' }, - telemetryProps, - router - ) - await queryClient.resetQueries() - router.push(getReturnToPath()) - }, - onError: (error) => { - toast.error(`Failed to sign in: ${(error as AuthError).message}`, { id: toastId }) - Sentry.captureMessage('[CRITICAL] Failed to sign in via MFA: ' + error.message) - }, - } - ) + mfaChallengeAndVerify({ factorId: selectedFactor.id, code, refreshFactors: false }) } } @@ -102,9 +95,9 @@ const SignInMfaForm = () => { name="code" type="text" autoFocus - icon={} + icon={} placeholder="XXXXXX" - disabled={isLoading} + disabled={isVerifying} autoComplete="off" spellCheck="false" autoCapitalize="none" @@ -121,7 +114,7 @@ const SignInMfaForm = () => { block type="outline" size="large" - disabled={isLoading || isSuccess} + disabled={isVerifying || isSuccess} onClick={onClickLogout} className="opacity-80 hover:opacity-100 transition" > @@ -132,10 +125,10 @@ const SignInMfaForm = () => { form="sign-in-mfa-form" htmlType="submit" size="large" - disabled={isLoading || isSuccess} - loading={isLoading || isSuccess} + disabled={isVerifying || isSuccess} + loading={isVerifying || isSuccess} > - {isLoading ? 'Verifying' : isSuccess ? 'Signing in' : 'Verify'} + {isVerifying ? 'Verifying' : isSuccess ? 'Signing in' : 'Verify'}
@@ -163,9 +156,9 @@ const SignInMfaForm = () => { )}
  • Reach out to us via support diff --git a/apps/studio/components/interfaces/SignIn/SignUpForm.tsx b/apps/studio/components/interfaces/SignIn/SignUpForm.tsx index 73c4b27475c..3e593e5dcca 100644 --- a/apps/studio/components/interfaces/SignIn/SignUpForm.tsx +++ b/apps/studio/components/interfaces/SignIn/SignUpForm.tsx @@ -1,9 +1,8 @@ import HCaptcha from '@hcaptcha/react-hcaptcha' -import { CheckCircle } from 'lucide-react' +import { CheckCircle, Eye, EyeOff } from 'lucide-react' import { useRef, useState } from 'react' import toast from 'react-hot-toast' import * as yup from 'yup' -import * as Sentry from '@sentry/nextjs' import { useSignUpMutation } from 'data/misc/signup-mutation' import { BASE_PATH } from 'lib/constants' @@ -14,14 +13,10 @@ import { Alert_Shadcn_, Button, Form, - IconEye, - IconEyeOff, Input, } from 'ui' import PasswordConditionsHelper from './PasswordConditionsHelper' -const WHITELIST_ERRORS = ['A user with this email already exists'] - const signUpSchema = passwordSchema.shape({ email: yup.string().email().required().label('Email'), }) @@ -42,10 +37,6 @@ const SignUpForm = () => { setCaptchaToken(null) captchaRef.current?.resetCaptcha() toast.error(`Failed to sign up: ${error.message}`) - - if (!WHITELIST_ERRORS.includes(error.message)) { - Sentry.captureMessage('[CRITICAL] Failed to sign up: ' + error.message) - } }, }) @@ -118,7 +109,7 @@ const SignUpForm = () => { onFocus={() => setShowConditions(true)} actions={ + + + + + ) +} + +export default PauseFailedState diff --git a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx index 52971bb3e0c..87e115bc729 100644 --- a/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx +++ b/apps/studio/components/layouts/ProjectLayout/ProjectLayout.tsx @@ -10,6 +10,7 @@ import ResourceExhaustionWarningBanner from 'components/ui/ResourceExhaustionWar import { useFlag, useSelectedOrganization, useSelectedProject, withAuth } from 'hooks' import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' +import { ResizableHandle, ResizablePanel, ResizablePanelGroup, cn } from 'ui' import AppLayout from '../AppLayout/AppLayout' import EnableBranchingModal from '../AppLayout/EnableBranchingButton/EnableBranchingModal' import BuildingState from './BuildingState' @@ -17,13 +18,15 @@ import ConnectingState from './ConnectingState' import { LayoutHeader } from './LayoutHeader' import LoadingState from './LoadingState' import NavigationBar from './NavigationBar/NavigationBar' +import PauseFailedState from './PauseFailedState' import PausingState from './PausingState' import ProductMenuBar from './ProductMenuBar' import { ProjectContextProvider } from './ProjectContext' import ProjectPausedState from './ProjectPausedState' +import RestartingState from './RestartingState' +import RestoreFailedState from './RestoreFailedState' import RestoringState from './RestoringState' import { UpgradingState } from './UpgradingState' -import { ResizableHandle, ResizablePanelGroup, ResizablePanel, cn } from 'ui' // [Joshen] This is temporary while we unblock users from managing their project // if their project is not responding well for any reason. Eventually needs a bit of an overhaul @@ -221,14 +224,17 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp const requiresPostgrestConnection = !routesToIgnorePostgrestConnection.includes(router.pathname) const requiresProjectDetails = !routesToIgnoreProjectDetailsRequest.includes(router.pathname) + const isRestarting = selectedProject?.status === PROJECT_STATUS.RESTARTING const isProjectUpgrading = selectedProject?.status === PROJECT_STATUS.UPGRADING const isProjectRestoring = selectedProject?.status === PROJECT_STATUS.RESTORING + const isProjectRestoreFailed = selectedProject?.status === PROJECT_STATUS.RESTORE_FAILED const isProjectBuilding = selectedProject?.status === PROJECT_STATUS.COMING_UP || selectedProject?.status === PROJECT_STATUS.UNKNOWN const isProjectPausing = selectedProject?.status === PROJECT_STATUS.GOING_DOWN || selectedProject?.status === PROJECT_STATUS.PAUSING + const isProjectPauseFailed = selectedProject?.status === PROJECT_STATUS.PAUSE_FAILED const isProjectOffline = selectedProject?.postgrestStatus === 'OFFLINE' useEffect(() => { @@ -239,6 +245,10 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp return router.pathname.endsWith('[ref]') ? : } + if (isRestarting) { + return + } + if (isProjectUpgrading) { return } @@ -247,6 +257,10 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp return } + if (isProjectPauseFailed) { + return + } + if (requiresPostgrestConnection && isProjectOffline) { return } @@ -255,6 +269,10 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp return } + if (isProjectRestoreFailed) { + return + } + if (requiresDbConnection && isProjectBuilding) { return } diff --git a/apps/studio/components/layouts/ProjectLayout/RestartingState.tsx b/apps/studio/components/layouts/ProjectLayout/RestartingState.tsx new file mode 100644 index 00000000000..6394fd5627d --- /dev/null +++ b/apps/studio/components/layouts/ProjectLayout/RestartingState.tsx @@ -0,0 +1,42 @@ +import { useParams } from 'common' +import { Loader2 } from 'lucide-react' + +import { useProjectDetailQuery } from 'data/projects/project-detail-query' +import { PROJECT_STATUS } from 'lib/constants' + +const RestartingState = () => { + const { ref } = useParams() + useProjectDetailQuery( + { ref }, + { + // setting a refetch interval here will cause the `useSelectedProject()` in `ProjectLayout.tsx` to + // rerender every 4 seconds while the project is restarting. Once restarting is complete, it will + // no longer show this state. + refetchInterval(data) { + return data?.status !== PROJECT_STATUS.ACTIVE_HEALTHY ? 4000 : false + }, + } + ) + + return ( +
    +
    +
    +
    +
    + +
    +
    +

    Restarting...

    +

    + Restarting can take a few minutes. Your project will be offline while it restarts. +

    +
    +
    +
    +
    +
    + ) +} + +export default RestartingState diff --git a/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx new file mode 100644 index 00000000000..5e6273c6149 --- /dev/null +++ b/apps/studio/components/layouts/ProjectLayout/RestoreFailedState.tsx @@ -0,0 +1,42 @@ +import Link from 'next/link' +import { Button } from 'ui' +import { CriticalIcon } from 'ui-patterns/Icons/StatusIcons' + +import { useProjectContext } from './ProjectContext' + +const RestoreFailedState = () => { + const { project } = useProjectContext() + + return ( +
    +
    +
    +
    +
    + +
    +
    +

    Something went wrong while restoring your project

    +

    + Your project's data is intact, but your project is inaccessible due to the + restoration failure. Please contact support for assistance. +

    +
    +
    + +
    + +
    +
    +
    +
    + ) +} + +export default RestoreFailedState diff --git a/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx b/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx index 67dc31d1560..a4f75929165 100644 --- a/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx +++ b/apps/studio/components/layouts/ProjectLayout/RestoringState.tsx @@ -1,9 +1,9 @@ import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' -import Link from 'next/link' import { useEffect, useRef, useState } from 'react' -import { Button, IconAlertCircle, IconCheckCircle, IconLoader } from 'ui' +import { Button, IconCheckCircle, IconLoader } from 'ui' +import { projectKeys } from 'data/projects/keys' import { invalidateProjectDetailsQuery } from 'data/projects/project-detail-query' import { getWithTimeout } from 'lib/common/fetch' import { API_URL, PROJECT_STATUS } from 'lib/constants' @@ -16,7 +16,6 @@ const RestoringState = () => { const checkServerInterval = useRef() const [loading, setLoading] = useState(false) - const [isFailed, setIsFailed] = useState(false) const [isCompleted, setIsCompleted] = useState(false) useEffect(() => { @@ -32,12 +31,11 @@ const RestoringState = () => { }) if (projectStatus && !projectStatus.error) { const { status } = projectStatus - if (status === PROJECT_STATUS.RESTORATION_FAILED) { - clearInterval(checkServerInterval.current) - setIsFailed(true) - } else if (status === PROJECT_STATUS.ACTIVE_HEALTHY) { + if (status === PROJECT_STATUS.ACTIVE_HEALTHY) { clearInterval(checkServerInterval.current) setIsCompleted(true) + } else { + queryClient.invalidateQueries(projectKeys.detail(ref)) } } } @@ -71,32 +69,6 @@ const RestoringState = () => { - ) : isFailed ? ( -
    -
    -
    - -
    -
    -

    Something went wrong while restoring your project

    -

    - Our engineers have already been notified of this, do hang tight while we are - investigating into the issue. -

    -
    -
    - {isFailed && ( -
    - -
    - )} -
    ) : (
    diff --git a/apps/studio/data/projects/project-detail-query.ts b/apps/studio/data/projects/project-detail-query.ts index 66427808b74..6f791b0c85b 100644 --- a/apps/studio/data/projects/project-detail-query.ts +++ b/apps/studio/data/projects/project-detail-query.ts @@ -10,13 +10,14 @@ export type ProjectDetailVariables = { ref?: string } export type ProjectMinimal = components['schemas']['ProjectInfo'] export type ProjectDetail = components['schemas']['ProjectDetailResponse'] -export interface Project extends ProjectDetail { +export interface Project extends Omit { /** * postgrestStatus is available on client side only. * We use this status to check if a project instance is HEALTHY or not * If not we will show ConnectingState and run a polling until it's back online */ postgrestStatus?: 'ONLINE' | 'OFFLINE' + status: components['schemas']['ResourceWithServicesStatusResponse']['status'] } export async function getProjectDetail({ ref }: ProjectDetailVariables, signal?: AbortSignal) { diff --git a/apps/studio/data/projects/projects-query.ts b/apps/studio/data/projects/projects-query.ts index cbb2067adb6..89d28eb0da6 100644 --- a/apps/studio/data/projects/projects-query.ts +++ b/apps/studio/data/projects/projects-query.ts @@ -11,13 +11,15 @@ export type ProjectsVariables = { ref?: string } -export type ProjectInfo = components['schemas']['ProjectInfo'] +export type ProjectInfo = components['schemas']['ProjectInfo'] & { + status: components['schemas']['ResourceWithServicesStatusResponse']['status'] +} export async function getProjects(signal?: AbortSignal) { const { data, error } = await get('/platform/projects', { signal }) if (error) handleError(error) - return data + return data as ProjectInfo[] } export type ProjectsData = Awaited> diff --git a/apps/studio/lib/constants/infrastructure.ts b/apps/studio/lib/constants/infrastructure.ts index a8f3212bd34..c7a15c75386 100644 --- a/apps/studio/lib/constants/infrastructure.ts +++ b/apps/studio/lib/constants/infrastructure.ts @@ -69,7 +69,7 @@ export const PROVIDERS = { } as const export const PROJECT_STATUS: { - [key: string]: components['schemas']['ProjectDetailResponse']['status'] + [key: string]: components['schemas']['ResourceWithServicesStatusResponse']['status'] } = { INACTIVE: 'INACTIVE', ACTIVE_HEALTHY: 'ACTIVE_HEALTHY', @@ -79,12 +79,12 @@ export const PROJECT_STATUS: { GOING_DOWN: 'GOING_DOWN', INIT_FAILED: 'INIT_FAILED', REMOVED: 'REMOVED', + RESTARTING: 'RESTARTING', RESTORING: 'RESTORING', + RESTORE_FAILED: 'RESTORE_FAILED', UPGRADING: 'UPGRADING', - // @ts-ignore [Joshen] API codegen seems to be wrong here, pausing is still a valid status PAUSING: 'PAUSING', - // @ts-ignore [Joshen] This is no longer part of the project status enum, but leaving here for now just in case - RESTORATION_FAILED: 'RESTORATION_FAILED', + PAUSE_FAILED: 'PAUSE_FAILED', } export const DEFAULT_MINIMUM_PASSWORD_STRENGTH = 4 diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index 7a9a56becf7..b0ed5dafb40 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -1,5 +1,6 @@ import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' +import { PopoverSeparator } from '@ui/components/shadcn/ui/popover' import { components } from 'api-types' import { useParams } from 'common' import { @@ -19,6 +20,7 @@ import { ProjectCreateVariables, useProjectCreateMutation, } from 'data/projects/project-create-mutation' +import { useProjectsQuery } from 'data/projects/projects-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import generator from 'generate-password-browser' import { useCheckPermissions, useFlag, withAuth } from 'hooks' @@ -26,6 +28,7 @@ import { CloudProvider, DEFAULT_MINIMUM_PASSWORD_STRENGTH, DEFAULT_PROVIDER, + PROJECT_STATUS, PROVIDERS, } from 'lib/constants' import { passwordStrength } from 'lib/helpers' @@ -59,10 +62,8 @@ import { } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' -import { z } from 'zod' import { InfoTooltip } from 'ui-patterns/info-tooltip' -import { useProjectsQuery } from 'data/projects/projects-query' -import { PopoverSeparator } from '@ui/components/shadcn/ui/popover' +import { z } from 'zod' type DesiredInstanceSize = components['schemas']['DesiredInstanceSize'] @@ -173,7 +174,8 @@ const Wizard: NextPageWithLayout = () => { const { data: allProjects } = useProjectsQuery({}) const organizationProjects = allProjects?.filter( - (project) => project.organization_id === currentOrg?.id && project.status !== 'paused' + (project) => + project.organization_id === currentOrg?.id && project.status !== PROJECT_STATUS.INACTIVE ) ?? [] const { data: orgSubscription } = useOrgSubscriptionQuery({ From 8c14611348b0784ed5494ff5aafa3ba154e45ec3 Mon Sep 17 00:00:00 2001 From: Chris Gwilliams <517923+encima@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:35:03 +0300 Subject: [PATCH 9/9] Fix for labelling issues and handling errors (#27393) * fix for labelling issues and handling errors * fix typos --- .github/workflows/auto-label-issues.yml | 66 +++++++++---------------- 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/.github/workflows/auto-label-issues.yml b/.github/workflows/auto-label-issues.yml index eb248c823e0..d5b441349a4 100644 --- a/.github/workflows/auto-label-issues.yml +++ b/.github/workflows/auto-label-issues.yml @@ -5,61 +5,41 @@ on: jobs: check-external: runs-on: ubuntu-latest - outputs: - result: ${{ steps.is-member.outputs.result }} + permissions: + issues: write steps: - - name: Get deps - run: | - sudo apt-get update && sudo apt-get install -y jq - - name: Get org members - id: is-member - env: - GH_ORG: supabase - GH_USER: ${{ github.event.issue.user.login }} - GH_TOKEN: ${{ secrets.SB_ORG_MEMBERSHIP_PAT }} - run: | - gh api /orgs/supabase/members/$GH_USER - - if: failure() - name: is-member - run: | - echo "User is not a member of Supabase org" - echo "result=false" >> "$GITHUB_OUTPUT" - - if: success() - run: | - echo "User is a member of Supabase org" - echo "result=true" >> "$GITHUB_OUTPUT" - - name: Label contributor - id: label-contributor - if: ${{ steps.is-member.outputs.result == 'false' }} + - name: Check if organization member + id: is-org-member + uses: JamesSingleton/is-organization-member@1.0.0 + with: + organization: ${{ github.repository_owner }} + username: ${{ github.event.issue.user.login }} + token: ${{ secrets.GITHUB_TOKEN }} + - name: label-member env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - USERNAME: ${{ github.event.issue.user.login }} GH_REPO: ${{ github.repository }} NUMBER: ${{ github.event.issue.number }} - LABELS: external-issue run: | - echo "applying external label" - gh issue edit $NUMBER --add-label $LABELS - - name: Label internal - id: label-internal - if: ${{ steps.is-member.outputs.result == 'true' }} - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - USERNAME: ${{ github.event.issue.user.login }} - GH_REPO: ${{ github.repository }} - NUMBER: ${{ github.event.issue.number }} - LABELS: internal-issue - run: | - echo "applying internal label" - gh issue edit $NUMBER --add-label $LABELS + if [ ${{ steps.is-org-member.outputs.result }} != "true" ]; then + echo "User is outside of organization, labeling external" + gh issue edit "$NUMBER" --add-label "external-issue" + else + echo "User is within the organization, labeling internal" + gh issue edit "$NUMBER" --add-label "internal-issue" + fi triage-new: runs-on: ubuntu-latest + permissions: + issues: write steps: - name: Label triage env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} NUMBER: ${{ github.event.issue.number }} LABELS: to-triage run: | - echo "applying triage label" - gh issue edit $NUMBER --add-label $LABELS + echo "Applying triage label for new issue" + gh issue edit "$NUMBER" --add-label "$LABELS" +