OverviewTab.tsx206 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import Link from 'next/link'
4import { parseAsBoolean, useQueryState } from 'nuqs'
5import { useState } from 'react'
6import { Button, Sheet, SheetContent } from 'ui'
7import { Admonition } from 'ui-patterns/admonition'
8
9import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
10import { IntegrationOverviewTabV2 } from '../Integration/IntegrationOverviewTabV2'
11import { useAvailableIntegrations } from '../Landing/useAvailableIntegrations'
12import { CreateIcebergWrapperSheet } from './CreateIcebergWrapperSheet'
13import { CreateWrapperSheet } from './CreateWrapperSheet'
14import { WRAPPERS } from './Wrappers.constants'
15import { WrapperTable } from './WrapperTable'
16import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
17import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold'
18import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
19import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
20import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
21import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
22import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
23import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
24
25const WrapperOverviewContent = () => {
26 const { id } = useParams()
27 const wrapperMeta = WRAPPERS.find((w) => w.name === id)
28
29 const [isDirty, setIsDirty] = useState(false)
30 const [createWrapperShown, setCreateWrapperShown] = useQueryState(
31 'new',
32 parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
33 )
34
35 const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
36 checkIsDirty: () => isDirty,
37 onClose: () => {
38 setCreateWrapperShown(false)
39 setIsDirty(false)
40 },
41 })
42
43 // [Joshen] Opting to declare custom wrapper sheets here instead of within Wrappers.constants.ts
44 // as we'll easily run into circular dependencies doing so unfortunately
45 const CreateWrapperSheetComponent = !wrapperMeta
46 ? null
47 : wrapperMeta.customComponent
48 ? wrapperMeta.name === 'iceberg_wrapper'
49 ? CreateIcebergWrapperSheet
50 : null
51 : CreateWrapperSheet
52
53 return (
54 <>
55 <div className="flex flex-col gap-y-5">
56 <p>Recent wrappers</p>
57 <WrapperTable />
58 </div>
59
60 {!!CreateWrapperSheetComponent && !!wrapperMeta && (
61 <Sheet open={!!createWrapperShown} onOpenChange={handleOpenChange}>
62 <SheetContent size="lg" tabIndex={undefined}>
63 <CreateWrapperSheetComponent
64 wrapperMeta={wrapperMeta}
65 onDirty={setIsDirty}
66 onClose={() => setCreateWrapperShown(false)}
67 onCloseWithConfirmation={confirmOnClose}
68 />
69 </SheetContent>
70 </Sheet>
71 )}
72
73 <DiscardChangesConfirmationDialog {...modalProps} />
74 </>
75 )
76}
77
78const AddNewWrapperCTA = () => {
79 const { id } = useParams()
80 const { data: project } = useSelectedProjectQuery()
81 const [, setCreateWrapperShown] = useQueryState(
82 'new',
83 parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
84 )
85
86 const { can: canCreateWrapper } = useAsyncCheckPermissions(
87 PermissionAction.TENANT_SQL_ADMIN_WRITE,
88 'wrappers'
89 )
90
91 const { data } = useDatabaseExtensionsQuery({
92 projectRef: project?.ref,
93 connectionString: project?.connectionString,
94 })
95
96 const wrapperMeta = WRAPPERS.find((w) => w.name === id)
97 const wrappersExtension = data?.find((ext) => ext.name === 'wrappers')
98 const isWrappersExtensionInstalled = !!wrappersExtension?.installed_version
99 const hasRequiredVersion =
100 (wrappersExtension?.installed_version ?? '') >= (wrapperMeta?.minimumExtensionVersion ?? '')
101 // [Joshen] Default version is what's on the DB, so if the installed version is already the default version
102 // but still doesnt meet the minimum extension version, then DB upgrade is required
103 const databaseNeedsUpgrading =
104 wrappersExtension?.installed_version === wrappersExtension?.default_version
105
106 if (!!wrapperMeta && isWrappersExtensionInstalled && !hasRequiredVersion) {
107 return (
108 <Admonition type="warning" title="Your extension version is outdated for this wrapper">
109 <div className="flex flex-col gap-y-2 [&>p]:mb-0!">
110 <p>
111 The {wrapperMeta.label} wrapper requires a minimum extension version of{' '}
112 {wrapperMeta.minimumExtensionVersion}. You have version{' '}
113 {wrappersExtension?.installed_version} installed. Please{' '}
114 {databaseNeedsUpgrading && 'upgrade your database then '}update the extension by
115 disabling and enabling the <code className="text-code-inline">wrappers</code> extension
116 to create this wrapper.
117 </p>
118 <p className="text-warning">
119 Warning: Before reinstalling the wrapper extension, you must first remove all existing
120 wrappers. Afterward, you can recreate the wrappers.
121 </p>
122 </div>
123 <Button asChild type="default" className="w-min mt-3">
124 <Link
125 href={
126 databaseNeedsUpgrading
127 ? `/project/${project?.ref}/settings/infrastructure`
128 : `/project/${project?.ref}/database/extensions?filter=wrappers`
129 }
130 >
131 {databaseNeedsUpgrading ? 'Upgrade database' : 'View wrappers extension'}
132 </Link>
133 </Button>
134 </Admonition>
135 )
136 }
137
138 return (
139 <div className="py-3 px-5 border rounded-md">
140 <ButtonTooltip
141 type="default"
142 onClick={() => setCreateWrapperShown(true)}
143 disabled={!canCreateWrapper}
144 tooltip={{
145 content: {
146 text: !canCreateWrapper
147 ? 'You need additional permissions to create a foreign data wrapper'
148 : undefined,
149 },
150 }}
151 >
152 Add new wrapper
153 </ButtonTooltip>
154 </div>
155 )
156}
157
158export const WrapperOverviewTab = () => {
159 const { id } = useParams()
160 const { data: project } = useSelectedProjectQuery()
161 const isMarketplaceEnabled = useIsMarketplaceEnabled()
162
163 const { data: integrations = [] } = useAvailableIntegrations()
164 const integration = integrations.find((i) => i.id === id)
165 const wrapperMeta = WRAPPERS.find((w) => w.name === id)
166
167 const { data: extensions } = useDatabaseExtensionsQuery({
168 projectRef: project?.ref,
169 connectionString: project?.connectionString,
170 })
171 const installableExtensions = (extensions ?? []).filter((ext) =>
172 (integration?.requiredExtensions ?? []).includes(ext.name)
173 )
174 const isInstalled = installableExtensions.every((x) => x.installed_version)
175
176 if (!wrapperMeta) {
177 return (
178 <ScaffoldContainer>
179 <ScaffoldSection isFullWidth>
180 <p className="text-sm text-foreground-light">Unsupported integration type</p>
181 </ScaffoldSection>
182 </ScaffoldContainer>
183 )
184 }
185
186 if (isMarketplaceEnabled) {
187 return (
188 <IntegrationOverviewTabV2>
189 {isInstalled && (
190 <>
191 <AddNewWrapperCTA />
192 <WrapperOverviewContent />
193 </>
194 )}
195 </IntegrationOverviewTabV2>
196 )
197 } else {
198 return (
199 <IntegrationOverviewTab actions={<AddNewWrapperCTA />}>
200 <div className="mx-10">
201 <WrapperOverviewContent />
202 </div>
203 </IntegrationOverviewTab>
204 )
205 }
206}