mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
12a43e5aba
commit
4fdeed448b
13 files changed
+527
-61
No files matched your search
+359
@@ -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>
|
||||
)
|
||||
}
|
||||
+42
@@ -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()
|
||||
}
|
||||
+26
@@ -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>
|
||||
}
|
||||
+7
-4
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user