mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
WIP
This commit is contained in:
1 parent
498d051d88
commit
0dcd46a52f
14 files changed
+71
-108
No files matched your search
+19
-37
@@ -4,9 +4,9 @@ import { toast } from 'sonner'
|
||||
import { Button } from 'ui'
|
||||
|
||||
import type { IntegrationDefinition } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
|
||||
import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
|
||||
import { useInstallOAuthIntegrationMutation } from '@/data/marketplace/install-oauth-integration-mutation'
|
||||
import { useSecretsQuery } from '@/data/secrets/secrets-query'
|
||||
import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
|
||||
interface InstallOAuthIntegrationButtonProps {
|
||||
integration: IntegrationDefinition
|
||||
@@ -14,22 +14,15 @@ interface InstallOAuthIntegrationButtonProps {
|
||||
|
||||
export function InstallOAuthIntegrationButton({ integration }: InstallOAuthIntegrationButtonProps) {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
|
||||
const requiresApiKeysCheck =
|
||||
integration.installIdentificationMethod === 'secret_key_prefix' && !!integration.secretKeyPrefix
|
||||
const clientId = integration.oauthClientId
|
||||
|
||||
const requiresEdgeFunctionSecretsCheck =
|
||||
integration.installIdentificationMethod === 'edge_function_secret_name' &&
|
||||
!!integration.edgeFunctionSecretName
|
||||
console.log({ integration })
|
||||
|
||||
const { data: apiKeys, isLoading: isApiKeysLoading } = useAPIKeysQuery(
|
||||
{ projectRef, reveal: false },
|
||||
{ enabled: requiresApiKeysCheck }
|
||||
)
|
||||
|
||||
const { data: edgeFunctionSecrets, isPending: isEdgeFunctionSecretsLoading } = useSecretsQuery(
|
||||
{ projectRef },
|
||||
{ enabled: requiresEdgeFunctionSecretsCheck }
|
||||
const { data: authorizedApps, isLoading: isAuthorizedAppsLoading } = useAuthorizedAppsQuery(
|
||||
{ slug: organization?.slug },
|
||||
{ enabled: !!clientId && !!organization?.slug }
|
||||
)
|
||||
|
||||
const { mutate: installOAuthIntegration, isPending: isInstalling } =
|
||||
@@ -47,27 +40,16 @@ export function InstallOAuthIntegrationButton({ integration }: InstallOAuthInteg
|
||||
},
|
||||
})
|
||||
|
||||
const isLoading =
|
||||
(requiresApiKeysCheck && isApiKeysLoading) ||
|
||||
(requiresEdgeFunctionSecretsCheck && isEdgeFunctionSecretsLoading)
|
||||
const isLoading = !!clientId && isAuthorizedAppsLoading
|
||||
|
||||
const isIntegrationInstalled = useMemo(() => {
|
||||
if (!integration) return false
|
||||
|
||||
if (integration.installIdentificationMethod === 'secret_key_prefix') {
|
||||
const prefix = integration.secretKeyPrefix
|
||||
if (!prefix || isApiKeysLoading || !apiKeys) return false
|
||||
return apiKeys.some((k) => k.type === 'secret' && k.name.startsWith(prefix))
|
||||
}
|
||||
|
||||
if (integration.installIdentificationMethod === 'edge_function_secret_name') {
|
||||
const secretName = integration.edgeFunctionSecretName
|
||||
if (!secretName || isEdgeFunctionSecretsLoading || !edgeFunctionSecrets) return false
|
||||
return edgeFunctionSecrets.some((secret) => secret.name === secretName)
|
||||
}
|
||||
|
||||
return false
|
||||
}, [apiKeys, edgeFunctionSecrets, integration, isApiKeysLoading, isEdgeFunctionSecretsLoading])
|
||||
const isIntegrationConnected = useMemo(() => {
|
||||
console.log({
|
||||
authorizedApps,
|
||||
clientId,
|
||||
})
|
||||
if (!clientId || !authorizedApps) return false
|
||||
return authorizedApps.some((app) => app.client_id === clientId)
|
||||
}, [authorizedApps, clientId])
|
||||
|
||||
const handleInstallClick = async () => {
|
||||
if (!integration || !projectRef) return
|
||||
@@ -78,9 +60,9 @@ export function InstallOAuthIntegrationButton({ integration }: InstallOAuthInteg
|
||||
|
||||
return (
|
||||
<>
|
||||
{isIntegrationInstalled ? (
|
||||
{isIntegrationConnected ? (
|
||||
<Button disabled type="outline" className="shrink-0">
|
||||
Installed
|
||||
Connected
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -117,6 +117,7 @@ export type IntegrationDefinition = {
|
||||
installIdentificationMethod?: InstallIdentificationMethod
|
||||
secretKeyPrefix?: string
|
||||
edgeFunctionSecretName?: string
|
||||
oauthClientId?: string
|
||||
listingId?: string
|
||||
} & (
|
||||
| { type: 'wrapper'; meta: WrapperMeta }
|
||||
|
||||
@@ -7,11 +7,10 @@ import {
|
||||
isInstalled as checkIsInstalled,
|
||||
findStripeSchema,
|
||||
} from '@/components/interfaces/Integrations/templates/StripeSyncEngine/stripe-sync-status'
|
||||
import { type APIKey } from '@/data/api-keys/api-keys-query'
|
||||
import { type DatabaseExtension } from '@/data/database-extensions/database-extensions-query'
|
||||
import { type Schema } from '@/data/database/schemas-query'
|
||||
import { type FDW } from '@/data/fdw/fdws-query'
|
||||
import { type ProjectSecret } from '@/data/secrets/secrets-query'
|
||||
import { type AuthorizedApp } from '@/data/oauth/authorized-apps-query'
|
||||
|
||||
export const isStripeSyncEngineInstalled = (schemas: Schema[]) => {
|
||||
const stripeSchema = findStripeSchema(schemas)
|
||||
@@ -21,28 +20,15 @@ export const isStripeSyncEngineInstalled = (schemas: Schema[]) => {
|
||||
|
||||
export const isOAuthInstalled = ({
|
||||
integration,
|
||||
apiKeys,
|
||||
secrets,
|
||||
authorizedApps,
|
||||
}: {
|
||||
integration: IntegrationDefinition
|
||||
apiKeys: APIKey[]
|
||||
secrets: ProjectSecret[]
|
||||
authorizedApps: AuthorizedApp[]
|
||||
}) => {
|
||||
if (integration.installIdentificationMethod === 'secret_key_prefix') {
|
||||
const prefix = integration.secretKeyPrefix
|
||||
if (!prefix) return false
|
||||
const clientId = integration.oauthClientId
|
||||
if (!clientId) return false
|
||||
|
||||
return apiKeys.some((key) => key.type === 'secret' && key.name.startsWith(prefix))
|
||||
}
|
||||
|
||||
if (integration.installIdentificationMethod === 'edge_function_secret_name') {
|
||||
const secretName = integration.edgeFunctionSecretName
|
||||
if (!secretName) return false
|
||||
|
||||
return secrets.some((secret) => secret.name === secretName)
|
||||
}
|
||||
|
||||
return false
|
||||
return authorizedApps.some((app) => app.client_id === clientId)
|
||||
}
|
||||
|
||||
export const hasMatchingWrapper = ({ meta, wrappers }: { meta: WrapperMeta; wrappers: FDW[] }) => {
|
||||
|
||||
@@ -53,6 +53,7 @@ export const useAvailableIntegrations = () => {
|
||||
installation_identification_method: installMethod,
|
||||
secret_key_prefix: secretKeyPrefix,
|
||||
edge_function_secret_name: edgeFunctionSecretName,
|
||||
oauth_client_id: oauthClientId,
|
||||
images,
|
||||
content,
|
||||
partner_name: authorName,
|
||||
@@ -82,6 +83,7 @@ export const useAvailableIntegrations = () => {
|
||||
installIdentificationMethod: installMethod ?? undefined,
|
||||
secretKeyPrefix: secretKeyPrefix ?? undefined,
|
||||
edgeFunctionSecretName: edgeFunctionSecretName ?? undefined,
|
||||
oauthClientId: oauthClientId ?? undefined,
|
||||
listingId: listingId ?? undefined,
|
||||
author,
|
||||
requiredExtensions: [],
|
||||
|
||||
+19
-45
@@ -7,16 +7,17 @@ import {
|
||||
isStripeSyncEngineInstalled,
|
||||
} from './Landing.utils'
|
||||
import { useAvailableIntegrations } from './useAvailableIntegrations'
|
||||
import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
|
||||
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
|
||||
import { useSchemasQuery } from '@/data/database/schemas-query'
|
||||
import { useFDWsQuery } from '@/data/fdw/fdws-query'
|
||||
import { useSecretsQuery } from '@/data/secrets/secrets-query'
|
||||
import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { EMPTY_ARR } from '@/lib/void'
|
||||
|
||||
export const useInstalledIntegrations = () => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { data: organization } = useSelectedOrganizationQuery()
|
||||
|
||||
const {
|
||||
data: allIntegrations = EMPTY_ARR,
|
||||
@@ -26,44 +27,21 @@ export const useInstalledIntegrations = () => {
|
||||
isError: isErrorAvailableIntegrations,
|
||||
} = useAvailableIntegrations()
|
||||
|
||||
const hasSecretKeyPrefixIntegration = useMemo(() => {
|
||||
const hasOAuthIntegration = useMemo(() => {
|
||||
return allIntegrations.some(
|
||||
(integration) =>
|
||||
integration.type === 'oauth' &&
|
||||
integration.installIdentificationMethod === 'secret_key_prefix' &&
|
||||
!!integration.secretKeyPrefix
|
||||
)
|
||||
}, [allIntegrations])
|
||||
|
||||
const hasEdgeFunctionSecretNameIntegration = useMemo(() => {
|
||||
return allIntegrations.some(
|
||||
(integration) =>
|
||||
integration.type === 'oauth' &&
|
||||
integration.installIdentificationMethod === 'edge_function_secret_name' &&
|
||||
!!integration.edgeFunctionSecretName
|
||||
(integration) => integration.type === 'oauth' && !!integration.oauthClientId
|
||||
)
|
||||
}, [allIntegrations])
|
||||
|
||||
const {
|
||||
data: apiKeys = EMPTY_ARR,
|
||||
error: apiKeysError,
|
||||
isError: isErrorApiKeys,
|
||||
isLoading: isApiKeysLoading,
|
||||
isSuccess: isSuccessApiKeys,
|
||||
} = useAPIKeysQuery(
|
||||
{ projectRef: project?.ref, reveal: false },
|
||||
{ enabled: hasSecretKeyPrefixIntegration }
|
||||
)
|
||||
|
||||
const {
|
||||
data: edgeFunctionSecrets = EMPTY_ARR,
|
||||
error: edgeFunctionSecretsError,
|
||||
isError: isErrorEdgeFunctionSecrets,
|
||||
isLoading: isEdgeFunctionSecretsLoading,
|
||||
isSuccess: isSuccessEdgeFunctionSecrets,
|
||||
} = useSecretsQuery(
|
||||
{ projectRef: project?.ref },
|
||||
{ enabled: hasEdgeFunctionSecretNameIntegration }
|
||||
data: authorizedApps = EMPTY_ARR,
|
||||
error: authorizedAppsError,
|
||||
isError: isErrorAuthorizedApps,
|
||||
isLoading: isAuthorizedAppsLoading,
|
||||
isSuccess: isSuccessAuthorizedApps,
|
||||
} = useAuthorizedAppsQuery(
|
||||
{ slug: organization?.slug },
|
||||
{ enabled: hasOAuthIntegration && !!organization?.slug }
|
||||
)
|
||||
|
||||
const {
|
||||
@@ -115,41 +93,37 @@ export const useInstalledIntegrations = () => {
|
||||
return hasRequiredExtensions({ integration, extensions })
|
||||
}
|
||||
if (integration.type === 'oauth') {
|
||||
return isOAuthInstalled({ integration, apiKeys, secrets: edgeFunctionSecrets })
|
||||
return isOAuthInstalled({ integration, authorizedApps })
|
||||
}
|
||||
return false
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}, [allIntegrations, wrappers, extensions, schemas, isHooksEnabled, apiKeys, edgeFunctionSecrets])
|
||||
}, [allIntegrations, wrappers, extensions, schemas, isHooksEnabled, authorizedApps])
|
||||
|
||||
const error =
|
||||
fdwError ||
|
||||
extensionsError ||
|
||||
schemasError ||
|
||||
availableIntegrationsError ||
|
||||
(hasSecretKeyPrefixIntegration ? apiKeysError : null) ||
|
||||
(hasEdgeFunctionSecretNameIntegration ? edgeFunctionSecretsError : null)
|
||||
(hasOAuthIntegration ? authorizedAppsError : null)
|
||||
const isLoading =
|
||||
isSchemasLoading ||
|
||||
isFDWLoading ||
|
||||
isExtensionsLoading ||
|
||||
isAvailableIntegrationsLoading ||
|
||||
(hasSecretKeyPrefixIntegration && isApiKeysLoading) ||
|
||||
(hasEdgeFunctionSecretNameIntegration && isEdgeFunctionSecretsLoading)
|
||||
(hasOAuthIntegration && isAuthorizedAppsLoading)
|
||||
const isError =
|
||||
isErrorFDWs ||
|
||||
isErrorExtensions ||
|
||||
isErrorSchemas ||
|
||||
isErrorAvailableIntegrations ||
|
||||
(hasSecretKeyPrefixIntegration && isErrorApiKeys) ||
|
||||
(hasEdgeFunctionSecretNameIntegration && isErrorEdgeFunctionSecrets)
|
||||
(hasOAuthIntegration && isErrorAuthorizedApps)
|
||||
const isSuccess =
|
||||
isSuccessFDWs &&
|
||||
isSuccessExtensions &&
|
||||
isSuccessSchemas &&
|
||||
isSuccessAvailableIntegrations &&
|
||||
(!hasSecretKeyPrefixIntegration || isSuccessApiKeys) &&
|
||||
(!hasEdgeFunctionSecretNameIntegration || isSuccessEdgeFunctionSecrets)
|
||||
(!hasOAuthIntegration || isSuccessAuthorizedApps)
|
||||
|
||||
return {
|
||||
// show all integrations at once instead of showing partial results
|
||||
|
||||
@@ -23,7 +23,11 @@ export const MarketplaceCard = ({ integration, isInstalled }: MarketplaceCardPro
|
||||
<Card className="flex min-h-[168px] h-full flex-col gap-2.5 hover:border-stronger p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<IntegrationLogo integration={integration} size="h-9 w-9" />
|
||||
{isInstalled && <Badge variant="success">Installed</Badge>}
|
||||
{isInstalled && (
|
||||
<Badge variant="success">
|
||||
{integration.type === 'oauth' ? 'Connected' : 'Installed'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-1 text-sm font-medium">{integration.name}</div>
|
||||
|
||||
@@ -89,6 +89,7 @@ export const MarketplaceDetail = () => {
|
||||
<MarketplaceDetailTopBar
|
||||
title={integration.name}
|
||||
isInstalled={isInstalled}
|
||||
installedLabel={integration.type === 'oauth' ? 'Connected' : 'Installed'}
|
||||
actions={
|
||||
<>
|
||||
{integration.docsUrl && (
|
||||
|
||||
+6
-1
@@ -79,7 +79,12 @@ export const MarketplaceDetailRail = ({ integration, isInstalled }: MarketplaceD
|
||||
<RailGroup title="About">
|
||||
<RailRow label="Type" value={typeLabel} />
|
||||
<RailRow label="Built by" value={integration.author?.name || 'Supabase'} />
|
||||
{isInstalled && <RailRow label="Status" value="Installed" />}
|
||||
{isInstalled && (
|
||||
<RailRow
|
||||
label="Status"
|
||||
value={integration.type === 'oauth' ? 'Connected' : 'Installed'}
|
||||
/>
|
||||
)}
|
||||
</RailGroup>
|
||||
|
||||
{(docsUrl || siteUrl) && (
|
||||
|
||||
+3
-1
@@ -14,11 +14,13 @@ interface MarketplaceDetailTopBarProps {
|
||||
title: string
|
||||
actions?: ReactNode
|
||||
isInstalled?: boolean
|
||||
installedLabel?: string
|
||||
}
|
||||
|
||||
export const MarketplaceDetailTopBar = ({
|
||||
title,
|
||||
isInstalled,
|
||||
installedLabel = 'Installed',
|
||||
actions,
|
||||
}: MarketplaceDetailTopBarProps) => {
|
||||
const { ref } = useParams()
|
||||
@@ -35,7 +37,7 @@ export const MarketplaceDetailTopBar = ({
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem className="min-w-0">
|
||||
<BreadcrumbPage className="block min-w-0 truncate">{title}</BreadcrumbPage>
|
||||
{isInstalled && <Badge variant="success">Installed</Badge>}
|
||||
{isInstalled && <Badge variant="success">{installedLabel}</Badge>}
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ export const MarketplaceFeaturedHero = ({
|
||||
</div>
|
||||
{isActiveInstalled && (
|
||||
<Badge variant="success" className="mt-2 @lg:mt-0 px-1.5 py-0 text-[10px]">
|
||||
Installed
|
||||
{active.type === 'oauth' ? 'Connected' : 'Installed'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -253,7 +253,7 @@ export const MarketplaceIndex = () => {
|
||||
<>
|
||||
{featured.length > 0 && (
|
||||
<MarketplaceFeaturedHero
|
||||
integrations={featured}
|
||||
integrations={featured.slice(0, 1)}
|
||||
installedIds={installedIds}
|
||||
categoryOptions={categoryOptions}
|
||||
/>
|
||||
|
||||
@@ -67,7 +67,11 @@ export const MarketplaceListRow = ({ integration, isInstalled }: MarketplaceList
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="w-24">
|
||||
{isInstalled ? <Badge variant="success">Installed</Badge> : null}
|
||||
{isInstalled ? (
|
||||
<Badge variant="success">
|
||||
{integration.type === 'oauth' ? 'Connected' : 'Installed'}
|
||||
</Badge>
|
||||
) : null}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ export type AuthorizedAppsVariables = {
|
||||
export type AuthorizedApp = {
|
||||
id: string
|
||||
app_id: string
|
||||
client_id: string | null
|
||||
icon: string | null
|
||||
name: string
|
||||
website: string
|
||||
|
||||
@@ -30,6 +30,7 @@ export type Database = MergeDeep<
|
||||
website_url: string
|
||||
documentation_url: string
|
||||
listing_logo: string
|
||||
oauth_client_id: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user