diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx index 2846e1dae461e..060612ba1d54d 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx @@ -49,9 +49,13 @@ export const IntegrationOverviewTab = ({ return (
- {alert &&
{alert}
} + + {!!alert &&
{alert}
} + + + {dependsOnExtension && (

Required extensions

@@ -66,16 +70,12 @@ export const IntegrationOverviewTab = ({ return (
  • -
    - - {requiredExtension} - -
    + {requiredExtension}
    {extension ? ( @@ -100,6 +100,7 @@ export const IntegrationOverviewTab = ({
    )} + {!!actions && (
    { @@ -86,8 +86,8 @@ export const useInstalledIntegrations = () => { } if (integration.id === 'stripe_sync_engine') { const stripeSchema = findStripeSchema(schemas) - const status = parseStripeSchemaStatus(stripeSchema) - return checkIsInstalled(status) + const parsedSchema = parseStripeSchema(stripeSchema) + return checkIsInstalled(parsedSchema.status) } if (integration.type === 'wrapper') { return wrappers.find((w) => wrapperMetaComparator(integration.meta, w)) diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationError.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationError.tsx new file mode 100644 index 0000000000000..2c8e63d7b5c66 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationError.tsx @@ -0,0 +1,60 @@ +import { useParams } from 'common' +import { Button } from 'ui' +import { Admonition } from 'ui-patterns' + +import { ContactSupportButton } from '@/components/ui/AlertError' + +export const InstallationError = ({ + error, + handleUninstall, + handleOpenInstallSheet, +}: { + error: 'install' | 'uninstall' + handleUninstall: () => void + handleOpenInstallSheet: () => void +}) => { + const { ref } = useParams() + + if (error === 'uninstall') { + return ( + + + +
    + } + /> + ) + } + + if (error === 'install') { + return ( + + + +
  • + } + /> + ) + } + + return null +} diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx index fa12685ee6640..d1a3343376fd7 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/InstallationOverview.tsx @@ -4,13 +4,12 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { useStripeSyncInstallMutation } from 'data/database-integrations/stripe/stripe-sync-install-mutation' import { useStripeSyncUninstallMutation } from 'data/database-integrations/stripe/stripe-sync-uninstall-mutation' import { useSchemasQuery } from 'data/database/schemas-query' -import { formatRelative } from 'date-fns' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useTrack } from 'lib/telemetry/track' -import { AlertCircle, BadgeCheck, Check, ExternalLink, RefreshCwIcon } from 'lucide-react' +import { ExternalLink } from 'lucide-react' import Link from 'next/link' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { @@ -32,6 +31,8 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' import { IntegrationOverviewTab } from '../../Integration/IntegrationOverviewTab' +import { InstallationError } from './InstallationError' +import { StatusDisplay } from './StatusDisplay' import { canInstall as checkCanInstall, hasInstallError, @@ -39,13 +40,13 @@ import { isInstallDone, isInstalled, isInstalling, - isSyncRunning, isUninstallDone, isUninstalling, } from './stripe-sync-status' import { StripeSyncChangesCard } from './StripeSyncChangesCard' import { useStripeSyncStatus } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus' import { InlineLink } from '@/components/ui/InlineLink' +import { useSSLEnforcementQuery } from '@/data/ssl-enforcement/ssl-enforcement-query' const installFormSchema = z.object({ stripeSecretKey: z.string().min(1, 'Stripe API key is required'), @@ -69,8 +70,9 @@ export const StripeSyncInstallationPage = () => { mode: 'onSubmit', }) - // Use the unified status hook - const { installationStatus, syncState } = useStripeSyncStatus({ + const { + parsedSchema: { status: installationStatus }, + } = useStripeSyncStatus({ projectRef: project?.ref, connectionString: project?.connectionString, }) @@ -81,13 +83,11 @@ export const StripeSyncInstallationPage = () => { '*' ) - const isSyncing = isSyncRunning(syncState) - const installed = isInstalled(installationStatus) const installError = hasInstallError(installationStatus) const uninstallError = hasUninstallError(installationStatus) const installInProgress = isInstalling(installationStatus) - const uninsallInProgress = isUninstalling(installationStatus) + const uninstallInProgress = isUninstalling(installationStatus) const installDone = isInstallDone(installationStatus) const uninstallDone = isUninstallDone(installationStatus) @@ -116,15 +116,25 @@ export const StripeSyncInstallationPage = () => { // Combine schema status with mutation/initiated states for UI const installing = installInProgress || isInstallRequested || isInstallInitiated - const uninstalling = uninsallInProgress || isUninstallRequested || isUninstallInitiated + const uninstalling = uninstallInProgress || isUninstallRequested || isUninstallInitiated const canInstall = checkCanInstall(installationStatus) && !installed && !installing + const hasError = (uninstallError || installError) && !uninstalling && !installing + // Poll for schema changes during transitions useSchemasQuery( { projectRef: project?.ref, connectionString: project?.connectionString }, { refetchInterval: installing || uninstalling ? 5000 : false } ) + const { data: sslEnforcementConfiguration, isSuccess: isSuccessSslEnforcement } = + useSSLEnforcementQuery({ + projectRef: project?.ref, + }) + const isSSLEnforced = + sslEnforcementConfiguration?.appliedSuccessfully && + sslEnforcementConfiguration?.currentConfig.database + const handleUninstall = useCallback(() => { if (!project?.ref) return @@ -148,152 +158,6 @@ export const StripeSyncInstallationPage = () => { } } - const tableEditorUrl = `/project/${project?.ref}/editor?schema=stripe` - - const alert = useMemo(() => { - if (uninstallError) { - return ( - -
    - There was an error during the uninstallation of the Stripe Sync Engine. Please try - again. If the problem persists, contact support. -
    -
    - -
    -
    - ) - } - - if (installError) { - return ( - -
    - There was an error during the installation of the Stripe Sync Engine. Please try - reinstalling the integration. If the problem persists, contact support. -
    -
    - - -
    -
    - ) - } - - if (syncState && installed && !uninstalling) { - return ( - -
    - {isSyncing ? ( - <> -
    - -
    Sync in progress...
    -
    -
    - Started {formatRelative(new Date(syncState.started_at!), new Date())} -
    - - ) : ( - <> -
    - -
    All up to date
    - -
    -
    - Last synced {formatRelative(new Date(syncState.closed_at!), new Date())} -
    - - )} -
    -
    - ) - } - - return null - }, [ - uninstallError, - installError, - syncState, - isSyncing, - installed, - isUninstallRequested, - tableEditorUrl, - uninstalling, - handleOpenInstallSheet, - handleUninstall, - ]) - - const statusDisplay = useMemo(() => { - if (uninstallError) { - return ( - - - Uninstallation error - - ) - } - if (uninstalling) { - return ( - - - Uninstalling... - - ) - } - if (installError) { - return ( - - - Installation error - - ) - } - if (installing) { - return ( - - - Installing... - - ) - } - if (isSyncing && installed) { - return ( - - - Sync in progress... - - ) - } - if (installed) { - return ( - - Installed - - ) - } - return ( - Not installed - ) - }, [uninstallError, uninstalling, installError, installing, isSyncing, installed]) - // Track install failures useEffect(() => { if (!installError) { @@ -324,39 +188,61 @@ export const StripeSyncInstallationPage = () => { }, [isUninstallInitiated, uninstallDone]) return ( - <> - - -
    - setShouldShowInstallSheet(true)} - disabled={!canInstall || !canManageSecrets} - tooltip={{ - content: { - text: !canInstall - ? 'Your database already uses a schema named "stripe"' - : !canManageSecrets - ? 'You need additional permissions to install the Stripe Sync Engine.' - : undefined, - }, - }} - > - Install integration - -
    - - ) : installed && !uninstalling ? ( + + ) : null + } + status={ + + } + actions={ + !installed && !uninstalling && !uninstallError ? ( + <> + +
    + setShouldShowInstallSheet(true)} + disabled={!canInstall || !canManageSecrets} + loading={installing} + tooltip={{ + content: { + text: !canInstall + ? 'Your database already uses a schema named "stripe"' + : !canManageSecrets + ? 'You need additional permissions to install the Stripe Sync Engine.' + : undefined, + }, + }} + > + {installError ? 'Retry installation' : 'Install integration'} + + {installError && ( + + )} +
    + + ) : installed || uninstalling || uninstallError ? ( + <> +
    setShowUninstallModal(true)} disabled={!canManageSecrets} + loading={uninstalling} tooltip={{ content: { text: !canManageSecrets @@ -368,30 +254,34 @@ export const StripeSyncInstallationPage = () => { Uninstall integration
    - ) : null - } - > - - - -
    { - if (!project?.ref) return - installStripeSync({ projectRef: project.ref, stripeSecretKey }) - })} - className="overflow-auto flex-grow px-0 flex flex-col" - > - - Install Stripe Sync Engine - - - + + ) : null + } + > + + + + { + if (!project?.ref) return + installStripeSync({ projectRef: project.ref, stripeSecretKey }) + })} + className="overflow-auto flex-grow px-0 flex flex-col" + > + + Install Stripe Sync Engine + + + + {isSuccessSslEnforcement && isSSLEnforced && (
    This integration currently requires{' '} - + SSL Enforcement {' '} to be disabled during initial setup. @@ -401,9 +291,11 @@ export const StripeSyncInstallationPage = () => { all webhook and sync operations use HTTPS/SSL.

    + )} -

    Configuration

    +

    Configuration

    +
    {
    +
    - {installRequestError && ( - - )} - - - - - - - - - - - - setShowUninstallModal(false)} - onConfirm={handleUninstall} - > -

    - Are you sure you want to uninstall the Stripe Sync Engine? This will: -

    - -

    - This action cannot be undone. -

    -
    - - + {installRequestError && ( + + )} + + + + + + + + + + + + setShowUninstallModal(false)} + onConfirm={handleUninstall} + > +

    + Are you sure you want to uninstall the Stripe Sync Engine? This will: +

    +
      +
    • + Remove the stripe schema and all tables +
    • +
    • Delete all synced Stripe data
    • +
    • Remove the associated Edge Functions
    • +
    • Remove the scheduled sync jobs
    • +
    +

    + This action cannot be undone. +

    +
    + ) } diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx new file mode 100644 index 0000000000000..da6e6ee44c7ae --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StatusDisplay.tsx @@ -0,0 +1,72 @@ +import { AlertCircle, Check, RefreshCwIcon } from 'lucide-react' + +import { + hasInstallError, + hasUninstallError, + isInstalled, + isInstalling, + isUninstalling, + StripeInstallationStatus, +} from './stripe-sync-status' + +export const StatusDisplay = ({ + status, + isInstallRequested, + isUninstallRequested, +}: { + status: StripeInstallationStatus + isInstallRequested: boolean + isUninstallRequested: boolean +}) => { + const installed = isInstalled(status) + const installError = hasInstallError(status) + const uninstallError = hasUninstallError(status) + const installInProgress = isInstalling(status) + const uninstallInProgress = isUninstalling(status) + + const installing = installInProgress || isInstallRequested + const uninstalling = uninstallInProgress || isUninstallRequested + + if (uninstallError) { + return ( + + + Uninstallation error + + ) + } + if (uninstalling) { + return ( + + + Uninstalling... + + ) + } + if (installError) { + return ( + + + Installation error + + ) + } + if (installing) { + return ( + + + Installing... + + ) + } + if (installed) { + return ( + + Installed + + ) + } + return ( + Not installed + ) +} diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx index 0271b45a9c74d..98db11118c332 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncChangesCard.tsx @@ -2,43 +2,119 @@ import { EdgeFunctions } from 'icons' import { Layers, Table } from 'lucide-react' import { Card, CardContent, cn } from 'ui' +import { + hasInstallError, + hasUninstallError, + isInstallDone, + isInstalled, + isInstalling, + isUninstallDone, + isUninstalling, + StripeInstallationStatus, +} from './stripe-sync-status' + type StripeSyncChangesCardProps = { + installationStatus: StripeInstallationStatus className?: string } const ListItemClassName = 'flex items-center gap-x-3 py-2 px-3 border-b' -export const StripeSyncChangesCard = ({ className }: StripeSyncChangesCardProps) => { +export const StripeSyncChangesCard = ({ + installationStatus, + className, +}: StripeSyncChangesCardProps) => { + const installed = isInstalled(installationStatus) + const installError = hasInstallError(installationStatus) + const uninstallError = hasUninstallError(installationStatus) + const installInProgress = isInstalling(installationStatus) + const uninstallInProgress = isUninstalling(installationStatus) + const installDone = isInstallDone(installationStatus) + const uninstallDone = isUninstallDone(installationStatus) + + const title = + uninstallDone || installError + ? 'This integration will modify your Supabase project:' + : installInProgress || uninstallInProgress + ? 'This integration is modifying your Supabase project:' + : installDone || installed || uninstallError + ? 'This integration has modified your Supabase project:' + : '' + + const dbLine = + uninstallDone || installError + ? 'Creates a new database schema named ' + : installInProgress + ? 'Creating a new database schema named ' + : installDone || installed || uninstallError + ? 'Created a new database schema named ' + : uninstallInProgress + ? 'Dropping database schema named ' + : '' + + const tableAndViewLine = + uninstallDone || installError + ? 'Creates tables and views in the ' + : installInProgress + ? 'Creating tables and views in the ' + : installDone || installed || uninstallError + ? 'Created tables and views in the ' + : uninstallInProgress + ? 'Dropping tables and views in the ' + : '' + + const edgeFunctionsLine = + uninstallDone || installError + ? 'Deploys Edge Functions to handle incoming webhooks from Stripe' + : installInProgress + ? 'Deploying Edge Functions to handle incoming webhooks from Stripe' + : installDone || installed || uninstallError + ? 'Deployed Edge Functions to handle incoming webhooks from Stripe' + : uninstallInProgress + ? 'Undeploying Edge Functions to handle incoming webhooks from Stripe' + : '' + + const scheduleLine = + uninstallDone || installError + ? 'Schedules automatic Stripe data syncs using Supabase Queues' + : installInProgress + ? 'Scheduling automatic Stripe data syncs using Supabase Queues' + : installDone || installed || uninstallError + ? 'Scheduled automatic Stripe data syncs using Supabase Queues' + : uninstallInProgress + ? 'Unscheduling automatic Stripe data syncs using Supabase Queues' + : '' + return (
    -

    This integration will modify your Supabase project:

    +

    {title}

    • - +
      - Creates a new database schema named stripe + {dbLine} stripe
    • -
    • +
      - Creates tables and views in the stripe{' '} - schema for synced Stripe data + {tableAndViewLine} stripe schema for + synced Stripe data
    • - Deploys Edge Functions to handle incoming webhooks from Stripe + {edgeFunctionsLine}
    • - - Schedules automatic Stripe data syncs using Supabase Queues + + {scheduleLine}
    • diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx index 3075db7778f34..311e61e292c85 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage.tsx @@ -1,9 +1,10 @@ +import { formatRelative } from 'date-fns' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { Table2 } from 'lucide-react' +import { BadgeCheck, RefreshCwIcon, Table2 } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' -import { useEffect } from 'react' import { Button, Card, CardContent } from 'ui' +import { Admonition } from 'ui-patterns' import { PageContainer } from 'ui-patterns/PageContainer' import { PageSection, @@ -14,28 +15,73 @@ import { PageSectionTitle, } from 'ui-patterns/PageSection' -import { isInstalled } from './stripe-sync-status' +import { isInstalled, isSyncRunning, isUninstalling } from './stripe-sync-status' import { useStripeSyncStatus } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus' export const StripeSyncSettingsPage = () => { const router = useRouter() const { data: project } = useSelectedProjectQuery() - const { installationStatus, isLoading: isLoadingInstallationStatus } = useStripeSyncStatus({ + const { + parsedSchema: { status: installationStatus }, + syncState, + isLoading: isLoadingInstallationStatus, + } = useStripeSyncStatus({ projectRef: project?.ref, connectionString: project?.connectionString, }) const installed = isInstalled(installationStatus) - - // Redirect to overview page when integration is not installed - useEffect(() => { - if (isLoadingInstallationStatus || installed || !project?.ref) return - - router.push(`/project/${project.ref}/integrations/stripe_sync_engine/overview`) - }, [isLoadingInstallationStatus, installed, project?.ref, router]) + const isSyncing = isSyncRunning(syncState) + const uninstalling = isUninstalling(installationStatus) + const tableEditorUrl = `/project/${project?.ref}/editor?schema=stripe` return ( - + + {syncState && installed && !uninstalling && ( + + + + Sync Status + + + + +
      + {isSyncing ? ( + <> +
      + +
      Sync in progress...
      +
      +
      + Started{' '} + {syncState.started_at + ? formatRelative(new Date(syncState.started_at), new Date()) + : 'recently'} +
      + + ) : ( + <> +
      + +
      All up to date
      + +
      +
      + Last synced{' '} + {syncState.closed_at + ? formatRelative(new Date(syncState.closed_at), new Date()) + : 'recently'} +
      + + )} +
      +
      +
      +
      + )} diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts index 190f76ad64307..d57ce7100b94b 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status.ts @@ -21,12 +21,26 @@ export type StripeInstallationStatus = | 'uninstalled' | 'uninstall_error' +/** + * Parsed Stripe schema status including version and error message + */ +export interface ParsedStripeSchema { + /** The installation status */ + status: StripeInstallationStatus + + /** The version from the schema comment (e.g., 'v1.2.3') */ + version?: string + + /** Error message if status is install_error or uninstall_error */ + errorMessage?: string +} + /** * Complete Stripe Sync status including schema, installation state, and sync state */ export interface StripeSyncStatusResult { - /** The installation status */ - installationStatus: StripeInstallationStatus + /** The parsed schema with status, version and error */ + parsedSchema: ParsedStripeSchema /** Current sync run state (only available when installationStatus is installed) */ syncState: StripeSyncState | undefined @@ -51,40 +65,72 @@ function isStripeSyncSchema(schema: Schema | undefined): boolean { return !!schema?.comment?.startsWith(STRIPE_SCHEMA_COMMENT_PREFIX) } -function hasStatusSuffix(schema: Schema | undefined, suffix: string): boolean { - return isStripeSyncSchema(schema) && !!schema?.comment?.endsWith(suffix) -} - /** * Parse the installation status from a stripe schema. * + * Schema comment format: {PREFIX} {version} {SUFFIX}[ - {error message}] + * Example: "stripe_sync v1.2.3 installation:error - Could not apply migration" + * * @param stripeSchema - The stripe schema from the database, if it exists - * @returns The installation status + * @returns The parsed status including version and optional error message */ -export function parseStripeSchemaStatus( - stripeSchema: Schema | undefined -): StripeInstallationStatus { - if (hasStatusSuffix(stripeSchema, UNINSTALLATION_ERROR_SUFFIX)) { - return 'uninstall_error' +export function parseStripeSchema(stripeSchema: Schema | undefined): ParsedStripeSchema { + if (!isStripeSyncSchema(stripeSchema)) { + return { status: 'uninstalled' } + } + + const comment = stripeSchema!.comment! + + // Remove prefix and leading space + const afterPrefix = comment.slice(STRIPE_SCHEMA_COMMENT_PREFIX.length).trimStart() + + // Split at first space to get version and trailing segment + const firstSpaceIndex = afterPrefix.indexOf(' ') + if (firstSpaceIndex === -1) { + return { status: 'uninstalled' } + } + + const version = afterPrefix.slice(0, firstSpaceIndex) + const trailing = afterPrefix.slice(firstSpaceIndex + 1) + + // Helper to extract error message if present after ' - ' separator + const extractError = (afterSuffix: string): string | undefined => { + if (afterSuffix.startsWith(' - ')) { + return afterSuffix.slice(3) // Remove ' - ' (3 characters) + } + return afterSuffix + } + + // Check status in priority order + if (trailing.startsWith(UNINSTALLATION_ERROR_SUFFIX)) { + return { + status: 'uninstall_error', + version, + errorMessage: extractError(trailing.slice(UNINSTALLATION_ERROR_SUFFIX.length)), + } } - if (hasStatusSuffix(stripeSchema, INSTALLATION_ERROR_SUFFIX)) { - return 'install_error' + if (trailing.startsWith(INSTALLATION_ERROR_SUFFIX)) { + return { + status: 'install_error', + version, + errorMessage: extractError(trailing.slice(INSTALLATION_ERROR_SUFFIX.length)), + } } - if (hasStatusSuffix(stripeSchema, UNINSTALLATION_STARTED_SUFFIX)) { - return 'uninstalling' + if (trailing.startsWith(UNINSTALLATION_STARTED_SUFFIX)) { + return { status: 'uninstalling', version } } - if (hasStatusSuffix(stripeSchema, INSTALLATION_STARTED_SUFFIX)) { - return 'installing' + if (trailing.startsWith(INSTALLATION_STARTED_SUFFIX)) { + return { status: 'installing', version } } - if (hasStatusSuffix(stripeSchema, INSTALLATION_INSTALLED_SUFFIX)) { - return 'installed' + if (trailing.startsWith(INSTALLATION_INSTALLED_SUFFIX)) { + return { status: 'installed', version } } - return 'uninstalled' + return { status: 'uninstalled' } } export function isInstalled(status: StripeInstallationStatus): boolean { diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts index a28f840598413..4332391c25c78 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus.ts @@ -6,7 +6,7 @@ import { findStripeSchema, isInProgress, isInstalled, - parseStripeSchemaStatus, + parseStripeSchema, type StripeSyncStatusResult, } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status' @@ -30,10 +30,10 @@ export function useStripeSyncStatus({ // Find and parse stripe schema status const stripeSchema = findStripeSchema(schemas) - const installationStatus = parseStripeSchemaStatus(stripeSchema) + const parsedSchema = parseStripeSchema(stripeSchema) - const installed = isInstalled(installationStatus) - const inProgress = isInProgress(installationStatus) + const installed = isInstalled(parsedSchema.status) + const inProgress = isInProgress(parsedSchema.status) // Poll schemas during install/uninstall operations useEffect(() => { @@ -58,7 +58,7 @@ export function useStripeSyncStatus({ ) return { - installationStatus, + parsedSchema, syncState: installed ? syncState : undefined, isLoading: isSchemasLoading, } diff --git a/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx b/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx index 79b5c1b1cbb28..9c74ca8ca7cbb 100644 --- a/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx +++ b/apps/studio/components/interfaces/Settings/Database/BannedIPs.tsx @@ -1,32 +1,29 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Globe } from 'lucide-react' -import { useState } from 'react' -import { toast } from 'sonner' - import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DocsButton } from 'components/ui/DocsButton' -import { FormHeader } from 'components/ui/Forms/FormHeader' -import { FormPanel } from 'components/ui/Forms/FormPanel' import { useBannedIPsDeleteMutation } from 'data/banned-ips/banned-ips-delete-mutations' import { useBannedIPsQuery } from 'data/banned-ips/banned-ips-query' import { useUserIPAddressQuery } from 'data/misc/user-ip-address-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import { Globe } from 'lucide-react' +import { useState } from 'react' +import { toast } from 'sonner' import { Badge, Card, CardContent, Skeleton } from 'ui' -import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { PageSection, + PageSectionContent, + PageSectionDescription, PageSectionMeta, PageSectionSummary, PageSectionTitle, - PageSectionContent, - PageSectionDescription, } from 'ui-patterns' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' -const BannedIPs = () => { +export const BannedIPs = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() @@ -100,11 +97,7 @@ const BannedIPs = () => { ) : ipListError ? ( - + ) : ipList.banned_ipv4_addresses.length > 0 ? ( {ipList.banned_ipv4_addresses.map((ip) => ( @@ -160,5 +153,3 @@ const BannedIPs = () => { ) } - -export default BannedIPs diff --git a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx index b7236b99aea7d..31253f28b1b34 100644 --- a/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Database/DiskSizeConfiguration.tsx @@ -1,13 +1,9 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { ExternalLink, Info } from 'lucide-react' -import Link from 'next/link' -import { SetStateAction } from 'react' -import { toast } from 'sonner' - import { useParams } from 'common' import { Markdown } from 'components/interfaces/Markdown' import DiskSizeConfigurationModal from 'components/interfaces/Settings/Database/DiskSizeConfigurationModal' import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { DocsButton } from 'components/ui/DocsButton' import Panel from 'components/ui/Panel' import { useProjectDiskResizeMutation } from 'data/config/project-disk-resize-mutation' import { useDatabaseSizeQuery } from 'data/database/database-size-query' @@ -18,21 +14,24 @@ import { useIsAwsNimbusCloudProvider, useSelectedProjectQuery } from 'hooks/misc import { useUrlState } from 'hooks/ui/useUrlState' import { DOCS_URL } from 'lib/constants' import { formatBytes } from 'lib/helpers' -import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, InfoIcon } from 'ui' -import { DocsButton } from 'components/ui/DocsButton' +import { ExternalLink, Info } from 'lucide-react' +import Link from 'next/link' +import { SetStateAction } from 'react' +import { toast } from 'sonner' +import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Button, InfoIcon } from 'ui' import { PageSection, + PageSectionContent, PageSectionMeta, PageSectionSummary, PageSectionTitle, - PageSectionContent, } from 'ui-patterns' export interface DiskSizeConfigurationProps { disabled?: boolean } -const DiskSizeConfiguration = ({ disabled = false }: DiskSizeConfigurationProps) => { +export const DiskSizeConfiguration = ({ disabled = false }: DiskSizeConfigurationProps) => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() const { data: organization } = useSelectedOrganizationQuery() @@ -215,5 +214,3 @@ Read more about [disk management](${DOCS_URL}/guides/platform/database-size#disk ) } - -export default DiskSizeConfiguration diff --git a/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx b/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx index 7caf8352e6d1a..cfd3b7c673eb1 100644 --- a/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx +++ b/apps/studio/components/interfaces/Settings/Database/SSLConfiguration.tsx @@ -1,9 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { template } from 'lodash' -import { Download, Loader2 } from 'lucide-react' -import { useEffect, useMemo, useState } from 'react' -import { toast } from 'sonner' - import { useParams } from 'common' import { SupportLink } from 'components/interfaces/Support/SupportLink' import { ButtonTooltip } from 'components/ui/ButtonTooltip' @@ -16,6 +11,10 @@ import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import { template } from 'lodash' +import { Download, Loader2 } from 'lucide-react' +import { useEffect, useMemo, useState } from 'react' +import { toast } from 'sonner' import { Alert, AlertDialog, @@ -44,7 +43,7 @@ import { } from 'ui-patterns' import { FormLayout } from 'ui-patterns/form/Layout/FormLayout' -const SSLConfiguration = () => { +export const SSLConfiguration = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const [isEnforced, setIsEnforced] = useState(false) @@ -231,5 +230,3 @@ const SSLConfiguration = () => { ) } - -export default SSLConfiguration diff --git a/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx b/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx index f43caee3d6ce6..162e2bf22d77b 100644 --- a/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx +++ b/apps/studio/components/layouts/AppLayout/StatusPageBanner.tsx @@ -16,5 +16,12 @@ export const StatusPageBanner = () => { if (!banner) return null - return + return ( + + ) } diff --git a/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.test.ts b/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.test.ts index 5b2618d773947..acb5f11c609d4 100644 --- a/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.test.ts +++ b/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.test.ts @@ -1,18 +1,22 @@ import { describe, expect, it } from 'vitest' -import { shouldShowBanner } from './StatusPageBanner.utils' +import { getRelevantIncidentIds, shouldShowBanner } from './StatusPageBanner.utils' -const noCache = { cache: null } as const +const noCache = { id: 'no-cache', cache: null } as const const noRestrictions = { + id: 'no-restrictions', cache: { affected_regions: null, affects_project_creation: false }, } const affectsCreation = { + id: 'affects-creation', cache: { affected_regions: null, affects_project_creation: true }, } const usEast1Only = { + id: 'us-east-1-only', cache: { affected_regions: ['us-east-1'], affects_project_creation: false }, } const usEast1AndCreation = { + id: 'us-east-1-and-creation', cache: { affected_regions: ['us-east-1'], affects_project_creation: true }, } @@ -87,7 +91,9 @@ describe('shouldShowBanner', () => { it('shows when affected_regions is an empty array', () => { expect( shouldShowBanner({ - incidents: [{ cache: { affected_regions: [], affects_project_creation: false } }], + incidents: [ + { id: 'test', cache: { affected_regions: [], affects_project_creation: false } }, + ], hasProjects: true, userRegions: new Set(['us-east-1']), }) @@ -110,7 +116,10 @@ describe('shouldShowBanner', () => { expect( shouldShowBanner({ incidents: [ - { cache: { affected_regions: ['eu-west-1'], affects_project_creation: false } }, + { + id: 'test', + cache: { affected_regions: ['eu-west-1'], affects_project_creation: false }, + }, ], hasProjects: true, userRegions: new Set(['us-east-1', 'eu-west-1']), @@ -123,6 +132,7 @@ describe('shouldShowBanner', () => { shouldShowBanner({ incidents: [ { + id: 'test', cache: { affected_regions: ['us-east-1', 'ap-southeast-1'], affects_project_creation: false, @@ -223,3 +233,107 @@ describe('shouldShowBanner', () => { }) }) }) + +describe('getRelevantIncidentIds', () => { + it('returns empty array when there are no incidents', () => { + expect( + getRelevantIncidentIds({ incidents: [], hasProjects: true, userRegions: new Set() }) + ).toEqual([]) + }) + + it('returns empty array when no incidents are relevant to the user', () => { + expect( + getRelevantIncidentIds({ + incidents: [usEast1Only], + hasProjects: true, + userRegions: new Set(['eu-west-1']), + }) + ).toEqual([]) + }) + + it('returns the ID of a single relevant incident', () => { + expect( + getRelevantIncidentIds({ + incidents: [noRestrictions], + hasProjects: true, + userRegions: new Set(['us-east-1']), + }) + ).toEqual(['no-restrictions']) + }) + + it('returns IDs of all relevant incidents', () => { + const euWest1Only = { + id: 'eu-west-1-only', + cache: { affected_regions: ['eu-west-1'], affects_project_creation: false }, + } + const apSoutheast1Only = { + id: 'ap-southeast-1-only', + cache: { affected_regions: ['ap-southeast-1'], affects_project_creation: false }, + } + + expect( + getRelevantIncidentIds({ + incidents: [euWest1Only, apSoutheast1Only], + hasProjects: true, + userRegions: new Set(['eu-west-1', 'ap-southeast-1']), + }) + ).toEqual(expect.arrayContaining(['ap-southeast-1-only', 'eu-west-1-only'])) + }) + + it('excludes incidents irrelevant to the user from the result', () => { + // User is in eu-west-1; us-east-1-only incident should not be included + expect( + getRelevantIncidentIds({ + incidents: [usEast1Only, noRestrictions], + hasProjects: true, + userRegions: new Set(['eu-west-1']), + }) + ).toEqual(['no-restrictions']) + }) + + describe('user has no projects', () => { + it('includes incidents with affects_project_creation', () => { + expect( + getRelevantIncidentIds({ + incidents: [affectsCreation], + hasProjects: false, + userRegions: new Set(), + }) + ).toEqual(['affects-creation']) + }) + + it('excludes incidents without affects_project_creation', () => { + expect( + getRelevantIncidentIds({ + incidents: [noRestrictions, usEast1Only], + hasProjects: false, + userRegions: new Set(), + }) + ).toEqual([]) + }) + }) + + describe('hasUnknownRegions', () => { + it('includes region-restricted incidents when regions are unknown', () => { + expect( + getRelevantIncidentIds({ + incidents: [usEast1Only], + hasProjects: true, + userRegions: new Set(), + hasUnknownRegions: true, + }) + ).toEqual(['us-east-1-only']) + }) + + it('still excludes incidents for no-project users even when regions are unknown', () => { + expect( + getRelevantIncidentIds({ + incidents: [usEast1Only], + hasProjects: false, + userRegions: new Set(), + hasUnknownRegions: true, + }) + ).toEqual([]) + }) + }) +}) diff --git a/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.ts b/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.ts index d8fcf5f8f7b80..ccd3fa98275c9 100644 --- a/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.ts +++ b/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.ts @@ -1,6 +1,6 @@ import type { IncidentCache } from 'lib/api/incident-status' -type BannerIncident = { cache?: IncidentCache | null } +type BannerIncident = { id: string; cache?: IncidentCache | null } /** * Determines whether the incident status banner should be shown to a given user, @@ -42,3 +42,27 @@ export function shouldShowBanner({ return affectedRegions.some((region) => userRegions.has(region)) }) } + +/** + * Returns the IDs of incidents that are relevant to the given user. + * + * An incident is considered relevant if it would trigger banner visibility for + * the user, per the same logic as shouldShowBanner. + */ +export function getRelevantIncidentIds({ + incidents, + hasProjects, + userRegions, + hasUnknownRegions = false, +}: { + incidents: Array + hasProjects: boolean + userRegions: Set + hasUnknownRegions?: boolean +}): Array { + return incidents + .filter((incident) => + shouldShowBanner({ incidents: [incident], hasProjects, userRegions, hasUnknownRegions }) + ) + .map((incident) => incident.id) +} diff --git a/apps/studio/components/layouts/AppLayout/useStatusPageBannerVisibility.ts b/apps/studio/components/layouts/AppLayout/useStatusPageBannerVisibility.ts index 8975cba3b9a13..4bce0648080ca 100644 --- a/apps/studio/components/layouts/AppLayout/useStatusPageBannerVisibility.ts +++ b/apps/studio/components/layouts/AppLayout/useStatusPageBannerVisibility.ts @@ -1,7 +1,8 @@ import { useQueries } from '@tanstack/react-query' -import { useFlag } from 'common' +import { LOCAL_STORAGE_KEYS, useFlag } from 'common' +import { useCallback, useMemo } from 'react' -import { shouldShowBanner } from './StatusPageBanner.utils' +import { getRelevantIncidentIds, shouldShowBanner } from './StatusPageBanner.utils' import { useOrganizationsQuery } from '@/data/organizations/organizations-query' import { useIncidentStatusQuery } from '@/data/platform/incident-status-query' import { projectKeys } from '@/data/projects/keys' @@ -9,8 +10,9 @@ import { getOrganizationProjects, type OrgProject, } from '@/data/projects/org-projects-infinite-query' +import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' -export type StatusPageBannerData = { title: string } +export type StatusPageBannerData = { title: string; dismiss?: () => void } export function useStatusPageBannerVisibility(): StatusPageBannerData | null { const showIncidentBannerOverride = @@ -40,22 +42,63 @@ export function useStatusPageBannerVisibility(): StatusPageBannerData | null { const allProjects = orgProjectsQueries.flatMap((q) => q.data?.projects ?? []) const hasProjects = allProjects.length > 0 - const userRegions = new Set( - allProjects.flatMap((project: OrgProject) => project.databases.map((db) => db.region)) + const userRegions = useMemo( + () => + new Set( + allProjects.flatMap((project: OrgProject) => project.databases.map((db) => db.region)) + ), + [allProjects] ) const hasUnknownRegions = orgProjectsQueries.some( (q) => q.isError || (q.data !== undefined && q.data.pagination.count > q.data.projects.length) ) + const [dismissedIds, setDismissedIds, { isSuccess: isDismissedLoaded }] = useLocalStorageQuery< + Array + >(LOCAL_STORAGE_KEYS.INCIDENT_BANNER_DISMISSED_IDS, []) + + const dismiss = useCallback(() => { + const activeIncidentIds = new Set(incidents.map((i) => i.id)) + const relevantIds = getRelevantIncidentIds({ + incidents, + hasProjects, + userRegions, + hasUnknownRegions, + }) + setDismissedIds((prev) => [ + ...new Set([...prev.filter((id) => activeIncidentIds.has(id)), ...relevantIds]), + ]) + }, [incidents, hasProjects, userRegions, hasUnknownRegions, setDismissedIds]) + if (showIncidentBannerOverride) return { title: 'We are investigating a technical issue' } if (!hasActiveIncidents || !isProjectsFetched) return null - if (!shouldShowBanner({ incidents, hasProjects, userRegions, hasUnknownRegions })) return null + // Filter out individually dismissed incidents. An incident stays dismissed as + // long as its ID remains in the stored set, regardless of whether other + // incidents are added or removed. + const dismissedIdSet = new Set(dismissedIds) + const undismissedIncidents = incidents.filter((i) => !dismissedIdSet.has(i.id)) + + // If dismissed state hasn't loaded yet, hide to prevent a flash of the banner. + // If all relevant incidents have been dismissed, hide the banner. + if ( + !isDismissedLoaded || + !shouldShowBanner({ + incidents: undismissedIncidents, + hasProjects, + userRegions, + hasUnknownRegions, + }) + ) + return null + + const title = hasProjects + ? 'We are investigating a technical issue' + : 'Project creation may be impacted in some regions' return { - title: hasProjects - ? 'We are investigating a technical issue' - : 'Project creation may be impacted in some regions', + title, + dismiss, } } diff --git a/apps/studio/components/ui/AlertError.tsx b/apps/studio/components/ui/AlertError.tsx index 0ec74d9448780..b95b2ea77aa69 100644 --- a/apps/studio/components/ui/AlertError.tsx +++ b/apps/studio/components/ui/AlertError.tsx @@ -1,11 +1,9 @@ import { SupportCategories } from '@supabase/shared-types/out/constants' import { SupportLink } from 'components/interfaces/Support/SupportLink' +import { useTrack } from 'lib/telemetry/track' import { PropsWithChildren, useEffect, useRef } from 'react' - -import { Admonition } from 'ui-patterns/admonition' - import { Button } from 'ui' -import { useTrack } from 'lib/telemetry/track' +import { Admonition } from 'ui-patterns/admonition' export interface AlertErrorProps { projectRef?: string @@ -19,7 +17,7 @@ export interface AlertErrorProps { additionalActions?: React.ReactNode } -const ContactSupportButton = ({ +export const ContactSupportButton = ({ projectRef, subject, error, diff --git a/apps/studio/pages/project/[ref]/database/settings.tsx b/apps/studio/pages/project/[ref]/database/settings.tsx index 8e0226bdba70a..45260e8a107fc 100644 --- a/apps/studio/pages/project/[ref]/database/settings.tsx +++ b/apps/studio/pages/project/[ref]/database/settings.tsx @@ -1,13 +1,14 @@ -import dynamic from 'next/dynamic' -import { IS_PLATFORM } from 'lib/constants' import { ConnectionPooling } from 'components/interfaces/Settings/Database/ConnectionPooling/ConnectionPooling' import { DatabaseReadOnlyAlert } from 'components/interfaces/Settings/Database/DatabaseReadOnlyAlert' import ResetDbPassword from 'components/interfaces/Settings/Database/DatabaseSettings/ResetDbPassword' import { PoolingModesModal } from 'components/interfaces/Settings/Database/PoolingModesModal' +import { SettingsDatabaseEmptyStateLocal } from 'components/interfaces/Settings/Database/SettingsDatabaseEmptyStateLocal' import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useIsAwsCloudProvider, useIsAwsK8sCloudProvider } from 'hooks/misc/useSelectedProject' +import { IS_PLATFORM } from 'lib/constants' +import type { NextPageWithLayout } from 'types' import { PageContainer } from 'ui-patterns/PageContainer' import { PageHeader, @@ -17,26 +18,12 @@ import { PageHeaderTitle, } from 'ui-patterns/PageHeader' import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' -import type { NextPageWithLayout } from 'types' -import { SettingsDatabaseEmptyStateLocal } from 'components/interfaces/Settings/Database/SettingsDatabaseEmptyStateLocal' -const SSLConfiguration = dynamic( - () => import('components/interfaces/Settings/Database/SSLConfiguration') -) -const DiskSizeConfiguration = dynamic( - () => import('components/interfaces/Settings/Database/DiskSizeConfiguration') -) -const NetworkRestrictions = dynamic(() => - import('components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions').then( - (mod) => mod.NetworkRestrictions - ) -) -const BannedIPs = dynamic(() => import('components/interfaces/Settings/Database/BannedIPs')) -const DiskManagementPanelForm = dynamic(() => - import('components/interfaces/DiskManagement/DiskManagementPanelForm').then( - (mod) => mod.DiskManagementPanelForm - ) -) +import { DiskManagementPanelForm } from '@/components/interfaces/DiskManagement/DiskManagementPanelForm' +import { BannedIPs } from '@/components/interfaces/Settings/Database/BannedIPs' +import { DiskSizeConfiguration } from '@/components/interfaces/Settings/Database/DiskSizeConfiguration' +import { NetworkRestrictions } from '@/components/interfaces/Settings/Database/NetworkRestrictions/NetworkRestrictions' +import { SSLConfiguration } from '@/components/interfaces/Settings/Database/SSLConfiguration' const ProjectSettings: NextPageWithLayout = () => { const isAws = useIsAwsCloudProvider() diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx index 22971a6f31703..a67fda5142d57 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx @@ -1,11 +1,11 @@ import { useFlag, useParams } from 'common' -import { IS_PLATFORM } from 'lib/constants' import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants' import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import { DefaultLayout } from 'components/layouts/DefaultLayout' import IntegrationsLayout from 'components/layouts/Integrations/layout' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { IS_PLATFORM } from 'lib/constants' import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect, useMemo } from 'react' diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 266def91985b6..b0ffc6549e7d0 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -11,7 +11,7 @@ export const LOCAL_STORAGE_KEYS = { PROJECTS_SORT: 'projects-sort', FEEDBACK_WIDGET_CONTENT: 'feedback-widget-content', FEEDBACK_WIDGET_SCREENSHOT: 'feedback-widget-screenshot', - INCIDENT_BANNER_DISMISSED: (id: string) => `incident-banner-dismissed-${id}`, + INCIDENT_BANNER_DISMISSED_IDS: 'incident-banner-dismissed-ids', MAINTENANCE_BANNER_DISMISSED: (id: string) => `maintenance-banner-dismissed-${id}`, UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',