mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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 --> [](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:
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 />
|
||||
}
|
||||
+11
@@ -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')
|
||||
|
||||
+45
-65
@@ -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>
|
||||
)
|
||||
}
|
||||
+2
-6
@@ -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>
|
||||
)
|
||||
|
||||
+3
-4
@@ -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} />
|
||||
}
|
||||
+11
@@ -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>
|
||||
}
|
||||
+84
@@ -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' ? (
|
||||
|
||||
+23
-17
@@ -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>
|
||||
|
||||
+177
-210
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user