OverviewTab.tsx206 lines · main
| 1 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 2 | import { useParams } from 'common' |
| 3 | import Link from 'next/link' |
| 4 | import { parseAsBoolean, useQueryState } from 'nuqs' |
| 5 | import { useState } from 'react' |
| 6 | import { Button, Sheet, SheetContent } from 'ui' |
| 7 | import { Admonition } from 'ui-patterns/admonition' |
| 8 | |
| 9 | import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' |
| 10 | import { IntegrationOverviewTabV2 } from '../Integration/IntegrationOverviewTabV2' |
| 11 | import { useAvailableIntegrations } from '../Landing/useAvailableIntegrations' |
| 12 | import { CreateIcebergWrapperSheet } from './CreateIcebergWrapperSheet' |
| 13 | import { CreateWrapperSheet } from './CreateWrapperSheet' |
| 14 | import { WRAPPERS } from './Wrappers.constants' |
| 15 | import { WrapperTable } from './WrapperTable' |
| 16 | import { useIsMarketplaceEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' |
| 17 | import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' |
| 18 | import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog' |
| 19 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 20 | import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' |
| 21 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 22 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 23 | import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose' |
| 24 | |
| 25 | const 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 | |
| 78 | const 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 | |
| 158 | export 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 | } |