OverviewTab.tsx 7.5 KB

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