fix integration overview tab contents (#46179)

Restores proper content in new marketplace detail overview pages
compared to the legacy overview pages.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Data API URL settings and a visible "Required extensions"
section across integration overviews.
* Unified install/manage UIs for webhooks, Stripe Sync, wrappers,
queues, and others; marketplace mode now shows marketplace-specific
overview content.

* **Style**
* Improved marketplace detail rail and filter-bar button styling;
refined list/link row visuals.

* **Refactor**
* Overview pages reorganized to branch on marketplace mode and extract
shared overview content for consistency.

* **Tests**
  * Stabilized integration overview test data for deterministic runs.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46179?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Francesco SansalvadoreandJoshen Lim authored and GitHub committed 2026-05-25 09:08:38 +02:00
1 parent 65c570e85b
commit 7edbedcbec
24 files changed
+556 -461

No files matched your search

@@ -0,0 +1,10 @@
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const CronOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return <IntegrationOverviewTab />
}
@@ -12,8 +12,8 @@ export const DataApiDisabledState = ({ description }: DataApiDisabledStateProps)
const { ref: projectRef } = useParams()
return (
<div className="flex w-full flex-1 items-center justify-center p-10">
<Alert className="max-w-md">
<div className="flex w-full p-10">
<Alert className="max-w-md mx-auto">
<AlertCircle size={16} />
<AlertTitle>Data API is disabled</AlertTitle>
<AlertDescription>
@@ -0,0 +1,47 @@
import { useParams } from 'common'
import { AlertCircle } from 'lucide-react'
import { Alert, AlertTitle, cn } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { DataApiEnableSwitch } from '@/components/interfaces/Settings/API/DataApiEnableSwitch'
import { DataApiProjectUrlCard } from '@/components/interfaces/Settings/API/DataApiProjectUrlCard'
import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
export const DataApiURLSettings = () => {
const { ref: projectRef } = useParams()
const { data: project, isPending: isProjectLoading } = useSelectedProjectQuery()
const { isEnabled, isPending: isConfigLoading } = useIsDataApiEnabled({ projectRef })
const isLoading = isProjectLoading || isConfigLoading
return (
<div className="flex flex-col">
{!isProjectLoading && project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY ? (
<Alert variant="destructive">
<AlertCircle size={16} />
<AlertTitle>API settings are unavailable as the project is not active</AlertTitle>
</Alert>
) : (
<>
<div
className={cn(
IS_PLATFORM && (isLoading || !isEnabled) && 'opacity-50 pointer-events-none'
)}
>
<DataApiProjectUrlCard />
</div>
{IS_PLATFORM ? (
<DataApiEnableSwitch />
) : (
<Admonition
type="default"
title="Managed via configuration variables"
description="Data API settings are configured via config.toml for CLI and local development, or via docker-compose.yml and .env for self-hosted deployments."
/>
)}
</>
)}
</div>
)
}
@@ -1,70 +1,17 @@
import { useParams } from 'common'
import { AlertCircle } from 'lucide-react'
import { Alert, AlertTitle, cn } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { IntegrationOverviewTabV2 } from '../Integration/IntegrationOverviewTabV2'
import { DataApiURLSettings } from './DataApiURLSettings'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { DataApiEnableSwitch } from '@/components/interfaces/Settings/API/DataApiEnableSwitch'
import { DataApiProjectUrlCard } from '@/components/interfaces/Settings/API/DataApiProjectUrlCard'
import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
const DataApiOverview = () => {
const { ref: projectRef } = useParams()
const { data: project, isPending: isProjectLoading } = useSelectedProjectQuery()
const { isEnabled, isPending: isConfigLoading } = useIsDataApiEnabled({ projectRef })
const isLoading = isProjectLoading || isConfigLoading
return (
<div className="max-w-4xl flex flex-col">
{!isProjectLoading && project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY ? (
<Alert variant="destructive">
<AlertCircle size={16} />
<AlertTitle>API settings are unavailable as the project is not active</AlertTitle>
</Alert>
) : (
<>
<div
className={cn(
IS_PLATFORM && (isLoading || !isEnabled) && 'opacity-50 pointer-events-none'
)}
>
<DataApiProjectUrlCard />
</div>
{IS_PLATFORM ? (
<DataApiEnableSwitch />
) : (
<Admonition
type="default"
title="Managed via configuration variables"
description="Data API settings are configured via config.toml for CLI and local development, or via docker-compose.yml and .env for self-hosted deployments."
/>
)}
</>
)}
</div>
)
}
export const DataApiOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) {
return (
<IntegrationOverviewTabV2>
<DataApiOverview />
</IntegrationOverviewTabV2>
)
} else {
return (
<IntegrationOverviewTab>
<div className="px-10">
<DataApiOverview />
</div>
</IntegrationOverviewTab>
)
}
if (isMarketplaceEnabled) return <DataApiURLSettings />
return (
<IntegrationOverviewTab>
<div className="px-4 md:px-10 max-w-4xl space-y-4">
<DataApiURLSettings />
</div>
</IntegrationOverviewTab>
)
}
@@ -12,7 +12,7 @@ export const DataApiSettingsTab = () => {
if (IS_PLATFORM && !isPending && !isEnabled) {
return (
<ConstrainedIntegrationTabScaffold>
<ConstrainedIntegrationTabScaffold className="p-0!">
<DataApiDisabledState description="configure settings" />
</ConstrainedIntegrationTabScaffold>
)
@@ -0,0 +1,10 @@
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const GraphQLOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return <IntegrationOverviewTab />
}
@@ -54,6 +54,17 @@ vi.mock('./MarkdownContent', () => ({
MarkdownContent: () => null,
}))
vi.mock('../Landing/useIntegrationDetail', () => ({
useIntegrationDetail: () => ({
integration: {
id: 'test-integration',
name: 'Test Integration',
requiredExtensions: ['pg_net'],
missingExtensionsAlert: null,
},
}),
}))
describe('IntegrationOverviewTab', () => {
beforeEach(() => {
routerMock.setCurrentUrl('/project/default/integrations/test-integration/overview')
@@ -1,11 +1,11 @@
import { useParams } from 'common'
import { PropsWithChildren, ReactNode } from 'react'
import { Badge, Card, CardContent, cn, Separator } from 'ui'
import { cn } from 'ui'
import { INTEGRATIONS } from '../Landing/Integrations.constants'
import { BuiltBySection } from './BuildBySection'
import { MarkdownContent } from './MarkdownContent'
import { MissingExtensionAlert } from './MissingExtensionAlert'
import { RequiredExtensionsSection } from './RequiredExtensionsSection'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
@@ -16,19 +16,20 @@ export interface IntegrationOverviewTabProps {
hideRequiredExtensionsSection?: boolean
}
/** [Joshen] This will eventually get replaced by IntegrationOverviewTabV2 */
export const IntegrationOverviewTab = ({
export const OverviewTabSharedContent = ({
integration,
hideRequiredExtensionsSection = false,
actions,
alert,
status,
children,
hideRequiredExtensionsSection = false,
}: PropsWithChildren<IntegrationOverviewTabProps>) => {
const { id } = useParams()
}: PropsWithChildren<{
integration: any
hideRequiredExtensionsSection?: boolean
actions?: ReactNode
alert?: ReactNode
}>) => {
const { data: project } = useSelectedProjectQuery()
const integration = INTEGRATIONS.find((i) => i.id === id)
const { data: extensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
@@ -38,71 +39,21 @@ export const IntegrationOverviewTab = ({
return <div>Unsupported integration type</div>
}
const dependsOnExtension = (integration.requiredExtensions ?? []).length > 0
const installableExtensions = (extensions ?? []).filter((ext) =>
(integration.requiredExtensions ?? []).includes(ext.name)
)
const hasToInstallExtensions = installableExtensions.some((x) => !x.installed_version)
// The integration requires extensions that are not available to install on the current database image
const hasMissingExtensions =
installableExtensions.length !== integration.requiredExtensions.length
return (
<div className="flex flex-col gap-8 py-10">
<BuiltBySection integration={integration} status={status} />
<>
{!!alert && <div className="px-10 max-w-4xl">{alert}</div>}
<MarkdownContent key={integration.id} integrationId={integration.id} />
<Separator />
{dependsOnExtension && !hideRequiredExtensionsSection && (
<div className="px-4 md:px-10 max-w-4xl flex flex-col gap-y-4">
<h4>Required extensions</h4>
<Card>
<CardContent className="p-0">
<ul className="text-foreground-light text-sm">
{(integration.requiredExtensions ?? []).map((requiredExtension, idx) => {
const extension = (extensions ?? []).find((ext) => ext.name === requiredExtension)
const isInstalled = !!extension?.installed_version
const isLastRow = idx === (integration.requiredExtensions?.length ?? 0) - 1
return (
<li
key={requiredExtension}
className={cn(
'flex items-center justify-between gap-3 py-2 px-3',
!isLastRow ? 'border-b' : ''
)}
>
<code className="text-xs">{requiredExtension}</code>
<div className="shrink-0">
{extension ? (
isInstalled ? (
<Badge>Installed</Badge>
) : (
<MissingExtensionAlert extension={extension} />
)
) : (
<span className="text-foreground-muted">Unavailable</span>
)}
</div>
</li>
)
})}
</ul>
{hasMissingExtensions && (
<div className="py-3 px-4 border-t">{integration.missingExtensionsAlert}</div>
)}
</CardContent>
</Card>
</div>
)}
<RequiredExtensionsSection
hide={hideRequiredExtensionsSection}
className="px-4 md:px-10 max-w-4xl"
/>
{!!actions && (
<div
@@ -118,6 +69,35 @@ export const IntegrationOverviewTab = ({
</div>
)}
{children}
</>
)
}
export const IntegrationOverviewTab = ({
actions,
alert,
status,
children,
hideRequiredExtensionsSection = false,
}: PropsWithChildren<IntegrationOverviewTabProps>) => {
const { id } = useParams()
const integration = INTEGRATIONS.find((i) => i.id === id)
if (!integration) {
return <div>Unsupported integration type</div>
}
return (
<div className="flex flex-col gap-8 py-10">
<BuiltBySection integration={integration} status={status} />
<OverviewTabSharedContent
integration={integration}
hideRequiredExtensionsSection={hideRequiredExtensionsSection}
alert={alert}
actions={actions}
>
{children}
</OverviewTabSharedContent>
</div>
)
}
@@ -104,13 +104,9 @@ export const InstallationOverview = ({
{!ext ? (
<Badge variant="warning">Unavailable</Badge>
) : ext.installed_version ? (
<Badge variant="secondary" className="text-foreground-lighter">
Installed
</Badge>
<Badge variant="secondary">Installed</Badge>
) : (
<Badge variant="default" className="text-foreground">
Required
</Badge>
<Badge variant="warning">Required</Badge>
)}
</div>
)
@@ -6,7 +6,6 @@
import { PropsWithChildren } from 'react'
import { IntegrationOverviewTab, IntegrationOverviewTabProps } from './IntegrationOverviewTab'
import { IntegrationOverviewTabV2 } from './IntegrationOverviewTabV2'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const IntegrationOverviewTabWrapper = (
@@ -15,8 +14,8 @@ export const IntegrationOverviewTabWrapper = (
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) {
return <IntegrationOverviewTabV2>{props.children}</IntegrationOverviewTabV2>
} else {
return <IntegrationOverviewTab {...props} />
return null
}
return <IntegrationOverviewTab {...props} />
}
@@ -0,0 +1,11 @@
import type { PropsWithChildren } from 'react'
import { IntegrationOverviewTab } from './IntegrationOverviewTab'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const MarketplaceIntegrationOverviewTab = (props: PropsWithChildren) => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return null
return <IntegrationOverviewTab>{props.children}</IntegrationOverviewTab>
}
@@ -0,0 +1,84 @@
import { Badge, Card, CardContent, cn, Separator } from 'ui'
import { useIsMarketplaceEnabled } from '../../App/FeaturePreview/FeaturePreviewContext'
import { MissingExtensionAlert } from './MissingExtensionAlert'
import { useIntegrationDetail } from '@/components/interfaces/Integrations/Landing/useIntegrationDetail'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export const RequiredExtensionsSection = ({
hide = false,
hideSeparator = false,
className,
}: {
hide?: boolean
hideSeparator?: boolean
className?: string
}) => {
const { integration } = useIntegrationDetail()
const { data: project } = useSelectedProjectQuery()
const { data: extensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const requiredExtensions = integration?.requiredExtensions ?? []
if (hide || requiredExtensions.length === 0) return null
const installableExtensions = (extensions ?? []).filter((ext) =>
requiredExtensions.includes(ext.name)
)
const hasMissingExtensions = installableExtensions.length !== requiredExtensions.length
return (
<>
{!hideSeparator && <Separator />}
<div className={cn('flex flex-col gap-y-4', className)}>
<h4>Required extensions</h4>
<Card>
<CardContent className="p-0">
<ul className="text-foreground-light text-sm">
{requiredExtensions.map((requiredExtension, idx) => {
const extension = (extensions ?? []).find((ext) => ext.name === requiredExtension)
const isInstalled = !!extension?.installed_version
const isLastRow = idx === requiredExtensions.length - 1
return (
<li
key={requiredExtension}
className={cn(
'flex items-center justify-between gap-3 py-2 px-3',
!isLastRow ? 'border-b' : ''
)}
>
<code className="text-xs">{requiredExtension}</code>
<div className="shrink-0">
{extension ? (
isInstalled ? (
<Badge variant="secondary">Installed</Badge>
) : isMarketplaceEnabled ? (
<Badge variant="warning">Required</Badge>
) : (
<MissingExtensionAlert extension={extension} />
)
) : (
<span className="text-foreground-muted">Unavailable</span>
)}
</div>
</li>
)
})}
</ul>
{hasMissingExtensions && integration?.missingExtensionsAlert && (
<div className="py-3 px-4 border-t">{integration.missingExtensionsAlert}</div>
)}
</CardContent>
</Card>
</div>
</>
)
}
@@ -231,8 +231,8 @@ const SUPABASE_INTEGRATIONS: Array<IntegrationDefinition> = [
case 'overview':
return dynamic(
() =>
import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabWrapper').then(
(mod) => mod.IntegrationOverviewTabWrapper
import('@/components/interfaces/Integrations/CronJobs/OverviewTab').then(
(mod) => mod.CronOverviewTab
),
{
loading: Loading,
@@ -275,8 +275,8 @@ const SUPABASE_INTEGRATIONS: Array<IntegrationDefinition> = [
case 'overview':
return dynamic(
() =>
import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabWrapper').then(
(mod) => mod.IntegrationOverviewTabWrapper
import('@/components/interfaces/Integrations/Vault/OverviewTab').then(
(mod) => mod.VaultOverviewTab
),
{
loading: Loading,
@@ -445,8 +445,8 @@ const SUPABASE_INTEGRATIONS: Array<IntegrationDefinition> = [
case 'overview':
return dynamic(
() =>
import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabWrapper').then(
(mod) => mod.IntegrationOverviewTabWrapper
import('@/components/interfaces/Integrations/GraphQL/OverviewTab').then(
(mod) => mod.GraphQLOverviewTab
),
{
loading: Loading,
@@ -114,8 +114,8 @@ export const useAvailableIntegrations = () => {
case 'overview':
return dynamic(
() =>
import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index').then(
(mod) => mod.IntegrationOverviewTabV2
import('@/components/interfaces/Integrations/Integration/MarketplaceIntegrationOverviewTab').then(
(mod) => mod.MarketplaceIntegrationOverviewTab
),
{
loading: Loading,
@@ -1,4 +1,4 @@
import { ArrowUpRight, BookOpen, Settings } from 'lucide-react'
import { ArrowUpRight, BookOpen } from 'lucide-react'
import { Button, cn } from 'ui'
import { GenericSkeletonLoader, ShimmeringLoader } from 'ui-patterns'
import { Admonition } from 'ui-patterns/admonition'
@@ -70,7 +70,7 @@ export const MarketplaceDetail = () => {
}
if (isInstalled) {
return (
<Button type="outline" disabled icon={<Settings size={13} />}>
<Button type="outline" disabled>
Installed
</Button>
)
@@ -78,6 +78,8 @@ export const MarketplaceDetail = () => {
return <InstallIntegrationSheet integration={integration} />
}
// For overview route, get the integration-specific overview component if available
const OverviewComponent = activeRoute === 'overview' ? Component : null
const CustomPageComponent = activeRoute !== 'overview' && isKnownRoute ? Component : null
return (
@@ -114,7 +116,9 @@ export const MarketplaceDetail = () => {
{activeRoute === 'overview' ? (
<div className={centeredContentClass}>
<OverviewTab integration={integration} isInstalled={isInstalled} />
<OverviewTab integration={integration} isInstalled={isInstalled}>
{OverviewComponent && <OverviewComponent />}
</OverviewTab>
</div>
) : CustomPageComponent ? (
layout === 'constrained' ? (
@@ -1,4 +1,4 @@
import { ArrowUpRight } from 'lucide-react'
import { ArrowUpRight, BookOpen, Globe } from 'lucide-react'
import type { ReactNode } from 'react'
import { cn } from 'ui'
@@ -6,16 +6,17 @@ import { getMarketplaceType, getMarketplaceTypeLabel } from './Marketplace.const
import type { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
interface RailRowProps {
label: string
label?: string
value: ReactNode
href?: string
mono?: boolean
icon?: ReactNode
}
const RailRow = ({ label, value, href, mono }: RailRowProps) => {
const RailRow = ({ label, value, href, mono, icon }: RailRowProps) => {
const valueCls = cn(
'flex items-center gap-1 text-sm',
href ? 'text-brand-link' : 'text-foreground',
href ? 'text-foreground-lighter hover:text-foreground' : 'text-foreground',
mono && 'font-mono'
)
const content = (
@@ -26,14 +27,17 @@ const RailRow = ({ label, value, href, mono }: RailRowProps) => {
)
return (
<div className="flex flex-col gap-1">
<div className="text-xs text-foreground-lighter">{label}</div>
{href ? (
<a href={href} target="_blank" rel="noreferrer" className={valueCls}>
{content}
</a>
) : (
<div className={valueCls}>{content}</div>
)}
{label && <div className="font-mono text-xs uppercase text-foreground-lighter">{label}</div>}
<div className="flex items-center gap-1.5">
{icon && <span className="shrink-0 text-foreground-lighter">{icon}</span>}
{href ? (
<a href={href} target="_blank" rel="noreferrer" className={valueCls}>
{content}
</a>
) : (
<div className={valueCls}>{content}</div>
)}
</div>
</div>
)
}
@@ -45,9 +49,7 @@ interface RailGroupProps {
const RailGroup = ({ title, children }: RailGroupProps) => (
<div className="flex flex-col gap-3 border-b pb-4 last:border-b-0">
<div className="font-mono text-[10px] uppercase tracking-widest text-foreground-lighter">
{title}
</div>
<div className="font-mono text-xs uppercase text-foreground">{title}</div>
<div className="flex flex-col gap-3">{children}</div>
</div>
)
@@ -82,8 +84,12 @@ export const MarketplaceDetailRail = ({ integration, isInstalled }: MarketplaceD
{(docsUrl || siteUrl) && (
<RailGroup title="Links">
{docsUrl && <RailRow label="Documentation" value="Read the docs" href={docsUrl} />}
{siteUrl && siteHost && <RailRow label="Website" value={siteHost} href={siteUrl} />}
{docsUrl && (
<RailRow value="Documentation" icon={<BookOpen size={14} />} href={docsUrl} />
)}
{siteUrl && siteHost && (
<RailRow value={siteHost} icon={<Globe size={14} />} href={siteUrl} />
)}
</RailGroup>
)}
</aside>
@@ -210,7 +210,7 @@ export const MarketplaceFilterBar = ({
aria-label="Grid view"
onClick={() => onViewModeChange('grid')}
className={cn(
'border-r px-2 py-1.5 rounded-l-md',
'border-r px-2 py-1.5 rounded-l-md cursor-pointer',
'focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background',
viewMode === 'grid'
? 'bg-surface-200 text-foreground'
@@ -224,7 +224,7 @@ export const MarketplaceFilterBar = ({
aria-label="List view"
onClick={() => onViewModeChange('list')}
className={cn(
'px-2 py-1.5 rounded-r-md',
'px-2 py-1.5 rounded-r-md cursor-pointer',
'focus-visible:z-10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-1 focus-visible:ring-offset-background',
viewMode === 'list'
? 'bg-surface-200 text-foreground'
@@ -1,23 +1,26 @@
import type { PropsWithChildren } from 'react'
import { MarketplaceDetailRail } from './MarketplaceDetailRail'
import { ConstrainedIntegrationTabScaffold } from '@/components/interfaces/Integrations/ConstrainedIntegrationTabScaffold'
import { FilesViewer } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/FilesViewer'
import { MarkdownContent } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent'
import type { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
interface OverviewTabProps {
interface OverviewTabProps extends PropsWithChildren {
integration: IntegrationDefinition
isInstalled: boolean
}
export const OverviewTab = ({ integration, isInstalled }: OverviewTabProps) => {
export const OverviewTab = ({ integration, isInstalled, children }: OverviewTabProps) => {
const { content, files = [] } = integration
return (
<ConstrainedIntegrationTabScaffold>
<div className="mx-auto grid w-full max-w-6xl grid-cols-1 items-start gap-10 @4xl:grid-cols-[minmax(0,760px)_240px]">
<div className="min-w-0">
<div className="min-w-0 flex flex-col gap-8">
{files.length > 0 && <FilesViewer files={files} />}
<MarkdownContent integrationId={integration.id} content={content} />
{children}
</div>
<MarketplaceDetailRail integration={integration} isInstalled={isInstalled} />
</div>
@@ -4,15 +4,19 @@ import { Button } from 'ui'
import { Admonition } from 'ui-patterns'
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { IntegrationOverviewTabV2 } from '../Integration/IntegrationOverviewTabV2'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
import { useQueuesExposePostgrestStatusQuery } from '@/data/database-queues/database-queues-expose-postgrest-status-query'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
const QueuesAdmonition = () => {
const QueuesContent = () => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const { data: isExposed } = useQueuesExposePostgrestStatusQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: extensions = [] } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
@@ -20,6 +24,8 @@ const QueuesAdmonition = () => {
})
const isQueuesInstalled = !!extensions.find((x) => x.name === 'pgmq')?.installed_version
if (isExposed) return null
return (
<Admonition
type="default"
@@ -39,17 +45,8 @@ const QueuesAdmonition = () => {
}
export const QueuesOverviewTab = () => {
const { data: project } = useSelectedProjectQuery()
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const { data: isExposed } = useQueuesExposePostgrestStatusQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
if (isMarketplaceEnabled) {
return <IntegrationOverviewTabV2>{!isExposed && <QueuesAdmonition />}</IntegrationOverviewTabV2>
} else {
return <IntegrationOverviewTab actions={!isExposed ? <QueuesAdmonition /> : null} />
}
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return <IntegrationOverviewTab actions={<QueuesContent />} />
}
@@ -0,0 +1,10 @@
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
export const VaultOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return <IntegrationOverviewTab />
}
@@ -5,7 +5,7 @@ import { Admonition } from 'ui-patterns'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { IntegrationOverviewTabV2 } from '../Integration/IntegrationOverviewTabV2'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import NoPermission from '@/components/ui/NoPermission'
@@ -14,12 +14,10 @@ import { useSchemasQuery } from '@/data/database/schemas-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
export const WebhooksOverviewTab = () => {
const WebhooksContent = () => {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const {
data: schemas,
isSuccess: isSchemasLoaded,
@@ -63,42 +61,39 @@ export const WebhooksOverviewTab = () => {
)
}
if (isSchemasLoaded && isHooksEnabled) return null
return (
<Admonition showIcon={false} type="default" title="Enable database webhooks on your project">
<p>
Database Webhooks can be used to trigger serverless functions or send requests to an HTTP
endpoint
</p>
<ButtonTooltip
className="mt-2 w-fit"
onClick={() => enableHooksForProject()}
disabled={isEnablingHooks}
tooltip={{
content: {
side: 'bottom',
text: !canReadWebhooks
? 'You need additional permissions to enable webhooks'
: undefined,
},
}}
>
Enable webhooks
</ButtonTooltip>
</Admonition>
)
}
export const WebhooksOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) {
return <IntegrationOverviewTabV2 />
} else {
return (
<IntegrationOverviewTab
hideRequiredExtensionsSection
actions={
isSchemasLoaded && isHooksEnabled ? null : (
<Admonition
showIcon={false}
type="default"
title="Enable database webhooks on your project"
>
<p>
Database Webhooks can be used to trigger serverless functions or send requests to an
HTTP endpoint
</p>
<ButtonTooltip
className="mt-2 w-fit"
onClick={() => enableHooksForProject()}
disabled={isEnablingHooks}
tooltip={{
content: {
side: 'bottom',
text: !canReadWebhooks
? 'You need additional permissions to enable webhooks'
: undefined,
},
}}
>
Enable webhooks
</ButtonTooltip>
</Admonition>
)
}
/>
)
return <RequiredExtensionsSection />
}
return <IntegrationOverviewTab hideRequiredExtensionsSection actions={<WebhooksContent />} />
}
@@ -7,7 +7,7 @@ import { Button, Sheet, SheetContent } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { IntegrationOverviewTabV2 } from '../Integration/IntegrationOverviewTabV2'
import { RequiredExtensionsSection } from '../Integration/RequiredExtensionsSection'
import { useAvailableIntegrations } from '../Landing/useAvailableIntegrations'
import { CreateIcebergWrapperSheet } from './CreateIcebergWrapperSheet'
import { CreateWrapperSheet } from './CreateWrapperSheet'
@@ -155,10 +155,9 @@ const AddNewWrapperCTA = () => {
)
}
export const WrapperOverviewTab = () => {
export const WrapperContent = () => {
const { id } = useParams()
const { data: project } = useSelectedProjectQuery()
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const { data: integrations = [] } = useAvailableIntegrations()
const integration = integrations.find((i) => i.id === id)
@@ -183,24 +182,25 @@ export const WrapperOverviewTab = () => {
)
}
if (isMarketplaceEnabled) {
return (
<IntegrationOverviewTabV2>
{isInstalled && (
<>
<AddNewWrapperCTA />
<WrapperOverviewContent />
</>
)}
</IntegrationOverviewTabV2>
)
} else {
return (
<IntegrationOverviewTab actions={<AddNewWrapperCTA />}>
<div className="mx-10">
<WrapperOverviewContent />
</div>
</IntegrationOverviewTab>
)
}
return (
<>
<RequiredExtensionsSection />
<AddNewWrapperCTA />
{isInstalled && <WrapperOverviewContent />}
</>
)
}
export const WrapperOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) return <RequiredExtensionsSection />
return (
<IntegrationOverviewTab actions={<AddNewWrapperCTA />}>
<div className="mx-10">
<WrapperOverviewContent />
</div>
</IntegrationOverviewTab>
)
}
@@ -9,6 +9,7 @@ import { wrapperMetaComparator } from './Wrappers.utils'
import { WrapperTable } from './WrapperTable'
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { DocsButton } from '@/components/ui/DocsButton'
import { useFDWsQuery } from '@/data/fdw/fdws-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
@@ -48,7 +49,7 @@ export const WrappersTab = () => {
const Container = useCallback(
({ ...props }: { children: ReactNode } & HTMLProps<HTMLDivElement>) => (
<div className="w-full mx-10 py-10 ">
<div className="w-full p-10">
{props.children}
<Sheet open={!!createWrapperShown} onOpenChange={handleOpenChange}>
<SheetContent size="lg" tabIndex={undefined}>
@@ -101,6 +102,23 @@ export const WrappersTab = () => {
return (
<Container>
<div className="max-w-5xl flex items-center gap-x-2 justify-end mb-4">
<DocsButton href={wrapperMeta.docsUrl} />
<ButtonTooltip
type="primary"
onClick={() => setCreateWrapperShown(true)}
disabled={!canCreateWrapper}
tooltip={{
content: {
text: !canCreateWrapper
? 'You need additional permissions to create a foreign data wrapper'
: undefined,
},
}}
>
Add new wrapper
</ButtonTooltip>
</div>
<WrapperTable />
<DiscardChangesConfirmationDialog {...modalProps} />
</Container>
@@ -24,10 +24,9 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'
import { IntegrationOverviewTab } from '../../Integration/IntegrationOverviewTab'
import { IntegrationOverviewTabV2 } from '../../Integration/IntegrationOverviewTabV2'
import { RequiredExtensionsSection } from '../../Integration/RequiredExtensionsSection'
import { InstallationError } from './InstallationError'
import { IntegrationInstalledActions, IntegrationNotInstalledActions } from './IntegrationActions'
import { StatusDisplay } from './StatusDisplay'
import {
canInstall as checkCanInstall,
hasInstallError,
@@ -52,11 +51,10 @@ const installFormSchema = z.object({
stripeSecretKey: z.string().min(1, 'Stripe API key is required'),
})
export const StripeSyncEngineOverviewTab = () => {
const StripeSyncContent = () => {
const track = useTrack()
const hasTrackedInstallFailed = useRef(false)
const { data: project } = useSelectedProjectQuery()
const isMarketplaceEnabled = useIsMarketplaceEnabled()
const [showUninstallModal, setShowUninstallModal] = useState(false)
const [shouldShowInstallSheet, setShouldShowInstallSheet] = useState(false)
@@ -203,219 +201,167 @@ export const StripeSyncEngineOverviewTab = () => {
return (
<>
{isMarketplaceEnabled ? (
<IntegrationOverviewTabV2>
{hasError && (
<InstallationError
error={uninstallError ? 'uninstall' : 'install'}
handleUninstall={handleUninstall}
handleOpenInstallSheet={handleOpenInstallSheet}
{hasError && (
<InstallationError
error={uninstallError ? 'uninstall' : 'install'}
handleUninstall={handleUninstall}
handleOpenInstallSheet={handleOpenInstallSheet}
isUpgrade={upgradeAvailable}
installing={installing}
uninstalling={uninstalling}
/>
)}
{!installed && !uninstalling && !uninstallError ? (
<>
<StripeSyncChangesCard
installationStatus={installationStatus}
isUpgrade={upgradeAvailable}
/>
<IntegrationNotInstalledActions
hideInstallCTA
installing={installing}
canInstall={canInstall}
isUninstallRequested={isUninstallRequested}
handleUninstall={handleUninstall}
setShouldShowInstallSheet={setShouldShowInstallSheet}
/>
</>
) : (
(installed || uninstalling || uninstallError) && (
<>
<StripeSyncChangesCard
installationStatus={installationStatus}
isUpgrade={upgradeAvailable}
/>
<IntegrationInstalledActions
disabled={installing || uninstalling || !canManageSecrets}
upgradeAvailable={upgradeAvailable}
installing={installing}
uninstalling={uninstalling}
/>
)}
{!installed && !uninstalling && !uninstallError ? (
<IntegrationNotInstalledActions
hideInstallCTA
installing={installing}
canInstall={canInstall}
isUninstallRequested={isUninstallRequested}
handleUninstall={handleUninstall}
setShouldShowInstallSheet={setShouldShowInstallSheet}
setShowUninstallModal={setShowUninstallModal}
/>
) : (
(installed || uninstalling || uninstallError) && (
<IntegrationInstalledActions
disabled={installing || uninstalling || !canManageSecrets}
upgradeAvailable={upgradeAvailable}
installing={installing}
uninstalling={uninstalling}
isUninstallRequested={isUninstallRequested}
setShouldShowInstallSheet={setShouldShowInstallSheet}
setShowUninstallModal={setShowUninstallModal}
/>
)
)}
</IntegrationOverviewTabV2>
) : (
<IntegrationOverviewTab
alert={
hasError ? (
<InstallationError
error={uninstallError ? 'uninstall' : 'install'}
handleUninstall={handleUninstall}
handleOpenInstallSheet={handleOpenInstallSheet}
isUpgrade={upgradeAvailable}
installing={installing}
uninstalling={uninstalling}
/>
) : null
}
status={
<StatusDisplay
status={installationStatus}
isInstallRequested={isInstallRequested}
isInstallInitiated={isInstallInitiated}
isUninstallRequested={isUninstallRequested}
isUninstallInitiated={isUninstallInitiated}
isUpgrade={upgradeAvailable}
timedOut={timedOut}
/>
}
actions={
!installed && !uninstalling && !uninstallError ? (
<>
<StripeSyncChangesCard
installationStatus={installationStatus}
isUpgrade={upgradeAvailable}
/>
<IntegrationNotInstalledActions
className="mt-4"
installing={installing}
canInstall={canInstall}
isUninstallRequested={isUninstallRequested}
handleUninstall={handleUninstall}
setShouldShowInstallSheet={setShouldShowInstallSheet}
/>
</>
) : installed || uninstalling || uninstallError ? (
<>
<StripeSyncChangesCard
installationStatus={installationStatus}
isUpgrade={upgradeAvailable}
/>
<IntegrationInstalledActions
className="mt-4"
disabled={installing || uninstalling || !canManageSecrets}
upgradeAvailable={upgradeAvailable}
installing={installing}
uninstalling={uninstalling}
isUninstallRequested={isUninstallRequested}
setShouldShowInstallSheet={setShouldShowInstallSheet}
setShowUninstallModal={setShowUninstallModal}
/>
</>
) : null
}
>
<Sheet open={!!shouldShowInstallSheet} onOpenChange={handleCloseInstallSheet}>
<SheetContent size="lg" tabIndex={undefined} className="flex flex-col gap-0">
<Form {...form}>
<form
id={formId}
onSubmit={form.handleSubmit(({ stripeSecretKey }) => {
if (!project?.ref) return
installStripeSync({
projectRef: project.ref,
stripeSecretKey,
startTime: Date.now(),
})
})}
className="overflow-auto grow px-0 flex flex-col"
>
<SheetHeader>
<SheetTitle>
{upgradeAvailable ? 'Upgrade' : 'Install'} Stripe Sync Engine
</SheetTitle>
</SheetHeader>
<SheetSection className="flex-1 flex flex-col gap-y-6">
<StripeSyncChangesCard
installationStatus={installationStatus}
isUpgrade={upgradeAvailable}
/>
<h3 className="heading-default">Configuration</h3>
<div className="flex flex-col gap-y-2">
<FormField
control={form.control}
name="stripeSecretKey"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Stripe API secret key"
description="Your Stripe secret key. Requires write access to Webhook Endpoints and read-only access to all other categories."
>
<FormControl className="col-span-8">
<Input
id="stripe_api_key"
name="stripe_api_key"
placeholder="Enter your Stripe API key"
autoComplete="stripe-api-key"
reveal={false}
disabled={isInstallRequested}
type="password"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
</FormItemLayout>
)}
/>
<div className="flex items-center gap-x-2">
<Button asChild type="default" icon={<ExternalLink />}>
<Link
target="_blank"
rel="noopener noreferrer"
href="https://dashboard.stripe.com/apikeys"
>
Get Stripe API key
</Link>
</Button>
<Button asChild type="default" icon={<ExternalLink />}>
<Link
target="_blank"
rel="noopener noreferrer"
href="https://support.stripe.com/questions/what-are-stripe-api-keys-and-how-to-find-them"
>
What are Stripe API keys?
</Link>
</Button>
</div>
</div>
{installRequestError && (
<Admonition
type="destructive"
title="Installation failed"
description={installRequestError.message}
/>
)}
</SheetSection>
<SheetFooter>
<Button
type="default"
disabled={isInstallRequested}
onClick={() => handleCloseInstallSheet(false)}
>
Cancel
</Button>
<Button
form={formId}
htmlType="submit"
type="primary"
loading={isInstallRequested}
disabled={!form.formState.isValid || isInstallRequested}
>
{isInstallRequested
? upgradeAvailable
? 'Upgrading'
: 'Installing'
: upgradeAvailable
? 'Upgrade integration'
: 'Install integration'}
</Button>
</SheetFooter>
</form>
</Form>
</SheetContent>
</Sheet>
</IntegrationOverviewTab>
</>
)
)}
<Sheet open={!!shouldShowInstallSheet} onOpenChange={handleCloseInstallSheet}>
<SheetContent size="lg" tabIndex={undefined} className="flex flex-col gap-0">
<Form {...form}>
<form
id={formId}
onSubmit={form.handleSubmit(({ stripeSecretKey }) => {
if (!project?.ref) return
installStripeSync({
projectRef: project.ref,
stripeSecretKey,
startTime: Date.now(),
})
})}
className="overflow-auto grow px-0 flex flex-col"
>
<SheetHeader>
<SheetTitle>
{upgradeAvailable ? 'Upgrade' : 'Install'} Stripe Sync Engine
</SheetTitle>
</SheetHeader>
<SheetSection className="flex-1 flex flex-col gap-y-6">
<StripeSyncChangesCard
installationStatus={installationStatus}
isUpgrade={upgradeAvailable}
/>
<h3 className="heading-default">Configuration</h3>
<div className="flex flex-col gap-y-2">
<FormField
control={form.control}
name="stripeSecretKey"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Stripe API secret key"
description="Your Stripe secret key. Requires write access to Webhook Endpoints and read-only access to all other categories."
>
<FormControl className="col-span-8">
<Input
id="stripe_api_key"
name="stripe_api_key"
placeholder="Enter your Stripe API key"
autoComplete="stripe-api-key"
reveal={false}
disabled={isInstallRequested}
type="password"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
/>
</FormControl>
</FormItemLayout>
)}
/>
<div className="flex items-center gap-x-2">
<Button asChild type="default" icon={<ExternalLink />}>
<Link
target="_blank"
rel="noopener noreferrer"
href="https://dashboard.stripe.com/apikeys"
>
Get Stripe API key
</Link>
</Button>
<Button asChild type="default" icon={<ExternalLink />}>
<Link
target="_blank"
rel="noopener noreferrer"
href="https://support.stripe.com/questions/what-are-stripe-api-keys-and-how-to-find-them"
>
What are Stripe API keys?
</Link>
</Button>
</div>
</div>
{installRequestError && (
<Admonition
type="destructive"
title="Installation failed"
description={installRequestError.message}
/>
)}
</SheetSection>
<SheetFooter>
<Button
type="default"
disabled={isInstallRequested}
onClick={() => handleCloseInstallSheet(false)}
>
Cancel
</Button>
<Button
form={formId}
htmlType="submit"
type="primary"
loading={isInstallRequested}
disabled={!form.formState.isValid || isInstallRequested}
>
{isInstallRequested
? upgradeAvailable
? 'Upgrading'
: 'Installing'
: upgradeAvailable
? 'Upgrade integration'
: 'Install integration'}
</Button>
</SheetFooter>
</form>
</Form>
</SheetContent>
</Sheet>
<ConfirmationModal
visible={showUninstallModal}
title="Uninstall Stripe Sync Engine"
@@ -444,3 +390,24 @@ export const StripeSyncEngineOverviewTab = () => {
</>
)
}
export const StripeSyncEngineOverviewTab = () => {
const isMarketplaceEnabled = useIsMarketplaceEnabled()
if (isMarketplaceEnabled) {
return (
<>
<RequiredExtensionsSection />
<StripeSyncContent />
</>
)
}
return (
<IntegrationOverviewTab>
<div className="px-4 md:px-10 max-w-4xl space-y-4">
<StripeSyncContent />
</div>
</IntegrationOverviewTab>
)
}