Merge branch 'master' of github.com:supabase/supabase

This commit is contained in:
Terry Sutton committed 2024-06-24 09:19:44 -02:30
commit 3a545b0d7a
28 files changed
+558 -413

No files matched your search

+23 -43
View File
@@ -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"
+2 -1
View File
@@ -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
@@ -14,9 +14,18 @@ export const inferProjectStatus = (project: ProjectInfo) => {
case PROJECT_STATUS.INACTIVE:
status = 'isPaused'
break
case PROJECT_STATUS.PAUSE_FAILED:
status = 'isPauseFailed'
break
case PROJECT_STATUS.RESTARTING:
status = 'isRestarting'
break
case PROJECT_STATUS.RESTORING:
status = 'isRestoring'
break
case PROJECT_STATUS.RESTORE_FAILED:
status = 'isRestoreFailed'
break
case PROJECT_STATUS.UNKNOWN:
case PROJECT_STATUS.COMING_UP:
status = 'isComingUp'
@@ -29,6 +38,9 @@ export type InferredProjectStatus =
| 'isHealthy'
| 'isPausing'
| 'isPaused'
| 'isPauseFailed'
| 'isRestarting'
| 'isRestoring'
| 'isRestoreFailed'
| 'isComingUp'
| undefined
@@ -39,8 +39,11 @@ export const ProjectCardStatus = ({
const getTitle = () => {
if (projectStatus === 'isPaused') return 'Project is paused'
if (projectStatus === 'isPausing') return 'Project is pausing'
if (projectStatus === 'isRestarting') return 'Project is restarting'
if (projectStatus === 'isComingUp') return 'Project is coming up'
if (projectStatus === 'isRestoring') return 'Project is restoring'
if (projectStatus === 'isRestoreFailed') return 'Project restore failed'
if (projectStatus === 'isPauseFailed') return 'Project pause failed'
if (!resourceWarnings) return undefined
@@ -55,8 +58,11 @@ export const ProjectCardStatus = ({
const getDescription = () => {
if (projectStatus === 'isPaused') return 'This project will not accept requests until resumed'
if (projectStatus === 'isPausing') return 'The pause process will complete in a few minutes'
if (projectStatus === 'isRestarting') return 'Your project will be ready in a few minutes'
if (projectStatus === 'isComingUp') return 'Your project will be ready in a few minutes'
if (projectStatus === 'isRestoring') return 'Your project will be ready in a few minutes'
if (projectStatus === 'isRestoreFailed') return 'Please contact support for assistance'
if (projectStatus === 'isPauseFailed') return 'Please contact support for assistance'
if (!resourceWarnings) return undefined
@@ -95,7 +101,9 @@ export const ProjectCardStatus = ({
>
{projectStatus === 'isPaused' || projectStatus === 'isPausing' ? (
<IconPauseCircle strokeWidth={1.5} size={12} />
) : projectStatus === 'isRestoring' || projectStatus === 'isComingUp' ? (
) : projectStatus === 'isRestoring' ||
projectStatus === 'isComingUp' ||
projectStatus === 'isRestarting' ? (
<RefreshCcw strokeWidth={1.5} size={12} />
) : (
<AlertTriangle strokeWidth={1.5} size={12} />
@@ -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',
},
]
@@ -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,
})
@@ -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
@@ -11,7 +11,7 @@ import {
} from 'components/layouts/ProjectLayout/ProjectContext'
import { useProjectRestartMutation } from 'data/projects/project-restart-mutation'
import { useProjectRestartServicesMutation } from 'data/projects/project-restart-services-mutation'
import { setProjectPostgrestStatus } from 'data/projects/projects-query'
import { setProjectStatus } from 'data/projects/projects-query'
import { useCheckPermissions, useFlag } from 'hooks'
import {
Button,
@@ -76,7 +76,7 @@ const RestartServerButton = () => {
}
const onRestartSuccess = () => {
setProjectPostgrestStatus(queryClient, projectRef, 'OFFLINE')
setProjectStatus(queryClient, projectRef, 'RESTARTING')
toast.success('Restarting server...')
router.push(`/project/${projectRef}`)
setServiceToRestart(undefined)
@@ -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)
}
},
})
@@ -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={<IconLock />}
icon={<Lock />}
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'}
</Button>
</div>
</div>
@@ -163,9 +156,9 @@ const SignInMfaForm = () => {
)}
<li>
<Link
href="/support/new?subject=Unable+to+sign+in+via+MFA&category=Login_issues"
target="_blank"
rel="noreferrer"
href="/support/new?subject=Unable+to+sign+in+via+MFA&category=Login_issues"
className="text-sm transition text-foreground-light hover:text-foreground"
>
Reach out to us via support
@@ -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={
<Button
icon={passwordHidden ? <IconEye /> : <IconEyeOff />}
icon={passwordHidden ? <Eye /> : <EyeOff />}
type="default"
className="!mr-1"
onClick={() => setPasswordHidden((prev) => !prev)}
@@ -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<any, any> = {}
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<any, any> = {}
const errors: any[] = []
return new Promise((resolve) => {
@@ -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 PauseFailedState = () => {
const { project } = useProjectContext()
return (
<div className="flex items-center justify-center h-full">
<div className="bg-surface-100 border border-overlay rounded-md w-3/4 lg:w-1/2">
<div className="space-y-6 pt-6">
<div className="flex px-8 space-x-8">
<div className="mt-1">
<CriticalIcon className="w-5 h-5" />
</div>
<div className="space-y-1">
<p>Something went wrong while pausing your project</p>
<p className="text-sm text-foreground-light">
Your project's data is intact, but your project is inaccessible due to the failure
while pausing. Please contact support for assistance.
</p>
</div>
</div>
<div className="border-t border-overlay flex items-center justify-end py-4 px-8">
<Button asChild type="default">
<Link
href={`/support/new?category=Database_unresponsive&ref=${project?.ref}&subject=Restoration%20failed%20for%20project`}
>
Contact support
</Link>
</Button>
</div>
</div>
</div>
</div>
)
}
export default PauseFailedState
@@ -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]') ? <LoadingState /> : <Loading />
}
if (isRestarting) {
return <RestartingState />
}
if (isProjectUpgrading) {
return <UpgradingState />
}
@@ -247,6 +257,10 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp
return <PausingState project={selectedProject} />
}
if (isProjectPauseFailed) {
return <PauseFailedState />
}
if (requiresPostgrestConnection && isProjectOffline) {
return <ConnectingState project={selectedProject} />
}
@@ -255,6 +269,10 @@ const ContentWrapper = ({ isLoading, isBlocking = true, children }: ContentWrapp
return <RestoringState />
}
if (isProjectRestoreFailed) {
return <RestoreFailedState />
}
if (requiresDbConnection && isProjectBuilding) {
return <BuildingState />
}
@@ -1,18 +1,20 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { 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 { useBackupsQuery } from 'data/database/backups-query'
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 { useCheckPermissions, useSelectedOrganization } from 'hooks'
import dayjs from 'dayjs'
import { useCheckPermissions, useFlag, useSelectedOrganization } from 'hooks'
import { PROJECT_STATUS } from 'lib/constants'
import {
AlertDescription_Shadcn_,
@@ -24,9 +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 { RestorePaidPlanProjectNotice } from './RestorePaidPlanProjectNotice'
export interface ProjectPausedStateProps {
product?: string
@@ -37,26 +41,36 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
const queryClient = useQueryClient()
const { project } = useProjectContext()
const selectedOrganization = useSelectedOrganization()
const enforceNinetyDayUnpauseExpiry = 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 && enforceNinetyDayUnpauseExpiry,
}
)
const finalDaysRemainingBeforeRestoreDisabled =
pauseStatus?.remaining_days_till_restore_disabled ??
pauseStatus?.max_days_till_restore_disabled ??
0
const isFreePlan = subscription?.plan?.id === 'free'
const isRestoreDisabled = false // !pauseStatus?.can_restore
const isRestoreDisabled = enforceNinetyDayUnpauseExpiry && 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 +83,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 +115,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 (
<>
<div className="space-y-4">
@@ -117,88 +162,95 @@ const ProjectPausedState = ({ product }: ProjectPausedStateProps) => {
</p>
</div>
{/* {isRestoreDisabled ? (
<Alert_Shadcn_ variant="warning">
<WarningIcon />
<AlertTitle_Shadcn_>
Project cannot be restored through the dashboard
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
This project has been paused for over{' '}
<span className="text-foreground">
{pauseStatus?.max_days_till_restore_disabled ?? 90} days
</span>{' '}
and cannot be restored through the dashboard. However, your data remains
intact and can be downloaded as a backup.
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="flex items-center gap-x-2 mt-3">
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<Button
type="default"
disabled={latestBackup === undefined}
className="pointer-events-auto"
>
Download backup
</Button>
</TooltipTrigger_Shadcn_>
{latestBackup === undefined && (
<TooltipContent_Shadcn_ side="bottom">
No backups available
</TooltipContent_Shadcn_>
)}
</Tooltip_Shadcn_>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
) : isFreePlan ? (
{enforceNinetyDayUnpauseExpiry && (
<>
<p className="text-sm text-foreground-light text-center">
To prevent future pauses, consider upgrading to Pro.
</p>
<Alert_Shadcn_>
<AlertTitle_Shadcn_>
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' : ''}
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
Free projects cannot be restored through the dashboard if they are paused
for more than{' '}
<span className="text-foreground">
{pauseStatus.max_days_till_restore_disabled} days
</span>
. However, your database backup will still be available for download.
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
{isLoading && <GenericSkeletonLoader />}
{isError && (
<AlertError
error={pauseStatusError}
subject="Failed to retrieve pause status"
/>
)}
{isSuccess && (
<>
{isRestoreDisabled ? (
<Alert_Shadcn_ variant="warning">
<WarningIcon />
<AlertTitle_Shadcn_>
Project cannot be restored through the dashboard
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
This project has been paused for over{' '}
<span className="text-foreground">
{pauseStatus?.max_days_till_restore_disabled ?? 90} days
</span>{' '}
and cannot be restored through the dashboard. However, your data
remains intact and can be downloaded as a backup.
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="flex items-center gap-x-2 mt-3">
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<Button
type="default"
icon={<Download />}
loading={isDownloading}
disabled={!latestBackup}
className="pointer-events-auto"
onClick={() => onSelectDownloadBackup()}
>
Download backup
</Button>
</TooltipTrigger_Shadcn_>
{!latestBackup && (
<TooltipContent_Shadcn_ side="bottom">
No backups available, please reach out via support for
assistance
</TooltipContent_Shadcn_>
)}
</Tooltip_Shadcn_>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
) : isFreePlan ? (
<>
<p className="text-sm text-foreground-light text-center">
To prevent future pauses, consider upgrading to Pro.
</p>
<Alert_Shadcn_>
<AlertTitle_Shadcn_>
Project can be restored through the dashboard within the next{' '}
{finalDaysRemainingBeforeRestoreDisabled} day
{finalDaysRemainingBeforeRestoreDisabled > 1 ? 's' : ''}
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
Free projects cannot be restored through the dashboard if they are
paused for more than{' '}
<span className="text-foreground">
{pauseStatus?.max_days_till_restore_disabled} days
</span>
. The latest that your project can be restored is by{' '}
<span className="text-foreground">
{dayjs()
.utc()
.add(pauseStatus.max_days_till_restore_disabled ?? 0, 'day')
.format('DD MMM YYYY')}
</span>
. However, your database backup will still be available for download
thereafter.
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</>
) : (
<RestorePaidPlanProjectNotice />
)}
</>
)}
</>
) : null} */}
{!isFreePlan && (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>
Project will count towards compute usage once restored
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
For every hour your instance is active, we will bill you based on the instance
size of your project.
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="mt-3">
<Button asChild type="default" icon={<ExternalLink />}>
<a
href="https://supabase.com/docs/guides/platform/org-based-billing#usage-based-billing-for-compute"
target="_blank"
rel="noreferrer"
>
More information
</a>
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
{!enforceNinetyDayUnpauseExpiry && !isFreePlan && <RestorePaidPlanProjectNotice />}
</div>
{!isRestoreDisabled && (
{(!enforceNinetyDayUnpauseExpiry || (isSuccess && !isRestoreDisabled)) && (
<div className="flex items-center justify-center gap-4">
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
@@ -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 (
<div className="flex items-center justify-center h-full">
<div className="bg-surface-100 border border-overlay rounded-md w-3/4 lg:w-1/2">
<div className="space-y-6 py-6">
<div className="flex px-8 space-x-8">
<div className="mt-1">
<Loader2 className="animate-spin" size={18} />
</div>
<div className="space-y-1">
<p>Restarting...</p>
<p className="text-sm text-foreground-light">
Restarting can take a few minutes. Your project will be offline while it restarts.
</p>
</div>
</div>
</div>
</div>
</div>
)
}
export default RestartingState
@@ -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 (
<div className="flex items-center justify-center h-full">
<div className="bg-surface-100 border border-overlay rounded-md w-3/4 lg:w-1/2">
<div className="space-y-6 pt-6">
<div className="flex px-8 space-x-8">
<div className="mt-1">
<CriticalIcon className="w-5 h-5" />
</div>
<div className="space-y-1">
<p>Something went wrong while restoring your project</p>
<p className="text-sm text-foreground-light">
Your project's data is intact, but your project is inaccessible due to the
restoration failure. Please contact support for assistance.
</p>
</div>
</div>
<div className="border-t border-overlay flex items-center justify-end py-4 px-8">
<Button asChild type="default">
<Link
href={`/support/new?category=Database_unresponsive&ref=${project?.ref}&subject=Restoration%20failed%20for%20project`}
>
Contact support
</Link>
</Button>
</div>
</div>
</div>
</div>
)
}
export default RestoreFailedState
@@ -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 (
<Alert_Shadcn_>
<WarningIcon />
<AlertTitle_Shadcn_>
Project will count towards compute usage once restored
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
For every hour your instance is active, we will bill you based on the instance size of your
project.
</AlertDescription_Shadcn_>
<AlertDescription_Shadcn_ className="mt-3">
<Button asChild type="default" icon={<ExternalLink />}>
<a
href="https://supabase.com/docs/guides/platform/org-based-billing#usage-based-billing-for-compute"
target="_blank"
rel="noreferrer"
>
More information
</a>
</Button>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)
}
@@ -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<number>()
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 = () => {
</Button>
</div>
</div>
) : isFailed ? (
<div className="space-y-6 pt-6">
<div className="flex px-8 space-x-8">
<div className="mt-1">
<IconAlertCircle size={18} strokeWidth={2} />
</div>
<div className="space-y-1">
<p>Something went wrong while restoring your project</p>
<p className="text-sm text-foreground-light">
Our engineers have already been notified of this, do hang tight while we are
investigating into the issue.
</p>
</div>
</div>
{isFailed && (
<div className="border-t border-overlay flex items-center justify-end py-4 px-8">
<Button asChild type="default">
<Link
href={`/support/new?category=Database_unresponsive&ref=${project?.ref}&subject=Restoration%20failed%20for%20project`}
>
Contact support
</Link>
</Button>
</div>
)}
</div>
) : (
<div className="space-y-6 py-6">
<div className="flex px-8 space-x-8">
@@ -1,9 +1,12 @@
import * as Sentry from '@sentry/nextjs'
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
const WHITELIST_ERRORS = ['email must be an email']
export type ResetPasswordVariables = {
email: string
hcaptchaToken: string | null
@@ -42,6 +45,9 @@ export const useResetPasswordMutation = ({
} else {
onError(data, variables, context)
}
if (!WHITELIST_ERRORS.some((error) => data.message.includes(error))) {
Sentry.captureMessage('[CRITICAL] Failed to send reset password email: ' + data.message)
}
},
...options,
}
+11
View File
@@ -1,9 +1,17 @@
import * as Sentry from '@sentry/nextjs'
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { handleError, post } from 'data/fetchers'
import type { ResponseError } from 'types'
const WHITELIST_ERRORS = [
'A user with this email already exists',
'Password should contain at least one character of each',
'You attempted to send email to an inactive recipient',
'email must be an email',
]
export type SignUpVariables = {
email: string
password: string
@@ -38,6 +46,9 @@ export const useSignUpMutation = ({
} else {
onError(data, variables, context)
}
if (!WHITELIST_ERRORS.some((error) => data.message.includes(error))) {
Sentry.captureMessage('[CRITICAL] Failed to sign up: ' + data.message)
}
},
...options,
})
@@ -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<
@@ -1,8 +1,13 @@
import * as Sentry from '@sentry/nextjs'
import type { AuthMFAVerifyResponse, MFAChallengeAndVerifyParams } from '@supabase/supabase-js'
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { UseMutationOptions, useMutation, useQueryClient } from '@tanstack/react-query'
import toast from 'react-hot-toast'
import { auth } from 'lib/gotrue'
import { profileKeys } from './keys'
const WHITELIST_ERRORS = ['Invalid TOTP code entered']
interface MFAChallengeAndVerifyVariables extends MFAChallengeAndVerifyParams {
refreshFactors?: boolean
}
@@ -18,6 +23,7 @@ type CustomMFAVerifyError = NonNullable<AuthMFAVerifyResponse['error']>
export const useMfaChallengeAndVerifyMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<CustomMFAVerifyResponse, CustomMFAVerifyError, MFAChallengeAndVerifyVariables>,
@@ -42,6 +48,16 @@ export const useMfaChallengeAndVerifyMutation = ({
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to sign in: ${data.message}`)
} else {
onError(data, variables, context)
}
if (!WHITELIST_ERRORS.some((error) => data.message.includes(error))) {
Sentry.captureMessage('[CRITICAL] Failed to sign in via MFA: ' + data.message)
}
},
...options,
}
)
@@ -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<ProjectDetail, 'status'> {
/**
* 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) {
+4 -2
View File
@@ -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<ReturnType<typeof getProjects>>
+4 -4
View File
@@ -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
+6 -4
View File
@@ -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({
+16 -166
View File
@@ -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<string, never>
billing_via_partner: boolean
}
BillingCustomerUpdateBody: {
address?: components['schemas']['CustomerBillingAddress']
}
CustomerUpdateResponse: {
id: string
object: string
address: Record<string, never>
balance: number
cash_balance?: Record<string, never>
created: number
currency: string
default_currency?: string
default_source: string
delinquent: boolean
description: string
discount: Record<string, never>
email: string
invoice_credit_balance?: Record<string, never>
invoice_prefix: string
invoice_settings: Record<string, never>
livemode: boolean
metadata: Record<string, never>
name: string
next_invoice_sequence?: number
phone: string
preferred_locales: string[]
shipping: Record<string, never>
sources?: Record<string, never>
subscriptions?: Record<string, never>
tax?: Record<string, never>
tax_exempt?: string
tax_ids?: Record<string, never>
test_clock?: Record<string, never>
lastResponse: {
headers?: Record<string, never>
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<string, never>
affirm?: Record<string, never>
afterpay_clearpay?: Record<string, never>
alipay?: Record<string, never>
au_becs_debit?: Record<string, never>
bacs_debit?: Record<string, never>
bancontact?: Record<string, never>
billing_details: Record<string, never>
blik?: Record<string, never>
boleto?: Record<string, never>
card?: Record<string, never>
card_present?: Record<string, never>
created: number
customer: Record<string, never>
customer_balance?: Record<string, never>
eps?: Record<string, never>
fpx?: Record<string, never>
giropay?: Record<string, never>
grabpay?: Record<string, never>
ideal?: Record<string, never>
interac_present?: Record<string, never>
klarna?: Record<string, never>
konbini?: Record<string, never>
link?: Record<string, never>
livemode: boolean
metadata: Record<string, never>
oxxo?: Record<string, never>
p24?: Record<string, never>
paynow?: Record<string, never>
promptpay?: Record<string, never>
radar_options?: Record<string, never>
sepa_debit?: Record<string, never>
sofort?: Record<string, never>
type: string
us_bank_account?: Record<string, never>
wechat_pay?: Record<string, never>
}
PaymentsResponse: {
object: string
defaultPaymentMethodId: string | null
data: components['schemas']['Payment'][]
has_more: boolean
url: string
lastResponse: {
headers?: Record<string, never>
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<string, never>
attach_to_self?: boolean
cancellation_reason: string
client_secret: string
created: number
customer: Record<string, never>
description: string
flow_directions: Record<string, never>
last_setup_error: Record<string, never>
latest_attempt: Record<string, never>
livemode: boolean
mandate: Record<string, never>
metadata: Record<string, never>
next_action: Record<string, never>
on_behalf_of: Record<string, never>
payment_method: Record<string, never>
payment_method_options: Record<string, never>
payment_method_types: string[]
single_use_mandate: Record<string, never>
status: string
usage: string
lastResponse: {
headers?: Record<string, never>
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<string, never>
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<string, never> | 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 */