mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
## What kind of change does this PR introduce? Feature and information architecture change. Builds on #50246. ## What is the current behaviour? Warehouse setup, progress, errors, table selection, and connection details all live in the transient Connect sheet. Closing the sheet hides the current replication state, and the integration is absent from the Integrations page. ## What is the new behaviour? Warehouse now has a persistent Overview page at `/project/{ref}/integrations/warehouse/overview`: - Before setup, the existing schema and table picker enables Warehouse. - During setup, the page shows the current phase and per-table backfill state where available. - Setup and status failures remain visible on the page with retry actions where possible. - Once complete, the page shows Status, Tables, then Connect. - The Connect sheet becomes read-only. Before Warehouse is ready, it links directly to the Overview page for setup, progress, or recovery. | Before | After | | --- | --- | | <img width="1200" height="907" alt="Chives Pantry Supabase" src="https://github.com/user-attachments/assets/82dc7fb0-3859-499e-96ab-56f70a5c7325" /> | <img width="1200" height="907" alt="Chives Pantry Supabase" src="https://github.com/user-attachments/assets/7428e301-27f8-48fe-91e0-6880b132920d" /> | | <img width="1200" height="907" alt="Chives Pantry Supabase" src="https://github.com/user-attachments/assets/82dc7fb0-3859-499e-96ab-56f70a5c7325" /> | <img width="1200" height="907" alt="54709" src="https://github.com/user-attachments/assets/0c589e5b-e13c-4554-a6ee-6730d0e95c07" /> | | <img width="1200" height="907" alt="ETL BigTable ETL Team Supabase" src="https://github.com/user-attachments/assets/18ec9f6c-3724-453f-bbbb-c7149758246e" /> | <img width="1200" height="907" alt="Regular AWS Teamer Supabase" src="https://github.com/user-attachments/assets/3819c9fe-e56d-4cec-988d-5e724f8d990a" /> | ## To test Use a project whose organisation is included in the Warehouse allow-list. 1. Open `/project/{ref}/integrations`, filter by **Data platform**, and open Warehouse. 2. Before setup, confirm the existing schema and table picker appears and starts with no tables selected. 3. Start setup and confirm the Status section polls through setup and table backfill phases. 4. Confirm setup failures remain visible and expose Retry when the API returns affected tables. 5. After setup, confirm the section order is Status, Tables, Connect. 6. Confirm existing replicated tables are selected and locked, while additional tables can be added. 7. Open `/project/{ref}?showConnect=true&connectTab=warehouse` and confirm it links to the Overview page before setup, during setup, and after a setup failure. 8. Once setup is complete, confirm the Connect sheet shows the FlightSQL and DuckDB connection controls from #50246. 9. Repeat the Overview checks with **One-Click Integrations** turned off. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Supabase Warehouse to the integrations catalog, with overview documentation and availability-aware display. * Added Warehouse setup and management flows, including schema and table selection, replication progress, status details, connection options, and retry actions. * Added DuckDB and FlightSQL engine selection with Connect sheet URL and preference synchronization. * Added table replication status, lag, timestamps, and size information. * **Bug Fixes** * Warehouse setup status requests no longer retry automatically after failures. * Improved recovery messaging and retry behavior for setup and connection errors. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
715 lines
21 KiB
TypeScript
715 lines
21 KiB
TypeScript
import { getEnableWebhooksSQL } from '@supabase/pg-meta'
|
|
import type { Tables } from 'common/marketplace.types'
|
|
import { Clock5, Code2, Layers, Timer, Vault, Warehouse, Webhook } from 'lucide-react'
|
|
import dynamic from 'next/dynamic'
|
|
import Image from 'next/image'
|
|
import { ComponentType, ReactNode } from 'react'
|
|
import { cn } from 'ui'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { UpgradeDatabaseAlert } from '../Queues/UpgradeDatabaseAlert'
|
|
import { getStripeSyncSchemaComment } from '../templates/StripeSyncEngine/useStripeSyncStatus'
|
|
import { WRAPPERS } from '../Wrappers/Wrappers.constants'
|
|
import { WrapperMeta } from '../Wrappers/Wrappers.types'
|
|
import { stripeSyncKeys } from '@/data/database-integrations/stripe/keys'
|
|
import { installStripeSync } from '@/data/database-integrations/stripe/stripe-sync-install-mutation'
|
|
import { enableDatabaseWebhooks } from '@/data/database/hooks-enable-mutation'
|
|
import { databaseKeys } from '@/data/database/keys'
|
|
import { getSchemas, invalidateSchemasQuery } from '@/data/database/schemas-query'
|
|
import { getQueryClient } from '@/data/query-client'
|
|
import { BASE_PATH, DOCS_URL } from '@/lib/constants'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
export type NavigationContentLayout = 'constrained' | 'full'
|
|
|
|
export type Navigation = {
|
|
route: string
|
|
label: string
|
|
hasChild?: boolean
|
|
childIcon?: React.ReactNode
|
|
children?: Navigation[]
|
|
layout?: NavigationContentLayout // applies only to the new marketplace
|
|
}
|
|
|
|
// [Joshen] Basing this on template.json for now
|
|
export type IntegrationInputs = {
|
|
[key: string]: {
|
|
label: string
|
|
type: 'text' | 'number' | 'password'
|
|
description?: string
|
|
required: boolean
|
|
actions: {
|
|
label: string
|
|
href: string
|
|
}[]
|
|
}
|
|
}
|
|
|
|
type IntegrationStep = {
|
|
label: string
|
|
description?: string
|
|
}
|
|
|
|
type Listing = Tables<'listings'>
|
|
type InstallUrlType = NonNullable<Listing['installation_url_type']>
|
|
type InstallIdentificationMethod = NonNullable<Listing['installation_identification_method']>
|
|
|
|
export type MarketplaceSource = 'Official' | 'Partner' | 'Community'
|
|
|
|
/**
|
|
* [Joshen] For marketplace, we probably need to revisit this definition
|
|
* What properties are obsolete, what properties we need from remote source
|
|
*/
|
|
export type IntegrationDefinition = {
|
|
id: string
|
|
name: string
|
|
status?: 'alpha' | 'beta'
|
|
categories?: string[]
|
|
featured?: boolean
|
|
icon: (props?: { className?: string; style?: Record<string, string | number> }) => ReactNode
|
|
description: string | null
|
|
content?: string | null
|
|
files?: { src: string; alt: string }[]
|
|
docsUrl: string | null
|
|
siteUrl?: string | null
|
|
author: {
|
|
name: string
|
|
websiteUrl: string
|
|
}
|
|
/** Provenance of the integration — Official (built by Supabase), Partner (formal third-party listing), Community (open-source, not officially endorsed). */
|
|
source: MarketplaceSource
|
|
requiredExtensions: Array<string>
|
|
/** Optional component to render if the integration requires extensions that are not available on the current database image */
|
|
missingExtensionsAlert?: ReactNode
|
|
navigation?: Array<Navigation>
|
|
navigate: (props: {
|
|
id: string | undefined
|
|
pageId: string | undefined
|
|
childId: string | undefined
|
|
}) => ComponentType<{}> | null
|
|
|
|
/** For showing the SQL query in the installation sheet */
|
|
installationSql?: string
|
|
/** Custom command to install the integration (if any - none atm) */
|
|
installationCommand?: (props: {
|
|
ref: string
|
|
track?: ReturnType<typeof useTrack>
|
|
[key: string]: unknown
|
|
}) => Promise<void>
|
|
/**
|
|
* Used for long polling to track the progress of the integration installation if async
|
|
* The component calling this handles the polling logic, and should terminate the poll depending on the returned value
|
|
* Depending on how we want this to work, this method will thereafter also call any RQ invalidation if required
|
|
* */
|
|
checkInstallationStatus?: (props: {
|
|
ref?: string
|
|
connectionString?: string | null
|
|
[key: string]: unknown
|
|
}) => Promise<'installed' | 'installing'>
|
|
/** User inputs for template integrations */
|
|
inputs?: IntegrationInputs
|
|
/** Purely visual, just to show what are the changes on the project from installing the integration */
|
|
steps?: IntegrationStep[]
|
|
|
|
/** These are for OAuth Integrations */
|
|
installUrl?: string | null
|
|
installUrlType?: InstallUrlType
|
|
installIdentificationMethod?: InstallIdentificationMethod
|
|
secretKeyPrefix?: string
|
|
edgeFunctionSecretName?: string
|
|
listingId?: string
|
|
oauthAppId?: string
|
|
} & (
|
|
| { type: 'wrapper'; meta: WrapperMeta }
|
|
| { type: 'postgres_extension' | 'custom' | 'oauth' | 'template' }
|
|
)
|
|
|
|
const authorSupabase = {
|
|
name: 'Supabase',
|
|
websiteUrl: 'https://supabase.com',
|
|
}
|
|
|
|
const SUPABASE_INTEGRATIONS: Array<IntegrationDefinition> = [
|
|
{
|
|
id: 'queues',
|
|
type: 'postgres_extension' as const,
|
|
source: 'Community' as const,
|
|
requiredExtensions: ['pgmq'],
|
|
missingExtensionsAlert: <UpgradeDatabaseAlert minimumVersion="15.6.1.143" />,
|
|
name: `Queues`,
|
|
icon: ({ className, ...props } = {}) => (
|
|
<Layers className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
|
|
),
|
|
description: 'Lightweight message queue in Postgres',
|
|
docsUrl: 'https://github.com/tembo-io/pgmq',
|
|
author: {
|
|
name: 'pgmq',
|
|
websiteUrl: 'https://github.com/tembo-io/pgmq',
|
|
},
|
|
navigation: [
|
|
{
|
|
route: 'overview',
|
|
label: 'Overview',
|
|
},
|
|
{
|
|
route: 'queues',
|
|
label: 'Queues',
|
|
hasChild: true,
|
|
childIcon: (
|
|
<Layers size={12} strokeWidth={1.5} className={cn('text-foreground w-full h-full')} />
|
|
),
|
|
},
|
|
{
|
|
route: 'settings',
|
|
label: 'Settings',
|
|
layout: 'constrained',
|
|
},
|
|
],
|
|
navigate: ({ pageId = 'overview', childId }) => {
|
|
if (childId) {
|
|
return dynamic(() => import('../Queues/QueuePage').then((mod) => mod.QueuePage), {
|
|
loading: Loading,
|
|
})
|
|
}
|
|
switch (pageId) {
|
|
case 'overview':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/Queues/OverviewTab').then(
|
|
(mod) => mod.QueuesOverviewTab
|
|
),
|
|
{ loading: Loading }
|
|
)
|
|
case 'queues':
|
|
return dynamic(() => import('../Queues/QueuesTab').then((mod) => mod.QueuesTab), {
|
|
loading: Loading,
|
|
})
|
|
case 'settings':
|
|
return dynamic(
|
|
() => import('../Queues/QueuesSettings').then((mod) => mod.QueuesSettings),
|
|
{ loading: Loading }
|
|
)
|
|
}
|
|
return null
|
|
},
|
|
},
|
|
{
|
|
id: 'cron',
|
|
type: 'postgres_extension' as const,
|
|
source: 'Community' as const,
|
|
requiredExtensions: ['pg_cron'],
|
|
name: `Cron`,
|
|
icon: ({ className, ...props } = {}) => (
|
|
<Clock5 className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
|
|
),
|
|
description: 'Schedule recurring Jobs in Postgres',
|
|
docsUrl: 'https://github.com/citusdata/pg_cron',
|
|
author: {
|
|
name: 'Citus Data',
|
|
websiteUrl: 'https://github.com/citusdata/pg_cron',
|
|
},
|
|
navigation: [
|
|
{
|
|
route: 'overview',
|
|
label: 'Overview',
|
|
},
|
|
{
|
|
route: 'jobs',
|
|
label: 'Jobs',
|
|
hasChild: true,
|
|
childIcon: (
|
|
<Timer size={12} strokeWidth={1.5} className={cn('text-foreground w-full h-full')} />
|
|
),
|
|
},
|
|
],
|
|
navigate: ({ pageId = 'overview', childId }) => {
|
|
if (childId) {
|
|
return dynamic(() => import('../CronJobs/CronJobPage').then((mod) => mod.CronJobPage), {
|
|
loading: Loading,
|
|
})
|
|
}
|
|
switch (pageId) {
|
|
case 'overview':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/CronJobs/OverviewTab').then(
|
|
(mod) => mod.CronOverviewTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
case 'jobs':
|
|
return dynamic(() => import('../CronJobs/CronJobsTab').then((mod) => mod.CronjobsTab), {
|
|
loading: Loading,
|
|
})
|
|
}
|
|
return null
|
|
},
|
|
},
|
|
{
|
|
id: 'vault',
|
|
type: 'postgres_extension' as const,
|
|
source: 'Official' as const,
|
|
requiredExtensions: ['supabase_vault'],
|
|
missingExtensionsAlert: <UpgradeDatabaseAlert />,
|
|
name: `Vault`,
|
|
status: 'beta',
|
|
icon: ({ className, ...props } = {}) => (
|
|
<Vault className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
|
|
),
|
|
description: 'Application level encryption for your project',
|
|
docsUrl: `${DOCS_URL}/guides/database/vault`,
|
|
author: authorSupabase,
|
|
navigation: [
|
|
{
|
|
route: 'overview',
|
|
label: 'Overview',
|
|
},
|
|
{
|
|
route: 'secrets',
|
|
label: 'Secrets',
|
|
},
|
|
],
|
|
navigate: ({ pageId = 'overview' }) => {
|
|
switch (pageId) {
|
|
case 'overview':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/Vault/OverviewTab').then(
|
|
(mod) => mod.VaultOverviewTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
case 'secrets':
|
|
return dynamic(
|
|
() => import('../Vault/Secrets/SecretsManagement').then((mod) => mod.SecretsManagement),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
}
|
|
return null
|
|
},
|
|
},
|
|
{
|
|
id: 'webhooks',
|
|
type: 'postgres_extension' as const,
|
|
source: 'Official' as const,
|
|
name: `Database Webhooks`,
|
|
icon: ({ className, ...props } = {}) => (
|
|
<Webhook className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
|
|
),
|
|
description:
|
|
'Send real-time data from your database to another system when a table event occurs',
|
|
docsUrl: `${DOCS_URL}/guides/database/webhooks`,
|
|
author: authorSupabase,
|
|
requiredExtensions: ['pg_net'],
|
|
navigation: [
|
|
{
|
|
route: 'overview',
|
|
label: 'Overview',
|
|
},
|
|
{
|
|
route: 'webhooks',
|
|
label: 'Webhooks',
|
|
layout: 'constrained',
|
|
},
|
|
],
|
|
navigate: ({ pageId = 'overview' }) => {
|
|
switch (pageId) {
|
|
case 'overview':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/Webhooks/OverviewTab').then(
|
|
(mod) => mod.WebhooksOverviewTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
case 'webhooks':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/Webhooks/ListTab').then(
|
|
(mod) => mod.WebhooksListTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
}
|
|
return null
|
|
},
|
|
installationSql: getEnableWebhooksSQL(),
|
|
installationCommand: async ({ ref }: { ref: string }) => {
|
|
const queryClient = getQueryClient()
|
|
await enableDatabaseWebhooks({ ref })
|
|
await invalidateSchemasQuery(queryClient, ref)
|
|
},
|
|
},
|
|
{
|
|
id: 'data_api',
|
|
type: 'custom' as const,
|
|
source: 'Official' as const,
|
|
requiredExtensions: [],
|
|
name: `Data API`,
|
|
icon: ({ className, ...props } = {}) => (
|
|
<Code2 className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
|
|
),
|
|
description: 'Auto-generate an API directly from your database schema',
|
|
docsUrl: `${DOCS_URL}/guides/api`,
|
|
author: authorSupabase,
|
|
navigation: [
|
|
{
|
|
route: 'overview',
|
|
label: 'Overview',
|
|
},
|
|
{
|
|
route: 'settings',
|
|
label: 'Settings',
|
|
layout: 'constrained',
|
|
},
|
|
{
|
|
route: 'docs',
|
|
label: 'Docs',
|
|
},
|
|
],
|
|
navigate: ({ pageId = 'overview' }) => {
|
|
switch (pageId) {
|
|
case 'overview':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/DataApi/OverviewTab').then(
|
|
(mod) => mod.DataApiOverviewTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
case 'settings':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/DataApi/SettingsTab').then(
|
|
(mod) => mod.DataApiSettingsTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
case 'docs':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/DataApi/DocsTab').then(
|
|
(mod) => mod.DataApiDocsTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
}
|
|
return null
|
|
},
|
|
},
|
|
{
|
|
id: 'warehouse',
|
|
type: 'custom' as const,
|
|
source: 'Official' as const,
|
|
status: 'alpha' as const,
|
|
requiredExtensions: [],
|
|
name: `Warehouse`,
|
|
icon: ({ className, ...props } = {}) => (
|
|
<Warehouse className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
|
|
),
|
|
description: 'Replicate your database to an analytical endpoint',
|
|
// No Warehouse guide exists yet. Point this at `${DOCS_URL}/guides/warehouse` once one does.
|
|
docsUrl: null,
|
|
author: authorSupabase,
|
|
navigation: [
|
|
{
|
|
route: 'overview',
|
|
label: 'Overview',
|
|
},
|
|
],
|
|
navigate: ({ pageId = 'overview' }) => {
|
|
if (pageId === 'overview') {
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/Warehouse/OverviewTab').then(
|
|
(mod) => mod.WarehouseOverviewTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
}
|
|
return null
|
|
},
|
|
},
|
|
{
|
|
id: 'graphiql',
|
|
type: 'postgres_extension' as const,
|
|
source: 'Official' as const,
|
|
requiredExtensions: ['pg_graphql'],
|
|
name: `GraphQL`,
|
|
icon: ({ className, ...props } = {}) => (
|
|
<Image
|
|
fill
|
|
src={`${BASE_PATH}/img/graphql.svg`}
|
|
alt="GraphiQL"
|
|
className={cn('p-2', className)}
|
|
{...props}
|
|
/>
|
|
),
|
|
description: 'Run GraphQL queries through our interactive in-browser IDE',
|
|
docsUrl: `${DOCS_URL}/guides/database/extensions/pg_graphql`,
|
|
author: authorSupabase,
|
|
navigation: [
|
|
{
|
|
route: 'overview',
|
|
label: 'Overview',
|
|
},
|
|
{
|
|
route: 'graphiql',
|
|
label: 'GraphiQL',
|
|
},
|
|
],
|
|
navigate: ({ pageId = 'overview' }) => {
|
|
switch (pageId) {
|
|
case 'overview':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/GraphQL/OverviewTab').then(
|
|
(mod) => mod.GraphQLOverviewTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
case 'graphiql':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/GraphQL/GraphiQLTab').then(
|
|
(mod) => mod.GraphiQLTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
}
|
|
return null
|
|
},
|
|
},
|
|
] as const
|
|
|
|
const WRAPPER_INTEGRATIONS: Array<IntegrationDefinition> = WRAPPERS.map((w) => {
|
|
return {
|
|
id: w.name,
|
|
type: 'wrapper' as const,
|
|
source: 'Official' as const,
|
|
name: `${w.label} Wrapper`,
|
|
icon: ({ className, ...props } = {}) => (
|
|
<Image fill src={w.icon} alt={w.name} className={cn('p-2', className)} {...props} />
|
|
),
|
|
requiredExtensions: ['wrappers', 'supabase_vault'],
|
|
description: w.description,
|
|
docsUrl: w.docsUrl,
|
|
categories: w.categories,
|
|
meta: w,
|
|
author: authorSupabase,
|
|
navigation: [
|
|
{
|
|
route: 'overview',
|
|
label: 'Overview',
|
|
},
|
|
{
|
|
route: 'wrappers',
|
|
label: 'Wrappers',
|
|
},
|
|
],
|
|
navigate: ({ pageId = 'overview' }) => {
|
|
switch (pageId) {
|
|
case 'overview':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/Wrappers/OverviewTab').then(
|
|
(mod) => mod.WrapperOverviewTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
case 'wrappers':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/Wrappers/WrappersTab').then(
|
|
(mod) => mod.WrappersTab
|
|
),
|
|
{
|
|
loading: Loading,
|
|
}
|
|
)
|
|
}
|
|
return null
|
|
},
|
|
}
|
|
})
|
|
|
|
const TEMPLATE_INTEGRATIONS: Array<IntegrationDefinition> = [
|
|
{
|
|
id: 'stripe_sync_engine',
|
|
type: 'template' as const,
|
|
source: 'Partner' as const,
|
|
requiredExtensions: ['pgmq', 'supabase_vault', 'pg_cron', 'pg_net'],
|
|
missingExtensionsAlert: <UpgradeDatabaseAlert minimumVersion="15.6.1.143" />,
|
|
name: `Stripe Sync Engine`,
|
|
status: 'alpha',
|
|
icon: ({ className, ...props } = {}) => (
|
|
<Image
|
|
fill
|
|
src={`${BASE_PATH}/img/icons/stripe-icon.svg`}
|
|
alt={'Stripe Logo'}
|
|
className={cn('p-2', className)}
|
|
{...props}
|
|
/>
|
|
),
|
|
description:
|
|
'Continuously sync your payments, customer, and other data from Stripe to your Postgres database',
|
|
docsUrl: 'https://github.com/stripe-experiments/sync-engine/',
|
|
author: {
|
|
name: 'Stripe',
|
|
websiteUrl: 'https://www.stripe.com',
|
|
},
|
|
navigation: [
|
|
{
|
|
route: 'overview',
|
|
label: 'Overview',
|
|
},
|
|
{
|
|
route: 'settings',
|
|
label: 'Settings',
|
|
layout: 'constrained',
|
|
},
|
|
],
|
|
navigate: ({ pageId = 'overview' }) => {
|
|
switch (pageId) {
|
|
case 'overview':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/templates/StripeSyncEngine/OverviewTab').then(
|
|
(mod) => mod.StripeSyncEngineOverviewTab
|
|
),
|
|
{ loading: Loading }
|
|
)
|
|
case 'settings':
|
|
return dynamic(
|
|
() =>
|
|
import('@/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage').then(
|
|
(mod) => mod.StripeSyncSettingsPage
|
|
),
|
|
{ loading: Loading }
|
|
)
|
|
}
|
|
return null
|
|
},
|
|
inputs: {
|
|
stripe_api_key: {
|
|
type: 'password',
|
|
required: true,
|
|
label: 'Stripe API secret key',
|
|
description:
|
|
'Requires write access to Webhook Endpoints and read-only access to all other categories.',
|
|
actions: [
|
|
{
|
|
label: 'Get API key',
|
|
href: 'https://dashboard.stripe.com/apikeys',
|
|
},
|
|
{
|
|
label: 'What are Stripe API keys?',
|
|
href: 'https://support.stripe.com/questions/what-are-stripe-api-keys-and-how-to-find-them',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
steps: [
|
|
{ label: 'Creates a new database schema named `stripe`' },
|
|
{ label: 'Creates tables and views in the `stripe` schema for synced Stripe data' },
|
|
{ label: 'Deploys Edge Functions to handle incoming webhooks from Stripe' },
|
|
{ label: 'Schedules automatic Stripe data syncs using Supabase Queues' },
|
|
],
|
|
installationCommand: async ({ ref: projectRef, track, stripe_api_key }) => {
|
|
const startTime = Date.now()
|
|
await installStripeSync({ projectRef, startTime, stripeSecretKey: stripe_api_key as string })
|
|
|
|
if (track)
|
|
track('integration_install_submitted', {
|
|
integrationName: 'stripe_sync_engine',
|
|
method: 'template',
|
|
})
|
|
|
|
const queryClient = getQueryClient()
|
|
await queryClient.invalidateQueries({ queryKey: stripeSyncKeys.all })
|
|
},
|
|
checkInstallationStatus: async (props) => {
|
|
const queryClient = getQueryClient()
|
|
const { projectRef, connectionString } = props || {}
|
|
|
|
const schemas = await getSchemas({
|
|
projectRef: projectRef as string,
|
|
connectionString: connectionString as string,
|
|
})
|
|
|
|
const { status, errorMessage } = getStripeSyncSchemaComment(schemas)
|
|
|
|
if (status === 'install error') {
|
|
throw new Error(errorMessage ?? 'Stripe Sync installation failed')
|
|
}
|
|
|
|
if (status === 'installed') {
|
|
await queryClient.invalidateQueries({
|
|
queryKey: databaseKeys.schemas(projectRef as string),
|
|
})
|
|
}
|
|
return status === 'installed' ? 'installed' : 'installing'
|
|
},
|
|
},
|
|
]
|
|
|
|
const INTEGRATIONS_WITH_CATEGORIES = [
|
|
...WRAPPER_INTEGRATIONS,
|
|
...SUPABASE_INTEGRATIONS.map((integration) => {
|
|
const categoryMap: Record<string, string[]> = {
|
|
queues: ['devtools'],
|
|
cron: ['devtools'],
|
|
vault: ['security'],
|
|
webhooks: ['api'],
|
|
data_api: ['api', 'data-platform'],
|
|
warehouse: ['data-platform'],
|
|
graphiql: ['api', 'devtools'],
|
|
}
|
|
return {
|
|
...integration,
|
|
categories: categoryMap[integration.id] || [],
|
|
}
|
|
}),
|
|
...TEMPLATE_INTEGRATIONS.map((integration) => {
|
|
const categoryMap: Record<string, string[]> = {
|
|
stripe_sync_engine: ['billing'],
|
|
}
|
|
return {
|
|
...integration,
|
|
categories: categoryMap[integration.id] || [],
|
|
}
|
|
}),
|
|
]
|
|
|
|
export const INTEGRATIONS: Array<IntegrationDefinition> = INTEGRATIONS_WITH_CATEGORIES
|
|
|
|
export const Loading = () => (
|
|
<div className="p-10">
|
|
<GenericSkeletonLoader />
|
|
</div>
|
|
)
|