IntegrationOverviewTab.tsx123 lines · main
1import { useParams } from 'common'
2import { PropsWithChildren, ReactNode } from 'react'
3import { Badge, Card, CardContent, cn, Separator } from 'ui'
4
5import { INTEGRATIONS } from '../Landing/Integrations.constants'
6import { BuiltBySection } from './BuildBySection'
7import { MarkdownContent } from './MarkdownContent'
8import { MissingExtensionAlert } from './MissingExtensionAlert'
9import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
10import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
11
12export interface IntegrationOverviewTabProps {
13 actions?: ReactNode
14 status?: string | ReactNode
15 alert?: ReactNode
16 hideRequiredExtensionsSection?: boolean
17}
18
19/** [Joshen] This will eventually get replaced by IntegrationOverviewTabV2 */
20export const IntegrationOverviewTab = ({
21 actions,
22 alert,
23 status,
24 children,
25 hideRequiredExtensionsSection = false,
26}: PropsWithChildren<IntegrationOverviewTabProps>) => {
27 const { id } = useParams()
28 const { data: project } = useSelectedProjectQuery()
29
30 const integration = INTEGRATIONS.find((i) => i.id === id)
31
32 const { data: extensions } = useDatabaseExtensionsQuery({
33 projectRef: project?.ref,
34 connectionString: project?.connectionString,
35 })
36
37 if (!integration) {
38 return <div>Unsupported integration type</div>
39 }
40
41 const dependsOnExtension = (integration.requiredExtensions ?? []).length > 0
42
43 const installableExtensions = (extensions ?? []).filter((ext) =>
44 (integration.requiredExtensions ?? []).includes(ext.name)
45 )
46 const hasToInstallExtensions = installableExtensions.some((x) => !x.installed_version)
47
48 // The integration requires extensions that are not available to install on the current database image
49 const hasMissingExtensions =
50 installableExtensions.length !== integration.requiredExtensions.length
51
52 return (
53 <div className="flex flex-col gap-8 py-10">
54 <BuiltBySection integration={integration} status={status} />
55
56 {!!alert && <div className="px-10 max-w-4xl">{alert}</div>}
57
58 <MarkdownContent key={integration.id} integrationId={integration.id} />
59
60 <Separator />
61
62 {dependsOnExtension && !hideRequiredExtensionsSection && (
63 <div className="px-4 md:px-10 max-w-4xl flex flex-col gap-y-4">
64 <h4>Required extensions</h4>
65 <Card>
66 <CardContent className="p-0">
67 <ul className="text-foreground-light text-sm">
68 {(integration.requiredExtensions ?? []).map((requiredExtension, idx) => {
69 const extension = (extensions ?? []).find((ext) => ext.name === requiredExtension)
70 const isInstalled = !!extension?.installed_version
71 const isLastRow = idx === (integration.requiredExtensions?.length ?? 0) - 1
72
73 return (
74 <li
75 key={requiredExtension}
76 className={cn(
77 'flex items-center justify-between gap-3 py-2 px-3',
78 !isLastRow ? 'border-b' : ''
79 )}
80 >
81 <code className="text-xs">{requiredExtension}</code>
82
83 <div className="shrink-0">
84 {extension ? (
85 isInstalled ? (
86 <Badge>Installed</Badge>
87 ) : (
88 <MissingExtensionAlert extension={extension} />
89 )
90 ) : (
91 <span className="text-foreground-muted">Unavailable</span>
92 )}
93 </div>
94 </li>
95 )
96 })}
97 </ul>
98
99 {hasMissingExtensions && (
100 <div className="py-3 px-4 border-t">{integration.missingExtensionsAlert}</div>
101 )}
102 </CardContent>
103 </Card>
104 </div>
105 )}
106
107 {!!actions && (
108 <div
109 aria-disabled={hasToInstallExtensions && !hideRequiredExtensionsSection}
110 className={cn(
111 'px-10 max-w-4xl',
112 hasToInstallExtensions &&
113 !hideRequiredExtensionsSection &&
114 'opacity-25 [&_button]:pointer-events-none'
115 )}
116 >
117 {actions}
118 </div>
119 )}
120 {children}
121 </div>
122 )
123}