diff --git a/apps/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json index c95bbd984d6..77b60c9fefa 100644 --- a/apps/studio/.github/eslint-rule-baselines.json +++ b/apps/studio/.github/eslint-rule-baselines.json @@ -639,7 +639,6 @@ "components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecret.tsx": 1, "components/interfaces/Integrations/CronJobs/CronJobsEmptyState.tsx": 1, "components/interfaces/Integrations/CronJobs/CronSyntaxChart.tsx": 1, - "components/interfaces/Integrations/Integration/LegacyIntegrationPage.tsx": 1, "components/interfaces/Integrations/Vercel/OrganizationPicker.tsx": 1, "components/interfaces/Integrations/Wrappers/InputField.tsx": 1, "components/interfaces/Integrations/Wrappers/WrapperTableEditor.tsx": 1, diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md index 387e808c2ce..19a7141e828 100644 --- a/apps/studio/TANSTACK_MIGRATION.md +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -96,7 +96,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] `routes/project/$ref/advisors.tsx` — AdvisorsLayout (reads `advisorsLayoutTitle` from leaf staticData). Honours `skipAdvisorsLayout: true` opt-out for the rules sub-shell, which provides its own AdvisorsLayout-less-DefaultLayout wrap. Scans whole match chain (same pattern as functions.tsx). - [x] `routes/project/$ref/advisors/rules.tsx` — sub-shell that inlines the inner body of `AdvisorRulesLayout` (AdvisorsLayout + PageLayout with title/tabs/feature-preview badge), minus the outer DefaultLayout (already provided by the parent project shell). Sets `skipAdvisorsLayout: true` on its own staticData. **Delta vs plan:** the existing `AdvisorRulesLayout` component wraps in DefaultLayout + AdvisorsLayout internally, so reusing it as-is would double-wrap both. Inlined the inner part; the Next-side component is untouched. - [x] `routes/project/$ref/settings.tsx` — SettingsLayout (reads `settingsLayoutTitle` from leaf staticData). Honours `skipSettingsLayout: true` for `settings/api` (redirect-only page). Adds a sub-shell at `routes/project/$ref/settings/api-keys.tsx` providing `ApiKeysLayout` for both api-keys leaves; `jwt/index` wraps in `JWTKeysLayout` inline since `jwt/legacy` doesn't share it. -- [x] `routes/project/$ref/integrations.tsx` — ProjectIntegrationsLayout (no staticData; all 4 leaves share identical layout). Layout is `withAuth(({ children }) => {children})`, so the shell just wraps `` once. +- [x] `routes/project/$ref/integrations.tsx` — ProjectMarketplaceLayout (no staticData; all 4 leaves share identical layout). Layout is `withAuth(({ children }) => {children})`, so the shell just wraps `` once. - [x] `routes/project/$ref/sql.tsx` — EditorBaseLayout + SQLEditorLayout. Twin of editor.tsx; all four leaves share identical layout props so the shell hardcodes them (no `staticData` overrides). EditorBaseLayout wraps in ProjectLayoutWithAuth; SQLEditorLayout adds its own `withAuth` HOC but no extra ProjectLayout — same shape as the table editor (auth check runs twice but no double render). - [x] `routes/project/$ref/editor.tsx` — EditorBaseLayout + TableEditorLayout. All three leaves share identical layout props so the shell hardcodes them (no `staticData` overrides). EditorBaseLayout wraps in `ProjectLayoutWithAuth` internally; TableEditorLayout's happy path is just a fragment + side-effect (banner) and only wraps in `ProjectLayoutWithAuth` on its no-permission branch — same as Next, no double-wrap in normal use. diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx index ce32634141e..fcce4010dde 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewContext.tsx @@ -134,12 +134,6 @@ export const useIsSqlEditorManualSaveEnabled = () => { return sqlEditorManualSaveEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE] } -export const useIsMarketplaceEnabled = () => { - const { flags } = useFeaturePreviewContext() - const isMarketplaceEnabled = useFlag('marketplaceIntegrations') - return isMarketplaceEnabled && flags[LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE] -} - export const useFeaturePreviewModal = () => { const featurePreviews = useFeaturePreviews() const [featurePreviewModal, setFeaturePreviewModal] = useQueryState('featurePreviewModal') diff --git a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx index 5453e536903..9cd78597ffc 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx +++ b/apps/studio/components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx @@ -30,7 +30,6 @@ import { import { AdvisorRulesPreview } from './AdvisorRulesPreview' import { CLSPreview } from './CLSPreview' import { useFeaturePreviewContext, useFeaturePreviewModal } from './FeaturePreviewContext' -import { IntegrationsLayoutPreview } from './IntegrationsLayoutPreview' import { JitDbAccessPreview } from './JitDbAccessPreview' import { PgDeltaDiffPreview } from './PgDeltaDiffPreview' import { PlatformWebhooksPreview } from './PlatformWebhooksPreview' @@ -50,7 +49,6 @@ const FEATURE_PREVIEW_KEY_TO_CONTENT: { [LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS]: , [LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE]: , - [LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE]: , } export const FeaturePreviewModal = () => { diff --git a/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx b/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx deleted file mode 100644 index 28c8e453fd1..00000000000 --- a/apps/studio/components/interfaces/App/FeaturePreview/IntegrationsLayoutPreview.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Image from 'next/image' -import { BASE_PATH } from 'ui-patterns/CommandMenu/prepackaged/shared/constants' - -export const IntegrationsLayoutPreview = () => ( -
-

- Install Dashboard Integrations in a single click and try the new layout. -

- - integrations layout preview -
-) diff --git a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts index 65be8e9df45..c14b1c185e5 100644 --- a/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts +++ b/apps/studio/components/interfaces/App/FeaturePreview/useFeaturePreviews.ts @@ -22,7 +22,6 @@ export type FeaturePreview = { export const useFeaturePreviews = (): FeaturePreview[] => { const platformWebhooksEnabled = useFlag('platformWebhooks') const jitDbAccessEnabled = useFlag('jitDbAccess') - const isMarketplaceEnabled = useFlag('marketplaceIntegrations') const sqlEditorManualSaveEnabled = useFlag('sqlEditorManualSave') const unifiedLogsDefaultOptIn = useFlag('unifiedLogsDefaultOptIn') @@ -90,16 +89,6 @@ export const useFeaturePreviews = (): FeaturePreview[] => { isDefaultOptIn: false, getRoute: (ref?: string) => `/project/${ref}/database/column-privileges`, }, - { - key: LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE, - name: 'One-Click Integrations', - discussionsUrl: undefined, - enabled: isMarketplaceEnabled, - isNew: true, - isPlatformOnly: false, - isDefaultOptIn: true, - getRoute: (ref?: string) => `/project/${ref}/integrations`, - }, { key: LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE, name: 'Disable snippet auto-saving', @@ -110,12 +99,6 @@ export const useFeaturePreviews = (): FeaturePreview[] => { enabled: sqlEditorManualSaveEnabled, }, ].sort((a, b) => Number(b.isNew) - Number(a.isNew)), - [ - unifiedLogsDefaultOptIn, - platformWebhooksEnabled, - jitDbAccessEnabled, - isMarketplaceEnabled, - sqlEditorManualSaveEnabled, - ] + [unifiedLogsDefaultOptIn, platformWebhooksEnabled, jitDbAccessEnabled, sqlEditorManualSaveEnabled] ) } diff --git a/apps/studio/components/interfaces/Integrations/ConstrainedIntegrationTabScaffold.tsx b/apps/studio/components/interfaces/Integrations/ConstrainedIntegrationTabScaffold.tsx index bcdacecb969..b5ee7aa8e11 100644 --- a/apps/studio/components/interfaces/Integrations/ConstrainedIntegrationTabScaffold.tsx +++ b/apps/studio/components/interfaces/Integrations/ConstrainedIntegrationTabScaffold.tsx @@ -1,8 +1,6 @@ import { PropsWithChildren } from 'react' import { cn } from 'ui' -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' - interface ConstrainedIntegrationTabScaffoldProps extends PropsWithChildren { className?: string } @@ -10,11 +8,6 @@ interface ConstrainedIntegrationTabScaffoldProps extends PropsWithChildren { export const ConstrainedIntegrationTabScaffold = ({ children, className, -}: ConstrainedIntegrationTabScaffoldProps) => { - const isMarketplace = useIsMarketplaceEnabled() - return ( -
- {children} -
- ) -} +}: ConstrainedIntegrationTabScaffoldProps) => ( +
{children}
+) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/OverviewTab.tsx index 491455185b1..f7240c4dec2 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/OverviewTab.tsx @@ -1,10 +1,3 @@ -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 - return -} +export const CronOverviewTab = () => diff --git a/apps/studio/components/interfaces/Integrations/DataApi/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/DataApi/OverviewTab.tsx index 54e702b3748..ced9aad1302 100644 --- a/apps/studio/components/interfaces/Integrations/DataApi/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/DataApi/OverviewTab.tsx @@ -1,17 +1,3 @@ -import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' import { DataApiURLSettings } from './DataApiURLSettings' -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' -export const DataApiOverviewTab = () => { - const isMarketplaceEnabled = useIsMarketplaceEnabled() - - if (isMarketplaceEnabled) return - - return ( - -
- -
-
- ) -} +export const DataApiOverviewTab = () => diff --git a/apps/studio/components/interfaces/Integrations/GraphQL/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/GraphQL/OverviewTab.tsx index 573b2b59e21..37e6d91ad06 100644 --- a/apps/studio/components/interfaces/Integrations/GraphQL/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/GraphQL/OverviewTab.tsx @@ -1,10 +1,3 @@ -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 - return -} +export const GraphQLOverviewTab = () => diff --git a/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx b/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx deleted file mode 100644 index 0fe6dc6dfad..00000000000 --- a/apps/studio/components/interfaces/Integrations/Integration/BuildBySection.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Book } from 'lucide-react' -import Link from 'next/link' -import { ComponentPropsWithoutRef, ElementRef, forwardRef, ReactNode } from 'react' -import { cn } from 'ui' - -import { IntegrationDefinition } from '../Landing/Integrations.constants' - -interface BuiltBySectionProps extends ComponentPropsWithoutRef<'div'> { - integration: IntegrationDefinition - status?: string | ReactNode -} - -export const BuiltBySection = forwardRef, BuiltBySectionProps>( - ({ integration, status, className, ...props }, ref) => { - const { docsUrl } = integration - const { name, websiteUrl } = integration?.author ?? {} - - if (!name && !docsUrl && !websiteUrl) return null - - return ( -
- {status && ( -
-
STATUS
-
{status}
-
- )} - {name && ( -
-
BUILT BY
-
{name}
-
- )} - {docsUrl && ( -
-
DOCS
- - - {docsUrl.includes('supabase.com/docs') - ? 'Supabase Docs' - : docsUrl.includes('github.com') - ? 'GitHub Docs' - : 'Documentation'} - -
- )} - {websiteUrl && ( -
-
WEBSITE
- - {websiteUrl.replace('https://', '')} - -
- )} -
- ) - } -) - -BuiltBySection.displayName = 'BuiltBySection' diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationDetailTabShortcuts.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationDetailTabShortcuts.tsx index a97a049d316..4a1f7b395cc 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationDetailTabShortcuts.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationDetailTabShortcuts.tsx @@ -8,9 +8,7 @@ const TAB_DIGIT_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9'] as const /** * Binds digit keys 1-9 to whichever tabs the active integration declares in - * its `navigation` array. Render this once per integration detail page — - * shared between `LegacyIntegrationPage` and the marketplace `MarketplaceDetail` - * since both surfaces drive off the same `tabs` shape from `useIntegrationDetail`. + * its `navigation` array. Render this once per integration detail page. * * The dynamic shortcut registry handles per-tab label overrides ("Go to Jobs", * "Go to Wrappers", etc.) so the reference sheet and Cmd+K pick up the actual diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.test.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.test.tsx deleted file mode 100644 index 68dd6a4a79f..00000000000 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.test.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { screen } from '@testing-library/dom' -import { mockAnimationsApi } from 'jsdom-testing-mocks' -import { beforeEach, describe, expect, it, vi } from 'vitest' - -import { IntegrationOverviewTab } from './IntegrationOverviewTab' -import { customRender } from '@/tests/lib/custom-render' -import { routerMock } from '@/tests/lib/route-mock' - -mockAnimationsApi() - -vi.mock('../Landing/Integrations.constants', () => ({ - INTEGRATIONS: [ - { - id: 'test-integration', - name: 'Test Integration', - requiredExtensions: ['pg_net'], - }, - ], -})) - -vi.mock('framer-motion', async (importOriginal) => { - const actual = (await importOriginal()) as any - return { - ...actual, - motion: { - ...actual.motion, - div: ({ children, ...props }: any) =>
{children}
, - }, - } -}) - -const mockExtensions = vi.fn() - -vi.mock('@/data/database-extensions/database-extensions-query', () => ({ - useDatabaseExtensionsQuery: () => ({ data: mockExtensions() }), -})) - -vi.mock('@/hooks/misc/useSelectedProject', () => ({ - useSelectedProjectQuery: () => ({ - data: { ref: 'default', connectionString: 'postgres://localhost' }, - }), - useIsOrioleDb: () => false, -})) - -vi.mock('common', async (importOriginal) => { - const actual = (await importOriginal()) as any - return { - ...actual, - useParams: () => ({ id: 'test-integration', ref: 'default' }), - } -}) - -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') - mockExtensions.mockReturnValue([ - { name: 'pg_net', installed_version: null, default_version: '0.6.0' }, - ]) - }) - - it('does not disable actions when hideRequiredExtensionsSection is true and extensions are uninstalled', () => { - customRender( - Enable webhooks} - /> - ) - - const actionsArea = screen.getByText('Enable webhooks').closest('[aria-disabled]') - expect(actionsArea).toHaveAttribute('aria-disabled', 'false') - expect(actionsArea).not.toHaveClass('opacity-25') - }) - - it('disables actions when extensions are uninstalled and hideRequiredExtensionsSection is false', () => { - customRender( - Enable integration} /> - ) - - const actionsArea = screen.getByText('Enable integration').closest('[aria-disabled]') - expect(actionsArea).toHaveAttribute('aria-disabled', 'true') - expect(actionsArea).toHaveClass('opacity-25') - }) -}) diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx deleted file mode 100644 index b86a488db39..00000000000 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useParams } from 'common' -import { PropsWithChildren, ReactNode } from 'react' -import { cn } from 'ui' - -import { IntegrationDefinition, INTEGRATIONS } from '../Landing/Integrations.constants' -import { BuiltBySection } from './BuildBySection' -import { MarkdownContent } from './MarkdownContent' -import { RequiredExtensionsSection } from './RequiredExtensionsSection' -import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' - -export interface IntegrationOverviewTabProps { - actions?: ReactNode - status?: string | ReactNode - alert?: ReactNode - hideRequiredExtensionsSection?: boolean -} - -export const OverviewTabSharedContent = ({ - integration, - hideRequiredExtensionsSection = false, - actions, - alert, - children, -}: PropsWithChildren<{ - integration: IntegrationDefinition - hideRequiredExtensionsSection?: boolean - actions?: ReactNode - alert?: ReactNode -}>) => { - const { data: project } = useSelectedProjectQuery() - - const { data: extensions } = useDatabaseExtensionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - if (!integration) { - return
Unsupported integration type
- } - - const installableExtensions = (extensions ?? []).filter((ext) => - (integration.requiredExtensions ?? []).includes(ext.name) - ) - const hasToInstallExtensions = installableExtensions.some((x) => !x.installed_version) - - return ( - <> - {!!alert &&
{alert}
} - - - - - - {!!actions && ( -
- {actions} -
- )} - {children} - - ) -} - -export const IntegrationOverviewTab = ({ - actions, - alert, - status, - children, - hideRequiredExtensionsSection = false, -}: PropsWithChildren) => { - const { id } = useParams() - const integration = INTEGRATIONS.find((i) => i.id === id) - - if (!integration) { - return
Unsupported integration type
- } - - return ( -
- - - {children} - -
- ) -} diff --git a/apps/studio/components/interfaces/Integrations/Integration/LegacyIntegrationPage.tsx b/apps/studio/components/interfaces/Integrations/Integration/LegacyIntegrationPage.tsx deleted file mode 100644 index 5f201f69ebe..00000000000 --- a/apps/studio/components/interfaces/Integrations/Integration/LegacyIntegrationPage.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import Link from 'next/link' -import { useMemo } from 'react' -import { - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, - NavMenu, - NavMenuItem, -} from 'ui' -import { Admonition } from 'ui-patterns/admonition' -import { PageContainer } from 'ui-patterns/PageContainer' -import { - PageHeader, - PageHeaderBreadcrumb, - PageHeaderDescription, - PageHeaderIcon, - PageHeaderMeta, - PageHeaderNavigationTabs, - PageHeaderSummary, - PageHeaderTitle, -} from 'ui-patterns/PageHeader' -import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' -import { GenericSkeletonLoader, ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' - -import { IntegrationLogo } from '@/components/interfaces/Integrations/Integration/IntegrationLogo' -import { InstallOAuthIntegrationButton } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallOAuthIntegrationButton' -import { useIntegrationDetail } from '@/components/interfaces/Integrations/Landing/useIntegrationDetail' -import { UnknownInterface } from '@/components/ui/UnknownInterface' - -const LegacyIntegrationPage = () => { - const { - ref, - id, - isReady, - isWrapperBlocked, - pageTitle, - pageSubTitle, - integration, - isAvailableLoading, - isInstalledLoading, - isIntegrationStatusLoading, - oauthIntegrationData, - tabs, - Component, - } = useIntegrationDetail() - - const content = useMemo(() => { - if (!isReady || isInstalledLoading || isAvailableLoading) { - return ( - - - - - - - - ) - } else if (!Component || !id || !integration) { - return ( - - - - - Please try again later or contact support if the problem persists. - - - - - ) - } else { - return - } - }, [isReady, isInstalledLoading, isAvailableLoading, id, integration, Component]) - - if (!isReady) return null - if (isWrapperBlocked) return - - return ( - <> - - - - - - Integrations - - - - - {integration?.name || 'Integration not found'} - - - - - {isAvailableLoading ? ( - - - - - - - - - - - ) : ( - - {integration && ( - - - - )} - - {pageTitle} - {pageSubTitle} - - - {integration?.type === 'oauth' && ( - - )} - - )} - - {tabs.length > 0 && ( - - - {tabs.map((tab) => ( - - {tab.label} - - ))} - - - )} - - -
{content}
- - ) -} - -export default LegacyIntegrationPage diff --git a/apps/studio/components/interfaces/Integrations/Integration/MarketplaceIntegrationOverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Integration/MarketplaceIntegrationOverviewTab.tsx deleted file mode 100644 index b6cc6f0aed3..00000000000 --- a/apps/studio/components/interfaces/Integrations/Integration/MarketplaceIntegrationOverviewTab.tsx +++ /dev/null @@ -1,11 +0,0 @@ -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 {props.children} -} diff --git a/apps/studio/components/interfaces/Integrations/Integration/MissingExtensionAlert.tsx b/apps/studio/components/interfaces/Integrations/Integration/MissingExtensionAlert.tsx deleted file mode 100644 index 29bad660b75..00000000000 --- a/apps/studio/components/interfaces/Integrations/Integration/MissingExtensionAlert.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useState } from 'react' -import { Button } from 'ui' - -import { EnableExtensionModal } from '@/components/interfaces/Database/Extensions/EnableExtensionModal' -import { DatabaseExtension } from '@/data/database-extensions/database-extensions-query' - -export const MissingExtensionAlert = ({ extension }: { extension: DatabaseExtension }) => { - const [showEnableExtensionModal, setShowEnableExtensionModal] = useState(false) - - const extensionInstalled = !!extension?.installed_version - if (!extensionInstalled) { - return ( - <> - - - setShowEnableExtensionModal(false)} - /> - - ) - } - return null -} diff --git a/apps/studio/components/interfaces/Integrations/Integration/RequiredExtensionsSection.tsx b/apps/studio/components/interfaces/Integrations/Integration/RequiredExtensionsSection.tsx index 070985b792c..16a352e8312 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/RequiredExtensionsSection.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/RequiredExtensionsSection.tsx @@ -1,7 +1,5 @@ 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' @@ -21,7 +19,6 @@ export const RequiredExtensionsSection = ({ projectRef: project?.ref, connectionString: project?.connectionString, }) - const isMarketplaceEnabled = useIsMarketplaceEnabled() const requiredExtensions = integration?.requiredExtensions ?? [] @@ -59,10 +56,8 @@ export const RequiredExtensionsSection = ({ {extension ? ( isInstalled ? ( Installed - ) : isMarketplaceEnabled ? ( - Required ) : ( - + Required ) ) : ( Unavailable diff --git a/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx b/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx index 26f807d0765..b8b942660dc 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/IntegrationCard.tsx @@ -1,18 +1,6 @@ -import { BadgeCheck } from 'lucide-react' -import Image from 'next/image' -import Link from 'next/link' -import { Badge, Card, CardContent, cn } from 'ui' +import { cn } from 'ui' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' -import { IntegrationDefinition } from './Integrations.constants' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' - -type IntegrationCardProps = IntegrationDefinition & { - isInstalled?: boolean - featured?: boolean - image?: string -} - const INTEGRATION_CARD_STYLE = cn( 'w-full h-full bg-surface-100 hover:bg-surface-200 hover:border-strong', 'border border-border rounded-md ease-out duration-200 transition-all' @@ -33,81 +21,3 @@ export const IntegrationLoadingCard = () => { ) } - -export const IntegrationCard = ({ - id, - listingId, - status, - name, - icon, - description, - isInstalled, - featured = false, - image, -}: IntegrationCardProps) => { - const { data: project } = useSelectedProjectQuery() - const shouldShowOfficialBadge = !listingId - - if (featured) { - return ( - - - {/* Full-width image/icon at the top */} -
- {image ? ( - {`${name} - ) : ( -
- {icon({ className: 'w-full h-full text-foreground' })} -
- )} -
- -
-

{name}

-

{description}

-
- {status && {status}} - {shouldShowOfficialBadge && Official} -
-
-
-
- - ) - } - - return ( - - - -
-
- {icon()} -
- {isInstalled && ( -
- - Installed -
- )} -
-
-

{name}

- -

{description}

-
- {status && {status}} - {shouldShowOfficialBadge && Official} -
-
-
-
- - ) -} diff --git a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx index c0817a4b86c..749bb8ac916 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx @@ -13,7 +13,6 @@ import { useContext, useMemo } from 'react' import { cn } from 'ui' import { INTEGRATIONS, Loading, type IntegrationDefinition } from './Integrations.constants' -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useMarketplaceIntegrationsQuery, type MarketplaceIntegration, @@ -55,16 +54,11 @@ const isPreviewEnabled = (featureFlags: FeatureFlagContextType, listingSlug: str const useMarketplaceListings = () => { const { hasLoaded } = useContext(FeatureFlagContext) - const isMarketplaceEnabled = useIsMarketplaceEnabled() - const { data: marketplaceData, error } = useMarketplaceIntegrationsQuery({ - enabled: isMarketplaceEnabled, - }) - const isPending = - IS_PLATFORM && (!hasLoaded || (isMarketplaceEnabled && !marketplaceData && !error)) - const isSuccess = - !IS_PLATFORM || (hasLoaded && (!isMarketplaceEnabled || (!!marketplaceData && !error))) - const isError = IS_PLATFORM && isMarketplaceEnabled && !!error + const { data: marketplaceData, error } = useMarketplaceIntegrationsQuery() + const isPending = IS_PLATFORM && (!hasLoaded || (!marketplaceData && !error)) + const isSuccess = !IS_PLATFORM || (hasLoaded && !!marketplaceData && !error) + const isError = IS_PLATFORM && !!error // This flag can globally enable preview listings for all partners for a given user (i.e. for Supabase users) const previewAllListingsEnabled = useFlag('previewMarketplaceListingsEnabled') @@ -177,15 +171,7 @@ export const useAvailableIntegrations = () => { navigate: ({ pageId = 'overview' }) => { switch (pageId) { case 'overview': - return dynamic( - () => - import('@/components/interfaces/Integrations/Integration/MarketplaceIntegrationOverviewTab').then( - (mod) => mod.MarketplaceIntegrationOverviewTab - ), - { - loading: Loading, - } - ) + return null case 'settings': return dynamic( () => diff --git a/apps/studio/components/interfaces/Integrations/Landing/useIntegrationDetail.ts b/apps/studio/components/interfaces/Integrations/Landing/useIntegrationDetail.ts index 2e5b3da8f54..4c7b7e65b24 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useIntegrationDetail.ts +++ b/apps/studio/components/interfaces/Integrations/Landing/useIntegrationDetail.ts @@ -5,14 +5,7 @@ import { useEffect, useMemo } from 'react' import { useProjectOAuthIntegrationData } from './Landing.utils' import { useAvailableIntegrations } from './useAvailableIntegrations' import { useInstalledIntegrations } from './useInstalledIntegrations' -import { - areRequiredExtensionsInstalledFor, - getFilteredNavItems, - getInstallActionType, - type InstallActionType, -} from './useIntegrationDetail.utils' -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' +import { getInstallActionType, type InstallActionType } from './useIntegrationDetail.utils' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' @@ -27,13 +20,8 @@ export const useIntegrationDetail = () => { const { ref, id, pageId, childId } = useParams() const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) - const isMarketplaceEnabled = useIsMarketplaceEnabled() const { data: project } = useSelectedProjectQuery() - const { data: extensions } = useDatabaseExtensionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) const { data: allIntegrations, isPending: isAvailableLoading } = useAvailableIntegrations() const { installedIntegrations, isLoading: isInstalledLoading } = useInstalledIntegrations() @@ -55,21 +43,7 @@ export const useIntegrationDetail = () => { const isInstalled = !!integration && !!installation - const areRequiredExtensionsInstalled = useMemo( - () => areRequiredExtensionsInstalledFor(integration, extensions), - [integration, extensions] - ) - - const navItems = useMemo( - () => - getFilteredNavItems({ - integration, - isInstalled, - isMarketplaceEnabled, - areRequiredExtensionsInstalled, - }), - [integration, isInstalled, isMarketplaceEnabled, areRequiredExtensionsInstalled] - ) + const navItems = useMemo(() => integration?.navigation ?? [], [integration]) const activeRoute = pageId ?? 'overview' @@ -102,14 +76,8 @@ export const useIntegrationDetail = () => { ) const installActionType: InstallActionType = useMemo( - () => - getInstallActionType({ - integration, - isMarketplaceEnabled, - areRequiredExtensionsInstalled, - isInstalled, - }), - [integration, isMarketplaceEnabled, areRequiredExtensionsInstalled, isInstalled] + () => getInstallActionType({ integration, isInstalled }), + [integration, isInstalled] ) const isReady = !!router?.isReady @@ -151,7 +119,6 @@ export const useIntegrationDetail = () => { installation, integrationStatus, isInstalled, - areRequiredExtensionsInstalled, installActionType, wrappersTabHref, isAvailableLoading, diff --git a/apps/studio/components/interfaces/Integrations/Landing/useIntegrationDetail.utils.ts b/apps/studio/components/interfaces/Integrations/Landing/useIntegrationDetail.utils.ts index a21552f9ad6..fc9246916d4 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useIntegrationDetail.utils.ts +++ b/apps/studio/components/interfaces/Integrations/Landing/useIntegrationDetail.utils.ts @@ -1,65 +1,20 @@ -import type { IntegrationDefinition, Navigation } from './Integrations.constants' +import type { IntegrationDefinition } from './Integrations.constants' export type InstallActionType = 'oauth' | 'add-wrapper' | 'installed' | 'install-sheet' | null -export type DatabaseExtension = { - name: string - installed_version: string | null -} - -/** - * Returns whether all required extensions for an integration are enabled in the database. - */ -export function areRequiredExtensionsInstalledFor( - integration: IntegrationDefinition | undefined, - extensions: DatabaseExtension[] | undefined -): boolean { - if (!integration?.requiredExtensions?.length || !extensions) return false - return integration.requiredExtensions.every( - (name) => !!extensions.find((ext) => ext.name === name)?.installed_version - ) -} - -/** - * Returns the filtered navigation items for an integration based on install / feature-flag state. - * If the marketplace flag is off, only expose the Wrappers tab once extensions are installed - */ -export function getFilteredNavItems({ - integration, - isInstalled, - isMarketplaceEnabled, - areRequiredExtensionsInstalled, -}: { - integration: IntegrationDefinition | undefined - isInstalled: boolean - isMarketplaceEnabled: boolean - areRequiredExtensionsInstalled: boolean -}): Navigation[] { - if (!integration?.navigation) return [] - if (isInstalled || integration.type !== 'wrapper') return integration.navigation - if (isMarketplaceEnabled || areRequiredExtensionsInstalled) return integration.navigation - return integration.navigation.filter((nav) => nav.route !== 'wrappers') -} - /** * Returns which install action type to render for an integration. */ export function getInstallActionType({ integration, - isMarketplaceEnabled, - areRequiredExtensionsInstalled, isInstalled, }: { integration: IntegrationDefinition | undefined - isMarketplaceEnabled: boolean - areRequiredExtensionsInstalled: boolean isInstalled: boolean }): InstallActionType { if (!integration) return null if (integration.type === 'oauth') return 'oauth' - if (integration.type === 'wrapper' && (isMarketplaceEnabled || areRequiredExtensionsInstalled)) { - return 'add-wrapper' - } + if (integration.type === 'wrapper') return 'add-wrapper' if (isInstalled) return 'installed' return 'install-sheet' } diff --git a/apps/studio/components/interfaces/Integrations/Queues/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Queues/OverviewTab.tsx index 26fc8374fc8..8d1405ac3e2 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/OverviewTab.tsx @@ -1,52 +1,3 @@ -import { useParams } from 'common' -import Link from 'next/link' -import { Button } from 'ui' -import { Admonition } from 'ui-patterns/admonition' - -import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' 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 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, - connectionString: project?.connectionString, - }) - const isQueuesInstalled = !!extensions.find((x) => x.name === 'pgmq')?.installed_version - - if (isExposed) return null - - return ( - -

- You may choose to toggle the exposure of Queues through Data APIs via the queues settings -

- - {isQueuesInstalled && ( - - )} -
- ) -} - -export const QueuesOverviewTab = () => { - const isMarketplaceEnabled = useIsMarketplaceEnabled() - - if (isMarketplaceEnabled) return - return } /> -} +export const QueuesOverviewTab = () => diff --git a/apps/studio/components/interfaces/Integrations/Vault/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Vault/OverviewTab.tsx index 120e6f7c16b..f5795d6b457 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/OverviewTab.tsx @@ -1,10 +1,3 @@ -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 - return -} +export const VaultOverviewTab = () => diff --git a/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx index 454acf34276..b32ee5d909c 100644 --- a/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx @@ -1,99 +1,3 @@ -import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' -import { toast } from 'sonner' -import { Admonition } from 'ui-patterns/admonition' -import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' - -import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' 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' -import { useHooksEnableMutation } from '@/data/database/hooks-enable-mutation' -import { useSchemasQuery } from '@/data/database/schemas-query' -import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -const WebhooksContent = () => { - const { ref: projectRef } = useParams() - const { data: project } = useSelectedProjectQuery() - - const { - data: schemas, - isSuccess: isSchemasLoaded, - refetch, - } = useSchemasQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const isHooksEnabled = schemas?.some((schema) => schema.name === 'supabase_functions') - const { can: canReadWebhooks, isLoading: isLoadingPermissions } = useAsyncCheckPermissions( - PermissionAction.TENANT_SQL_ADMIN_READ, - 'triggers' - ) - - const { mutate: enableHooks, isPending: isEnablingHooks } = useHooksEnableMutation({ - onSuccess: async () => { - await refetch() - toast.success('Successfully enabled webhooks') - }, - }) - - const enableHooksForProject = async () => { - if (!projectRef) return console.error('Project ref is required') - enableHooks({ ref: projectRef }) - } - - if (!isSchemasLoaded || isLoadingPermissions) { - return ( -
- -
- ) - } - - if (!canReadWebhooks) { - return ( -
- -
- ) - } - - if (isSchemasLoaded && isHooksEnabled) return null - - return ( - -

- Database Webhooks can be used to trigger serverless functions or send requests to an HTTP - endpoint -

- enableHooksForProject()} - disabled={isEnablingHooks} - tooltip={{ - content: { - side: 'bottom', - text: !canReadWebhooks - ? 'You need additional permissions to enable webhooks' - : undefined, - }, - }} - > - Enable webhooks - -
- ) -} - -export const WebhooksOverviewTab = () => { - const isMarketplaceEnabled = useIsMarketplaceEnabled() - - if (isMarketplaceEnabled) { - return - } - - return } /> -} +export const WebhooksOverviewTab = () => diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx index d231545c009..2c990424a0c 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/CreateWrapperSheet.tsx @@ -32,7 +32,6 @@ import { NewTable, } from './Wrappers.utils' import WrapperTableEditor from './WrapperTableEditor' -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { getExtensionDefaultSchema } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/IntegrationOverviewTabV2.utils' import { RequiredExtensionsSection } from '@/components/interfaces/Integrations/Integration/RequiredExtensionsSection' import { useIntegrationDetail } from '@/components/interfaces/Integrations/Landing/useIntegrationDetail' @@ -66,7 +65,6 @@ export const CreateWrapperSheet = ({ onCloseWithConfirmation, }: CreateWrapperSheetProps) => { const queryClient = useQueryClient() - const isMarketplaceEnabled = useIsMarketplaceEnabled() const { integration } = useIntegrationDetail() const { data: project } = useSelectedProjectQuery() @@ -85,8 +83,8 @@ export const CreateWrapperSheet = ({ const wrappersExtension = extensions?.find((ext) => ext.name === 'wrappers') const hasRequiredVersionForeignSchema = hasForeignSchemaSupport(wrappersExtension) - const needsExtensions = isMarketplaceEnabled && (requiredExtensionsToInstall?.length ?? 0) > 0 - const isExtensionDataLoading = isMarketplaceEnabled && requiredExtensionsToInstall === null + const needsExtensions = (requiredExtensionsToInstall?.length ?? 0) > 0 + const isExtensionDataLoading = requiredExtensionsToInstall === null const { data: schemas } = useSchemasQuery({ projectRef: project?.ref!, @@ -261,18 +259,16 @@ export const CreateWrapperSheet = ({ Create a {wrapperMeta.label} wrapper
- {isMarketplaceEnabled && ( -
- {needsExtensions && ( - - )} - -
- )} +
+ {needsExtensions && ( + + )} + +
Wrapper Configuration}> { - const { id } = useParams() - const wrapperMeta = WRAPPERS.find((w) => w.name === id) - - const [isDirty, setIsDirty] = useState(false) - const [createWrapperShown, setCreateWrapperShown] = useQueryState( - 'new', - parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) - ) - - const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({ - checkIsDirty: () => isDirty, - onClose: () => { - setCreateWrapperShown(false) - setIsDirty(false) - }, - }) - - // [Joshen] Opting to declare custom wrapper sheets here instead of within Wrappers.constants.ts - // as we'll easily run into circular dependencies doing so unfortunately - const CreateWrapperSheetComponent = !wrapperMeta - ? null - : wrapperMeta.customComponent - ? wrapperMeta.name === 'iceberg_wrapper' - ? CreateIcebergWrapperSheet - : null - : CreateWrapperSheet - - return ( - <> -
-

Recent wrappers

- -
- - {!!CreateWrapperSheetComponent && !!wrapperMeta && ( - - - setCreateWrapperShown(false)} - onCloseWithConfirmation={confirmOnClose} - /> - - - )} - - - - ) -} - -const AddNewWrapperCTA = () => { - const { id } = useParams() - const { data: project } = useSelectedProjectQuery() - const [, setCreateWrapperShown] = useQueryState( - 'new', - parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) - ) - - const { can: canCreateWrapper } = useAsyncCheckPermissions( - PermissionAction.TENANT_SQL_ADMIN_WRITE, - 'wrappers' - ) - - const { data } = useDatabaseExtensionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const wrapperMeta = WRAPPERS.find((w) => w.name === id) - const wrappersExtension = data?.find((ext) => ext.name === 'wrappers') - const isWrappersExtensionInstalled = !!wrappersExtension?.installed_version - const hasRequiredVersion = - (wrappersExtension?.installed_version ?? '') >= (wrapperMeta?.minimumExtensionVersion ?? '') - // [Joshen] Default version is what's on the DB, so if the installed version is already the default version - // but still doesnt meet the minimum extension version, then DB upgrade is required - const databaseNeedsUpgrading = - wrappersExtension?.installed_version === wrappersExtension?.default_version - - if (!!wrapperMeta && isWrappersExtensionInstalled && !hasRequiredVersion) { - return ( - -
-

- The {wrapperMeta.label} wrapper requires a minimum extension version of{' '} - {wrapperMeta.minimumExtensionVersion}. You have version{' '} - {wrappersExtension?.installed_version} installed. Please{' '} - {databaseNeedsUpgrading && 'upgrade your database then '}update the extension by - disabling and enabling the wrappers extension - to create this wrapper. -

-

- Warning: Before reinstalling the wrapper extension, you must first remove all existing - wrappers. Afterward, you can recreate the wrappers. -

-
- -
- ) - } - - return ( -
- setCreateWrapperShown(true)} - disabled={!canCreateWrapper} - tooltip={{ - content: { - text: !canCreateWrapper - ? 'You need additional permissions to create a foreign data wrapper' - : undefined, - }, - }} - > - Add new wrapper - -
- ) -} - -export const WrapperContent = () => { - const { id } = useParams() - const { data: project } = useSelectedProjectQuery() - - const { data: integrations = [] } = useAvailableIntegrations() - const integration = integrations.find((i) => i.id === id) - const wrapperMeta = WRAPPERS.find((w) => w.name === id) - - const { data: extensions } = useDatabaseExtensionsQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - const installableExtensions = (extensions ?? []).filter((ext) => - (integration?.requiredExtensions ?? []).includes(ext.name) - ) - const isInstalled = installableExtensions.every((x) => x.installed_version) - - if (!wrapperMeta) { - return ( - - -

Unsupported integration type

-
-
- ) - } - - return ( - <> - - - {isInstalled && } - - ) -} - -export const WrapperOverviewTab = () => { - const isMarketplaceEnabled = useIsMarketplaceEnabled() - - if (isMarketplaceEnabled) return - - return ( - }> -
- -
-
- ) -} +export const WrapperOverviewTab = () => diff --git a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/OverviewTab.tsx index e05b84a39ea..31c586de8cf 100644 --- a/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/templates/StripeSyncEngine/OverviewTab.tsx @@ -23,7 +23,6 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import * as z from 'zod' -import { IntegrationOverviewTab } from '../../Integration/IntegrationOverviewTab' import { RequiredExtensionsSection } from '../../Integration/RequiredExtensionsSection' import { InstallationError } from './InstallationError' import { IntegrationInstalledActions, IntegrationNotInstalledActions } from './IntegrationActions' @@ -38,7 +37,6 @@ import { isUninstalling, } from './stripe-sync-status' import { StripeSyncChangesCard } from './StripeSyncChangesCard' -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { useStripeSyncStatus } from '@/components/interfaces/Integrations/templates/StripeSyncEngine/useStripeSyncStatus' import { useStripeSyncInstallMutation } from '@/data/database-integrations/stripe/stripe-sync-install-mutation' import { useStripeSyncUninstallMutation } from '@/data/database-integrations/stripe/stripe-sync-uninstall-mutation' @@ -391,23 +389,9 @@ const StripeSyncContent = ({ hideInstallCTA = false }: { hideInstallCTA?: boolea ) } -export const StripeSyncEngineOverviewTab = () => { - const isMarketplaceEnabled = useIsMarketplaceEnabled() - - if (isMarketplaceEnabled) { - return ( - <> - - - - ) - } - - return ( - -
- -
-
- ) -} +export const StripeSyncEngineOverviewTab = () => ( + <> + + + +) diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx deleted file mode 100644 index 51ca726cb0b..00000000000 --- a/apps/studio/components/layouts/ProjectIntegrationsLayout.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { IS_PLATFORM, useFeatureFlags, useParams } from 'common' -import { useRouter } from 'next/router' -import { PropsWithChildren } from 'react' -import { Menu, Separator } from 'ui' -import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' - -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations' -import { ProjectLayout } from '@/components/layouts/ProjectLayout' -import { AlertError } from '@/components/ui/AlertError' -import { ProductMenu } from '@/components/ui/ProductMenu' -import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query' -import { useMarketplaceIntegrationsQuery } from '@/data/marketplace/integrations-query' -import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' -import { withAuth } from '@/hooks/misc/withAuth' - -/** - * Layout component for the Integrations section - * Provides sidebar navigation for integrations - */ -export const ProjectIntegrationsLayout = withAuth(({ children }: PropsWithChildren) => { - const router = useRouter() - const segments = router.asPath.split('/') - // construct the page url to be used to determine the active state for the sidebar - const page = `${segments[3]}${segments[4] ? `/${segments[4]}` : ''}` - - return ( - - - - - - } - > - {children} - - ) -}) - -const IntegrationCategoriesMenu = ({ page }: { page: string }) => { - const router = useRouter() - const { ref } = useParams() - const { hasLoaded: flagsLoaded } = useFeatureFlags() - const isMarketplaceEnabled = useIsMarketplaceEnabled() - - const urlParams = new URLSearchParams(router.asPath.split('?')[1] || '') - const categoryParam = urlParams.get('category') - const pageKey = categoryParam || page - - const { integrationsWrappers: showWrappers } = useIsFeatureEnabled(['integrations:wrappers']) - const { data: categories = [], isPending: isPendingCategories } = useMarketplaceCategoriesQuery({ - enabled: isMarketplaceEnabled, - }) - const { data: listings = [], isPending: isPendingListings } = useMarketplaceIntegrationsQuery({ - enabled: isMarketplaceEnabled, - }) - - const populatedCategoryIds = new Set( - listings.flatMap((listing) => listing.categories.map((c) => c.id)) - ) - const nonEmptyCategories = categories.filter( - (category) => category.id && populatedCategoryIds.has(category.id) - ) - - const isLoading = IS_PLATFORM - ? !flagsLoaded || (isMarketplaceEnabled && (isPendingCategories || isPendingListings)) - : false - - const allCategories = [ - { - name: 'All', - key: 'integrations', - url: `/project/${ref}/integrations`, - pages: ['integrations'], - items: [], - }, - ...(showWrappers - ? [ - { - name: 'Wrappers', - key: 'wrapper', - url: `/project/${ref}/integrations?category=wrapper`, - items: [], - }, - ] - : []), - { - name: 'Postgres Modules', - key: 'postgres_extension', - url: `/project/${ref}/integrations?category=postgres_extension`, - items: [], - }, - ...nonEmptyCategories.map((category) => ({ - name: category.name ?? '', - key: category.slug ?? '', - url: `/project/${ref}/integrations?category=${category.slug}`, - items: [], - })), - ] - - return ( - <> - {isLoading ? ( -
- - Explore} /> - - -
- ) : ( - - )} - - ) -} - -const InstalledIntegrationsMenu = ({ page }: { page: string }) => { - const { ref } = useParams() - - const { - installedIntegrations: integrations, - error, - isLoading, - isSuccess, - isError, - } = useInstalledIntegrations() - - const installedIntegrationItems = integrations.map((integration) => ({ - name: integration.name, - label: integration.status, - key: `integrations/${integration.id}`, - url: `/project/${ref}/integrations/${integration.id}/overview`, - icon: ( -
- {integration.icon({ className: 'p-1' })} -
- ), - items: [], - })) - - return ( - <> - {(isLoading || isError) && ( -
- - Installed} /> - - {isLoading && } - {isError && ( - - )} -
- )} - {isSuccess && ( - - )} - - ) -} diff --git a/apps/studio/components/layouts/ProjectIntegrationsLayoutDispatch.tsx b/apps/studio/components/layouts/ProjectIntegrationsLayoutDispatch.tsx deleted file mode 100644 index 31cd36043a5..00000000000 --- a/apps/studio/components/layouts/ProjectIntegrationsLayoutDispatch.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { PropsWithChildren } from 'react' - -import { ProjectIntegrationsLayout } from './ProjectIntegrationsLayout' -import { ProjectMarketplaceLayout } from './ProjectMarketplaceLayout' -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' - -export const ProjectIntegrationsLayoutDispatch = ({ children }: PropsWithChildren) => { - const isMarketplaceEnabled = useIsMarketplaceEnabled() - if (isMarketplaceEnabled) { - return {children} - } - return {children} -} diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx index 2e88ba89ded..bed0f884008 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/[childId]/index.tsx @@ -16,7 +16,7 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { useAvailableIntegrations } from '@/components/interfaces/Integrations/Landing/useAvailableIntegrations' import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations' import { DefaultLayout } from '@/components/layouts/DefaultLayout' -import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectIntegrationsLayoutDispatch' +import { ProjectMarketplaceLayout } from '@/components/layouts/ProjectMarketplaceLayout' import type { NextPageWithLayout } from '@/types' const IntegrationPage: NextPageWithLayout = () => { @@ -98,7 +98,7 @@ const IntegrationPage: NextPageWithLayout = () => { IntegrationPage.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx index 6f2e09f5412..b0bab49bb82 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx @@ -1,19 +1,13 @@ -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import LegacyIntegrationPage from '@/components/interfaces/Integrations/Integration/LegacyIntegrationPage' import { MarketplaceDetail } from '@/components/interfaces/Integrations/Marketplace/MarketplaceDetail' import { DefaultLayout } from '@/components/layouts/DefaultLayout' -import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectIntegrationsLayoutDispatch' +import { ProjectMarketplaceLayout } from '@/components/layouts/ProjectMarketplaceLayout' import type { NextPageWithLayout } from '@/types' -const IntegrationPage: NextPageWithLayout = () => { - const isMarketplaceEnabled = useIsMarketplaceEnabled() - if (isMarketplaceEnabled) return - return -} +const IntegrationPage: NextPageWithLayout = () => IntegrationPage.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx b/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx index 7f1980fa16c..5de8b67e6f3 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/index.tsx @@ -6,7 +6,7 @@ import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { DefaultLayout } from '@/components/layouts/DefaultLayout' -import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectIntegrationsLayoutDispatch' +import { ProjectMarketplaceLayout } from '@/components/layouts/ProjectMarketplaceLayout' import type { NextPageWithLayout } from '@/types' const INTEGRATION_FLAGS: Record = { @@ -57,7 +57,7 @@ const IntegrationPage: NextPageWithLayout = () => { IntegrationPage.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/pages/project/[ref]/integrations/index.tsx b/apps/studio/pages/project/[ref]/integrations/index.tsx index b8c4ddbb007..a55ae603cba 100644 --- a/apps/studio/pages/project/[ref]/integrations/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/index.tsx @@ -1,325 +1,13 @@ -import { IS_PLATFORM, useFeatureFlags } from 'common' -import { Database } from 'common/marketplace.types' -import { Search } from 'lucide-react' -import { useRouter } from 'next/router' -import { parseAsString, useQueryState } from 'nuqs' -import { useMemo, type ReactNode } from 'react' -import { Input } from 'ui-patterns/DataInputs/Input' -import { PageContainer } from 'ui-patterns/PageContainer' -import { - PageHeader, - PageHeaderAside, - PageHeaderDescription, - PageHeaderMeta, - PageHeaderSummary, - PageHeaderTitle, -} from 'ui-patterns/PageHeader' -import { PageSection, PageSectionContent, PageSectionMeta } from 'ui-patterns/PageSection' -import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' - -import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' -import { - IntegrationCard, - IntegrationLoadingCard, -} from '@/components/interfaces/Integrations/Landing/IntegrationCard' -import { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants' -import { useAvailableIntegrations } from '@/components/interfaces/Integrations/Landing/useAvailableIntegrations' -import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations' -import { useIntegrationFilteringAndSort } from '@/components/interfaces/Integrations/Landing/useIntegrationFilteringAndSort' import { MarketplaceIndex } from '@/components/interfaces/Integrations/Marketplace/MarketplaceIndex' import { DefaultLayout } from '@/components/layouts/DefaultLayout' -import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectIntegrationsLayoutDispatch' -import { AlertError } from '@/components/ui/AlertError' -import { DocsButton } from '@/components/ui/DocsButton' -import { NoSearchResults } from '@/components/ui/NoSearchResults' -import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query' -import { BASE_PATH, DOCS_URL } from '@/lib/constants' +import { ProjectMarketplaceLayout } from '@/components/layouts/ProjectMarketplaceLayout' import type { NextPageWithLayout } from '@/types' -const FEATURED_INTEGRATIONS = ['cron', 'queues', 'stripe_sync_engine'] - -// Featured integration images -const FEATURED_INTEGRATION_IMAGES: Record = { - cron: `${BASE_PATH}/img/integrations/covers/cron-cover.webp`, - queues: `${BASE_PATH}/img/integrations/covers/queues-cover.png`, - stripe_wrapper: `${BASE_PATH}/img/integrations/covers/stripe-cover.png`, - stripe_sync_engine: `${BASE_PATH}/img/integrations/covers/stripe-cover.png`, -} - -function getIntegrationImage(integration: IntegrationDefinition) { - let featured_image = FEATURED_INTEGRATION_IMAGES[integration.id] - if (featured_image) { - return featured_image - } - - if (integration.files?.length) { - const heroImage = integration?.files?.[0] - return heroImage?.src ?? undefined - } -} - -type PageContent = { - title: string - subtitle: string - secondaryActions?: ReactNode -} - -const DEFAULT_PAGE_CONTENT: PageContent = { - title: 'Extend your database', - subtitle: - 'Extensions and wrappers that add functionality to your database and connect to external services.', -} - -const CATEGORY_PAGE_CONTENT = { - wrapper: { - title: 'Wrappers', - subtitle: - 'Connect to external data sources and services by querying APIs, databases, and files as if they were Postgres tables.', - secondaryActions: ( - - ), - }, - postgres_extension: { - title: 'Postgres Modules', - subtitle: 'Extend your database with powerful Postgres extensions.', - }, -} satisfies Record - -// Converts a category string to title -// Example: some_catory -> Some Category -function formatCategoryTitle(category: string) { - return category - .split(/[-_]/) - .filter(Boolean) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') -} - -const PageHeaderContentSkeleton = () => ( -
- - -
-) - -// Returns selected category to filter by -function useFilterCategory() { - const router = useRouter() - const [selectedCategory] = useQueryState( - 'category', - parseAsString.withDefault('all').withOptions({ clearOnDefault: true }) - ) - const categoryFromUrl = useMemo(() => { - const queryString = router.asPath.split('?')[1] - if (!queryString) return null - - return new URLSearchParams(queryString).get('category') - }, [router.asPath]) - const resolvedSelectedCategory = router.isReady - ? (categoryFromUrl ?? selectedCategory) - : undefined - const filterCategory = resolvedSelectedCategory ?? 'all' - return filterCategory -} - -// Dynamic page content based on selected category -function usePageContent( - integrationFilterCategory: string, - categories: Database['public']['Views']['categories']['Row'][] -) { - const pageContent = useMemo(() => { - if (integrationFilterCategory === 'all') { - return DEFAULT_PAGE_CONTENT - } - - if (integrationFilterCategory in CATEGORY_PAGE_CONTENT) { - return CATEGORY_PAGE_CONTENT[integrationFilterCategory as keyof typeof CATEGORY_PAGE_CONTENT] - } - - const selectedMarketplaceCategory = categories.find( - (category) => category.slug === integrationFilterCategory - ) - - return { - title: selectedMarketplaceCategory?.name ?? formatCategoryTitle(integrationFilterCategory), - subtitle: selectedMarketplaceCategory?.description ?? DEFAULT_PAGE_CONTENT.subtitle, - } - }, [categories, integrationFilterCategory]) - return pageContent -} - -const IntegrationsPage: NextPageWithLayout = () => { - const isMarketplaceEnabled = useIsMarketplaceEnabled() - if (isMarketplaceEnabled) return - return -} - -const LegacyIntegrationsPage = () => { - const { hasLoaded: flagsLoaded } = useFeatureFlags() - const isMarketplaceEnabled = useIsMarketplaceEnabled() - const [search, setSearch] = useQueryState( - 'search', - parseAsString.withDefault('').withOptions({ clearOnDefault: true }) - ) - - const { - data: availableIntegrations, - error, - isPending: isLoadingAvailableIntegrations, - isError, - isSuccess: isSuccessAvailableIntegrations, - } = useAvailableIntegrations() - - const { - installedIntegrations, - isLoading: isLoadingInstalledIntegrations, - isSuccess: isSuccessInstalledIntegrations, - } = useInstalledIntegrations() - - const installedIds = installedIntegrations.map((i) => i.id) - const isLoading = isLoadingAvailableIntegrations || isLoadingInstalledIntegrations - const isSuccess = isSuccessAvailableIntegrations && isSuccessInstalledIntegrations - - const selectedCategory = useFilterCategory() - - const { data: categories = [], isPending: isPendingCategories } = useMarketplaceCategoriesQuery({ - enabled: isMarketplaceEnabled, - }) - - const isLoadingSelectedCategory = - selectedCategory !== 'all' && - !(selectedCategory in CATEGORY_PAGE_CONTENT) && - (IS_PLATFORM ? !flagsLoaded || (isMarketplaceEnabled && isPendingCategories) : false) - - const pageContent = usePageContent(selectedCategory, categories) - - const filteredIntegrations = useMemo(() => { - let filtered = availableIntegrations ?? [] - - if (selectedCategory !== 'all') { - filtered = filtered.filter( - (i) => i.type === selectedCategory || i.categories?.includes(selectedCategory) - ) - } - - if (search.length > 0) { - filtered = filtered.filter((i) => i.name.toLowerCase().includes(search.toLowerCase())) - } - - return filtered - }, [availableIntegrations, selectedCategory, search]) - - const hasActiveFilter = selectedCategory !== 'all' || search.length > 0 - - const { sorted: filteredAndSortedIntegrations, featured: featuredIntegrations } = - useIntegrationFilteringAndSort(filteredIntegrations, availableIntegrations, installedIds, { - featuredIds: FEATURED_INTEGRATIONS, - hasActiveFilter, - includeFeaturedFlag: true, - }) - - return ( - <> - - - - {isLoadingSelectedCategory ? ( - - ) : ( - <> - {pageContent.title} - {pageContent.subtitle} - - )} - - {pageContent.secondaryActions && ( - {pageContent.secondaryActions} - )} - - - - - - - setSearch(e.target.value)} - placeholder="Search integrations..." - icon={} - className="w-52" - /> - - - - {isLoading && ( -
- {Array.from({ length: 8 }).map((_, idx) => ( - - ))} -
- )} - - {/* Error State */} - {isError && ( - - )} - - {/* Success State */} - {isSuccess && ( - <> - {/* No Search Results */} - {search.length > 0 && filteredAndSortedIntegrations.length === 0 && ( - setSearch('')} /> - )} - - {/* Featured Integrations */} - {featuredIntegrations && featuredIntegrations.length > 0 && ( -
- {featuredIntegrations.map((integration) => ( - - ))} -
- )} - - {/* All Filtered and Sorted Integrations */} - {filteredAndSortedIntegrations.length > 0 && ( -
- {filteredAndSortedIntegrations.map((integration) => ( - - ))} -
- )} - - )} -
-
-
- - ) -} +const IntegrationsPage: NextPageWithLayout = () => IntegrationsPage.getLayout = (page) => ( - {page} + {page} ) diff --git a/apps/studio/public/img/previews/integrations-layout-preview.png b/apps/studio/public/img/previews/integrations-layout-preview.png deleted file mode 100644 index d026663a373..00000000000 Binary files a/apps/studio/public/img/previews/integrations-layout-preview.png and /dev/null differ diff --git a/apps/studio/routes/project/$ref/integrations.tsx b/apps/studio/routes/project/$ref/integrations.tsx index 25dd7ae2c2f..4251afd7bc0 100644 --- a/apps/studio/routes/project/$ref/integrations.tsx +++ b/apps/studio/routes/project/$ref/integrations.tsx @@ -1,6 +1,6 @@ import { createFileRoute, Outlet } from '@tanstack/react-router' -import { ProjectIntegrationsLayoutDispatch } from '@/components/layouts/ProjectIntegrationsLayoutDispatch' +import { ProjectMarketplaceLayout } from '@/components/layouts/ProjectMarketplaceLayout' export const Route = createFileRoute('/project/$ref/integrations')({ component: IntegrationsShell, @@ -8,8 +8,8 @@ export const Route = createFileRoute('/project/$ref/integrations')({ function IntegrationsShell() { return ( - + - + ) } diff --git a/apps/studio/tests/components/Integrations/Landing/useIntegrationDetail.utils.test.ts b/apps/studio/tests/components/Integrations/Landing/useIntegrationDetail.utils.test.ts index ef3e01da8ef..7cc654bbc59 100644 --- a/apps/studio/tests/components/Integrations/Landing/useIntegrationDetail.utils.test.ts +++ b/apps/studio/tests/components/Integrations/Landing/useIntegrationDetail.utils.test.ts @@ -1,11 +1,7 @@ import { describe, expect, it } from 'vitest' import type { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants' -import { - areRequiredExtensionsInstalledFor, - getFilteredNavItems, - getInstallActionType, -} from '@/components/interfaces/Integrations/Landing/useIntegrationDetail.utils' +import { getInstallActionType } from '@/components/interfaces/Integrations/Landing/useIntegrationDetail.utils' // Minimal stubs — only fields exercised by the utils const oauthIntegration = { @@ -32,235 +28,44 @@ const extensionIntegration = { navigation: [{ route: 'overview', label: 'Overview' }], } as unknown as IntegrationDefinition -// ─── areRequiredExtensionsInstalledFor ─────────────────────────────────────── - -describe('areRequiredExtensionsInstalledFor', () => { - it('returns false when integration is undefined', () => { - expect(areRequiredExtensionsInstalledFor(undefined, [])).toBe(false) - }) - - it('returns false when extensions list is undefined', () => { - expect(areRequiredExtensionsInstalledFor(extensionIntegration, undefined)).toBe(false) - }) - - it('returns false when integration has no required extensions', () => { - const noExts = { - ...extensionIntegration, - requiredExtensions: [], - } as unknown as IntegrationDefinition - expect( - areRequiredExtensionsInstalledFor(noExts, [{ name: 'pgmq', installed_version: '1.0' }]) - ).toBe(false) - }) - - it('returns false when a required extension is not installed', () => { - expect( - areRequiredExtensionsInstalledFor(extensionIntegration, [ - { name: 'pgmq', installed_version: null }, - ]) - ).toBe(false) - }) - - it('returns false when a required extension is missing from list entirely', () => { - expect(areRequiredExtensionsInstalledFor(extensionIntegration, [])).toBe(false) - }) - - it('returns true when all required extensions are installed', () => { - expect( - areRequiredExtensionsInstalledFor(extensionIntegration, [ - { name: 'pgmq', installed_version: '1.4.4' }, - ]) - ).toBe(true) - }) - - it('returns true only when every required extension is installed (multiple)', () => { - expect( - areRequiredExtensionsInstalledFor(wrapperIntegration, [ - { name: 'wrappers', installed_version: '0.4.1' }, - { name: 'supabase_vault', installed_version: '0.2.8' }, - ]) - ).toBe(true) - }) - - it('returns false when only some of multiple required extensions are installed', () => { - expect( - areRequiredExtensionsInstalledFor(wrapperIntegration, [ - { name: 'wrappers', installed_version: '0.4.1' }, - { name: 'supabase_vault', installed_version: null }, - ]) - ).toBe(false) - }) -}) - -// ─── getFilteredNavItems ───────────────────────────────────────────────────── - -describe('getFilteredNavItems', () => { - it('returns empty array when integration is undefined', () => { - expect( - getFilteredNavItems({ - integration: undefined, - isInstalled: false, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - }) - ).toEqual([]) - }) - - it('returns empty array when integration has no navigation', () => { - const noNav = { - ...wrapperIntegration, - navigation: undefined, - } as unknown as IntegrationDefinition - expect( - getFilteredNavItems({ - integration: noNav, - isInstalled: false, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - }) - ).toEqual([]) - }) - - it('returns full navigation for non-wrapper integrations regardless of flags', () => { - const result = getFilteredNavItems({ - integration: extensionIntegration, - isInstalled: false, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - }) - expect(result).toEqual(extensionIntegration.navigation) - }) - - it('returns full navigation for installed wrapper integrations', () => { - const result = getFilteredNavItems({ - integration: wrapperIntegration, - isInstalled: true, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - }) - expect(result).toEqual(wrapperIntegration.navigation) - }) - - it('hides wrappers tab for uninstalled wrapper when marketplace is off and extensions missing', () => { - const result = getFilteredNavItems({ - integration: wrapperIntegration, - isInstalled: false, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - }) - expect(result.some((nav) => nav.route === 'wrappers')).toBe(false) - expect(result.some((nav) => nav.route === 'overview')).toBe(true) - }) - - it('shows wrappers tab when marketplace flag is enabled', () => { - const result = getFilteredNavItems({ - integration: wrapperIntegration, - isInstalled: false, - isMarketplaceEnabled: true, - areRequiredExtensionsInstalled: false, - }) - expect(result).toEqual(wrapperIntegration.navigation) - }) - - it('shows wrappers tab when required extensions are installed', () => { - const result = getFilteredNavItems({ - integration: wrapperIntegration, - isInstalled: false, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: true, - }) - expect(result).toEqual(wrapperIntegration.navigation) - }) -}) - -// ─── getInstallActionType ──────────────────────────────────────────────────── - describe('getInstallActionType', () => { it('returns null when integration is undefined', () => { - expect( - getInstallActionType({ - integration: undefined, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - isInstalled: false, - }) - ).toBeNull() + expect(getInstallActionType({ integration: undefined, isInstalled: false })).toBeNull() }) it('returns "oauth" for OAuth integrations', () => { - expect( - getInstallActionType({ - integration: oauthIntegration, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - isInstalled: false, - }) - ).toBe('oauth') + expect(getInstallActionType({ integration: oauthIntegration, isInstalled: false })).toBe( + 'oauth' + ) }) it('returns "oauth" for installed OAuth integrations (type check takes priority)', () => { - expect( - getInstallActionType({ - integration: oauthIntegration, - isMarketplaceEnabled: true, - areRequiredExtensionsInstalled: true, - isInstalled: true, - }) - ).toBe('oauth') + expect(getInstallActionType({ integration: oauthIntegration, isInstalled: true })).toBe( + 'oauth' + ) }) - it('returns "add-wrapper" for wrapper when marketplace is enabled', () => { - expect( - getInstallActionType({ - integration: wrapperIntegration, - isMarketplaceEnabled: true, - areRequiredExtensionsInstalled: false, - isInstalled: false, - }) - ).toBe('add-wrapper') + it('returns "add-wrapper" for wrapper integrations', () => { + expect(getInstallActionType({ integration: wrapperIntegration, isInstalled: false })).toBe( + 'add-wrapper' + ) }) - it('returns "add-wrapper" for wrapper when required extensions are installed', () => { - expect( - getInstallActionType({ - integration: wrapperIntegration, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: true, - isInstalled: false, - }) - ).toBe('add-wrapper') + it('returns "add-wrapper" for installed wrapper integrations (type check takes priority)', () => { + expect(getInstallActionType({ integration: wrapperIntegration, isInstalled: true })).toBe( + 'add-wrapper' + ) }) - it('returns "installed" for a non-wrapper installed integration when marketplace is off', () => { - expect( - getInstallActionType({ - integration: extensionIntegration, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - isInstalled: true, - }) - ).toBe('installed') + it('returns "installed" for a non-wrapper installed integration', () => { + expect(getInstallActionType({ integration: extensionIntegration, isInstalled: true })).toBe( + 'installed' + ) }) it('returns "install-sheet" for an uninstalled non-wrapper integration', () => { - expect( - getInstallActionType({ - integration: extensionIntegration, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - isInstalled: false, - }) - ).toBe('install-sheet') - }) - - it('returns "install-sheet" for wrapper when marketplace off and extensions missing and not installed', () => { - expect( - getInstallActionType({ - integration: wrapperIntegration, - isMarketplaceEnabled: false, - areRequiredExtensionsInstalled: false, - isInstalled: false, - }) - ).toBe('install-sheet') + expect(getInstallActionType({ integration: extensionIntegration, isInstalled: false })).toBe( + 'install-sheet' + ) }) }) diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 6b74bf66803..9a96b713c27 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -26,7 +26,6 @@ export const LOCAL_STORAGE_KEYS = { UI_PREVIEW_PLATFORM_WEBHOOKS: 'supabase-ui-platform-webhooks', UI_PREVIEW_JIT_DB_ACCESS: 'supabase-ui-jit-db-access', UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE: 'supabase-ui-sql-editor-manual-save', - UI_PREVIEW_MARKETPLACE: 'supabase-ui-marketplace', AI_ASSISTANT_MCP_OPT_IN: 'ai-assistant-mcp-opt-in', SIGN_IN_CHATGPT_ENABLED: 'siwc-enabled', @@ -155,7 +154,6 @@ const LOCAL_STORAGE_KEYS_ALLOWLIST = [ LOCAL_STORAGE_KEYS.UI_PREVIEW_PLATFORM_WEBHOOKS, LOCAL_STORAGE_KEYS.UI_PREVIEW_JIT_DB_ACCESS, LOCAL_STORAGE_KEYS.UI_PREVIEW_SQL_EDITOR_MANUAL_SAVE, - LOCAL_STORAGE_KEYS.UI_PREVIEW_MARKETPLACE, LOCAL_STORAGE_KEYS.LAST_SIGN_IN_METHOD, LOCAL_STORAGE_KEYS.HIDE_PROMO_TOAST, LOCAL_STORAGE_KEYS.BLOG_VIEW,