Files
supabase/apps/studio/components/interfaces/Integrations/Landing/useInstalledIntegrations.tsx
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

128 lines
4.5 KiB
TypeScript

import { useFlag } from 'common'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useFDWsQuery } from 'data/fdw/fdws-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { IS_PLATFORM } from 'lib/constants'
import { EMPTY_ARR } from 'lib/void'
import { useMemo } from 'react'
import { wrapperMetaComparator } from '../Wrappers/Wrappers.utils'
import { INTEGRATIONS } from './Integrations.constants'
import {
isInstalled as checkIsInstalled,
findStripeSchema,
parseStripeSchemaStatus,
} from '@/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status'
export const useInstalledIntegrations = () => {
const { data: project } = useSelectedProjectQuery()
const { integrationsWrappers } = useIsFeatureEnabled(['integrations:wrappers'])
const stripeSyncEnabled = useFlag('enableStripeSyncEngineIntegration')
const allIntegrations = useMemo(() => {
return INTEGRATIONS.filter((integration) => {
if (
!integrationsWrappers &&
(integration.type === 'wrapper' || integration.id.endsWith('_wrapper'))
) {
return false
}
if (!stripeSyncEnabled && integration.id === 'stripe_sync_engine') {
return false
}
if (!IS_PLATFORM && integration.id === 'data_api') {
return false
}
return true
})
}, [integrationsWrappers, stripeSyncEnabled])
const {
data,
error: fdwError,
isError: isErrorFDWs,
isPending: isFDWLoading,
isSuccess: isSuccessFDWs,
} = useFDWsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const {
data: extensions,
error: extensionsError,
isError: isErrorExtensions,
isPending: isExtensionsLoading,
isSuccess: isSuccessExtensions,
} = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const {
data: schemas,
error: schemasError,
isError: isErrorSchemas,
isPending: isSchemasLoading,
isSuccess: isSuccessSchemas,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const isHooksEnabled = schemas?.some((schema) => schema.name === 'supabase_functions')
const wrappers = useMemo(() => data ?? EMPTY_ARR, [data])
const installedIntegrations = useMemo(() => {
return allIntegrations
.filter((integration) => {
// special handling for supabase webhooks
if (integration.id === 'webhooks') {
return isHooksEnabled
}
if (integration.id === 'data_api') {
return true
}
if (integration.id === 'stripe_sync_engine') {
const stripeSchema = findStripeSchema(schemas)
const status = parseStripeSchemaStatus(stripeSchema)
return checkIsInstalled(status)
}
if (integration.type === 'wrapper') {
return wrappers.find((w) => wrapperMetaComparator(integration.meta, w))
}
if (integration.type === 'postgres_extension') {
return integration.requiredExtensions.every((extName) => {
const foundExtension = (extensions ?? []).find((ext) => ext.name === extName)
return !!foundExtension?.installed_version
})
}
return false
})
.sort((a, b) => a.name.localeCompare(b.name))
}, [allIntegrations, wrappers, extensions, schemas, isHooksEnabled])
// available integrations are all integrations that can be installed. If an integration can't be installed (needed
// extensions are not available on this DB image), the UI will provide a tooltip explaining why.
const availableIntegrations = useMemo(
() => allIntegrations.sort((a, b) => a.name.localeCompare(b.name)),
[allIntegrations]
)
const error = fdwError || extensionsError || schemasError
const isLoading = isSchemasLoading || isFDWLoading || isExtensionsLoading
const isError = isErrorFDWs || isErrorExtensions || isErrorSchemas
const isSuccess = isSuccessFDWs && isSuccessExtensions && isSuccessSchemas
return {
// show all integrations at once instead of showing partial results
installedIntegrations: isLoading ? EMPTY_ARR : installedIntegrations,
availableIntegrations: isLoading ? EMPTY_ARR : availableIntegrations,
error,
isError,
isLoading,
isSuccess,
}
}