mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 06:39:09 +03:00
## Context For a project that might have multiple connections to Vercel projects, the Settings -> Integrations -> Vercel section can get really long as we're showing the configuration fields for all projects by default Opting to make each vercel integration a collapsible instead Also improves the "Configure connection" CTA from org integration settings to include an id in the URL, so that redirecting users to the project settings integrations page will land them on the right section ### Before Organization settings -> integrations <img width="500" alt="image" src="https://github.com/user-attachments/assets/f5b9ea2c-0a95-4aff-9416-e16f853d3b8e" /> Project settings -> Integrations <img width="500" alt="image" src="https://github.com/user-attachments/assets/361d5ac7-5919-4103-aaae-cdb5b1a1ce8c" /> ### After Organization settings -> integrations <img width="500" alt="image" src="https://github.com/user-attachments/assets/232cfef7-affe-4f06-a019-7f86ee7805db" /> Project Settings -> Integrations <img width="500" alt="image" src="https://github.com/user-attachments/assets/c3902451-0578-4bfc-8149-2d6f9c276ebd" />
228 lines
8.1 KiB
TypeScript
228 lines
8.1 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { Plus } from 'lucide-react'
|
|
import { useRouter } from 'next/router'
|
|
import { useCallback, useMemo } from 'react'
|
|
import { toast } from 'sonner'
|
|
import { Button, Card, CardContent } from 'ui'
|
|
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
|
|
import {
|
|
PageSection,
|
|
PageSectionContent,
|
|
PageSectionDescription,
|
|
PageSectionMeta,
|
|
PageSectionSummary,
|
|
PageSectionTitle,
|
|
} from 'ui-patterns/PageSection'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { IntegrationSectionIcon } from '../IntegrationsSettings'
|
|
import { GitHubIntegrationConnectionForm } from './GitHubIntegrationConnectionForm'
|
|
import { IntegrationConnectionItem } from '@/components/interfaces/Integrations/VercelGithub/IntegrationConnection'
|
|
import { EmptyIntegrationConnection } from '@/components/interfaces/Integrations/VercelGithub/IntegrationPanels'
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
import { InlineLink } from '@/components/ui/InlineLink'
|
|
import { NoPermission } from '@/components/ui/NoPermission'
|
|
import { useGitHubAuthorizationQuery } from '@/data/integrations/github-authorization-query'
|
|
import { useGitHubConnectionDeleteMutation } from '@/data/integrations/github-connection-delete-mutation'
|
|
import {
|
|
useGitHubConnectionsQuery,
|
|
type GitHubConnection,
|
|
} from '@/data/integrations/github-connections-query'
|
|
import type { IntegrationProjectConnection } from '@/data/integrations/integrations.types'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import {
|
|
GITHUB_INTEGRATION_INSTALLATION_URL,
|
|
GITHUB_INTEGRATION_REVOKE_AUTHORIZATION_URL,
|
|
} from '@/lib/github'
|
|
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
|
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
|
|
|
const toIntegrationProjectConnection = (
|
|
connection: GitHubConnection
|
|
): IntegrationProjectConnection => ({
|
|
id: String(connection.id),
|
|
added_by: {
|
|
id: String(connection.user?.id),
|
|
primary_email: connection.user?.primary_email ?? '',
|
|
username: connection.user?.username ?? '',
|
|
},
|
|
foreign_project_id: String(connection.repository.id),
|
|
supabase_project_ref: connection.project.ref,
|
|
organization_integration_id: 'unused',
|
|
inserted_at: connection.inserted_at,
|
|
updated_at: connection.updated_at,
|
|
metadata: {
|
|
name: connection.repository.name,
|
|
} as IntegrationProjectConnection['metadata'],
|
|
})
|
|
|
|
export const GitHubSection = ({ isProjectScoped = false }: { isProjectScoped?: boolean }) => {
|
|
const router = useRouter()
|
|
const { ref: projectRef } = useParams()
|
|
const { data: org } = useSelectedOrganizationQuery()
|
|
|
|
const { can: canReadGitHubConnection, isLoading: isLoadingPermissions } =
|
|
useAsyncCheckPermissions(PermissionAction.READ, 'integrations.github_connections')
|
|
const { can: canCreateGitHubConnection } = useAsyncCheckPermissions(
|
|
PermissionAction.CREATE,
|
|
'integrations.github_connections'
|
|
)
|
|
const { can: canUpdateGitHubConnection } = useAsyncCheckPermissions(
|
|
PermissionAction.UPDATE,
|
|
'integrations.github_connections'
|
|
)
|
|
|
|
const { data: gitHubAuthorization } = useGitHubAuthorizationQuery({
|
|
enabled: !isProjectScoped,
|
|
})
|
|
const { data: connections } = useGitHubConnectionsQuery(
|
|
{ organizationId: org?.id },
|
|
{ enabled: isProjectScoped ? !!projectRef && !!org?.id : !!org?.id }
|
|
)
|
|
|
|
const { mutate: deleteGitHubConnection } = useGitHubConnectionDeleteMutation({
|
|
onSuccess: () => {
|
|
toast.success('GitHub connection deleted')
|
|
},
|
|
})
|
|
|
|
const existingConnection = useMemo(
|
|
() => connections?.find((c) => c.project.ref === projectRef),
|
|
[connections, projectRef]
|
|
)
|
|
|
|
const onAddGitHubConnection = useCallback(() => {
|
|
router.push('/project/_/settings/integrations')
|
|
}, [router])
|
|
|
|
useShortcut(SHORTCUT_IDS.ORG_INTEGRATIONS_ADD_CONNECTION, onAddGitHubConnection, {
|
|
enabled: !isProjectScoped && canCreateGitHubConnection,
|
|
})
|
|
|
|
const description = isProjectScoped
|
|
? 'Preview branches and production deploys from a connected GitHub repository.'
|
|
: 'Preview branches and production deploys from connected GitHub repositories.'
|
|
|
|
const onDeleteGitHubConnection = useCallback(
|
|
async (connection: IntegrationProjectConnection) => {
|
|
if (!org?.id) {
|
|
toast.error('Organization not found')
|
|
return
|
|
}
|
|
|
|
deleteGitHubConnection({
|
|
connectionId: connection.id,
|
|
organizationId: org.id,
|
|
})
|
|
},
|
|
[deleteGitHubConnection, org?.id]
|
|
)
|
|
|
|
return (
|
|
<PageSection id="github">
|
|
<PageSectionMeta>
|
|
<div className="flex flex-1 items-start gap-5">
|
|
<IntegrationSectionIcon title="github" />
|
|
<PageSectionSummary>
|
|
<PageSectionTitle>GitHub</PageSectionTitle>
|
|
<PageSectionDescription>{description}</PageSectionDescription>
|
|
</PageSectionSummary>
|
|
</div>
|
|
</PageSectionMeta>
|
|
<PageSectionContent>
|
|
{isLoadingPermissions ? (
|
|
<GenericSkeletonLoader />
|
|
) : !canReadGitHubConnection ? (
|
|
<NoPermission resourceText="view GitHub connections" />
|
|
) : isProjectScoped ? (
|
|
<GitHubIntegrationConnectionForm connection={existingConnection} />
|
|
) : (
|
|
<div className="space-y-6">
|
|
<div className="flex flex-col gap-y-2">
|
|
{(connections?.length ?? 0) > 0 ? (
|
|
<>
|
|
<ul className="flex flex-col gap-y-2">
|
|
{connections?.map((connection) => (
|
|
<IntegrationConnectionItem
|
|
key={connection.id}
|
|
disabled={!canUpdateGitHubConnection}
|
|
connection={toIntegrationProjectConnection(connection)}
|
|
type="GitHub"
|
|
isProjectScoped={isProjectScoped}
|
|
onDeleteConnection={onDeleteGitHubConnection}
|
|
/>
|
|
))}
|
|
</ul>
|
|
<EmptyIntegrationConnection
|
|
onClick={onAddGitHubConnection}
|
|
showNode={false}
|
|
disabled={!canCreateGitHubConnection}
|
|
icon={<Plus />}
|
|
>
|
|
Add connection
|
|
</EmptyIntegrationConnection>
|
|
</>
|
|
) : (
|
|
<GitHubOrgEmptyState
|
|
disabled={!canCreateGitHubConnection}
|
|
onClick={onAddGitHubConnection}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{gitHubAuthorization && (
|
|
<p className="text-sm text-foreground-light">
|
|
You are authorized with the Supabase GitHub app. You can configure your{' '}
|
|
<InlineLink href={GITHUB_INTEGRATION_INSTALLATION_URL}>
|
|
GitHub App installations and repository access
|
|
</InlineLink>
|
|
, or{' '}
|
|
<InlineLink href={GITHUB_INTEGRATION_REVOKE_AUTHORIZATION_URL}>
|
|
revoke your authorization
|
|
</InlineLink>
|
|
.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</PageSectionContent>
|
|
</PageSection>
|
|
)
|
|
}
|
|
|
|
function GitHubOrgEmptyState({ disabled, onClick }: { disabled: boolean; onClick: () => void }) {
|
|
return (
|
|
<Card>
|
|
<CardContent>
|
|
<FormLayout
|
|
layout="flex-row-reverse"
|
|
label="GitHub repository"
|
|
description="Add a connection to link a repository to a project"
|
|
>
|
|
{disabled ? (
|
|
<ButtonTooltip
|
|
icon={<Plus />}
|
|
size="tiny"
|
|
disabled
|
|
tooltip={{
|
|
content: {
|
|
side: 'bottom',
|
|
text: 'Additional permissions required to add connection',
|
|
},
|
|
}}
|
|
>
|
|
Add connection
|
|
</ButtonTooltip>
|
|
) : (
|
|
<Button icon={<Plus />} size="tiny" type="button" onClick={onClick}>
|
|
Add connection
|
|
</Button>
|
|
)}
|
|
</FormLayout>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|