Files
supabase/apps/studio/components/interfaces/Integrations/Integration/IntegrationOverviewTab.tsx
T
Raminder SinghandJoshen Lim 3d14b5e7bf fix: show uninstall progress (#42860)
This PR makes the following changes:

* Refactors the Stripe sync engine integration so that impossible states
are unrepresentable in the code.
* Factors out common code and functions into a single location to avoid
duplication.
* Factors out code to fetch installation/uninstallation and sync status
into a separate hook to make the code more readable.
* Shows uninstall progress when the user uninstalls the integration.
* Moves the **Uninstall integration** button from the **Settings** page
to the **Overview** page. This is done to avoid us having to track the
uninstallation progress via the url query params when the user is
redirected to the **Overview** page during uninstallation. Also it makes
sense for installation/uninstallation to be available on the same
location.

Note that even with the above changes there are some limitations to how
the uninstallation progress is shown. In particular, if the user
refreshes the page, the uninstallation status is lost because the
background uninstallation procedure currently doesn't update the comment
on the stripe schema during uninstallation. We are making a change in
the stripe sync engine code for that here:
https://github.com/stripe-experiments/sync-engine/pull/113

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-02-20 10:02:11 +00:00

118 lines
4.2 KiB
TypeScript

import { useParams } from 'common'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { PropsWithChildren, ReactNode } from 'react'
import { Badge, Card, CardContent, cn, Separator } from 'ui'
import { INTEGRATIONS } from '../Landing/Integrations.constants'
import { BuiltBySection } from './BuildBySection'
import { MarkdownContent } from './MarkdownContent'
import { MissingExtensionAlert } from './MissingExtensionAlert'
interface IntegrationOverviewTabProps {
actions?: ReactNode
status?: string | ReactNode
alert?: ReactNode
}
export const IntegrationOverviewTab = ({
actions,
alert,
status,
children,
}: PropsWithChildren<IntegrationOverviewTabProps>) => {
const { id } = useParams()
const { data: project } = useSelectedProjectQuery()
const integration = INTEGRATIONS.find((i) => i.id === id)
const { data: extensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
if (!integration) {
return <div>Unsupported integration type</div>
}
const dependsOnExtension = (integration.requiredExtensions ?? []).length > 0
const installableExtensions = (extensions ?? []).filter((ext) =>
(integration.requiredExtensions ?? []).includes(ext.name)
)
const hasToInstallExtensions = installableExtensions.some((x) => !x.installed_version)
// The integration requires extensions that are not available to install on the current database image
const hasMissingExtensions =
installableExtensions.length !== integration.requiredExtensions.length
return (
<div className="flex flex-col gap-8 py-10">
<BuiltBySection integration={integration} status={status} />
{alert && <div className="px-10 max-w-4xl">{alert}</div>}
<MarkdownContent key={integration.id} integrationId={integration.id} />
<Separator />
{dependsOnExtension && (
<div className="px-4 md:px-10 max-w-4xl flex flex-col gap-y-4">
<h4>Required extensions</h4>
<Card>
<CardContent className="p-0">
<ul className="text-foreground-light text-sm">
{(integration.requiredExtensions ?? []).map((requiredExtension, idx) => {
const extension = (extensions ?? []).find((ext) => ext.name === requiredExtension)
const isInstalled = !!extension?.installed_version
const isLastRow = idx === (integration.requiredExtensions?.length ?? 0) - 1
return (
<li
key={requiredExtension}
className={[
'flex items-center justify-between gap-3 py-2 px-3',
!isLastRow ? 'border-b' : '',
].join(' ')}
>
<div className="flex items-center gap-2 min-w-0">
<span className="truncate">
<code>{requiredExtension}</code>
</span>
</div>
<div className="shrink-0">
{extension ? (
isInstalled ? (
<Badge>Installed</Badge>
) : (
<MissingExtensionAlert extension={extension} />
)
) : (
<span className="text-foreground-muted">Unavailable</span>
)}
</div>
</li>
)
})}
</ul>
{hasMissingExtensions && (
<div className="py-3 px-4 border-t">{integration.missingExtensionsAlert}</div>
)}
</CardContent>
</Card>
</div>
)}
{!!actions && (
<div
aria-disabled={hasToInstallExtensions}
className={cn(
'px-10 max-w-4xl',
hasToInstallExtensions && 'opacity-25 [&_button]:pointer-events-none'
)}
>
{actions}
</div>
)}
{children}
</div>
)
}