Joshen/fe 2854 migrate queues integration to new UI (#44270)

## Context

Related to marketplace related work, just moves the Queues integration
to the new UI (Changes are feature flagged)
<img width="1145" height="584" alt="image"
src="https://github.com/user-attachments/assets/d3245889-597d-44e2-9850-f20907e42056"
/>

Installation is now in a side panel with the intention that it'll just
be a single click to install integrations that involve multiple parts
<img width="400" height="955" alt="image"
src="https://github.com/user-attachments/assets/71903b61-6bd2-486c-903e-b48ae2133887"
/>


## To test
- Verify that you can install the integration and everything else should
be status quo
- Verify that everything should be status quo if the flag is off
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-03-30 14:23:21 +08:00
1 parent 12a43e5aba
commit 4fdeed448b
13 files changed
+527 -61

No files matched your search

@@ -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(
<SonnerProgress progress={0} message={`Installing ${integration.name}`} />
)
try {
if (requiredExtensions.length > 0) {
toast.loading(
<SonnerProgress
progress={0}
message={`Installing ${integration.name}`}
description={`Enabling ${requiredExtensions.length} database extension${requiredExtensions.length > 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 (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button type="primary">Install integration</Button>
</SheetTrigger>
<SheetContent
size="default"
aria-describedby={undefined}
className="flex flex-col gap-0 !w-[550px]"
>
<SheetHeader className="flex items-center gap-x-4">
<div className="shrink-0 w-11 h-11 relative bg-white border rounded-md flex items-center justify-center">
{integration.icon()}
</div>
<div className="flex flex-col">
<SheetTitle>Install {integration.name}</SheetTitle>
<SheetDescription>Review and configure this integration</SheetDescription>
</div>
</SheetHeader>
<div className="flex-grow">
<SheetSection className="flex flex-col gap-y-4 py-5">
<div>
<h4>Installs</h4>
<p className="text-sm text-foreground-light">
What this integration will run on your project
</p>
</div>
{hasMissingExtensions && integration.missingExtensionsAlert}
<Card>
<CardContent className="px-0 pt-1.5 pb-0">
<Tabs_Shadcn_ defaultValue="extensions">
<TabsList_Shadcn_ className="px-4 space-x-4">
{involvesExtensions && (
<>
<TabsTrigger_Shadcn_
value="extensions"
className="font-mono uppercase text-xs"
>
Extensions
</TabsTrigger_Shadcn_>
<TabsTrigger_Shadcn_ value="sql" className="font-mono uppercase text-xs">
SQL
</TabsTrigger_Shadcn_>
</>
)}
{involvesEdgeFunctions && (
<TabsTrigger_Shadcn_
value="edge_functions"
className="font-mono uppercase text-xs"
>
Edge Functions
</TabsTrigger_Shadcn_>
)}
</TabsList_Shadcn_>
<TabsContent_Shadcn_ value="extensions" className="mt-0 px-4">
{requiredExtensionNames.map((extName) => {
const ext = extensions.find((x) => x.name === extName)
return (
<div key={extName} className="py-3 flex items-center justify-between">
<code className="text-xs">{extName}</code>
{!ext ? (
<Badge>Unavailable</Badge>
) : ext.installed_version ? (
<Badge>Installed</Badge>
) : (
<Badge>Required</Badge>
)}
</div>
)
})}
</TabsContent_Shadcn_>
<TabsContent_Shadcn_ value="sql" className="mt-0">
<CodeBlock
hideCopy
hideLineNumbers
language="pgsql"
value={enableExtensionsSQL}
wrapperClassName={cn('[&_pre]:px-4 [&_pre]:py-3')}
className="border-0 rounded-none [&_code]:text-[12px] [&_code]:text-foreground"
/>
</TabsContent_Shadcn_>
<TabsContent_Shadcn_ value="edge_functions" className="mt-0">
TBD
</TabsContent_Shadcn_>
</Tabs_Shadcn_>
</CardContent>
</Card>
</SheetSection>
<DialogSectionSeparator />
<SheetSection>
<Accordion_Shadcn_ type="single" collapsible>
<AccordionItem_Shadcn_ value="advanced-settings" className="border-none">
<AccordionTrigger_Shadcn_ className="font-normal gap-2 py-0 justify-between text-sm hover:no-underline">
Advanced settings
</AccordionTrigger_Shadcn_>
<AccordionContent_Shadcn_ className="!pb-0 pt-3 [&>div]:flex [&>div]:flex-col [&>div]:gap-y-4">
<p className="text-foreground-light">
Select which schemas to install the database extensions under
</p>
{requiredExtensionNames.map((extName) => {
const extMeta = extensionsSchema[extName as keyof typeof extensionsSchema]
const { schema, value } = extMeta
const recommendedSchema = extensionsWithRecommendedSchemas[extName]
return (
<FormItemLayout
key={extName}
isReactForm={false}
layout="horizontal"
label={extName}
>
<Select_Shadcn_
value={schema}
onValueChange={(schema) =>
setExtensionsSchema((prev) => ({
...prev,
[extName]: {
schema,
value: schema === 'custom' ? extName : undefined,
},
}))
}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select a schema" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="custom">
Create a new schema
</SelectItem_Shadcn_>
<SelectSeparator_Shadcn_ />
{availableSchemas.map((schema) => {
return (
<SelectItem_Shadcn_ key={schema.id} value={schema.name}>
{schema.name}
{schema.name === recommendedSchema ? (
<Badge className="ml-2" variant="success">
Recommended
</Badge>
) : schema.name === 'extensions' ? (
<Badge className="ml-2">Default</Badge>
) : null}
</SelectItem_Shadcn_>
)
})}
</SelectContent_Shadcn_>
</Select_Shadcn_>
{schema === 'custom' && (
<FormItemLayout
isReactForm={false}
className="mt-2"
label="Provide a name for your new schema"
>
<Input
value={value}
onChange={(e) =>
setExtensionsSchema((prev) => ({
...prev,
[extName]: {
schema: prev[extName].schema,
value: e.target.value,
},
}))
}
placeholder="Provide a name for your schema"
/>
</FormItemLayout>
)}
</FormItemLayout>
)
})}
</AccordionContent_Shadcn_>
</AccordionItem_Shadcn_>
</Accordion_Shadcn_>
</SheetSection>
<DialogSectionSeparator />
</div>
<SheetFooter>
<SheetClose asChild>
<Button type="default" disabled={isInstalling}>
Cancel
</Button>
</SheetClose>
<Button
type="primary"
disabled={hasMissingExtensions}
loading={isInstalling}
onClick={onInstallIntegration}
>
Install integration
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
)
}
@@ -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()
}
@@ -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<string>('')
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 <Markdown className="flex flex-col gap-y-4 text-foreground-light">{content}</Markdown>
}
@@ -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 = () => {
<div className="grid grid-cols-3 gap-x-8 px-10 py-8">
<div className="col-span-2 flex flex-col gap-y-8">
{files.length > 0 && <FilesViewer files={files} />}
<Markdown className="flex flex-col gap-y-4 text-foreground-light">{content}</Markdown>
<MarkdownContent integrationId={id} content={content} />
{children}
</div>
<div className="text-sm col-span-1 flex flex-col gap-y-8">
@@ -61,7 +64,7 @@ export const IntegrationOverviewTabV2 = () => {
<div className="flex flex-col gap-y-1">
<p className="font-mono uppercase text-foreground-light">Type</p>
<p className="capitalize">{type === 'oauth' ? 'OAuth' : type}</p>
<p className="capitalize">{type === 'oauth' ? 'OAuth' : type.replaceAll('_', ' ')}</p>
</div>
<div className="flex flex-col gap-y-1">
@@ -90,7 +93,7 @@ export const IntegrationOverviewTabV2 = () => {
)}
</div>
{/* Subsequent sections here */}
{/* Subsequent other actions here */}
</div>
</div>
)
@@ -29,7 +29,7 @@ export type IntegrationDefinition = {
name: string
status?: 'alpha' | 'beta'
categories?: string[]
icon: (props?: { className?: string; style?: Record<string, any> }) => ReactNode
icon: (props?: { className?: string; style?: Record<string, string | number> }) => 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<Navigation>
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<IntegrationDefinition> = [
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<IntegrationDefinition> = [
),
},
],
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<IntegrationDefinition> = [
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<IntegrationDefinition> = [
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<IntegrationDefinition> = [
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<IntegrationDefinition> = [
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<IntegrationDefinition> = 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<IntegrationDefinition> = [
label: 'Settings',
},
],
navigate: (_id: string, pageId: string = 'overview', _childId: string | undefined) => {
navigate: ({ pageId = 'overview' }) => {
switch (pageId) {
case 'overview':
return dynamic(
@@ -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(
@@ -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 (
<Admonition
type="default"
title="Queues can be managed via any Supabase client library or PostgREST endpoints"
>
<p>
You may choose to toggle the exposure of Queues through Data APIs via the queues settings
</p>
{isQueuesInstalled && (
<Button asChild type="default" className="mt-2">
<Link href={`/project/${ref}/integrations/queues/settings`}>Manage queues settings</Link>
</Button>
)}
</Admonition>
)
}
export const QueuesOverviewTab = () => {
const { data: project } = useSelectedProjectQuery()
const isMarketplaceEnabled = useFlag('marketplaceIntegrations')
const { data: isExposed } = useQueuesExposePostgrestStatusQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
return (
<IntegrationOverviewTab
actions={
!isExposed ? (
<Admonition
type="default"
title="Queues can be managed via any Supabase client library or PostgREST endpoints"
>
<p>
You may choose to toggle the exposure of Queues through Data APIs via the queues
settings
</p>
<Button asChild type="default">
<Link href={`/project/${ref}/integrations/queues/settings`}>
Manage queues settings
</Link>
</Button>
</Admonition>
) : null
}
/>
)
if (isMarketplaceEnabled) {
return <IntegrationOverviewTabV2>{!isExposed && <QueuesAdmonition />}</IntegrationOverviewTabV2>
} else {
return <IntegrationOverviewTab actions={!isExposed ? <QueuesAdmonition /> : null} />
}
}
@@ -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(
@@ -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 (
<Admonition
type="warning"
className="mt-4"
type="default"
title="Database upgrade needed"
childProps={{ description: { className: 'flex flex-col gap-y-2' } }}
>
@@ -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'],
})
@@ -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]
)
@@ -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 = () => {
<PageHeaderDescription className="truncate">{pageSubTitle}</PageHeaderDescription>
</PageHeaderSummary>
{integration?.type === 'oauth' && (
{integration?.type === 'oauth' ? (
<Button asChild type="primary" className="shrink-0">
<a target="_blank" rel="noreferrer" href={integration.siteUrl ?? '/'}>
Install integration
</a>
</Button>
)}
) : isMarketplaceEnabled && !!integration && !installation ? (
<InstallIntegrationSheet integration={integration} />
) : null}
</PageHeaderMeta>
)}
@@ -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()
}