diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet.tsx new file mode 100644 index 00000000000..390f2e0745f --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet.tsx @@ -0,0 +1,359 @@ +import { useState } from 'react' +import { toast } from 'sonner' +import { + Accordion_Shadcn_, + AccordionContent_Shadcn_, + AccordionItem_Shadcn_, + AccordionTrigger_Shadcn_, + Badge, + Button, + Card, + CardContent, + cn, + CodeBlock, + DialogSectionSeparator, + Select_Shadcn_, + SelectContent_Shadcn_, + SelectItem_Shadcn_, + SelectSeparator_Shadcn_, + SelectTrigger_Shadcn_, + SelectValue_Shadcn_, + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetSection, + SheetTitle, + SheetTrigger, + SonnerProgress, + Tabs_Shadcn_, + TabsContent_Shadcn_, + TabsList_Shadcn_, + TabsTrigger_Shadcn_, +} from 'ui' +import { Input } from 'ui-patterns/DataInputs/Input' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +import { IntegrationDefinition } from '../../Landing/Integrations.constants' +import { getEnableExtensionsSQL } from './IntegrationOverviewTabV2.utils' +import { extensionsWithRecommendedSchemas } from '@/components/interfaces/Database/Extensions/Extensions.constants' +import { useDatabaseExtensionEnableMutation } from '@/data/database-extensions/database-extension-enable-mutation' +import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' +import { useSchemasQuery } from '@/data/database/schemas-query' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { useProtectedSchemas } from '@/hooks/useProtectedSchemas' +import { ResponseError } from '@/types' + +interface InstallIntegrationSheetProps { + integration: IntegrationDefinition +} + +export const InstallIntegrationSheet = ({ integration }: InstallIntegrationSheetProps) => { + const { requiredExtensions: requiredExtensionNames } = integration + const { data: project } = useSelectedProjectQuery() + const { data: protectedSchemas } = useProtectedSchemas({ excludeSchemas: ['extensions'] }) + + const [open, setOpen] = useState(false) + const [isInstalling, setIsInstalling] = useState(false) + + const defaultExtensionsSchema = Object.fromEntries( + requiredExtensionNames.map((extName) => [extName, { schema: 'extensions', value: undefined }]) + ) + const [extensionsSchema, setExtensionsSchema] = useState<{ + [key: string]: { schema: string; value: string | undefined } + }>(defaultExtensionsSchema) + + const involvesExtensions = requiredExtensionNames.length > 0 + // [Joshen] Will hook these up in the future, applicable for stripe sync engine + const involvesEdgeFunctions = false + + const { data: extensions = [] } = useDatabaseExtensionsQuery( + { projectRef: project?.ref, connectionString: project?.connectionString }, + { enabled: involvesExtensions } + ) + const requiredExtensions = extensions.filter((ext) => requiredExtensionNames.includes(ext.name)) + // [Joshen] Integration requires extensions that are not available to install on the current database image + const hasMissingExtensions = requiredExtensionNames.length !== requiredExtensions.length + + const { data: schemas = [] } = useSchemasQuery( + { projectRef: project?.ref, connectionString: project?.connectionString }, + { enabled: involvesExtensions } + ) + const availableSchemas = schemas.filter( + (schema) => !protectedSchemas.some((protectedSchema) => protectedSchema.name === schema.name) + ) + + const { mutateAsync: enableExtension } = useDatabaseExtensionEnableMutation({ onError: () => {} }) + + const enableExtensionsSQL = getEnableExtensionsSQL({ + extensions: requiredExtensions, + extensionsSchema, + }) + + /** + * [Joshen] This will be pretty simple for now, but will expand as we bring over more integrations to the new UI to see what else is needed + */ + const onInstallIntegration = async () => { + if (!project) return console.error('Project is required') + + setIsInstalling(true) + const toastId = toast.loading( + + ) + + try { + if (requiredExtensions.length > 0) { + toast.loading( + 1 ? 's' : ''}`} + />, + { id: toastId } + ) + + const results = await Promise.allSettled( + requiredExtensions.map((ext) => { + const { name, default_version: version } = ext + const createSchema = extensionsSchema[name].schema === 'custom' + const schema = + name === 'pg_cron' + ? 'pg_catalog' + : createSchema + ? (extensionsSchema[name].value as string) + : extensionsSchema[name].schema + + return enableExtension({ + projectRef: project.ref, + connectionString: project.connectionString, + schema, + name, + version, + cascade: true, + createSchema, + }) + }) + ) + + const failure = results.find((r) => r.status === 'rejected') + if (!!failure) throw new Error(failure.reason.message) + } + + toast.success(`Successfully installed ${integration.name}`, { id: toastId }) + setOpen(false) + } catch (error) { + toast.error(`Failed to install ${integration.name}: ${(error as ResponseError).message}`, { + id: toastId, + }) + } finally { + setIsInstalling(false) + } + } + + return ( + + + + + + +
+ {integration.icon()} +
+
+ Install {integration.name} + Review and configure this integration +
+
+ +
+ +
+

Installs

+

+ What this integration will run on your project +

+
+ + {hasMissingExtensions && integration.missingExtensionsAlert} + + + + + + {involvesExtensions && ( + <> + + Extensions + + + SQL + + + )} + {involvesEdgeFunctions && ( + + Edge Functions + + )} + + + + {requiredExtensionNames.map((extName) => { + const ext = extensions.find((x) => x.name === extName) + return ( +
+ {extName} + {!ext ? ( + Unavailable + ) : ext.installed_version ? ( + Installed + ) : ( + Required + )} +
+ ) + })} +
+ + + + + TBD + +
+
+
+
+ + + + + + + + Advanced settings + + +

+ Select which schemas to install the database extensions under +

+ {requiredExtensionNames.map((extName) => { + const extMeta = extensionsSchema[extName as keyof typeof extensionsSchema] + const { schema, value } = extMeta + const recommendedSchema = extensionsWithRecommendedSchemas[extName] + + return ( + + + setExtensionsSchema((prev) => ({ + ...prev, + [extName]: { + schema, + value: schema === 'custom' ? extName : undefined, + }, + })) + } + > + + + + + + Create a new schema + + + {availableSchemas.map((schema) => { + return ( + + {schema.name} + {schema.name === recommendedSchema ? ( + + Recommended + + ) : schema.name === 'extensions' ? ( + Default + ) : null} + + ) + })} + + + + {schema === 'custom' && ( + + + setExtensionsSchema((prev) => ({ + ...prev, + [extName]: { + schema: prev[extName].schema, + value: e.target.value, + }, + })) + } + placeholder="Provide a name for your schema" + /> + + )} + + ) + })} +
+
+
+
+ + +
+ + + + + + + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/IntegrationOverviewTabV2.utils.ts b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/IntegrationOverviewTabV2.utils.ts new file mode 100644 index 00000000000..764dc085733 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/IntegrationOverviewTabV2.utils.ts @@ -0,0 +1,42 @@ +import { getEnableDatabaseExtensionSQL } from '@supabase/pg-meta' + +import { DatabaseExtension } from '@/data/database-extensions/database-extensions-query' + +export const getEnableExtensionsSQL = ({ + extensions, + extensionsSchema, +}: { + extensions: DatabaseExtension[] + extensionsSchema: { + [key: string]: { schema: string; value: string | undefined } + } +}) => { + return extensions + .map((extension) => { + /** + * [Joshen] Hard-coding pg_cron here as this is enforced on our end (Not via pg_available_extension_versions) + * Also temp hardcoding to `extensions` for now, but we should be retrieving the default schema from `pg_available_extension_versions` + * Am checking with pg-meta team whether we can just return that data directly from the /pg-meta/extensions endpoint, rather + * than using dashboard's `useDatabaseExtensionDefaultSchemaQuery` - we can technically save a query if so + */ + const { name, default_version: version } = extension + const createSchema = extensionsSchema[name].schema === 'custom' + const schema = + name === 'pg_cron' + ? 'pg_catalog' + : createSchema + ? (extensionsSchema[name].value as string) + : extensionsSchema[name].schema + + return getEnableDatabaseExtensionSQL({ + schema, + name, + version, + cascade: true, + createSchema, + }) + }) + .filter(Boolean) + .join('\n\n') + .trim() +} diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx new file mode 100644 index 00000000000..88387eb2931 --- /dev/null +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/MarkdownContent.tsx @@ -0,0 +1,26 @@ +import { Markdown } from 'components/interfaces/Markdown' +import { useEffect, useState } from 'react' + +interface MarkdownContentProps { + content: string | null | undefined + integrationId?: string +} + +export const MarkdownContent = ({ + content: remoteContent, + integrationId, +}: MarkdownContentProps) => { + const [localContent, setLocalContent] = useState('') + + const content = remoteContent || localContent + + useEffect(() => { + if (!!integrationId && !content) { + import(`static-data/integrations/${integrationId}/overview.md`) + .then((module) => setLocalContent(String(module.default))) + .catch((error) => console.error('Error loading markdown:', error)) + } + }, [integrationId, content]) + + return {content} +} diff --git a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index.tsx b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index.tsx index 5b4b88a430c..9fe3907590c 100644 --- a/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index.tsx +++ b/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/index.tsx @@ -1,9 +1,11 @@ import { useParams } from 'common' import { Markdown } from 'components/interfaces/Markdown' +import { PropsWithChildren } from 'react' import { cn } from 'ui' import { useAvailableIntegrations } from '../../Landing/useAvailableIntegrations' import { FilesViewer } from './FilesViewer' +import { MarkdownContent } from './MarkdownContent' import { InlineLinkClassName } from '@/components/ui/InlineLink' const getSiteUrlLabel = (url: string | undefined | null) => { @@ -29,7 +31,7 @@ const isGithubHost = (url: string | undefined | null) => { /** * [Joshen] This will serve as the overview tab for remotely fetched integrations */ -export const IntegrationOverviewTabV2 = () => { +export const IntegrationOverviewTabV2 = ({ children }: PropsWithChildren) => { const { id } = useParams() const { data: allIntegrations } = useAvailableIntegrations() @@ -52,7 +54,8 @@ export const IntegrationOverviewTabV2 = () => {
{files.length > 0 && } - {content} + + {children}
@@ -61,7 +64,7 @@ export const IntegrationOverviewTabV2 = () => {

Type

-

{type === 'oauth' ? 'OAuth' : type}

+

{type === 'oauth' ? 'OAuth' : type.replaceAll('_', ' ')}

@@ -90,7 +93,7 @@ export const IntegrationOverviewTabV2 = () => { )}
- {/* Subsequent sections here */} + {/* Subsequent other actions here */}
) diff --git a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx index 5cc6384f5bf..8b3f4568153 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/Integrations.constants.tsx @@ -29,7 +29,7 @@ export type IntegrationDefinition = { name: string status?: 'alpha' | 'beta' categories?: string[] - icon: (props?: { className?: string; style?: Record }) => ReactNode + icon: (props?: { className?: string; style?: Record }) => ReactNode description: string | null content?: string | null files?: string[] @@ -43,11 +43,11 @@ export type IntegrationDefinition = { /** Optional component to render if the integration requires extensions that are not available on the current database image */ missingExtensionsAlert?: ReactNode navigation?: Array - navigate: ( - id: string, - pageId: string | undefined, + navigate: (props: { + id: string | undefined + pageId: string | undefined childId: string | undefined - ) => ComponentType<{}> | null + }) => ComponentType<{}> | null } & ( | { type: 'wrapper'; meta: WrapperMeta } | { type: 'postgres_extension' | 'custom' | 'oauth' | 'template' } @@ -92,7 +92,7 @@ const SUPABASE_INTEGRATIONS: Array = [ label: 'Settings', }, ], - navigate: (id: string, pageId: string = 'overview', childId: string | undefined) => { + navigate: ({ pageId = 'overview', childId }) => { if (childId) { return dynamic(() => import('../Queues/QueuePage').then((mod) => mod.QueuePage), { loading: Loading, @@ -148,7 +148,7 @@ const SUPABASE_INTEGRATIONS: Array = [ ), }, ], - navigate: (id: string, pageId: string = 'overview', childId: string | undefined) => { + navigate: ({ pageId = 'overview', childId }) => { if (childId) { return dynamic(() => import('../CronJobs/CronJobPage').then((mod) => mod.CronJobPage), { loading: Loading, @@ -196,7 +196,7 @@ const SUPABASE_INTEGRATIONS: Array = [ label: 'Secrets', }, ], - navigate: (id: string, pageId: string = 'overview', childId: string | undefined) => { + navigate: ({ pageId = 'overview' }) => { switch (pageId) { case 'overview': return dynamic( @@ -241,7 +241,7 @@ const SUPABASE_INTEGRATIONS: Array = [ label: 'Webhooks', }, ], - navigate: (id: string, pageId: string = 'overview', childId: string | undefined) => { + navigate: ({ pageId = 'overview' }) => { switch (pageId) { case 'overview': return dynamic( @@ -292,7 +292,7 @@ const SUPABASE_INTEGRATIONS: Array = [ label: 'Docs', }, ], - navigate: (_id: string, pageId: string = 'overview', _childId: string | undefined) => { + navigate: ({ pageId = 'overview' }) => { switch (pageId) { case 'overview': return dynamic( @@ -355,7 +355,7 @@ const SUPABASE_INTEGRATIONS: Array = [ label: 'GraphiQL', }, ], - navigate: (id: string, pageId: string = 'overview', childId: string | undefined) => { + navigate: ({ pageId = 'overview' }) => { switch (pageId) { case 'overview': return dynamic( @@ -406,7 +406,7 @@ const WRAPPER_INTEGRATIONS: Array = WRAPPERS.map((w) => { label: 'Wrappers', }, ], - navigate: (id: string, pageId: string = 'overview', childId: string | undefined) => { + navigate: ({ pageId = 'overview' }) => { switch (pageId) { case 'overview': return dynamic( @@ -468,7 +468,7 @@ const TEMPLATE_INTEGRATIONS: Array = [ label: 'Settings', }, ], - navigate: (_id: string, pageId: string = 'overview', _childId: string | undefined) => { + navigate: ({ pageId = 'overview' }) => { switch (pageId) { case 'overview': return dynamic( diff --git a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx index dd6aaa902f5..076f4907f3b 100644 --- a/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx +++ b/apps/studio/components/interfaces/Integrations/Landing/useAvailableIntegrations.tsx @@ -71,7 +71,7 @@ export const useAvailableIntegrations = () => { label: 'Overview', }, ], - navigate: (id: string, pageId: string = 'overview', childId: string | undefined) => { + navigate: ({ pageId = 'overview' }) => { switch (pageId) { case 'overview': return dynamic( diff --git a/apps/studio/components/interfaces/Integrations/Queues/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Queues/OverviewTab.tsx index 4189e9287f7..1648b13247a 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/OverviewTab.tsx @@ -1,40 +1,54 @@ -import { useParams } from 'common' +import { useFlag, useParams } from 'common' import { useQueuesExposePostgrestStatusQuery } from 'data/database-queues/database-queues-expose-postgrest-status-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import Link from 'next/link' import { Button } from 'ui' import { Admonition } from 'ui-patterns' -import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' -export const QueuesOverviewTab = () => { +import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' +import { IntegrationOverviewTabV2 } from '../Integration/IntegrationOverviewTabV2' +import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' + +const QueuesAdmonition = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() + const { data: extensions = [] } = useDatabaseExtensionsQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + const isQueuesInstalled = !!extensions.find((x) => x.name === 'pgmq')?.installed_version + + return ( + +

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

+ + {isQueuesInstalled && ( + + )} +
+ ) +} + +export const QueuesOverviewTab = () => { + const { data: project } = useSelectedProjectQuery() + const isMarketplaceEnabled = useFlag('marketplaceIntegrations') + const { data: isExposed } = useQueuesExposePostgrestStatusQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) - return ( - -

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

- - - ) : null - } - /> - ) + if (isMarketplaceEnabled) { + return {!isExposed && } + } else { + return : null} /> + } } diff --git a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx index 65ae0420e1a..658db382174 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/QueuesSettings.tsx @@ -35,8 +35,6 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { z } from 'zod' -// [Joshen] Not convinced with the UI and layout but getting the functionality out first - export const QueuesSettings = () => { const { data: project } = useSelectedProjectQuery() const { can: canUpdatePostgrestConfig } = useAsyncCheckPermissions( diff --git a/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx b/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx index fcabbb76d85..ed2042424e6 100644 --- a/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx +++ b/apps/studio/components/interfaces/Integrations/Queues/UpgradeDatabaseAlert.tsx @@ -1,6 +1,5 @@ -import Link from 'next/link' - import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import Link from 'next/link' import { Button } from 'ui' import { Admonition } from 'ui-patterns/admonition' @@ -13,8 +12,7 @@ export const UpgradeDatabaseAlert = ({ minimumVersion = '15.6' }: UpgradeDatabas return ( diff --git a/apps/studio/data/database-extensions/database-extension-enable-mutation.ts b/apps/studio/data/database-extensions/database-extension-enable-mutation.ts index 256d916aaf3..172d597cede 100644 --- a/apps/studio/data/database-extensions/database-extension-enable-mutation.ts +++ b/apps/studio/data/database-extensions/database-extension-enable-mutation.ts @@ -1,11 +1,10 @@ -import pgMeta from '@supabase/pg-meta' -import { ident } from '@supabase/pg-meta/src/pg-format' +import { getEnableDatabaseExtensionSQL } from '@supabase/pg-meta' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { toast } from 'sonner' - import { configKeys } from 'data/config/keys' import { executeSql } from 'data/sql/execute-sql-query' +import { toast } from 'sonner' import type { ResponseError, UseCustomMutationOptions } from 'types' + import { databaseExtensionsKeys } from './keys' export type DatabaseExtensionEnableVariables = { @@ -30,11 +29,11 @@ export async function enableDatabaseExtension({ let headers = new Headers() if (connectionString) headers.set('x-connection-encrypted', connectionString) - const { sql } = pgMeta.extensions.create({ schema, name, version, cascade }) + const sql = getEnableDatabaseExtensionSQL({ schema, name, version, cascade, createSchema }) const { result } = await executeSql({ projectRef, connectionString, - sql: createSchema ? `create schema if not exists ${ident(schema)}; ${sql}` : sql, + sql, queryKey: ['extension', 'create'], }) 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 d2892ad1926..bf95284e40f 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 @@ -38,7 +38,7 @@ const IntegrationPage: NextPageWithLayout = () => { // Get the corresponding component dynamically const Component = useMemo( - () => integration?.navigate(id!, pageId, childId), + () => integration?.navigate({ id, pageId, childId }), [integration, id, pageId, childId] ) 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 8a9135c6541..56692fa6b0c 100644 --- a/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx +++ b/apps/studio/pages/project/[ref]/integrations/[id]/[pageId]/index.tsx @@ -1,4 +1,4 @@ -import { useParams } from 'common' +import { useFlag, useParams } from 'common' import { INTEGRATIONS } from 'components/interfaces/Integrations/Landing/Integrations.constants' import { useInstalledIntegrations } from 'components/interfaces/Integrations/Landing/useInstalledIntegrations' import { DefaultLayout } from 'components/layouts/DefaultLayout' @@ -35,6 +35,7 @@ import { } from 'ui-patterns' import ShimmeringLoader, { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' +import { InstallIntegrationSheet } from '@/components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet' import { useAvailableIntegrations } from '@/components/interfaces/Integrations/Landing/useAvailableIntegrations' import { ProjectIntegrationsLayout } from '@/components/layouts/ProjectIntegrationsLayout' @@ -44,6 +45,7 @@ const IntegrationPage: NextPageWithLayout = () => { const router = useRouter() const { ref, id, pageId, childId } = useParams() const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers']) + const isMarketplaceEnabled = useFlag('marketplaceIntegrations') const { data: allIntegrations, isPending: isAvailableIntegrationsLoading } = useAvailableIntegrations() @@ -62,7 +64,7 @@ const IntegrationPage: NextPageWithLayout = () => { // Get the corresponding component dynamically const Component = useMemo( - () => integration?.navigate(id!, pageId, childId), + () => integration?.navigate({ id, pageId, childId }), [integration, id, pageId, childId] ) @@ -191,13 +193,15 @@ const IntegrationPage: NextPageWithLayout = () => { {pageSubTitle} - {integration?.type === 'oauth' && ( + {integration?.type === 'oauth' ? ( - )} + ) : isMarketplaceEnabled && !!integration && !installation ? ( + + ) : null} )} diff --git a/packages/pg-meta/src/sql/studio/database/extensions.ts b/packages/pg-meta/src/sql/studio/database/extensions.ts index 44c50c90353..a6b1605c67c 100644 --- a/packages/pg-meta/src/sql/studio/database/extensions.ts +++ b/packages/pg-meta/src/sql/studio/database/extensions.ts @@ -1,4 +1,5 @@ -import { literal } from '../../../pg-format' +import { ident, literal } from '../../../pg-format' +import pgMetaExtensions from '../../../pg-meta-extensions' export const getDatabaseExtensionDefaultSchemaSQL = ({ extension }: { extension: string }) => { const sql = /* SQL */ ` @@ -7,3 +8,25 @@ select name, version, schema from pg_available_extension_versions where name = $ return sql } + +export const getEnableDatabaseExtensionSQL = ({ + schema, + name, + version, + cascade, + createSchema = false, +}: { + schema: string + name: string + version: string + cascade?: boolean + createSchema?: boolean +}) => { + const { sql } = pgMetaExtensions.create({ schema, name, version, cascade }) + return createSchema + ? ` +CREATE SCHEMA IF NOT EXISTS ${ident(schema)}; +${sql} +`.trim() + : sql.trim() +}