mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
chore(branching): GitHub integration 2.0 (#21008)
* start new github integration * query/mutation updates * progress * branch management * update codegen * progress * progress * Refactor GitHub integration URLs * Refactor GitHubIntegrationAuthorize component * Updates * Do not remove GitHub connection when creating new one * Deleting a GH connection when branching is enabled for the project, will also disable branching for that project * Add link to configure connection from org integration settings page * Slight refactor * Support updating CWD path * Change cwd_path to workdir and disallow empty values * Allow for triggering branches on supabase directory changes only * Pass missing supabaseChangesOnly value * Small style fix * Add Authorization GitHub step * Small change * Fix supabase integrations form in project settings * Revert URLa nd client ID * Fix UI issues --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com> Co-authored-by: Kamil Ogórek <kamil.ogorek@gmail.com>
This commit is contained in:
41 files changed
+1316
-1578
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { useParams } from 'common'
|
||||
import { partition } from 'lodash'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
@@ -11,20 +12,18 @@ import {
|
||||
IconAlertTriangle,
|
||||
IconExternalLink,
|
||||
IconGitHub,
|
||||
IconSearch,
|
||||
Input,
|
||||
Modal,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
|
||||
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
|
||||
import { useBranchDeleteMutation } from 'data/branches/branch-delete-mutation'
|
||||
import { useBranchesDisableMutation } from 'data/branches/branches-disable-mutation'
|
||||
import { Branch, useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useGithubPullRequestsQuery } from 'data/integrations/integrations-github-pull-requests-query'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { useGitHubPullRequestsQuery } from 'data/integrations/github-pull-requests-query'
|
||||
import { useSelectedOrganization, useSelectedProject, useStore } from 'hooks'
|
||||
import { BranchLoader, BranchManagementSection, BranchRow } from './BranchPanels'
|
||||
import CreateBranchModal from './CreateBranchModal'
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
PullRequestsEmptyState,
|
||||
} from './EmptyStates'
|
||||
import Overview from './Overview'
|
||||
import { MessageCircle } from 'lucide-react'
|
||||
|
||||
const BranchManagement = () => {
|
||||
const { ui } = useStore()
|
||||
@@ -55,12 +53,14 @@ const BranchManagement = () => {
|
||||
const [selectedBranchToDelete, setSelectedBranchToDelete] = useState<Branch>()
|
||||
|
||||
const {
|
||||
data: integrations,
|
||||
error: integrationsError,
|
||||
isLoading: isLoadingIntegrations,
|
||||
isError: isErrorIntegrations,
|
||||
isSuccess: isSuccessIntegrations,
|
||||
} = useOrgIntegrationsQuery({ orgSlug: selectedOrg?.slug })
|
||||
data: connections,
|
||||
error: connectionsError,
|
||||
isLoading: isLoadingConnections,
|
||||
isSuccess: isSuccessConnections,
|
||||
isError: isErrorConnections,
|
||||
} = useGitHubConnectionsQuery({
|
||||
organizationId: selectedOrg?.id,
|
||||
})
|
||||
|
||||
const {
|
||||
data: branches,
|
||||
@@ -79,16 +79,8 @@ const BranchManagement = () => {
|
||||
? (branchesWithPRs.map((branch) => branch.pr_number).filter(Boolean) as number[])
|
||||
: undefined
|
||||
|
||||
const githubIntegration = integrations?.find(
|
||||
(integration) =>
|
||||
integration.integration.name === 'GitHub' &&
|
||||
integration.organization.slug === selectedOrg?.slug
|
||||
)
|
||||
const githubConnection = githubIntegration?.connections?.find(
|
||||
(connection) => connection.supabase_project_ref === projectRef
|
||||
)
|
||||
const repo = githubConnection?.metadata.name ?? ''
|
||||
const [repoOwner, repoName] = githubConnection?.metadata.name.split('/') || []
|
||||
const githubConnection = connections?.find((connection) => connection.project.ref === projectRef)
|
||||
const repo = githubConnection?.repository.name ?? ''
|
||||
|
||||
const {
|
||||
data: allPullRequests,
|
||||
@@ -96,17 +88,15 @@ const BranchManagement = () => {
|
||||
isLoading: isLoadingPullRequests,
|
||||
isError: isErrorPullRequests,
|
||||
isSuccess: isSuccessPullRequests,
|
||||
} = useGithubPullRequestsQuery({
|
||||
organizationIntegrationId: githubIntegration?.id,
|
||||
repoOwner,
|
||||
repoName,
|
||||
} = useGitHubPullRequestsQuery({
|
||||
connectionId: githubConnection?.id,
|
||||
prNumbers,
|
||||
})
|
||||
const pullRequests = allPullRequests ?? []
|
||||
|
||||
const isError = isErrorIntegrations || isErrorBranches
|
||||
const isLoading = isLoadingIntegrations || isLoadingBranches
|
||||
const isSuccess = isSuccessIntegrations && isSuccessBranches
|
||||
const isError = isErrorConnections || isErrorBranches
|
||||
const isLoading = isLoadingConnections || isLoadingBranches
|
||||
const isSuccess = isSuccessConnections && isSuccessBranches
|
||||
|
||||
const { mutate: deleteBranch, isLoading: isDeleting } = useBranchDeleteMutation({
|
||||
onSuccess: () => {
|
||||
@@ -138,8 +128,8 @@ const BranchManagement = () => {
|
||||
if (githubConnection === undefined) return 'https://github.com'
|
||||
|
||||
return branch !== undefined
|
||||
? `https://github.com/${githubConnection.metadata.name}/compare/${mainBranch?.git_branch}...${branch}`
|
||||
: `https://github.com/${githubConnection.metadata.name}/compare`
|
||||
? `https://github.com/${githubConnection.repository.name}/compare/${mainBranch?.git_branch}...${branch}`
|
||||
: `https://github.com/${githubConnection.repository.name}/compare`
|
||||
}
|
||||
|
||||
const onConfirmDeleteBranch = () => {
|
||||
@@ -219,14 +209,14 @@ const BranchManagement = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isErrorIntegrations && (
|
||||
{isErrorConnections && (
|
||||
<AlertError
|
||||
error={integrationsError}
|
||||
error={connectionsError}
|
||||
subject="Failed to retrieve GitHub integration connection"
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSuccessIntegrations && (
|
||||
{isSuccessConnections && (
|
||||
<div className="border rounded-lg px-6 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="w-8 h-8 bg-scale-300 border rounded-md flex items-center justify-center">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useInView } from 'react-intersection-observer'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useBranchQuery } from 'data/branches/branch-query'
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
import { GitHubPullRequest } from 'data/integrations/integrations-github-pull-requests-query'
|
||||
import { GitHubPullRequest } from 'data/integrations/github-pull-requests-query'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
|
||||
@@ -4,6 +4,15 @@ import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import * as z from 'zod'
|
||||
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-check-query'
|
||||
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
@@ -21,15 +30,6 @@ import {
|
||||
Input_Shadcn_,
|
||||
Modal,
|
||||
} from 'ui'
|
||||
import * as z from 'zod'
|
||||
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useCheckGithubBranchValidity } from 'data/integrations/integrations-github-branch-check'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
|
||||
interface CreateBranchModalProps {
|
||||
visible: boolean
|
||||
@@ -51,13 +51,13 @@ const CreateBranchModal = ({ visible, onClose }: CreateBranchModalProps) => {
|
||||
projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
|
||||
|
||||
const {
|
||||
data: integrations,
|
||||
error: integrationsError,
|
||||
isLoading: isLoadingIntegrations,
|
||||
isSuccess: isSuccessIntegrations,
|
||||
isError: isErrorIntegrations,
|
||||
} = useOrgIntegrationsQuery({
|
||||
orgSlug: selectedOrg?.slug,
|
||||
data: connections,
|
||||
error: connectionsError,
|
||||
isLoading: isLoadingConnections,
|
||||
isSuccess: isSuccessConnections,
|
||||
isError: isErrorConnections,
|
||||
} = useGitHubConnectionsQuery({
|
||||
organizationId: selectedOrg?.id,
|
||||
})
|
||||
|
||||
const { data: branches } = useBranchesQuery({ projectRef })
|
||||
@@ -73,17 +73,10 @@ const CreateBranchModal = ({ visible, onClose }: CreateBranchModalProps) => {
|
||||
},
|
||||
})
|
||||
|
||||
const githubIntegration = integrations?.find(
|
||||
(integration) =>
|
||||
integration.integration.name === 'GitHub' &&
|
||||
integration.connections.some(
|
||||
(connection) => connection.supabase_project_ref === projectDetails?.parentRef
|
||||
)
|
||||
const githubConnection = connections?.find(
|
||||
(connection) => connection.project.ref === projectDetails?.parentRef
|
||||
)
|
||||
const githubConnection = githubIntegration?.connections?.find(
|
||||
(connection) => connection.supabase_project_ref === projectDetails?.parentRef
|
||||
)
|
||||
const [repoOwner, repoName] = githubConnection?.metadata.name.split('/') || []
|
||||
const [repoOwner, repoName] = githubConnection?.repository.name.split('/') ?? []
|
||||
|
||||
const formId = 'create-branch-form'
|
||||
const FormSchema = z.object({
|
||||
@@ -98,10 +91,12 @@ const CreateBranchModal = ({ visible, onClose }: CreateBranchModalProps) => {
|
||||
|
||||
if (val.length > 0) {
|
||||
try {
|
||||
if (!githubConnection?.id) {
|
||||
throw new Error('No GitHub connection found')
|
||||
}
|
||||
|
||||
await checkGithubBranchValidity({
|
||||
organizationIntegrationId: githubIntegration?.id,
|
||||
repoOwner,
|
||||
repoName,
|
||||
connectionId: githubConnection.id,
|
||||
branchName: val,
|
||||
})
|
||||
setIsValid(true)
|
||||
@@ -154,20 +149,20 @@ const CreateBranchModal = ({ visible, onClose }: CreateBranchModalProps) => {
|
||||
confirmText="Create Preview Branch"
|
||||
>
|
||||
<Modal.Content className="pt-3 pb-1">
|
||||
{isLoadingIntegrations && <GenericSkeletonLoader />}
|
||||
{isErrorIntegrations && (
|
||||
{isLoadingConnections && <GenericSkeletonLoader />}
|
||||
{isErrorConnections && (
|
||||
<AlertError
|
||||
error={integrationsError}
|
||||
error={connectionsError}
|
||||
subject="Failed to retrieve Github repository information"
|
||||
/>
|
||||
)}
|
||||
{isSuccessIntegrations && (
|
||||
{isSuccessConnections && (
|
||||
<div>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your project is currently connected to the repository:
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<p>{githubConnection?.metadata.name}</p>
|
||||
<p>{githubConnection?.repository.name}</p>
|
||||
<Link
|
||||
href={`https://github.com/${repoOwner}/${repoName}`}
|
||||
target="_blank"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { noop } from 'lodash'
|
||||
import { Button, IconChevronRight } from 'ui'
|
||||
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
import { GitHubPullRequest } from 'data/integrations/integrations-github-pull-requests-query'
|
||||
import { GitHubPullRequest } from 'data/integrations/github-pull-requests-query'
|
||||
import { BranchLoader, BranchManagementSection, BranchRow, BranchRowLoader } from './BranchPanels'
|
||||
import { PreviewBranchesEmptyState } from './EmptyStates'
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { groupBy } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { Button, IconPlus } from 'ui'
|
||||
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import NoSearchResults from 'components/ui/NoSearchResults'
|
||||
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import {
|
||||
OverdueInvoicesResponse,
|
||||
@@ -17,6 +17,7 @@ import { useSelectedOrganization } from 'hooks'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { makeRandomString } from 'lib/helpers'
|
||||
import { Organization, ResponseError } from 'types'
|
||||
import { Button, IconPlus } from 'ui'
|
||||
import ProjectCard from './ProjectCard'
|
||||
import ShimmeringCard from './ShimmeringCard'
|
||||
|
||||
@@ -139,9 +140,23 @@ const OrganizationProjects = ({
|
||||
: sortedProjects
|
||||
|
||||
const { data: integrations } = useOrgIntegrationsQuery({ orgSlug: organization?.slug })
|
||||
const githubConnections = integrations
|
||||
?.filter((integration) => integration.integration.name === 'GitHub')
|
||||
.flatMap((integration) => integration.connections)
|
||||
const { data: connections } = useGitHubConnectionsQuery({ organizationId: organization?.id })
|
||||
const githubConnections = connections?.map((connection) => ({
|
||||
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 any,
|
||||
}))
|
||||
const vercelConnections = integrations
|
||||
?.filter((integration) => integration.integration.name === 'Vercel')
|
||||
.flatMap((integration) => integration.connections)
|
||||
|
||||
@@ -2,6 +2,9 @@ import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { forwardRef, useCallback, useState } from 'react'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -14,14 +17,16 @@ import {
|
||||
IconTrash,
|
||||
Modal,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
|
||||
import {
|
||||
IntegrationConnection,
|
||||
IntegrationConnectionProps,
|
||||
} from 'components/interfaces/Integrations/IntegrationPanels'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { WarningIcon } from 'components/ui/Icons'
|
||||
import { useIntegrationsVercelConnectionSyncEnvsMutation } from 'data/integrations/integrations-vercel-connection-sync-envs-mutation'
|
||||
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useStore } from 'hooks'
|
||||
|
||||
interface IntegrationConnectionItemProps extends IntegrationConnectionProps {
|
||||
@@ -32,18 +37,23 @@ interface IntegrationConnectionItemProps extends IntegrationConnectionProps {
|
||||
const IntegrationConnectionItem = forwardRef<HTMLLIElement, IntegrationConnectionItemProps>(
|
||||
({ disabled, onDeleteConnection, ...props }, ref) => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const { type, connection } = props
|
||||
const { data: projects } = useProjectsQuery()
|
||||
const project = projects?.find((project) => project.ref === connection.supabase_project_ref)
|
||||
const isBranchingEnabled = project?.is_branch_enabled === true
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const onConfirm = useCallback(async () => {
|
||||
try {
|
||||
await onDeleteConnection(props.connection)
|
||||
await onDeleteConnection(connection)
|
||||
} finally {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [props.connection, onDeleteConnection])
|
||||
}, [connection, onDeleteConnection])
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setIsOpen(false)
|
||||
@@ -61,14 +71,15 @@ const IntegrationConnectionItem = forwardRef<HTMLLIElement, IntegrationConnectio
|
||||
})
|
||||
|
||||
const onReSyncEnvVars = useCallback(async () => {
|
||||
syncEnvs({ connectionId: props.connection.id })
|
||||
}, [props.connection, syncEnvs])
|
||||
syncEnvs({ connectionId: connection.id })
|
||||
}, [connection, syncEnvs])
|
||||
|
||||
const projectIntegrationUrl = `/project/[ref]/settings/integrations`
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntegrationConnection
|
||||
showNode={false}
|
||||
actions={
|
||||
disabled ? (
|
||||
<Button asChild disabled iconRight={<IconChevronDown />} type="default">
|
||||
@@ -86,37 +97,37 @@ const IntegrationConnectionItem = forwardRef<HTMLLIElement, IntegrationConnectio
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end">
|
||||
{props.type === 'Vercel' && (
|
||||
<>
|
||||
{router.pathname !== projectIntegrationUrl && (
|
||||
<DropdownMenuItem disabled={isSyncEnvLoading} asChild>
|
||||
<Link
|
||||
href={projectIntegrationUrl.replace(
|
||||
'[ref]',
|
||||
props.connection.supabase_project_ref
|
||||
)}
|
||||
>
|
||||
View project configuration
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault()
|
||||
onReSyncEnvVars()
|
||||
}}
|
||||
disabled={isSyncEnvLoading}
|
||||
>
|
||||
{isSyncEnvLoading ? (
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
) : (
|
||||
<IconRefreshCw size={14} />
|
||||
{router.pathname !== projectIntegrationUrl && (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={projectIntegrationUrl.replace(
|
||||
'[ref]',
|
||||
connection.supabase_project_ref
|
||||
)}
|
||||
<p>Resync environment variables</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
>
|
||||
Configure connection
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{type === 'Vercel' && (
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault()
|
||||
onReSyncEnvVars()
|
||||
}}
|
||||
disabled={isSyncEnvLoading}
|
||||
>
|
||||
{isSyncEnvLoading ? (
|
||||
<IconLoader className="animate-spin" size={14} />
|
||||
) : (
|
||||
<IconRefreshCw size={14} />
|
||||
)}
|
||||
<p>Resync environment variables</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(type === 'Vercel' || router.pathname !== projectIntegrationUrl) && (
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
<DropdownMenuItem className="space-x-2" onSelect={() => setIsOpen(true)}>
|
||||
<IconTrash size={14} />
|
||||
@@ -130,16 +141,27 @@ const IntegrationConnectionItem = forwardRef<HTMLLIElement, IntegrationConnectio
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
danger
|
||||
size={type === 'GitHub' && isBranchingEnabled ? 'medium' : 'small'}
|
||||
visible={isOpen}
|
||||
danger={true}
|
||||
header="Confirm to delete"
|
||||
buttonLabel="Delete"
|
||||
header={`Confirm to delete ${type} connection`}
|
||||
buttonLabel="Delete connection"
|
||||
onSelectCancel={onCancel}
|
||||
onSelectConfirm={onConfirm}
|
||||
>
|
||||
<Modal.Content>
|
||||
<p className="py-4 text-sm text-foreground-light">
|
||||
{`This action cannot be undone. Are you sure you want to delete this connection?`}
|
||||
<Modal.Content className="py-4 flex flex-col gap-y-4">
|
||||
{type === 'GitHub' && isBranchingEnabled && (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<WarningIcon />
|
||||
<AlertTitle_Shadcn_>Branching will be disabled for this project</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Deleting this GitHub connection will remove all preview branches on this project,
|
||||
and also disable branching for {project.name}
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
<p className="text-sm text-foreground-light">
|
||||
This action cannot be undone. Are you sure you want to delete this {type} connection?
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
|
||||
@@ -154,7 +154,7 @@ const IntegrationConnection = React.forwardRef<HTMLLIElement, IntegrationConnect
|
||||
ref={ref}
|
||||
key={connection.id}
|
||||
{...props}
|
||||
className={cn(showNode && 'pl-8 ml-6 border-l border-muted', 'pb-2', 'relative')}
|
||||
className={cn(showNode && 'pl-8 ml-6 border-l border-muted', 'relative')}
|
||||
>
|
||||
{showNode && (
|
||||
<div className="absolute w-8 rounded-bl-full border-b border-l border-muted h-10 -left-px"></div>
|
||||
@@ -164,14 +164,16 @@ const IntegrationConnection = React.forwardRef<HTMLLIElement, IntegrationConnect
|
||||
orientation === 'horizontal'
|
||||
? 'flex items-center justify-between'
|
||||
: 'flex flex-col gap-3',
|
||||
'bg-surface-100 border shadow-sm px-8 py-4 rounded-lg',
|
||||
'bg-surface-100 border shadow-sm px-6 py-4 rounded-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={'flex flex-col gap-1'}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<HandleIcon type={'Supabase'} />
|
||||
<span className="text-sm">{project?.name}</span>
|
||||
<span title={project?.name} className="text-sm truncate">
|
||||
{project?.name}
|
||||
</span>
|
||||
<IconArrowRight size={14} className="text-foreground-lighter" strokeWidth={1.5} />
|
||||
{!connection?.metadata?.framework ? (
|
||||
<div className="bg-black text-white w-4 h-4 rounded flex items-center justify-center">
|
||||
@@ -185,7 +187,21 @@ const IntegrationConnection = React.forwardRef<HTMLLIElement, IntegrationConnect
|
||||
alt={`icon`}
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm truncate">{connection.metadata?.name}</span>
|
||||
{type === 'GitHub' ? (
|
||||
<a
|
||||
title={connection.metadata.name}
|
||||
href={`https://github.com/${connection.metadata?.name}`}
|
||||
className="text-sm truncate"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{connection.metadata?.name}
|
||||
</a>
|
||||
) : (
|
||||
<span title={connection.metadata.name} className="text-sm truncate">
|
||||
{connection.metadata?.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-0">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import {
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
CommandSeparator_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
IconChevronDown,
|
||||
PopoverContent_Shadcn_,
|
||||
@@ -14,6 +16,7 @@ import {
|
||||
Popover_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import ShimmerLine from 'components/ui/ShimmerLine'
|
||||
import {
|
||||
@@ -22,6 +25,7 @@ import {
|
||||
} from 'data/integrations/integrations.types'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { openInstallGitHubIntegrationWindow } from 'lib/github'
|
||||
|
||||
export interface Project {
|
||||
id: string
|
||||
@@ -32,14 +36,15 @@ export interface Project {
|
||||
export interface ForeignProject {
|
||||
id: string
|
||||
name: string
|
||||
installation_id?: number
|
||||
}
|
||||
|
||||
export interface ProjectLinkerProps {
|
||||
organizationIntegrationId: string | undefined
|
||||
organizationIntegrationId?: string
|
||||
foreignProjects: ForeignProject[]
|
||||
supabaseProjects: Project[]
|
||||
onCreateConnections: (variables: IntegrationConnectionsCreateVariables) => void
|
||||
installedConnections: IntegrationProjectConnection[] | undefined
|
||||
installedConnections?: IntegrationProjectConnection[]
|
||||
isLoading?: boolean
|
||||
integrationIcon: ReactNode
|
||||
getForeignProjectIcon?: (project: ForeignProject) => ReactNode
|
||||
@@ -47,6 +52,7 @@ export interface ProjectLinkerProps {
|
||||
onSkip?: () => void
|
||||
loadingForeignProjects?: boolean
|
||||
loadingSupabaseProjects?: boolean
|
||||
showNoEntitiesState?: boolean
|
||||
|
||||
defaultSupabaseProjectRef?: string
|
||||
defaultForeignProjectId?: string
|
||||
@@ -65,10 +71,12 @@ const ProjectLinker = ({
|
||||
onSkip,
|
||||
loadingForeignProjects,
|
||||
loadingSupabaseProjects,
|
||||
showNoEntitiesState = true,
|
||||
|
||||
defaultSupabaseProjectRef,
|
||||
defaultForeignProjectId,
|
||||
}: ProjectLinkerProps) => {
|
||||
const router = useRouter()
|
||||
const [supabaseProjectsComboBoxOpen, setSupabaseProjectsComboboxOpen] = useState(false)
|
||||
const [foreignProjectsComboBoxOpen, setForeignProjectsComboboxOpen] = useState(false)
|
||||
const supabaseProjectsComboBoxRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -106,7 +114,6 @@ const ProjectLinker = ({
|
||||
function onCreateConnections() {
|
||||
const projectDetails = selectedForeignProject
|
||||
|
||||
if (!organizationIntegrationId) return console.error('No integration ID set')
|
||||
if (!selectedForeignProject?.id) return console.error('No Foreign project ID set')
|
||||
if (!selectedSupabaseProject?.ref) return console.error('No Supabase project ref set')
|
||||
|
||||
@@ -118,7 +125,7 @@ const ProjectLinker = ({
|
||||
}
|
||||
|
||||
_onCreateConnections({
|
||||
organizationIntegrationId,
|
||||
organizationIntegrationId: organizationIntegrationId!,
|
||||
connection: {
|
||||
foreign_project_id: selectedForeignProject?.id,
|
||||
supabase_project_ref: selectedSupabaseProject?.ref,
|
||||
@@ -128,6 +135,11 @@ const ProjectLinker = ({
|
||||
},
|
||||
},
|
||||
orgSlug: selectedOrganization?.slug,
|
||||
new: {
|
||||
installation_id: selectedForeignProject.installation_id!,
|
||||
project_ref: selectedSupabaseProject.ref,
|
||||
repository_id: Number(selectedForeignProject.id),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -160,10 +172,10 @@ const ProjectLinker = ({
|
||||
|
||||
{loadingForeignProjects || loadingSupabaseProjects ? (
|
||||
<div className="w-1/2 mx-auto space-y-2 py-4">
|
||||
<p className="text-foreground text-center">Loading projects</p>
|
||||
<p className="text-sm text-foreground text-center">Loading projects</p>
|
||||
<ShimmerLine active />
|
||||
</div>
|
||||
) : noSupabaseProjects || noForeignProjects ? (
|
||||
) : showNoEntitiesState && (noSupabaseProjects || noForeignProjects) ? (
|
||||
<div className="text-center">
|
||||
<h5 className="text-foreground">No {missingEntity} Projects found</h5>
|
||||
<p className="text-foreground-light text-sm">
|
||||
@@ -174,7 +186,7 @@ const ProjectLinker = ({
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-0 w-full relative">
|
||||
<div className="flex justify-center gap-0 w-full relative">
|
||||
<Panel>
|
||||
<div className="bg-white shadow border rounded p-1 w-12 h-12 flex justify-center items-center">
|
||||
<img src={`${BASE_PATH}/img/supabase-logo.svg`} alt="Supabase" className="w-6" />
|
||||
@@ -188,7 +200,6 @@ const ProjectLinker = ({
|
||||
<Button
|
||||
ref={supabaseProjectsComboBoxRef}
|
||||
type="default"
|
||||
size="medium"
|
||||
block
|
||||
disabled={defaultSupabaseProjectRef !== undefined || loadingSupabaseProjects}
|
||||
loading={loadingSupabaseProjects}
|
||||
@@ -216,7 +227,7 @@ const ProjectLinker = ({
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0 w-full"
|
||||
className="p-0 !w-72"
|
||||
side="bottom"
|
||||
align="center"
|
||||
style={{ width: supabaseProjectsComboBoxRef.current?.offsetWidth }}
|
||||
@@ -248,13 +259,31 @@ const ProjectLinker = ({
|
||||
</CommandItem_Shadcn_>
|
||||
)
|
||||
})}
|
||||
{supabaseProjects.length === 0 && (
|
||||
<p className="text-xs text-foreground-lighter px-2 py-2">
|
||||
No projects found in this organization
|
||||
</p>
|
||||
)}
|
||||
</CommandGroup_Shadcn_>
|
||||
<CommandSeparator_Shadcn_ />
|
||||
<CommandGroup_Shadcn_>
|
||||
<CommandItem_Shadcn_
|
||||
className="flex gap-2 items-center cursor-pointer"
|
||||
onClick={() => router.push(`/new/${selectedOrganization?.slug}`)}
|
||||
onSelect={() => router.push(`/new/${selectedOrganization?.slug}`)}
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
<span>Create a new project</span>
|
||||
</CommandItem_Shadcn_>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</Panel>
|
||||
<div className="border border-foreground-lighter h-px w-16 border-dashed self-end mb-5"></div>
|
||||
|
||||
<div className="border border-foreground-lighter h-px w-16 border-dashed self-end mb-4" />
|
||||
|
||||
<Panel>
|
||||
<div className="bg-black shadow rounded p-1 w-12 h-12 flex justify-center items-center">
|
||||
{integrationIcon}
|
||||
@@ -268,7 +297,6 @@ const ProjectLinker = ({
|
||||
<Button
|
||||
ref={foreignProjectsComboBoxRef}
|
||||
type="default"
|
||||
size="medium"
|
||||
block
|
||||
disabled={loadingForeignProjects}
|
||||
loading={loadingForeignProjects}
|
||||
@@ -288,7 +316,7 @@ const ProjectLinker = ({
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0 w-full"
|
||||
className="p-0 !w-72"
|
||||
side="bottom"
|
||||
align="center"
|
||||
style={{ width: foreignProjectsComboBoxRef.current?.offsetWidth }}
|
||||
@@ -309,11 +337,28 @@ const ProjectLinker = ({
|
||||
setForeignProjectsComboboxOpen(false)
|
||||
}}
|
||||
>
|
||||
{getForeignProjectIcon?.(project) ?? integrationIcon}
|
||||
<span>{project.name}</span>
|
||||
<div>{getForeignProjectIcon?.(project) ?? integrationIcon}</div>
|
||||
<span className="truncate" title={project.name}>
|
||||
{project.name}
|
||||
</span>
|
||||
</CommandItem_Shadcn_>
|
||||
)
|
||||
})}
|
||||
{foreignProjects.length === 0 && (
|
||||
<p className="text-xs text-foreground-lighter px-2 py-2">
|
||||
No GitHub repositories found
|
||||
</p>
|
||||
)}
|
||||
</CommandGroup_Shadcn_>
|
||||
<CommandSeparator_Shadcn_ />
|
||||
<CommandGroup_Shadcn_>
|
||||
<CommandItem_Shadcn_
|
||||
className="flex gap-2 items-center cursor-pointer"
|
||||
onSelect={() => openInstallGitHubIntegrationWindow('install')}
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
<span>Add GitHub Repositories</span>
|
||||
</CommandItem_Shadcn_>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
|
||||
+74
-83
@@ -1,12 +1,8 @@
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import {
|
||||
EmptyIntegrationConnection,
|
||||
IntegrationConnectionHeader,
|
||||
IntegrationInstallation,
|
||||
} from 'components/interfaces/Integrations/IntegrationPanels'
|
||||
import VercelSection from 'components/interfaces/Settings/Integrations/VercelIntegration/VercelSection'
|
||||
import { EmptyIntegrationConnection } from 'components/interfaces/Integrations/IntegrationPanels'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import VercelSection from 'components/interfaces/Settings/Integrations/VercelIntegration/VercelSection'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDivider,
|
||||
@@ -14,12 +10,11 @@ import {
|
||||
ScaffoldSectionContent,
|
||||
ScaffoldSectionDetail,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import { useIntegrationsGitHubInstalledConnectionDeleteMutation } from 'data/integrations/integrations-github-connection-delete-mutation'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { IntegrationName, IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { useGitHubConnectionDeleteMutation } from 'data/integrations/github-connection-delete-mutation'
|
||||
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { useSelectedOrganization, useStore } from 'hooks'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { pluralize } from 'lib/helpers'
|
||||
import { BASE_PATH, OPT_IN_TAGS } from 'lib/constants'
|
||||
import { useSidePanelsStateSnapshot } from 'state/side-panels'
|
||||
import { IntegrationConnectionItem } from '../../Integrations/IntegrationConnection'
|
||||
import SidePanelGitHubRepoLinker from './SidePanelGitHubRepoLinker'
|
||||
@@ -38,41 +33,36 @@ const IntegrationImageHandler = ({ title }: { title: 'vercel' | 'github' }) => {
|
||||
const IntegrationSettings = () => {
|
||||
const { ui } = useStore()
|
||||
const org = useSelectedOrganization()
|
||||
const { data } = useOrgIntegrationsQuery({ orgSlug: org?.slug })
|
||||
const hasAccessToBranching = org?.opt_in_tags?.includes(OPT_IN_TAGS.PREVIEW_BRANCHES) ?? false
|
||||
const { data: connections } = useGitHubConnectionsQuery({ organizationId: org?.id })
|
||||
|
||||
const { mutate: deleteGitHubConnection } = useGitHubConnectionDeleteMutation({
|
||||
onSuccess: () => {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully deleted Github connection',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const sidePanelsStateSnapshot = useSidePanelsStateSnapshot()
|
||||
|
||||
const { mutate: deleteGitHubConnection } = useIntegrationsGitHubInstalledConnectionDeleteMutation(
|
||||
{
|
||||
onSuccess: () => {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully deleted Github connection',
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const githubIntegrations = data?.filter(
|
||||
(integration) => integration.integration.name === 'GitHub'
|
||||
)
|
||||
|
||||
const onAddGitHubConnection = useCallback(
|
||||
(integrationId: string) => {
|
||||
sidePanelsStateSnapshot.setGithubConnectionsIntegrationId(integrationId)
|
||||
sidePanelsStateSnapshot.setGithubConnectionsOpen(true)
|
||||
},
|
||||
[sidePanelsStateSnapshot]
|
||||
)
|
||||
const onAddGitHubConnection = useCallback(() => {
|
||||
sidePanelsStateSnapshot.setGithubConnectionsOpen(true)
|
||||
}, [sidePanelsStateSnapshot])
|
||||
|
||||
const onDeleteGitHubConnection = useCallback(
|
||||
async (connection: IntegrationProjectConnection) => {
|
||||
if (!org?.id) {
|
||||
throw new Error('Organization not found')
|
||||
}
|
||||
|
||||
deleteGitHubConnection({
|
||||
connectionId: connection.id,
|
||||
integrationId: connection.organization_integration_id,
|
||||
orgSlug: org?.slug,
|
||||
organizationId: org.id,
|
||||
})
|
||||
},
|
||||
[deleteGitHubConnection, org?.slug]
|
||||
[deleteGitHubConnection, org?.id]
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -91,8 +81,6 @@ Connect any of your GitHub repositories to a project.
|
||||
|
||||
You will be able to connect a GitHub repository to a Supabase project.
|
||||
The GitHub app will watch for changes in your repository such as file changes, branch changes as well as pull request activity.
|
||||
|
||||
These connections will be part of a GitHub workflow that is currently in development.
|
||||
`
|
||||
|
||||
const GitHubSection = () => (
|
||||
@@ -104,51 +92,54 @@ These connections will be part of a GitHub workflow that is currently in develop
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent>
|
||||
<Markdown content={GitHubContentSectionTop} />
|
||||
{githubIntegrations &&
|
||||
githubIntegrations.length > 0 &&
|
||||
githubIntegrations.map((integration, i) => {
|
||||
const ConnectionHeaderTitle = `${integration.connections.length} project ${pluralize(
|
||||
integration.connections.length,
|
||||
'connection'
|
||||
)} `
|
||||
|
||||
return (
|
||||
<div key={integration.id}>
|
||||
<IntegrationInstallation title={'GitHub'} integration={integration} />
|
||||
{integration.connections.length > 0 ? (
|
||||
<>
|
||||
<IntegrationConnectionHeader
|
||||
title={ConnectionHeaderTitle}
|
||||
markdown={`Repository connections for GitHub`}
|
||||
/>
|
||||
<ul className="flex flex-col">
|
||||
{integration.connections.map((connection) => (
|
||||
<IntegrationConnectionItem
|
||||
key={connection.id}
|
||||
connection={connection}
|
||||
type={'GitHub' as IntegrationName}
|
||||
onDeleteConnection={onDeleteGitHubConnection}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<IntegrationConnectionHeader
|
||||
markdown={`### ${integration.connections.length} project ${pluralize(
|
||||
integration.connections.length,
|
||||
'connection'
|
||||
)} Repository connections for GitHub`}
|
||||
/>
|
||||
)}
|
||||
<EmptyIntegrationConnection
|
||||
onClick={() => onAddGitHubConnection(integration.id)}
|
||||
orgSlug={org?.slug}
|
||||
>
|
||||
Add new project connection
|
||||
</EmptyIntegrationConnection>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<ul className="flex flex-col gap-y-2">
|
||||
{connections?.map((connection) => (
|
||||
<IntegrationConnectionItem
|
||||
key={connection.id}
|
||||
connection={{
|
||||
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 any,
|
||||
}}
|
||||
type="GitHub"
|
||||
onDeleteConnection={onDeleteGitHubConnection}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
{hasAccessToBranching ? (
|
||||
<EmptyIntegrationConnection
|
||||
onClick={onAddGitHubConnection}
|
||||
orgSlug={org?.slug}
|
||||
showNode={false}
|
||||
>
|
||||
Add new project connection
|
||||
</EmptyIntegrationConnection>
|
||||
) : (
|
||||
<p className="text-sm text-foreground-light">
|
||||
Access to{' '}
|
||||
<a
|
||||
href="https://supabase.com/docs/guides/platform/branching"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-foreground"
|
||||
>
|
||||
branching
|
||||
</a>{' '}
|
||||
is required to add GitHub connections.
|
||||
</p>
|
||||
)}
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
|
||||
+61
-67
@@ -1,16 +1,17 @@
|
||||
import { useMemo } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { Button, SidePanel } from 'ui'
|
||||
|
||||
import ProjectLinker from 'components/interfaces/Integrations/ProjectLinker'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { useIntegrationGitHubConnectionsCreateMutation } from 'data/integrations/integrations-github-connections-create-mutation'
|
||||
import { useGitHubReposQuery } from 'data/integrations/integrations-github-repos-query'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useGitHubAuthorizationQuery } from 'data/integrations/github-authorization-query'
|
||||
import { useGitHubConnectionCreateMutation } from 'data/integrations/github-connection-create-mutation'
|
||||
import { useGitHubRepositoriesQuery } from 'data/integrations/github-repositories-query'
|
||||
import { IntegrationConnectionsCreateVariables } from 'data/integrations/integrations.types'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { openInstallGitHubIntegrationWindow } from 'lib/github'
|
||||
import { EMPTY_ARR } from 'lib/void'
|
||||
import { SidePanel } from 'ui'
|
||||
import { useIntegrationsGitHubInstalledConnectionDeleteMutation } from 'data/integrations/integrations-github-connection-delete-mutation'
|
||||
import { IntegrationConnectionsCreateVariables } from 'data/integrations/integrations.types'
|
||||
import { useSidePanelsStateSnapshot } from 'state/side-panels'
|
||||
|
||||
const GITHUB_ICON = (
|
||||
@@ -32,31 +33,17 @@ const SidePanelGitHubRepoLinker = ({ projectRef }: SidePanelGitHubRepoLinkerProp
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
const sidePanelStateSnapshot = useSidePanelsStateSnapshot()
|
||||
|
||||
const organizationIntegrationId = sidePanelStateSnapshot.githubConnectionsIntegrationId
|
||||
const { data: gitHubAuthorization, isLoading: isLoadingGitHubAuthorization } =
|
||||
useGitHubAuthorizationQuery()
|
||||
|
||||
const { data: integrationData } = useOrgIntegrationsQuery({
|
||||
orgSlug: selectedOrganization?.slug,
|
||||
})
|
||||
const githubIntegrations = integrationData?.filter(
|
||||
(integration) => integration.integration.name === 'GitHub'
|
||||
) // github
|
||||
const existingProjectGithubConnection = githubIntegrations?.[0]?.connections.find(
|
||||
(connection) => connection.supabase_project_ref === projectRef
|
||||
)
|
||||
|
||||
/**
|
||||
* Find the right integration
|
||||
*
|
||||
* we use the snapshot.organizationIntegrationId which should be set whenever this sidepanel is opened
|
||||
*/
|
||||
const selectedIntegration = githubIntegrations?.find((x) => x.id === organizationIntegrationId)
|
||||
// [Alaister]: temp override with <any> until the typegen is fixed
|
||||
const { data: githubReposData, isLoading: isLoadingGitHubRepos } =
|
||||
useGitHubRepositoriesQuery<any[]>()
|
||||
|
||||
/**
|
||||
* Supabase projects available
|
||||
*/
|
||||
const { data: supabaseProjectsData, isLoading: isLoadingSupabaseProjects } = useProjectsQuery({
|
||||
enabled: organizationIntegrationId !== undefined,
|
||||
})
|
||||
const { data: supabaseProjectsData, isLoading: isLoadingSupabaseProjects } = useProjectsQuery()
|
||||
|
||||
const supabaseProjects = useMemo(
|
||||
() =>
|
||||
@@ -67,45 +54,36 @@ const SidePanelGitHubRepoLinker = ({ projectRef }: SidePanelGitHubRepoLinkerProp
|
||||
[selectedOrganization?.id, supabaseProjectsData]
|
||||
)
|
||||
|
||||
const { data: githubProjectsData, isLoading: isLoadingGitHubRepos } = useGitHubReposQuery(
|
||||
{
|
||||
integrationId: organizationIntegrationId,
|
||||
},
|
||||
{ enabled: organizationIntegrationId !== undefined }
|
||||
)
|
||||
|
||||
const githubRepos = useMemo(
|
||||
() =>
|
||||
githubProjectsData?.map((repo) => ({
|
||||
githubReposData?.map((repo: any) => ({
|
||||
id: repo.id.toString(),
|
||||
name: repo.full_name,
|
||||
name: repo.name,
|
||||
installation_id: repo.installation_id,
|
||||
})) ?? EMPTY_ARR,
|
||||
[githubProjectsData]
|
||||
[githubReposData]
|
||||
)
|
||||
|
||||
const { mutate: createConnections, isLoading: isCreatingConnection } =
|
||||
useIntegrationGitHubConnectionsCreateMutation({
|
||||
useGitHubConnectionCreateMutation({
|
||||
onSuccess() {
|
||||
toast.success('Successfully linked project to repository!')
|
||||
sidePanelStateSnapshot.setGithubConnectionsOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: deleteGitHubConnection } =
|
||||
useIntegrationsGitHubInstalledConnectionDeleteMutation()
|
||||
|
||||
const createGithubConnection = (variables: IntegrationConnectionsCreateVariables) => {
|
||||
const existingProjectGithubConnection = githubIntegrations?.[0].connections.find(
|
||||
(connection) => connection.supabase_project_ref === variables.connection.supabase_project_ref
|
||||
)
|
||||
if (existingProjectGithubConnection !== undefined && selectedOrganization !== undefined) {
|
||||
deleteGitHubConnection({
|
||||
connectionId: existingProjectGithubConnection.id,
|
||||
integrationId: existingProjectGithubConnection.organization_integration_id,
|
||||
orgSlug: selectedOrganization.slug,
|
||||
})
|
||||
const createGithubConnection = async (variables: IntegrationConnectionsCreateVariables) => {
|
||||
if (!selectedOrganization?.id) {
|
||||
throw new Error('No organization id')
|
||||
}
|
||||
if (!variables.new) {
|
||||
throw new Error('No new connection')
|
||||
}
|
||||
|
||||
createConnections(variables)
|
||||
createConnections({
|
||||
organizationId: selectedOrganization.id,
|
||||
connection: variables.new,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -125,22 +103,38 @@ const SidePanelGitHubRepoLinker = ({ projectRef }: SidePanelGitHubRepoLinkerProp
|
||||
Check the details below before proceeding
|
||||
`}
|
||||
/>
|
||||
</SidePanel.Content>
|
||||
<SidePanel.Content className="flex flex-col gap-2">
|
||||
<ProjectLinker
|
||||
defaultSupabaseProjectRef={projectRef}
|
||||
defaultForeignProjectId={existingProjectGithubConnection?.foreign_project_id}
|
||||
organizationIntegrationId={selectedIntegration?.id}
|
||||
foreignProjects={githubRepos}
|
||||
supabaseProjects={supabaseProjects}
|
||||
onCreateConnections={createGithubConnection}
|
||||
installedConnections={selectedIntegration?.connections}
|
||||
isLoading={isCreatingConnection}
|
||||
loadingForeignProjects={isLoadingGitHubRepos}
|
||||
loadingSupabaseProjects={isLoadingSupabaseProjects}
|
||||
integrationIcon={GITHUB_ICON}
|
||||
choosePrompt="Choose GitHub Repo"
|
||||
/>
|
||||
|
||||
{gitHubAuthorization === null ? (
|
||||
<div className="flex flex-col items-center justify-center mt-8 relative border rounded-lg p-12 bg shadow px-20s">
|
||||
<p className="text-sm text-center">
|
||||
Connect your Supabase projects with your GitHub repositories
|
||||
</p>
|
||||
<p className="text-sm text-center text-foreground-light">
|
||||
Authorize with GitHub to retrieve your GitHub repositories
|
||||
</p>
|
||||
<Button
|
||||
className="w-min mt-3"
|
||||
onClick={() => {
|
||||
openInstallGitHubIntegrationWindow('authorize')
|
||||
}}
|
||||
>
|
||||
Authorize GitHub
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<ProjectLinker
|
||||
defaultSupabaseProjectRef={projectRef}
|
||||
foreignProjects={githubRepos}
|
||||
supabaseProjects={supabaseProjects}
|
||||
onCreateConnections={createGithubConnection}
|
||||
isLoading={isCreatingConnection}
|
||||
loadingForeignProjects={isLoadingGitHubRepos}
|
||||
loadingSupabaseProjects={isLoadingSupabaseProjects}
|
||||
integrationIcon={GITHUB_ICON}
|
||||
choosePrompt="Choose GitHub Repo"
|
||||
showNoEntitiesState={false}
|
||||
/>
|
||||
)}
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
|
||||
+165
-170
@@ -2,12 +2,12 @@ import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { GitBranch, RotateCcw, Shield } from 'lucide-react'
|
||||
import { useRef, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import toast from 'react-hot-toast'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
cn,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
@@ -27,46 +27,45 @@ import {
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
Switch,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useGithubConnectionUpdateMutation } from 'data/integrations/github-connection-update-mutate'
|
||||
import { useGithubBranchesQuery } from 'data/integrations/integrations-github-branches-query'
|
||||
import { Integration, IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { useSelectedProject, useStore } from 'hooks'
|
||||
import { useGitHubBranchesQuery } from 'data/integrations/github-branches-query'
|
||||
import { useGitHubConnectionUpdateMutation } from 'data/integrations/github-connection-update-mutation'
|
||||
import { IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
|
||||
const GitHubIntegrationConnectionForm = ({
|
||||
connection,
|
||||
integration,
|
||||
}: {
|
||||
interface GitHubIntegrationConnectionFormProps {
|
||||
connection: IntegrationProjectConnection
|
||||
integration: Integration
|
||||
}) => {
|
||||
const { ui } = useStore()
|
||||
}
|
||||
|
||||
const GitHubIntegrationConnectionForm = ({ connection }: GitHubIntegrationConnectionFormProps) => {
|
||||
const org = useSelectedOrganization()
|
||||
const project = useSelectedProject()
|
||||
const [open, setOpen] = useState(false)
|
||||
const comboBoxRef = useRef<HTMLButtonElement>(null)
|
||||
const isBranchingEnabled =
|
||||
project?.is_branch_enabled === true || project?.parent_project_ref !== undefined
|
||||
|
||||
const githubProjectIntegration = integration?.connections.find(
|
||||
(connection) => connection.supabase_project_ref === project?.parentRef
|
||||
const { data: githubBranches, isLoading: isLoadingBranches } = useGitHubBranchesQuery(
|
||||
{
|
||||
connectionId: Number(connection.id),
|
||||
},
|
||||
{ enabled: isBranchingEnabled }
|
||||
)
|
||||
|
||||
const [repoOwner, repoName] = githubProjectIntegration?.metadata.name.split('/') ?? []
|
||||
|
||||
const { data: githubBranches, isLoading: isLoadingBranches } = useGithubBranchesQuery({
|
||||
organizationIntegrationId: integration?.id,
|
||||
repoOwner,
|
||||
repoName,
|
||||
})
|
||||
const { mutate: updateConnection, isLoading: isUpdatingConnection } =
|
||||
useGitHubConnectionUpdateMutation({
|
||||
onSuccess: () => toast.success('Successfully updated connection settings'),
|
||||
})
|
||||
|
||||
const { mutate: updateBranch, isLoading: isUpdatingProdBranch } = useBranchUpdateMutation({
|
||||
onSuccess: (data) => {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Changed Production Branch to ${data.git_branch}`,
|
||||
})
|
||||
toast.success(`Successfully updated production branch to ${data.git_branch}`)
|
||||
setOpen(false)
|
||||
},
|
||||
})
|
||||
@@ -93,156 +92,129 @@ const GitHubIntegrationConnectionForm = ({
|
||||
supabaseDirectory: z
|
||||
.string()
|
||||
.default(connection.metadata?.supabaseConfig?.supabaseDirectory ?? ''),
|
||||
supabaseChangesOnly: z
|
||||
.boolean()
|
||||
.default(connection.metadata?.supabaseConfig?.supabaseChangesOnly ?? false),
|
||||
})
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
supabaseDirectory: connection?.metadata?.supabaseConfig?.supabaseDirectory,
|
||||
supabaseChangesOnly: connection?.metadata?.supabaseConfig?.supabaseChangesOnly,
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: updateGithubConnection, isLoading: isUpdatingGithubConnection } =
|
||||
useGithubConnectionUpdateMutation({
|
||||
onSuccess: () => {
|
||||
ui.setNotification({ category: 'success', message: `Updated Supabase directory` })
|
||||
setOpen(false)
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
const metadata = {
|
||||
...connection.metadata,
|
||||
supabaseConfig: {
|
||||
...connection.metadata?.supabaseConfig,
|
||||
supabaseDirectory: data.supabaseDirectory,
|
||||
},
|
||||
}
|
||||
|
||||
updateGithubConnection({
|
||||
id: connection.id,
|
||||
metadata,
|
||||
organizationIntegrationId: integration.id,
|
||||
if (org?.id === undefined) return console.error('Org ID is required')
|
||||
updateConnection({
|
||||
connectionId: connection.id,
|
||||
organizationId: org?.id,
|
||||
workdir: data.supabaseDirectory,
|
||||
supabaseChangesOnly: data.supabaseChangesOnly,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 px-8 py-8">
|
||||
<div>
|
||||
<Label_Shadcn_ className="text-foreground">Production branch</Label_Shadcn_>
|
||||
<p className="text-xs text-foreground-light mb-3">
|
||||
All other branches will be treated as Preview branches
|
||||
</p>
|
||||
<div className="flex flex-col gap-6 px-6 py-4">
|
||||
{isBranchingEnabled && (
|
||||
<div>
|
||||
<Label_Shadcn_ className="text-foreground">Production branch</Label_Shadcn_>
|
||||
<p className="text-xs text-foreground-light mb-3">
|
||||
All other branches will be treated as Preview branches
|
||||
</p>
|
||||
|
||||
<Alert_Shadcn_ className="mb-4 w-96">
|
||||
<AlertTitle_Shadcn_ className="text-sm">
|
||||
Changing Git branch for Production Branch coming soon
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="text-xs">
|
||||
If you wish to change the Git branch that is used for the Production Branch you will
|
||||
need to disable Branching and opt back in.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
{/* <pre>! This should only work if branching is turned on !</pre> */}
|
||||
|
||||
{/* <pre>! This should only work if branching is turned on !</pre> */}
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild name="branch-selector">
|
||||
<Button
|
||||
disabled
|
||||
type="default"
|
||||
size="medium"
|
||||
ref={comboBoxRef}
|
||||
className={cn(
|
||||
'justify-start w-64',
|
||||
productionPreviewBranch?.git_branch === undefined ? 'text-foreground-light' : 'text'
|
||||
)}
|
||||
icon={
|
||||
productionPreviewBranch?.git_branch && (
|
||||
<Shield className="w-4 h-4 text-warning" strokeWidth={1} />
|
||||
)
|
||||
}
|
||||
loading={isUpdatingProdBranch || isLoadingBranches}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
<IconChevronDown className={''} />
|
||||
</span>
|
||||
}
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild name="branch-selector">
|
||||
<Button
|
||||
disabled
|
||||
type="default"
|
||||
size="medium"
|
||||
ref={comboBoxRef}
|
||||
className={cn(
|
||||
'justify-start w-80 mt-4',
|
||||
productionPreviewBranch?.git_branch === undefined
|
||||
? 'text-foreground-light'
|
||||
: 'text'
|
||||
)}
|
||||
icon={
|
||||
productionPreviewBranch?.git_branch && (
|
||||
<Shield className="w-4 h-4 text-warning" strokeWidth={1} />
|
||||
)
|
||||
}
|
||||
loading={isUpdatingProdBranch || isLoadingBranches}
|
||||
iconRight={
|
||||
<span className="grow flex justify-end">
|
||||
<IconChevronDown className={''} />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{productionPreviewBranch?.git_branch || 'Select a branch'}
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
style={{ width: comboBoxRef.current?.offsetWidth }}
|
||||
>
|
||||
{productionPreviewBranch?.git_branch || 'Select a branch'}
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
style={{ width: comboBoxRef.current?.offsetWidth }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Find branch..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No branches found</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{githubBranches?.map((branch) => {
|
||||
const active = branch.name === productionPreviewBranch?.git_branch
|
||||
return (
|
||||
<CommandItem_Shadcn_
|
||||
key={branch.name}
|
||||
value={branch.name.replaceAll('"', '')}
|
||||
className="cursor-pointer w-full flex items-center justify-between"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
onUpdateProductionBranch(branch.name)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{active ? (
|
||||
<Shield className="w-4 h-4 text-warning" strokeWidth={1} />
|
||||
) : (
|
||||
<GitBranch className="w-4 h-4" strokeWidth={1} />
|
||||
)}
|
||||
{branch.name}
|
||||
</div>
|
||||
{branch.name === productionPreviewBranch?.git_branch && <IconCheck />}
|
||||
</CommandItem_Shadcn_>
|
||||
)
|
||||
})}
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</div>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Find branch..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No branches found</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{githubBranches?.map((branch) => {
|
||||
const active = branch.name === productionPreviewBranch?.git_branch
|
||||
return (
|
||||
<CommandItem_Shadcn_
|
||||
key={branch.name}
|
||||
value={(branch.name as string).replaceAll('"', '')}
|
||||
className="cursor-pointer w-full flex items-center justify-between"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
onUpdateProductionBranch(branch.name)
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{active ? (
|
||||
<Shield className="w-4 h-4 text-warning" strokeWidth={1} />
|
||||
) : (
|
||||
<GitBranch className="w-4 h-4" strokeWidth={1} />
|
||||
)}
|
||||
{branch.name}
|
||||
</div>
|
||||
{branch.name === productionPreviewBranch?.git_branch && <IconCheck />}
|
||||
</CommandItem_Shadcn_>
|
||||
)
|
||||
})}
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormLabel_Shadcn_ className="!text">Supabase directory</FormLabel_Shadcn_>
|
||||
<FormDescription_Shadcn_ className="text-xs text-foreground-lighter mb-3">
|
||||
Migration and seed SQL files will be run from this directory.
|
||||
</FormDescription_Shadcn_>
|
||||
|
||||
<Alert_Shadcn_ className="mb-4 w-96">
|
||||
<AlertTitle_Shadcn_ className="text-sm">
|
||||
Changing Supabase directory is currently not supported
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_ className="text-xs">
|
||||
You will need to disable Branching and opt back into Branching to change the
|
||||
Production Branch. your Git repository.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-6">
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="supabaseDirectory"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="flex flex-row items-center gap-3 !space-y-0">
|
||||
<FormControl_Shadcn_ className="xl:w-96">
|
||||
<FormItem_Shadcn_ className="flex flex-col">
|
||||
<FormLabel_Shadcn_ className="!text">Supabase directory</FormLabel_Shadcn_>
|
||||
<FormDescription_Shadcn_ className="text-xs text-foreground-lighter !mt-0 !mb-1">
|
||||
Path in your repository where <code>supabase</code> directory for this connection
|
||||
lives.
|
||||
</FormDescription_Shadcn_>
|
||||
<FormControl_Shadcn_ className="flex gap-3">
|
||||
<div className="relative">
|
||||
<Input_Shadcn_
|
||||
disabled
|
||||
{...field}
|
||||
className="w-80"
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
form.reset()
|
||||
}
|
||||
if (event.key === 'Escape') form.reset()
|
||||
}}
|
||||
/>
|
||||
<RotateCcw
|
||||
@@ -250,34 +222,57 @@ const GitHubIntegrationConnectionForm = ({
|
||||
'text-foreground-lighter transition hover:text cursor-pointer',
|
||||
'w-4 h-4 absolute right-3 top-3',
|
||||
'duration-150',
|
||||
isUpdatingGithubConnection ||
|
||||
field.value !== connection.metadata?.supabaseConfig?.supabaseDirectory
|
||||
|
||||
field.value !== connection.metadata?.supabaseConfig?.supabaseDirectory
|
||||
? 'opacity-100 transition'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
onClick={() => form.reset()}
|
||||
/>
|
||||
<Button
|
||||
loading={isUpdatingConnection}
|
||||
className={cn(
|
||||
'duration-150 transition',
|
||||
field.value !== connection.metadata?.supabaseConfig?.supabaseDirectory
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
field.value === '' ||
|
||||
field.value === connection.metadata?.supabaseConfig?.supabaseDirectory
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl_Shadcn_>
|
||||
<Button
|
||||
className={cn(
|
||||
'duration-150',
|
||||
isUpdatingGithubConnection ||
|
||||
field.value !== connection.metadata?.supabaseConfig?.supabaseDirectory
|
||||
? 'opacity-100 transition'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
htmlType="submit"
|
||||
type="secondary"
|
||||
size="medium"
|
||||
loading={isUpdatingGithubConnection}
|
||||
disabled={
|
||||
field.value === connection.metadata?.supabaseConfig?.supabaseDirectory ||
|
||||
isUpdatingGithubConnection
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField_Shadcn_
|
||||
control={form.control}
|
||||
name="supabaseChangesOnly"
|
||||
render={({ field }) => (
|
||||
<FormItem_Shadcn_ className="space-y-0 flex gap-x-4">
|
||||
<FormControl_Shadcn_>
|
||||
<Switch
|
||||
className="mt-1"
|
||||
checked={field.value}
|
||||
onCheckedChange={(e) => {
|
||||
field.onChange(e)
|
||||
form.handleSubmit(onSubmit)()
|
||||
}}
|
||||
/>
|
||||
</FormControl_Shadcn_>
|
||||
<div>
|
||||
<FormLabel_Shadcn_ className="!text">Supabase changes only</FormLabel_Shadcn_>
|
||||
<FormDescription_Shadcn_ className="text-xs text-foreground-lighter">
|
||||
Trigger branch creation only when there were changes to <code>supabase</code>{' '}
|
||||
directory.
|
||||
</FormDescription_Shadcn_>
|
||||
</div>
|
||||
</FormItem_Shadcn_>
|
||||
)}
|
||||
/>
|
||||
|
||||
+125
-81
@@ -1,11 +1,8 @@
|
||||
import { useParams } from 'common'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { IntegrationConnectionItem } from 'components/interfaces/Integrations/IntegrationConnection'
|
||||
import {
|
||||
IntegrationConnectionHeader,
|
||||
IntegrationInstallation,
|
||||
} from 'components/interfaces/Integrations/IntegrationPanels'
|
||||
import { EmptyIntegrationConnection } from 'components/interfaces/Integrations/IntegrationPanels'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
@@ -13,13 +10,17 @@ import {
|
||||
ScaffoldSectionContent,
|
||||
ScaffoldSectionDetail,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import { useIntegrationsGitHubInstalledConnectionDeleteMutation } from 'data/integrations/integrations-github-connection-delete-mutation'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useBranchesDisableMutation } from 'data/branches/branches-disable-mutation'
|
||||
import { useGitHubConnectionDeleteMutation } from 'data/integrations/github-connection-delete-mutation'
|
||||
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { IntegrationName, IntegrationProjectConnection } from 'data/integrations/integrations.types'
|
||||
import { useSelectedOrganization, useSelectedProject, useStore } from 'hooks'
|
||||
import { pluralize } from 'lib/helpers'
|
||||
import { OPT_IN_TAGS } from 'lib/constants'
|
||||
import { useSidePanelsStateSnapshot } from 'state/side-panels'
|
||||
import { cn } from 'ui'
|
||||
import { IntegrationImageHandler } from '../IntegrationsSettings'
|
||||
import GitHubIntegrationConnectionForm from './GitHubIntegrationConnectionForm'
|
||||
import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
|
||||
const GitHubTitle = `GitHub Connections`
|
||||
|
||||
@@ -33,43 +34,63 @@ const GitHubContentSectionTop = `
|
||||
|
||||
You will be able to connect a GitHub repository to a Supabase project.
|
||||
The GitHub app will watch for changes in your repository such as file changes, branch changes as well as pull request activity.
|
||||
|
||||
These connections will be part of a GitHub workflow that is currently in development.
|
||||
`
|
||||
|
||||
const GitHubSection = () => {
|
||||
const { ui } = useStore()
|
||||
const { ref: projectRef } = useParams()
|
||||
const project = useSelectedProject()
|
||||
const org = useSelectedOrganization()
|
||||
const { data } = useOrgIntegrationsQuery({ orgSlug: org?.slug })
|
||||
const { ref: projectRef } = useParams()
|
||||
const sidePanelsStateSnapshot = useSidePanelsStateSnapshot()
|
||||
|
||||
const { data: allConnections } = useGitHubConnectionsQuery({ organizationId: org?.id })
|
||||
const { data: branches } = useBranchesQuery({ projectRef })
|
||||
|
||||
const { mutate: deleteGitHubConnection } = useGitHubConnectionDeleteMutation({
|
||||
onSuccess: () => {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully deleted Github connection',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const { mutate: disableBranching } = useBranchesDisableMutation()
|
||||
|
||||
const previewBranches = (branches ?? []).filter((branch) => !branch.is_default)
|
||||
const hasAccessToBranching = org?.opt_in_tags?.includes(OPT_IN_TAGS.PREVIEW_BRANCHES) ?? false
|
||||
const isBranch = project?.parent_project_ref !== undefined
|
||||
const isBranchingEnabled =
|
||||
project?.is_branch_enabled === true || project?.parent_project_ref !== undefined
|
||||
|
||||
const githubIntegrations = data?.filter(
|
||||
(integration) => integration.integration.name === 'GitHub'
|
||||
)
|
||||
const connections =
|
||||
allConnections?.filter((connection) =>
|
||||
isBranch
|
||||
? connection.project.ref === project.parent_project_ref
|
||||
: connection.project.ref === projectRef
|
||||
) ?? []
|
||||
|
||||
const { mutate: deleteGitHubConnection } = useIntegrationsGitHubInstalledConnectionDeleteMutation(
|
||||
{
|
||||
onSuccess: () => {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: 'Successfully deleted Github connection',
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
const onAddGitHubConnection = useCallback(() => {
|
||||
sidePanelsStateSnapshot.setGithubConnectionsOpen(true)
|
||||
}, [sidePanelsStateSnapshot])
|
||||
|
||||
const onDeleteGitHubConnection = useCallback(
|
||||
async (connection: IntegrationProjectConnection) => {
|
||||
deleteGitHubConnection({
|
||||
connectionId: connection.id,
|
||||
integrationId: connection.organization_integration_id,
|
||||
orgSlug: org?.slug,
|
||||
})
|
||||
if (isBranchingEnabled) {
|
||||
if (!projectRef) throw new Error('Project ref not found')
|
||||
disableBranching({ projectRef, branchIds: previewBranches?.map((branch) => branch.id) })
|
||||
}
|
||||
if (!org?.id) throw new Error('Organization not found')
|
||||
deleteGitHubConnection({ connectionId: connection.id, organizationId: org.id })
|
||||
},
|
||||
[deleteGitHubConnection, org?.slug]
|
||||
[
|
||||
deleteGitHubConnection,
|
||||
disableBranching,
|
||||
isBranchingEnabled,
|
||||
org?.id,
|
||||
previewBranches,
|
||||
projectRef,
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -81,62 +102,85 @@ const GitHubSection = () => {
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent>
|
||||
<Markdown content={GitHubContentSectionTop} />
|
||||
{githubIntegrations &&
|
||||
githubIntegrations.length > 0 &&
|
||||
githubIntegrations.map((integration, i) => {
|
||||
const connections = integration.connections.filter((connection) =>
|
||||
isBranch
|
||||
? connection.supabase_project_ref === project.parent_project_ref
|
||||
: connection.supabase_project_ref === projectRef
|
||||
)
|
||||
|
||||
return (
|
||||
<div key={integration.id}>
|
||||
<IntegrationInstallation
|
||||
title={'GitHub'}
|
||||
integration={integration}
|
||||
{connections.length > 0 ? (
|
||||
<ul className="flex flex-col">
|
||||
{connections.map((connection) => (
|
||||
<div key={connection.id} className="relative flex flex-col -gap-[1px] [&>li]:pb-0">
|
||||
<IntegrationConnectionItem
|
||||
showNode={false}
|
||||
disabled={isBranch}
|
||||
key={connection.id}
|
||||
connection={{
|
||||
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 any,
|
||||
}}
|
||||
type={'GitHub' as IntegrationName}
|
||||
onDeleteConnection={onDeleteGitHubConnection}
|
||||
className="!rounded-b-none !mb-0"
|
||||
/>
|
||||
{connections.length > 0 ? (
|
||||
<>
|
||||
<IntegrationConnectionHeader />
|
||||
<ul className="flex flex-col">
|
||||
{connections.map((connection) => (
|
||||
<div
|
||||
key={connection.id}
|
||||
className="relative flex flex-col -gap-[1px] [&>li]:pb-0"
|
||||
>
|
||||
<IntegrationConnectionItem
|
||||
showNode={false}
|
||||
disabled={isBranch}
|
||||
key={connection.id}
|
||||
connection={connection}
|
||||
type={'GitHub' as IntegrationName}
|
||||
onDeleteConnection={onDeleteGitHubConnection}
|
||||
className="!rounded-b-none !mb-0"
|
||||
/>
|
||||
|
||||
<div className="border-b border-l border-r rounded-b-lg">
|
||||
<GitHubIntegrationConnectionForm
|
||||
connection={connection}
|
||||
integration={integration}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<IntegrationConnectionHeader
|
||||
markdown={`### ${connections.length} project ${pluralize(
|
||||
connections.length,
|
||||
'connection'
|
||||
)} Repository connections for GitHub`}
|
||||
<div className="border-b border-l border-r rounded-b-lg">
|
||||
<GitHubIntegrationConnectionForm
|
||||
connection={{
|
||||
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,
|
||||
supabaseConfig: {
|
||||
supabaseDirectory: connection.workdir,
|
||||
supabaseChangesOnly: connection.supabase_changes_only,
|
||||
},
|
||||
} as any,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
))}
|
||||
</ul>
|
||||
) : hasAccessToBranching ? (
|
||||
<EmptyIntegrationConnection
|
||||
onClick={onAddGitHubConnection}
|
||||
orgSlug={org?.slug}
|
||||
showNode={false}
|
||||
>
|
||||
Add new project connection
|
||||
</EmptyIntegrationConnection>
|
||||
) : (
|
||||
<p className="text-sm text-foreground-light">
|
||||
Access to{' '}
|
||||
<a
|
||||
href="https://supabase.com/docs/guides/platform/branching"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-foreground"
|
||||
>
|
||||
branching
|
||||
</a>{' '}
|
||||
is required to add GitHub connections.
|
||||
</p>
|
||||
)}
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
|
||||
+35
-39
@@ -1,6 +1,7 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useParams } from 'common'
|
||||
import { last } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
@@ -19,17 +20,17 @@ import AlertError from 'components/ui/AlertError'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
|
||||
import { useProjectUpgradeEligibilityQuery } from 'data/config/project-upgrade-eligibility-query'
|
||||
import { useCheckGithubBranchValidity } from 'data/integrations/integrations-github-branch-check'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useCheckGithubBranchValidity } from 'data/integrations/github-branch-check-query'
|
||||
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { useGitHubRepositoriesQuery } from 'data/integrations/github-repositories-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useSelectedOrganization, useStore } from 'hooks'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import BranchingPITRNotice from './BranchingPITRNotice'
|
||||
import BranchingPlanNotice from './BranchingPlanNotice'
|
||||
import BranchingPostgresVersionNotice from './BranchingPostgresVersionNotice'
|
||||
import GithubRepositorySelection from './GithubRepositorySelection'
|
||||
import Link from 'next/link'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import BranchingPlanNotice from './BranchingPlanNotice'
|
||||
|
||||
const EnableBranchingModal = () => {
|
||||
const { ui } = useStore()
|
||||
@@ -43,14 +44,20 @@ const EnableBranchingModal = () => {
|
||||
const [isValid, setIsValid] = useState(false)
|
||||
|
||||
const {
|
||||
data: integrations,
|
||||
error: integrationsError,
|
||||
isLoading: isLoadingIntegrations,
|
||||
isSuccess: isSuccessIntegrations,
|
||||
isError: isErrorIntegrations,
|
||||
} = useOrgIntegrationsQuery({
|
||||
orgSlug: selectedOrg?.slug,
|
||||
})
|
||||
data: repositories,
|
||||
error: repositoriesError,
|
||||
isLoading: isLoadingRepositories,
|
||||
isSuccess: isSuccessRepositories,
|
||||
isError: isErrorRepositories,
|
||||
} = useGitHubRepositoriesQuery()
|
||||
|
||||
const {
|
||||
data: connections,
|
||||
error: connectionsError,
|
||||
isLoading: isLoadingConnections,
|
||||
isSuccess: isSuccessConnections,
|
||||
isError: isErrorConnections,
|
||||
} = useGitHubConnectionsQuery({ organizationId: selectedOrg?.id })
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -71,17 +78,8 @@ const EnableBranchingModal = () => {
|
||||
const hasPitrEnabled =
|
||||
(addons?.selected_addons ?? []).find((addon) => addon.type === 'pitr') !== undefined
|
||||
|
||||
const hasGithubIntegrationInstalled =
|
||||
integrations?.some((integration) => integration.integration.name === 'GitHub') ?? false
|
||||
const githubIntegration = integrations?.find(
|
||||
(integration) =>
|
||||
integration.integration.name === 'GitHub' &&
|
||||
integration.organization.slug === selectedOrg?.slug
|
||||
)
|
||||
const githubConnection = githubIntegration?.connections.find(
|
||||
(connection) => connection.supabase_project_ref === ref
|
||||
)
|
||||
const [repoOwner, repoName] = githubConnection?.metadata.name.split('/') ?? []
|
||||
const githubConnection = connections?.find((connection) => connection.project.ref === ref)
|
||||
const [repoOwner, repoName] = githubConnection?.repository.name.split('/') ?? []
|
||||
|
||||
const { mutateAsync: checkGithubBranchValidity, isLoading: isChecking } =
|
||||
useCheckGithubBranchValidity({ onError: () => {} })
|
||||
@@ -101,10 +99,12 @@ const EnableBranchingModal = () => {
|
||||
.refine(async (val) => {
|
||||
try {
|
||||
if (val.length > 0) {
|
||||
if (!githubConnection?.id) {
|
||||
throw new Error('No GitHub connection found')
|
||||
}
|
||||
|
||||
await checkGithubBranchValidity({
|
||||
organizationIntegrationId: githubIntegration?.id,
|
||||
repoOwner,
|
||||
repoName,
|
||||
connectionId: githubConnection.id,
|
||||
branchName: val,
|
||||
})
|
||||
setIsValid(true)
|
||||
@@ -123,9 +123,9 @@ const EnableBranchingModal = () => {
|
||||
defaultValues: { branchName: '' },
|
||||
})
|
||||
|
||||
const isLoading = isLoadingIntegrations || isLoadingUpgradeEligibility
|
||||
const isError = isErrorIntegrations || isErrorUpgradeEligibility
|
||||
const isSuccess = isSuccessIntegrations && isSuccessUpgradeEligibility
|
||||
const isLoading = isLoadingRepositories
|
||||
const isError = isErrorRepositories || isErrorUpgradeEligibility
|
||||
const isSuccess = isSuccessRepositories && isSuccessUpgradeEligibility
|
||||
|
||||
const canSubmit = form.getValues('branchName').length > 0 && !isChecking && isValid
|
||||
const onSubmit = (data: z.infer<typeof FormSchema>) => {
|
||||
@@ -183,15 +183,14 @@ const EnableBranchingModal = () => {
|
||||
<Modal.Separator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<>
|
||||
<Modal.Separator />
|
||||
<Modal.Content className="px-7 py-6">
|
||||
{isErrorIntegrations ? (
|
||||
{isErrorRepositories ? (
|
||||
<AlertError
|
||||
error={integrationsError}
|
||||
subject="Failed to retrieve integrations"
|
||||
error={repositoriesError}
|
||||
subject="Failed to retrieve repositories"
|
||||
/>
|
||||
) : isErrorUpgradeEligibility ? (
|
||||
<AlertError
|
||||
@@ -203,7 +202,6 @@ const EnableBranchingModal = () => {
|
||||
<Modal.Separator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isSuccess && (
|
||||
<>
|
||||
{isFreePlan ? (
|
||||
@@ -216,8 +214,7 @@ const EnableBranchingModal = () => {
|
||||
form={form}
|
||||
isChecking={isChecking}
|
||||
isValid={canSubmit}
|
||||
integration={githubIntegration}
|
||||
hasGithubIntegrationInstalled={hasGithubIntegrationInstalled}
|
||||
githubConnection={githubConnection}
|
||||
/>
|
||||
{!hasPitrEnabled && <BranchingPITRNotice />}
|
||||
</>
|
||||
@@ -262,11 +259,10 @@ const EnableBranchingModal = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Modal.Separator />
|
||||
|
||||
<Modal.Content className="px-7">
|
||||
<div className="flex items-center space-x-2 py-2 pb-4">
|
||||
<Button
|
||||
|
||||
+36
-49
@@ -1,6 +1,11 @@
|
||||
import clsx from 'clsx'
|
||||
import { useParams } from 'common'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
EmptyIntegrationConnection,
|
||||
IntegrationConnection,
|
||||
} from 'components/interfaces/Integrations/IntegrationPanels'
|
||||
import { GitHubConnection } from 'data/integrations/github-connections-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { useSidePanelsStateSnapshot } from 'state/side-panels'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -14,58 +19,30 @@ import {
|
||||
Modal,
|
||||
} from 'ui'
|
||||
|
||||
import {
|
||||
EmptyIntegrationConnection,
|
||||
IntegrationConnection,
|
||||
} from 'components/interfaces/Integrations/IntegrationPanels'
|
||||
import { Integration } from 'data/integrations/integrations.types'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { useSidePanelsStateSnapshot } from 'state/side-panels'
|
||||
|
||||
interface GithubRepositorySelectionProps {
|
||||
form: any
|
||||
isChecking: boolean
|
||||
isValid: boolean
|
||||
integration?: Integration
|
||||
hasGithubIntegrationInstalled: boolean
|
||||
githubConnection?: GitHubConnection
|
||||
}
|
||||
|
||||
const GithubRepositorySelection = ({
|
||||
form,
|
||||
isChecking,
|
||||
isValid,
|
||||
integration,
|
||||
hasGithubIntegrationInstalled,
|
||||
githubConnection,
|
||||
}: GithubRepositorySelectionProps) => {
|
||||
const { ref } = useParams()
|
||||
const org = useSelectedOrganization()
|
||||
|
||||
const githubConnection = integration?.connections.find(
|
||||
(connection) => connection.supabase_project_ref === ref
|
||||
)
|
||||
|
||||
const sidePanels = useSidePanelsStateSnapshot()
|
||||
const githubIntegrationAppUrl =
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
|
||||
? `https://github.com/apps/supabase/installations/new?state=${ref}`
|
||||
: process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
|
||||
? `https://github.com/apps/supabase-staging/installations/new?state=${ref}`
|
||||
: `https://github.com/apps/supabase-local-testing/installations/new?state=${ref}`
|
||||
|
||||
function onSelectConnectRepo() {
|
||||
if (integration) {
|
||||
sidePanels.setGithubConnectionsOpen(true)
|
||||
sidePanels.setGithubConnectionsIntegrationId(integration.id)
|
||||
}
|
||||
sidePanels.setGithubConnectionsOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'border-t border-b',
|
||||
!hasGithubIntegrationInstalled ? 'border-warning-300 bg-warning-200' : ''
|
||||
)}
|
||||
>
|
||||
<div className="border-t border-b">
|
||||
<Modal.Content className="px-7">
|
||||
<div className="py-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -77,31 +54,35 @@ const GithubRepositorySelection = ({
|
||||
? 'Your database preview branches will be based on the branches in the following repository that your project is connected with:'
|
||||
: 'Your database preview branches will be based on the branches in the Git repository that your project is connected with.'}
|
||||
</p>
|
||||
{!hasGithubIntegrationInstalled && (
|
||||
<Button asChild type="default" className="!mt-3">
|
||||
<Link href={githubIntegrationAppUrl}>Install Github Integration</Link>
|
||||
</Button>
|
||||
)}
|
||||
{hasGithubIntegrationInstalled && !githubConnection && (
|
||||
<EmptyIntegrationConnection
|
||||
showNode={false}
|
||||
onClick={() => onSelectConnectRepo()}
|
||||
orgSlug={org?.slug}
|
||||
/>
|
||||
)}
|
||||
{integration && githubConnection && (
|
||||
|
||||
{githubConnection ? (
|
||||
<>
|
||||
<ul className="mb-3">
|
||||
<IntegrationConnection
|
||||
type={'GitHub'}
|
||||
connection={githubConnection}
|
||||
connection={{
|
||||
id: String(githubConnection.id),
|
||||
added_by: {
|
||||
id: String(githubConnection.user?.id),
|
||||
primary_email: githubConnection.user?.primary_email ?? '',
|
||||
username: githubConnection.user?.username ?? '',
|
||||
},
|
||||
foreign_project_id: String(githubConnection.repository.id),
|
||||
supabase_project_ref: githubConnection.project.ref,
|
||||
organization_integration_id: 'unused',
|
||||
inserted_at: githubConnection.inserted_at,
|
||||
updated_at: githubConnection.updated_at,
|
||||
metadata: {
|
||||
name: githubConnection.repository.name,
|
||||
} as any,
|
||||
}}
|
||||
showNode={false}
|
||||
actions={
|
||||
<Button type="default" onClick={() => onSelectConnectRepo()}>
|
||||
Configure connection
|
||||
</Button>
|
||||
}
|
||||
orientation="vertical"
|
||||
orientation="horizontal"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
@@ -129,6 +110,12 @@ const GithubRepositorySelection = ({
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<EmptyIntegrationConnection
|
||||
showNode={false}
|
||||
onClick={() => onSelectConnectRepo()}
|
||||
orgSlug={org?.slug}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal.Content>
|
||||
|
||||
@@ -10,7 +10,7 @@ import ProjectDropdown from 'components/layouts/AppLayout/ProjectDropdown'
|
||||
import { getResourcesExceededLimitsOrg } from 'components/ui/OveragesBanner/OveragesBanner.utils'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useOrgUsageQuery } from 'data/usage/org-usage-query'
|
||||
import { useFlag, useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import BreadcrumbsView from './BreadcrumbsView'
|
||||
import FeedbackDropdown from './FeedbackDropdown'
|
||||
@@ -21,9 +21,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
const { ref: projectRef } = useParams()
|
||||
const selectedProject = useSelectedProject()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const isBranchingEnabled =
|
||||
selectedProject?.is_branch_enabled === true || selectedProject?.parent_project_ref !== undefined
|
||||
const isBranchingEnabled = selectedProject?.is_branch_enabled === true
|
||||
|
||||
const { data: orgUsage } = useOrgUsageQuery({ orgSlug: selectedOrganization?.slug })
|
||||
|
||||
|
||||
Vendored
+121
-143
@@ -796,15 +796,15 @@ export interface paths {
|
||||
/** Updates a Vercel connection for a supabase project */
|
||||
patch: operations['VercelConnectionsController_updateVercelConnection']
|
||||
}
|
||||
'/platform/integrations/github': {
|
||||
/** Create github integration */
|
||||
post: operations['GitHubIntegrationController_createGitHubIntegration']
|
||||
}
|
||||
'/platform/integrations/github/connections/{organization_integration_id}': {
|
||||
/** Gets installed github project connections for the given organization integration */
|
||||
get: operations['GitHubConnectionsController_getGitHubConnections']
|
||||
'/platform/integrations/github/authorization': {
|
||||
/** Get GitHub authorization */
|
||||
get: operations['GitHubAuthorizationsController_getGitHubAuthorization']
|
||||
/** Create GitHub authorization */
|
||||
post: operations['GitHubAuthorizationsController_createGitHubAuthorization']
|
||||
}
|
||||
'/platform/integrations/github/connections': {
|
||||
/** List organization GitHub connections */
|
||||
get: operations['GitHubConnectionsController_listOrganizationGitHubConnections']
|
||||
/** Connects a GitHub project to a supabase project */
|
||||
post: operations['GitHubConnectionsController_createGitHubConnection']
|
||||
}
|
||||
@@ -814,25 +814,25 @@ export interface paths {
|
||||
/** Updates a GitHub connection for a supabase project */
|
||||
patch: operations['GitHubConnectionsController_updateGitHubConnection']
|
||||
}
|
||||
'/platform/integrations/github/repos/{organization_integration_id}': {
|
||||
/** Gets github repos for the given organization */
|
||||
get: operations['GitHubRepoController_getRepos']
|
||||
'/platform/integrations/github/branches/{connectionId}': {
|
||||
/** List GitHub connection branches */
|
||||
get: operations['GitHubBranchesController_listConnectionBranches']
|
||||
}
|
||||
'/platform/integrations/github/branches/{organization_integration_id}/{repo_owner}/{repo_name}': {
|
||||
/** Gets github branches for a given repo */
|
||||
get: operations['GitHubBranchController_getBranches']
|
||||
'/platform/integrations/github/branches/{connectionId}/{branchName}': {
|
||||
/** Get GitHub connection branch */
|
||||
get: operations['GitHubBranchesController_getConnectionBranch']
|
||||
}
|
||||
'/platform/integrations/github/branches/{organization_integration_id}/{repo_owner}/{repo_name}/{branch_name}': {
|
||||
/** Gets a specific github branch for a given repo */
|
||||
get: operations['GitHubBranchController_getBranchByName']
|
||||
'/platform/integrations/github/pull-requests/{connectionId}': {
|
||||
/** List GitHub connection pull requests */
|
||||
get: operations['GitHubPullRequestsController_getConnectionPullRequests']
|
||||
}
|
||||
'/platform/integrations/github/pull-requests/{organization_integration_id}/{repo_owner}/{repo_name}': {
|
||||
/** Gets github pull requests for a given repo */
|
||||
get: operations['GitHubPullRequestController_getPullRequestsByNumber']
|
||||
'/platform/integrations/github/pull-requests/{connectionId}/{branchName}': {
|
||||
/** List GitHub pull requests for a specific branch */
|
||||
get: operations['GitHubPullRequestsController_validateConnectionBranch']
|
||||
}
|
||||
'/platform/integrations/github/pull-requests/{organization_integration_id}/{repo_owner}/{repo_name}/{target}': {
|
||||
/** Gets github pull requests for a given repo */
|
||||
get: operations['GitHubPullRequestController_getPullRequests']
|
||||
'/platform/integrations/github/repositories': {
|
||||
/** Gets GitHub repositories for user */
|
||||
get: operations['GitHubRepositoriesController_listRepositories']
|
||||
}
|
||||
'/platform/cli/login': {
|
||||
/** Create CLI login session */
|
||||
@@ -4278,58 +4278,45 @@ export interface components {
|
||||
DeleteVercelConnectionResponse: {
|
||||
id: string
|
||||
}
|
||||
CreateGitHubIntegrationBody: {
|
||||
installation_id: number
|
||||
organization_slug: string
|
||||
metadata: Record<string, never>
|
||||
CreateGitHubAuthorizationBody: {
|
||||
code: string
|
||||
}
|
||||
CreateGitHubIntegrationResponse: {
|
||||
id: string
|
||||
}
|
||||
GetGitHubConnections: {
|
||||
id: string
|
||||
inserted_at: string
|
||||
updated_at: string
|
||||
organization_integration_id: string
|
||||
supabase_project_ref: string
|
||||
foreign_project_id: string
|
||||
metadata: Record<string, never>
|
||||
}
|
||||
IntegrationConnectionGithub: {
|
||||
foreign_project_id: string
|
||||
supabase_project_ref: string
|
||||
integration_id: string
|
||||
metadata: Record<string, never>
|
||||
}
|
||||
CreateGitHubConnectionsBody: {
|
||||
organization_integration_id: string
|
||||
connection: components['schemas']['IntegrationConnectionGithub']
|
||||
}
|
||||
UpdateGitHubConnectionsBody: {
|
||||
metadata: Record<string, never>
|
||||
}
|
||||
GetGithubRepo: {
|
||||
ListGitHubConnectionsProject: {
|
||||
id: number
|
||||
full_name: string
|
||||
}
|
||||
GetGithubBranch: {
|
||||
ref: string
|
||||
name: string
|
||||
}
|
||||
GitRef: {
|
||||
repo: string
|
||||
branch: string
|
||||
label?: string
|
||||
}
|
||||
GetGithubPullRequest: {
|
||||
ListGitHubConnectionsRepository: {
|
||||
id: number
|
||||
url: string
|
||||
title: string
|
||||
target: components['schemas']['GitRef']
|
||||
created_at: string
|
||||
created_by?: string
|
||||
repo: string
|
||||
branch: string
|
||||
label?: string
|
||||
name: string
|
||||
}
|
||||
ListGitHubConnectionsUser: {
|
||||
id: number
|
||||
username: string
|
||||
primary_email: string | null
|
||||
}
|
||||
ListGitHubConnectionsConnection: {
|
||||
id: number
|
||||
inserted_at: string
|
||||
updated_at: string
|
||||
installation_id: number
|
||||
project: components['schemas']['ListGitHubConnectionsProject']
|
||||
repository: components['schemas']['ListGitHubConnectionsRepository']
|
||||
user: components['schemas']['ListGitHubConnectionsUser'] | null
|
||||
workdir: string
|
||||
supabase_changes_only: boolean
|
||||
}
|
||||
ListGitHubConnectionsResponse: {
|
||||
connections: components['schemas']['ListGitHubConnectionsConnection'][]
|
||||
}
|
||||
CreateGitHubConnectionsBody: {
|
||||
project_ref: string
|
||||
installation_id: number
|
||||
repository_id: number
|
||||
}
|
||||
UpdateGitHubConnectionsBody: {
|
||||
workdir?: string
|
||||
supabase_changes_only?: boolean
|
||||
}
|
||||
CreateCliLoginSessionBody: {
|
||||
session_id: string
|
||||
@@ -4587,6 +4574,7 @@ export interface components {
|
||||
| 'sa-east-1'
|
||||
/** @deprecated */
|
||||
kps_enabled?: boolean
|
||||
desired_instance_size?: components['schemas']['DesiredInstanceSize']
|
||||
}
|
||||
ApiKeyResponse: {
|
||||
name: string
|
||||
@@ -10866,39 +10854,48 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Create github integration */
|
||||
GitHubIntegrationController_createGitHubIntegration: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateGitHubIntegrationBody']
|
||||
}
|
||||
}
|
||||
/** Get GitHub authorization */
|
||||
GitHubAuthorizationsController_getGitHubAuthorization: {
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateGitHubIntegrationResponse']
|
||||
}
|
||||
}
|
||||
/** @description Failed to create github integration */
|
||||
/** @description Failed to get GitHub authorization */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets installed github project connections for the given organization integration */
|
||||
GitHubConnectionsController_getGitHubConnections: {
|
||||
/** Create GitHub authorization */
|
||||
GitHubAuthorizationsController_createGitHubAuthorization: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateGitHubAuthorizationBody']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
'application/json': Record<string, never>
|
||||
}
|
||||
}
|
||||
/** @description Failed to create GitHub authorization */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** List organization GitHub connections */
|
||||
GitHubConnectionsController_listOrganizationGitHubConnections: {
|
||||
parameters: {
|
||||
path: {
|
||||
organization_integration_id: string
|
||||
query: {
|
||||
organization_id: number
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['GetGitHubConnections'][]
|
||||
'application/json': components['schemas']['ListGitHubConnectionsResponse']
|
||||
}
|
||||
}
|
||||
/** @description Failed to get installed github connections for the given organization integration */
|
||||
/** @description Failed to list organization GitHub connections */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
@@ -10929,7 +10926,7 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
204: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to delete github integration project connection */
|
||||
@@ -10951,7 +10948,7 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
204: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to update GitHub connection */
|
||||
@@ -10960,121 +10957,102 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets github repos for the given organization */
|
||||
GitHubRepoController_getRepos: {
|
||||
/** List GitHub connection branches */
|
||||
GitHubBranchesController_listConnectionBranches: {
|
||||
parameters: {
|
||||
query?: {
|
||||
per_page?: number
|
||||
page?: number
|
||||
}
|
||||
path: {
|
||||
organization_integration_id: string
|
||||
connectionId: number
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['GetGithubRepo'][]
|
||||
'application/json': Record<string, never>[]
|
||||
}
|
||||
}
|
||||
/** @description Failed to get github repos for the given organization */
|
||||
/** @description Failed to list GitHub connection branches */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets github branches for a given repo */
|
||||
GitHubBranchController_getBranches: {
|
||||
parameters: {
|
||||
query?: {
|
||||
per_page?: number
|
||||
page?: number
|
||||
}
|
||||
path: {
|
||||
organization_integration_id: string
|
||||
repo_owner: string
|
||||
repo_name: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['GetGithubBranch'][]
|
||||
}
|
||||
}
|
||||
/** @description Failed to get github branches for a given repo */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets a specific github branch for a given repo */
|
||||
GitHubBranchController_getBranchByName: {
|
||||
/** Get GitHub connection branch */
|
||||
GitHubBranchesController_getConnectionBranch: {
|
||||
parameters: {
|
||||
path: {
|
||||
organization_integration_id: string
|
||||
repo_owner: string
|
||||
repo_name: string
|
||||
branch_name: string
|
||||
connectionId: number
|
||||
branchName: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['GetGithubBranch']
|
||||
'application/json': Record<string, never>
|
||||
}
|
||||
}
|
||||
/** @description Failed to get github branch for a given repo */
|
||||
/** @description Failed to get GitHub connection branch */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets github pull requests for a given repo */
|
||||
GitHubPullRequestController_getPullRequestsByNumber: {
|
||||
/** List GitHub connection pull requests */
|
||||
GitHubPullRequestsController_getConnectionPullRequests: {
|
||||
parameters: {
|
||||
query: {
|
||||
pr_number: number[]
|
||||
}
|
||||
path: {
|
||||
organization_integration_id: string
|
||||
repo_owner: string
|
||||
repo_name: string
|
||||
connectionId: number
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['GetGithubPullRequest'][]
|
||||
'application/json': Record<string, never>[]
|
||||
}
|
||||
}
|
||||
/** @description Failed to get github pull requests for a given repo */
|
||||
/** @description Failed to list GitHub connection pull requests */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets github pull requests for a given repo */
|
||||
GitHubPullRequestController_getPullRequests: {
|
||||
/** List GitHub pull requests for a specific branch */
|
||||
GitHubPullRequestsController_validateConnectionBranch: {
|
||||
parameters: {
|
||||
query?: {
|
||||
per_page?: number
|
||||
page?: number
|
||||
}
|
||||
path: {
|
||||
organization_integration_id: string
|
||||
repo_owner: string
|
||||
repo_name: string
|
||||
target: string
|
||||
connectionId: number
|
||||
branchName: string
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['GetGithubPullRequest'][]
|
||||
'application/json': Record<string, never>[]
|
||||
}
|
||||
}
|
||||
/** @description Failed to get github pull requests for a given repo */
|
||||
/** @description Failed to validate GitHub connection branch */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Gets GitHub repositories for user */
|
||||
GitHubRepositoriesController_listRepositories: {
|
||||
responses: {
|
||||
200: {
|
||||
content: never
|
||||
}
|
||||
/** @description Failed to get GitHub repositories for user */
|
||||
500: {
|
||||
content: never
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export const useBranchCreateMutation = ({
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(branchKeys.list(projectRef))
|
||||
await queryClient.invalidateQueries(projectKeys.detail(projectRef))
|
||||
await queryClient.invalidateQueries(projectKeys.list())
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
import { post } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type GitHubAuthorizationCreateVariables = {
|
||||
code: string
|
||||
}
|
||||
|
||||
export async function createGitHubAuthorization({ code }: GitHubAuthorizationCreateVariables) {
|
||||
const { data, error } = await post('/platform/integrations/github/authorization', {
|
||||
body: { code },
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
type GitHubAuthorizationCreateData = Awaited<ReturnType<typeof createGitHubAuthorization>>
|
||||
|
||||
export const useGitHubAuthorizationCreateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<
|
||||
GitHubAuthorizationCreateData,
|
||||
ResponseError,
|
||||
GitHubAuthorizationCreateVariables
|
||||
>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation<
|
||||
GitHubAuthorizationCreateData,
|
||||
ResponseError,
|
||||
GitHubAuthorizationCreateVariables
|
||||
>((vars) => createGitHubAuthorization(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
// const { projectRef, id } = variables
|
||||
|
||||
// await Promise.all([
|
||||
// queryClient.invalidateQueries(githubAuthorizationKeys.list(projectRef)),
|
||||
// queryClient.invalidateQueries(githubAuthorizationKeys.githubAuthorization(projectRef, id)),
|
||||
// ])
|
||||
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to mutate: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
// FIXME(kamil): Do not retry, a single check is fine.
|
||||
export async function getGitHubAuthorization(signal?: AbortSignal) {
|
||||
const { data, error } = await get('/platform/integrations/github/authorization', {
|
||||
signal,
|
||||
})
|
||||
return error ? null : data
|
||||
}
|
||||
|
||||
export type GitHubAuthorizationData = Awaited<ReturnType<typeof getGitHubAuthorization>>
|
||||
export type ProjectGitHubRepositoryConnectionsData = Awaited<
|
||||
ReturnType<typeof getGitHubAuthorization>
|
||||
>
|
||||
export type GitHubAuthorizationError = ResponseError
|
||||
|
||||
export const useGitHubAuthorizationQuery = <TData = GitHubAuthorizationData>({
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<GitHubAuthorizationData, GitHubAuthorizationError, TData> = {}) => {
|
||||
return useQuery<GitHubAuthorizationData, GitHubAuthorizationError, TData>(
|
||||
integrationKeys.githubAuthorization(),
|
||||
({ signal }) => getGitHubAuthorization(signal),
|
||||
{ enabled, staleTime: 0, ...options }
|
||||
)
|
||||
}
|
||||
+5
-11
@@ -5,27 +5,21 @@ import { get } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type GithubBranchVariables = {
|
||||
organizationIntegrationId?: string
|
||||
repoOwner: string
|
||||
repoName: string
|
||||
connectionId: number
|
||||
branchName: string
|
||||
}
|
||||
|
||||
export async function checkGithubBranchValidity(
|
||||
{ organizationIntegrationId, repoOwner, repoName, branchName }: GithubBranchVariables,
|
||||
{ connectionId, branchName }: GithubBranchVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!organizationIntegrationId) throw new Error('Organization integration ID is required')
|
||||
|
||||
const { data, error } = await get(
|
||||
'/platform/integrations/github/branches/{organization_integration_id}/{repo_owner}/{repo_name}/{branch_name}',
|
||||
'/platform/integrations/github/branches/{connectionId}/{branchName}',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
organization_integration_id: organizationIntegrationId,
|
||||
repo_owner: repoOwner,
|
||||
repo_name: repoName,
|
||||
branch_name: branchName,
|
||||
connectionId,
|
||||
branchName,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'data/fetchers'
|
||||
import { integrationKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type GitHubBranchesVariables = {
|
||||
connectionId?: number
|
||||
}
|
||||
|
||||
export async function getGitHubBranches(
|
||||
{ connectionId }: GitHubBranchesVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!connectionId) throw new Error('connectionId is required')
|
||||
|
||||
const { data, error } = await get(`/platform/integrations/github/branches/{connectionId}`, {
|
||||
params: { path: { connectionId } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) throw new Error((error as ResponseError).message)
|
||||
return data
|
||||
}
|
||||
|
||||
export type GitHubBranchesData = Awaited<ReturnType<typeof getGitHubBranches>>
|
||||
export type GitHubBranchesError = ResponseError
|
||||
|
||||
export const useGitHubBranchesQuery = <TData = GitHubBranchesData>(
|
||||
{ connectionId }: GitHubBranchesVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<GitHubBranchesData, GitHubBranchesError, TData> = {}
|
||||
) =>
|
||||
useQuery<GitHubBranchesData, GitHubBranchesError, TData>(
|
||||
integrationKeys.githubBranchesList(connectionId),
|
||||
({ signal }) => getGitHubBranches({ connectionId }, signal),
|
||||
{
|
||||
enabled: enabled && typeof connectionId !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { post } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { GitHubConnectionCreateVariables } from './integrations.types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
export async function createGitHubConnection({ connection }: GitHubConnectionCreateVariables) {
|
||||
const { data, error } = await post('/platform/integrations/github/connections', {
|
||||
body: connection,
|
||||
})
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export type GitHubConnectionCreateData = Awaited<ReturnType<typeof createGitHubConnection>>
|
||||
|
||||
export const useGitHubConnectionCreateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<GitHubConnectionCreateData, ResponseError, GitHubConnectionCreateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<GitHubConnectionCreateData, ResponseError, GitHubConnectionCreateVariables>(
|
||||
(vars) => createGitHubConnection(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(
|
||||
integrationKeys.githubConnectionsList(variables.organizationId)
|
||||
),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to create Github connection: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
+6
-10
@@ -6,15 +6,14 @@ import { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
type DeleteVariables = {
|
||||
connectionId: string
|
||||
|
||||
integrationId: string
|
||||
orgSlug: string | undefined
|
||||
connectionId: string | number
|
||||
organizationId: number
|
||||
}
|
||||
|
||||
export async function deleteConnection({ connectionId }: DeleteVariables, signal?: AbortSignal) {
|
||||
const { data, error } = await del('/platform/integrations/github/connections/{connection_id}', {
|
||||
params: { path: { connection_id: connectionId } },
|
||||
params: { path: { connection_id: String(connectionId) } },
|
||||
signal,
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
@@ -23,7 +22,7 @@ export async function deleteConnection({ connectionId }: DeleteVariables, signal
|
||||
|
||||
type DeleteContentData = Awaited<ReturnType<typeof deleteConnection>>
|
||||
|
||||
export const useIntegrationsGitHubInstalledConnectionDeleteMutation = ({
|
||||
export const useGitHubConnectionDeleteMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
@@ -37,11 +36,8 @@ export const useIntegrationsGitHubInstalledConnectionDeleteMutation = ({
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(integrationKeys.integrationsList()),
|
||||
queryClient.invalidateQueries(integrationKeys.integrationsListWithOrg(variables.orgSlug)),
|
||||
queryClient.invalidateQueries(integrationKeys.githubRepoList(variables.integrationId)),
|
||||
queryClient.invalidateQueries(
|
||||
integrationKeys.githubConnectionsList(variables.integrationId)
|
||||
integrationKeys.githubConnectionsList(variables.organizationId)
|
||||
),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
+26
-23
@@ -3,46 +3,49 @@ import { toast } from 'react-hot-toast'
|
||||
|
||||
import { patch } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { UpdateConnectionPayload } from './integrations.types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
export async function updateGithubConnection({
|
||||
id,
|
||||
metadata,
|
||||
organizationIntegrationId,
|
||||
}: UpdateConnectionPayload) {
|
||||
const { data, error } = await patch('/platform/integrations/github/connections/{connection_id}', {
|
||||
params: {
|
||||
path: { connection_id: id },
|
||||
},
|
||||
body: {
|
||||
// @ts-expect-error
|
||||
metadata,
|
||||
},
|
||||
})
|
||||
type UpdateVariables = {
|
||||
connectionId: string | number
|
||||
organizationId: number
|
||||
workdir: string
|
||||
supabaseChangesOnly: boolean
|
||||
}
|
||||
|
||||
export async function updateConnection(
|
||||
{ connectionId, workdir, supabaseChangesOnly }: UpdateVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const { data, error } = await patch('/platform/integrations/github/connections/{connection_id}', {
|
||||
params: { path: { connection_id: String(connectionId) } },
|
||||
signal,
|
||||
body: { workdir, supabase_changes_only: supabaseChangesOnly },
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
type UpdateGithubConnectionData = Awaited<ReturnType<typeof updateGithubConnection>>
|
||||
type UpdateContentData = Awaited<ReturnType<typeof updateConnection>>
|
||||
|
||||
export const useGithubConnectionUpdateMutation = ({
|
||||
export const useGitHubConnectionUpdateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<UpdateGithubConnectionData, ResponseError, UpdateConnectionPayload>,
|
||||
UseMutationOptions<UpdateContentData, ResponseError, UpdateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<UpdateGithubConnectionData, ResponseError, UpdateConnectionPayload>(
|
||||
(vars) => updateGithubConnection(vars),
|
||||
return useMutation<UpdateContentData, ResponseError, UpdateVariables>(
|
||||
(args) => updateConnection(args),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
await queryClient.invalidateQueries(
|
||||
integrationKeys.githubConnectionsList(variables.organizationIntegrationId)
|
||||
)
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(
|
||||
integrationKeys.githubConnectionsList(variables.organizationId)
|
||||
),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
export type GitHubConnectionsVariables = {
|
||||
organizationId?: number
|
||||
}
|
||||
|
||||
export async function getGitHubConnections(
|
||||
{ organizationId }: GitHubConnectionsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!organizationId) throw new Error('organizationId is required')
|
||||
|
||||
const { data, error } = await get('/platform/integrations/github/connections', {
|
||||
params: {
|
||||
query: {
|
||||
organization_id: organizationId,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
})
|
||||
if (error) throw new Error((error as ResponseError).message)
|
||||
return data.connections
|
||||
}
|
||||
|
||||
export type GitHubConnectionsData = Awaited<ReturnType<typeof getGitHubConnections>>
|
||||
export type GitHubConnectionsError = ResponseError
|
||||
|
||||
export type GitHubConnection = GitHubConnectionsData[0]
|
||||
|
||||
export const useGitHubConnectionsQuery = <TData = GitHubConnectionsData>(
|
||||
{ organizationId }: GitHubConnectionsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<GitHubConnectionsData, GitHubConnectionsError, TData> = {}
|
||||
) => {
|
||||
return useQuery<GitHubConnectionsData, GitHubConnectionsError, TData>(
|
||||
integrationKeys.githubConnectionsList(organizationId),
|
||||
({ signal }) => getGitHubConnections({ organizationId }, signal),
|
||||
{ enabled, ...options }
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { post } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
export type GitHubIntegrationCreateVariables = {
|
||||
installationId: number
|
||||
orgSlug: string
|
||||
metadata: { supabaseConfig: { supabaseDirectory: string } }
|
||||
}
|
||||
|
||||
export async function createGitHubIntegration({
|
||||
installationId,
|
||||
orgSlug,
|
||||
metadata,
|
||||
}: GitHubIntegrationCreateVariables) {
|
||||
const { data, error } = await post('/platform/integrations/github', {
|
||||
body: {
|
||||
installation_id: installationId,
|
||||
organization_slug: orgSlug,
|
||||
metadata: metadata as any,
|
||||
},
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
type GitHubIntegrationCreateData = Awaited<ReturnType<typeof createGitHubIntegration>>
|
||||
|
||||
export const useGitHubIntegrationCreateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<GitHubIntegrationCreateData, ResponseError, GitHubIntegrationCreateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<GitHubIntegrationCreateData, ResponseError, GitHubIntegrationCreateVariables>(
|
||||
(vars) => createGitHubIntegration(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(integrationKeys.integrationsList()),
|
||||
queryClient.invalidateQueries(integrationKeys.integrationsListWithOrg(variables.orgSlug)),
|
||||
queryClient.invalidateQueries(integrationKeys.githubRepoList(data.id)),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to create Github integration: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'data/fetchers'
|
||||
import { integrationKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
import { components } from 'data/api'
|
||||
|
||||
export type GitHubPullRequestsVariables = {
|
||||
connectionId?: number
|
||||
prNumbers?: number[]
|
||||
}
|
||||
|
||||
// TODO(alaister): find the actual type
|
||||
export type GitHubPullRequest = any
|
||||
|
||||
export async function getGitHubPullRequests(
|
||||
{ connectionId, prNumbers = [] }: GitHubPullRequestsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!connectionId) throw new Error('connectionId is required')
|
||||
|
||||
const { data, error } = await get(`/platform/integrations/github/pull-requests/{connectionId}`, {
|
||||
params: {
|
||||
path: {
|
||||
connectionId,
|
||||
},
|
||||
query: {
|
||||
pr_number: prNumbers,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
export type GitHubPullRequestsData = Awaited<ReturnType<typeof getGitHubPullRequests>>
|
||||
export type GitHubPullRequestsError = ResponseError
|
||||
|
||||
export const useGitHubPullRequestsQuery = <TData = GitHubPullRequestsData>(
|
||||
{ connectionId, prNumbers }: GitHubPullRequestsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<GitHubPullRequestsData, GitHubPullRequestsError, TData> = {}
|
||||
) =>
|
||||
useQuery<GitHubPullRequestsData, GitHubPullRequestsError, TData>(
|
||||
integrationKeys.githubPullRequestsList(connectionId, prNumbers),
|
||||
({ signal }) => getGitHubPullRequests({ connectionId, prNumbers }, signal),
|
||||
{
|
||||
enabled: enabled && typeof connectionId !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
export async function getGitHubRepositories(signal?: AbortSignal) {
|
||||
const { data, error } = await get('/platform/integrations/github/repositories', {
|
||||
signal,
|
||||
})
|
||||
if (error) throw new Error((error as ResponseError).message)
|
||||
|
||||
// [Alaister]: temp fix until we have a proper response type
|
||||
return (data as any).repositories
|
||||
}
|
||||
|
||||
export type GitHubRepositoriesData = Awaited<ReturnType<typeof getGitHubRepositories>>
|
||||
export type ProjectGitHubRepositoryConnectionsData = Awaited<
|
||||
ReturnType<typeof getGitHubRepositories>
|
||||
>
|
||||
export type GitHubRepositoriesError = ResponseError
|
||||
|
||||
export const useGitHubRepositoriesQuery = <TData = GitHubRepositoriesData>({
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<GitHubRepositoriesData, GitHubRepositoriesError, TData> = {}) => {
|
||||
return useQuery<GitHubRepositoriesData, GitHubRepositoriesError, TData>(
|
||||
integrationKeys.githubRepositoriesList(),
|
||||
({ signal }) => getGitHubRepositories(signal),
|
||||
{ enabled, staleTime: 0, ...options }
|
||||
)
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'data/fetchers'
|
||||
import { integrationKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type GithubBranchesVariables = {
|
||||
organizationIntegrationId?: string
|
||||
repoOwner: string
|
||||
repoName: string
|
||||
}
|
||||
|
||||
export async function getGithubBranches(
|
||||
{ organizationIntegrationId, repoOwner, repoName }: GithubBranchesVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!organizationIntegrationId) throw new Error('Organization integration ID is required')
|
||||
|
||||
const { data, error } = await get(
|
||||
`/platform/integrations/github/branches/{organization_integration_id}/{repo_owner}/{repo_name}`,
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
organization_integration_id: organizationIntegrationId,
|
||||
repo_owner: repoOwner,
|
||||
repo_name: repoName,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
}
|
||||
)
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
export type GithubBranchesData = Awaited<ReturnType<typeof getGithubBranches>>
|
||||
export type GithubBranchesError = ResponseError
|
||||
|
||||
export const useGithubBranchesQuery = <TData = GithubBranchesData>(
|
||||
{ organizationIntegrationId, repoOwner, repoName }: GithubBranchesVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<GithubBranchesData, GithubBranchesError, TData> = {}
|
||||
) =>
|
||||
useQuery<GithubBranchesData, GithubBranchesError, TData>(
|
||||
integrationKeys.githubBranchesList(organizationIntegrationId, repoOwner, repoName),
|
||||
({ signal }) => getGithubBranches({ organizationIntegrationId, repoOwner, repoName }, signal),
|
||||
{
|
||||
enabled:
|
||||
enabled &&
|
||||
typeof organizationIntegrationId !== 'undefined' &&
|
||||
typeof repoOwner !== 'undefined' &&
|
||||
typeof repoName !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,70 +0,0 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { post } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
import { IntegrationConnectionsCreateVariables } from './integrations.types'
|
||||
|
||||
export async function createIntegrationGitHubConnections({
|
||||
organizationIntegrationId,
|
||||
connection,
|
||||
}: IntegrationConnectionsCreateVariables) {
|
||||
const { data, error } = await post('/platform/integrations/github/connections', {
|
||||
body: {
|
||||
organization_integration_id: organizationIntegrationId,
|
||||
connection,
|
||||
},
|
||||
})
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export type IntegrationGitHubConnectionsCreateData = Awaited<
|
||||
ReturnType<typeof createIntegrationGitHubConnections>
|
||||
>
|
||||
|
||||
export const useIntegrationGitHubConnectionsCreateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<
|
||||
IntegrationGitHubConnectionsCreateData,
|
||||
ResponseError,
|
||||
IntegrationConnectionsCreateVariables
|
||||
>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<
|
||||
IntegrationGitHubConnectionsCreateData,
|
||||
ResponseError,
|
||||
IntegrationConnectionsCreateVariables
|
||||
>((vars) => createIntegrationGitHubConnections(vars), {
|
||||
async onSuccess(data, variables, context) {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(integrationKeys.integrationsList()),
|
||||
queryClient.invalidateQueries(integrationKeys.integrationsListWithOrg(variables.orgSlug)),
|
||||
queryClient.invalidateQueries(
|
||||
integrationKeys.githubRepoList(variables.organizationIntegrationId)
|
||||
),
|
||||
queryClient.invalidateQueries(
|
||||
integrationKeys.githubConnectionsList(variables.organizationIntegrationId)
|
||||
),
|
||||
])
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to create Github connection: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'data/fetchers'
|
||||
import { integrationKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
import { components } from 'data/api'
|
||||
|
||||
export type GithubPullRequestsVariables = {
|
||||
organizationIntegrationId?: string
|
||||
repoOwner: string
|
||||
repoName: string
|
||||
prNumbers?: number[]
|
||||
}
|
||||
|
||||
export type GitHubPullRequest = components['schemas']['GetGithubPullRequest']
|
||||
|
||||
export async function getGitHubPullRequests(
|
||||
{ organizationIntegrationId, repoOwner, repoName, prNumbers }: GithubPullRequestsVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!organizationIntegrationId) throw new Error('Organization integration ID is required')
|
||||
if (!prNumbers) throw new Error('A list of PR numbers is required')
|
||||
|
||||
const { data, error } = await get(
|
||||
`/platform/integrations/github/pull-requests/{organization_integration_id}/{repo_owner}/{repo_name}`,
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
organization_integration_id: organizationIntegrationId,
|
||||
repo_owner: repoOwner,
|
||||
repo_name: repoName,
|
||||
},
|
||||
query: {
|
||||
// @ts-ignore generated api types is incorrect here, to remove ignore statement once fixed
|
||||
pr_number: prNumbers,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
}
|
||||
)
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
export type GithubPullRequestsData = Awaited<ReturnType<typeof getGitHubPullRequests>>
|
||||
export type GithubPullRequestsError = ResponseError
|
||||
|
||||
export const useGithubPullRequestsQuery = <TData = GithubPullRequestsData>(
|
||||
{ organizationIntegrationId, repoOwner, repoName, prNumbers }: GithubPullRequestsVariables,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<GithubPullRequestsData, GithubPullRequestsError, TData> = {}
|
||||
) =>
|
||||
useQuery<GithubPullRequestsData, GithubPullRequestsError, TData>(
|
||||
// [Joshen] Just fyi im not sure if we need to include prNumber in the RQ key here
|
||||
integrationKeys.githubPullRequestsList(organizationIntegrationId, repoOwner, repoName),
|
||||
({ signal }) =>
|
||||
getGitHubPullRequests({ organizationIntegrationId, repoOwner, repoName, prNumbers }, signal),
|
||||
{
|
||||
enabled:
|
||||
enabled &&
|
||||
typeof organizationIntegrationId !== 'undefined' &&
|
||||
typeof repoOwner !== 'undefined' &&
|
||||
typeof repoName !== 'undefined' &&
|
||||
typeof prNumbers !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
|
||||
import { get } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { integrationKeys } from './keys'
|
||||
|
||||
export type GitHubReposVariables = {
|
||||
integrationId: string | undefined
|
||||
}
|
||||
|
||||
// We don't want to fetch more than 1000 repositories, as it's already more than enough.
|
||||
const MAX_PAGES = 10
|
||||
|
||||
export async function getGitHubRepos(
|
||||
{ integrationId }: GitHubReposVariables,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!integrationId) {
|
||||
throw new Error('integrationId is required')
|
||||
}
|
||||
|
||||
const repos: Awaited<ReturnType<typeof getSingleGithubReposPage>> = []
|
||||
const perPage = 100
|
||||
let page = 1
|
||||
|
||||
// This is unfortunate, because we will do redundant API call in case there are exactly 100 results,
|
||||
// but the API is returning an array instead of `{ repos: [], total_count: n }`.
|
||||
while (true) {
|
||||
const reposChunk = await getSingleGithubReposPage(integrationId, perPage, page, signal)
|
||||
repos.push(...reposChunk)
|
||||
page += 1
|
||||
|
||||
// Stop asking for more data if last request was exhaustive or we reached a limit, .
|
||||
if (reposChunk.length < perPage || page > MAX_PAGES) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return repos
|
||||
}
|
||||
|
||||
async function getSingleGithubReposPage(
|
||||
integrationId: string,
|
||||
perPage: number,
|
||||
page: number,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
const { data, error } = await get(
|
||||
'/platform/integrations/github/repos/{organization_integration_id}',
|
||||
{
|
||||
params: {
|
||||
path: {
|
||||
organization_integration_id: integrationId,
|
||||
},
|
||||
query: {
|
||||
per_page: perPage,
|
||||
page,
|
||||
},
|
||||
},
|
||||
signal,
|
||||
}
|
||||
)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export type GitHubReposData = Awaited<ReturnType<typeof getGitHubRepos>>
|
||||
export type GitHubReposError = ResponseError
|
||||
|
||||
export const useGitHubReposQuery = <TData = GitHubReposData>(
|
||||
{ integrationId }: GitHubReposVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<GitHubReposData, GitHubReposError, TData> = {}
|
||||
) =>
|
||||
useQuery<GitHubReposData, GitHubReposError, TData>(
|
||||
integrationKeys.githubRepoList(integrationId),
|
||||
({ signal }) => getGitHubRepos({ integrationId }, signal),
|
||||
{
|
||||
enabled: enabled && typeof integrationId !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
import { components } from 'data/api'
|
||||
|
||||
export type VercelFramework =
|
||||
| (
|
||||
| 'blitzjs'
|
||||
@@ -137,6 +139,7 @@ export type Imetadata = {
|
||||
preview: boolean
|
||||
}
|
||||
supabaseDirectory?: string
|
||||
supabaseChangesOnly?: boolean
|
||||
}
|
||||
link?: VercelGitLink
|
||||
name: string
|
||||
@@ -249,6 +252,16 @@ export type IntegrationConnectionsCreateVariables = {
|
||||
metadata: any
|
||||
}
|
||||
orgSlug: string | undefined
|
||||
new?: {
|
||||
installation_id: number
|
||||
project_ref: string
|
||||
repository_id: number
|
||||
}
|
||||
}
|
||||
|
||||
export type GitHubConnectionCreateVariables = {
|
||||
organizationId: number
|
||||
connection: components['schemas']['CreateGitHubConnectionsBody']
|
||||
}
|
||||
|
||||
export type UpdateConnectionPayload = {
|
||||
|
||||
@@ -4,8 +4,6 @@ export const integrationKeys = {
|
||||
integrationsList: () => ['organizations', 'integrations'] as const,
|
||||
vercelProjectList: (organization_integration_id: string | undefined) =>
|
||||
['organizations', organization_integration_id, 'vercel-projects'] as const,
|
||||
githubRepoList: (organization_integration_id: string | undefined) =>
|
||||
['organizations', organization_integration_id, 'github-repos'] as const,
|
||||
vercelConnectionsList: (organization_integration_id: string | undefined) =>
|
||||
['organizations', organization_integration_id, 'vercel-connections'] as const,
|
||||
githubBranch: (
|
||||
@@ -21,16 +19,14 @@ export const integrationKeys = {
|
||||
repo_name,
|
||||
branch_name,
|
||||
],
|
||||
githubBranchesList: (
|
||||
organization_integration_id: string | undefined,
|
||||
repo_owner: string,
|
||||
repo_name: string
|
||||
) => ['organizations', organization_integration_id, 'branches', repo_owner, repo_name],
|
||||
githubPullRequestsList: (
|
||||
organization_integration_id: string | undefined,
|
||||
repo_owner: string,
|
||||
repo_name: string
|
||||
) => ['organizations', organization_integration_id, 'pull-requests', repo_owner, repo_name],
|
||||
githubConnectionsList: (organization_integration_id: string | undefined) =>
|
||||
['organizations', organization_integration_id, 'github-connections'] as const,
|
||||
githubAuthorization: () => ['github-authorization'] as const,
|
||||
githubRepositoriesList: () => ['github-repositories'] as const,
|
||||
githubBranchesList: (connectionId: number | undefined) => ['github-branches', connectionId],
|
||||
githubPullRequestsList: (connectionId: number | undefined, prNumbers: number[] | undefined) => [
|
||||
'github-pull-requests',
|
||||
connectionId,
|
||||
{ prNumbers },
|
||||
],
|
||||
githubConnectionsList: (organizationId: number | undefined) =>
|
||||
['organizations', organizationId, 'github-connections'] as const,
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { useParams } from 'common'
|
||||
import { useGitHubIntegrationCreateMutation } from 'data/integrations/github-integration-create-mutation'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export function useGitHubIntegrationAutoInstall(callback?: (id: string, orgSlug: string) => void) {
|
||||
const { installation_id: installationId, state: projectRef } = useParams()
|
||||
const shouldAttemptInstall = projectRef !== undefined && installationId !== undefined
|
||||
|
||||
const [isAutoInstalling, setIsAutoInstalling] = useState(false)
|
||||
|
||||
const { data: projects, isSuccess: isProjectsSuccess } = useProjectsQuery({
|
||||
enabled: shouldAttemptInstall,
|
||||
})
|
||||
const { data: organizations, isSuccess: isOrganizationsSuccess } = useOrganizationsQuery({
|
||||
enabled: shouldAttemptInstall,
|
||||
})
|
||||
|
||||
const { mutate: createIntegration } = useGitHubIntegrationCreateMutation()
|
||||
|
||||
useEffect(() => {
|
||||
async function autoInstall() {
|
||||
if (shouldAttemptInstall) {
|
||||
setIsAutoInstalling(true)
|
||||
}
|
||||
|
||||
if (projectRef && isProjectsSuccess && isOrganizationsSuccess) {
|
||||
const project = projects.find((x) => x.ref === projectRef)
|
||||
const organization = organizations.find((x) => x.id === project?.organization_id)
|
||||
|
||||
if (project && organization) {
|
||||
createIntegration(
|
||||
{
|
||||
installationId: Number(installationId),
|
||||
orgSlug: organization.slug,
|
||||
metadata: {
|
||||
supabaseConfig: {
|
||||
supabaseDirectory: '/supabase',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess({ id }) {
|
||||
setIsAutoInstalling(false)
|
||||
|
||||
callback?.(id, organization.slug)
|
||||
},
|
||||
onError() {
|
||||
setIsAutoInstalling(false)
|
||||
},
|
||||
}
|
||||
)
|
||||
} else {
|
||||
setIsAutoInstalling(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
autoInstall()
|
||||
}, [
|
||||
callback,
|
||||
createIntegration,
|
||||
installationId,
|
||||
isOrganizationsSuccess,
|
||||
isProjectsSuccess,
|
||||
organizations,
|
||||
projectRef,
|
||||
projects,
|
||||
shouldAttemptInstall,
|
||||
])
|
||||
|
||||
return isAutoInstalling
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
const GITHUB_INTEGRATION_INSTALLATION_URL =
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
|
||||
? `https://github.com/apps/supabase/installations/new`
|
||||
: process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
|
||||
? `https://github.com/apps/supabase-staging/installations/new`
|
||||
: `https://github.com/apps/supabase-github-v2-migration-test/installations/new`
|
||||
|
||||
const GITHUB_INTEGRATION_CLIENT_ID =
|
||||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod'
|
||||
? `Iv1.b91a6d8eaa272168`
|
||||
: process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
|
||||
? `Iv1.2681ab9a0360d8ad`
|
||||
: `Iv1.5022a3b44d150fbf`
|
||||
|
||||
const GITHUB_INTEGRATION_AUTHORIZATION_URL = `https://github.com/login/oauth/authorize?client_id=${GITHUB_INTEGRATION_CLIENT_ID}`
|
||||
|
||||
export function openInstallGitHubIntegrationWindow(type: 'install' | 'authorize') {
|
||||
const w = 600
|
||||
const h = 800
|
||||
|
||||
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : window.screenX
|
||||
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : window.screenY
|
||||
|
||||
const width = window.innerWidth
|
||||
? window.innerWidth
|
||||
: document.documentElement.clientWidth
|
||||
? document.documentElement.clientWidth
|
||||
: screen.width
|
||||
const height = window.innerHeight
|
||||
? window.innerHeight
|
||||
: document.documentElement.clientHeight
|
||||
? document.documentElement.clientHeight
|
||||
: screen.height
|
||||
|
||||
const windowUrl =
|
||||
type === 'install' ? GITHUB_INTEGRATION_INSTALLATION_URL : GITHUB_INTEGRATION_AUTHORIZATION_URL
|
||||
const systemZoom = width / window.screen.availWidth
|
||||
const left = (width - w) / 2 / systemZoom + dualScreenLeft
|
||||
const top = (height - h) / 2 / systemZoom + dualScreenTop
|
||||
const newWindow = window.open(
|
||||
windowUrl,
|
||||
'GitHub',
|
||||
`scrollbars=yes,resizable=no,status=no,location=no,toolbar=no,menubar=no,
|
||||
width=${w / systemZoom},
|
||||
height=${h / systemZoom},
|
||||
top=${top},
|
||||
left=${left}
|
||||
`
|
||||
)
|
||||
if (newWindow) {
|
||||
newWindow.focus()
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import ProjectLinker from 'components/interfaces/Integrations/ProjectLinker'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import GitHubIntegrationWindowLayout from 'components/layouts/IntegrationsLayout/GitHubIntegrationWindowLayout'
|
||||
import { ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold'
|
||||
import { useIntegrationGitHubConnectionsCreateMutation } from 'data/integrations/integrations-github-connections-create-mutation'
|
||||
import { useGitHubReposQuery } from 'data/integrations/integrations-github-repos-query'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { EMPTY_ARR } from 'lib/void'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { IconBook, IconLifeBuoy, LoadingLine } from 'ui'
|
||||
|
||||
const GITHUB_ICON = (
|
||||
<img
|
||||
src={`${BASE_PATH}/img/icons/github-icon-dark.svg`}
|
||||
alt="GitHub Icon"
|
||||
className="w-6 text-red-500"
|
||||
/>
|
||||
)
|
||||
|
||||
const ChooseProjectGitHubPage: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { integrationId, slug, organizationSlug, state: projectRef } = useParams()
|
||||
const orgSlug = slug ?? organizationSlug
|
||||
|
||||
const { data: integrations } = useOrgIntegrationsQuery({
|
||||
orgSlug,
|
||||
})
|
||||
const { data: organizations } = useOrganizationsQuery()
|
||||
const { data: allProjects, isLoading: isLoadingSupabaseProjectsData } = useProjectsQuery()
|
||||
const { data: allRepos, isLoading: isLoadingGithubReposData } = useGitHubReposQuery({
|
||||
integrationId,
|
||||
})
|
||||
|
||||
const integration = integrations?.find((integration) => integration.id === integrationId)
|
||||
const organization = organizations?.find((organization) => organization.slug === orgSlug)
|
||||
const projects = useMemo(
|
||||
() =>
|
||||
allProjects
|
||||
?.filter((project) => project.organization_id === organization?.id)
|
||||
.map((project) => ({ id: project.id.toString(), name: project.name, ref: project.ref })) ??
|
||||
EMPTY_ARR,
|
||||
[allProjects, organization?.id]
|
||||
)
|
||||
|
||||
const repos = useMemo(
|
||||
() =>
|
||||
allRepos?.map((repo) => ({
|
||||
id: repo.id.toString(),
|
||||
name: repo.full_name,
|
||||
})) ?? EMPTY_ARR,
|
||||
[allRepos]
|
||||
)
|
||||
|
||||
function onDone() {
|
||||
if (projectRef) {
|
||||
router.push(`/project/${projectRef}?enableBranching=true`)
|
||||
} else {
|
||||
router.push(`/org/${orgSlug}/integrations`)
|
||||
}
|
||||
}
|
||||
|
||||
const { mutate: createConnections, isLoading: isCreatingConnection } =
|
||||
useIntegrationGitHubConnectionsCreateMutation({
|
||||
onSuccess: onDone,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="overflow-auto flex flex-col h-full">
|
||||
<LoadingLine loading={isCreatingConnection} />
|
||||
<>
|
||||
<ScaffoldContainer className="flex flex-col gap-6 grow py-8">
|
||||
<header>
|
||||
<h1 className="text-xl text-foreground">
|
||||
Link a Supabase project to a GitHub repository
|
||||
</h1>
|
||||
<Markdown
|
||||
className="text-foreground-lighter"
|
||||
// explain what this integration does
|
||||
content={`
|
||||
This Supabase integration will allow you to link a Supabase project to a GitHub repository. This will allow you to deploy your database schema to your Supabase project.
|
||||
`}
|
||||
/>
|
||||
</header>
|
||||
<ProjectLinker
|
||||
organizationIntegrationId={integration?.id}
|
||||
foreignProjects={repos ?? EMPTY_ARR}
|
||||
supabaseProjects={projects}
|
||||
onCreateConnections={createConnections}
|
||||
installedConnections={integration?.connections}
|
||||
isLoading={isCreatingConnection}
|
||||
integrationIcon={GITHUB_ICON}
|
||||
choosePrompt="Choose GitHub Repo"
|
||||
onSkip={onDone}
|
||||
loadingForeignProjects={isLoadingGithubReposData}
|
||||
loadingSupabaseProjects={isLoadingSupabaseProjectsData}
|
||||
defaultSupabaseProjectRef={projectRef}
|
||||
/>
|
||||
</ScaffoldContainer>
|
||||
</>
|
||||
|
||||
<ScaffoldDivider />
|
||||
</main>
|
||||
<ScaffoldContainer className="bg-studio flex flex-row gap-6 py-6 border-t">
|
||||
<div className="flex items-center gap-2 text-xs text-foreground-lighter">
|
||||
<IconBook size={16} /> Docs
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-foreground-lighter">
|
||||
<IconLifeBuoy size={16} /> Support
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
ChooseProjectGitHubPage.getLayout = (page) => (
|
||||
<GitHubIntegrationWindowLayout>{page}</GitHubIntegrationWindowLayout>
|
||||
)
|
||||
|
||||
export default ChooseProjectGitHubPage
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useGitHubAuthorizationCreateMutation } from 'data/integrations/github-authorization-create-mutation'
|
||||
|
||||
const GitHubIntegrationAuthorize = () => {
|
||||
const { code } = useParams()
|
||||
|
||||
const { mutate, isSuccess } = useGitHubAuthorizationCreateMutation({
|
||||
onSuccess() {
|
||||
window.close()
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
mutate({ code })
|
||||
}
|
||||
}, [code, mutate])
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col justify-center items-center gap-4">
|
||||
<h2 className="text-xl">Completing GitHub Authorization...</h2>
|
||||
|
||||
{isSuccess ? <p>You can now close this window.</p> : <p />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GitHubIntegrationAuthorize
|
||||
@@ -1,197 +0,0 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import OrganizationPicker from 'components/interfaces/Integrations/OrganizationPicker'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import GitHubIntegrationWindowLayout from 'components/layouts/IntegrationsLayout/GitHubIntegrationWindowLayout'
|
||||
import { getHasInstalledObject } from 'components/layouts/IntegrationsLayout/Integrations.utils'
|
||||
import { ScaffoldColumn, ScaffoldContainer, ScaffoldDivider } from 'components/layouts/Scaffold'
|
||||
import { useGitHubIntegrationCreateMutation } from 'data/integrations/github-integration-create-mutation'
|
||||
import { useIntegrationsQuery } from 'data/integrations/integrations-query'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { NextPageWithLayout, Organization } from 'types'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
IconAlertTriangle,
|
||||
IconBook,
|
||||
IconLifeBuoy,
|
||||
LoadingLine,
|
||||
} from 'ui'
|
||||
import { useGitHubIntegrationAutoInstall } from 'lib/github-integration'
|
||||
|
||||
const GitHubIntegration: NextPageWithLayout = () => {
|
||||
const router = useRouter()
|
||||
const { ui } = useStore()
|
||||
const { installation_id: installationId } = useParams()
|
||||
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null)
|
||||
|
||||
/**
|
||||
* Fetch the list of organization based integration installations for GitHub.
|
||||
*
|
||||
* Array of integrations installed on all
|
||||
*/
|
||||
const { data: integrationData } = useIntegrationsQuery()
|
||||
|
||||
const { data: organizationsData } = useOrganizationsQuery({
|
||||
onSuccess(organizations) {
|
||||
const firstOrg = organizations?.[0]
|
||||
if (firstOrg && selectedOrg === null) {
|
||||
setSelectedOrg(firstOrg)
|
||||
router.query.organizationSlug = firstOrg.slug
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const onInstalled = useCallback(
|
||||
(id: string, orgSlug?: string) => {
|
||||
router.push({
|
||||
pathname: `/integrations/github/${id}/choose-project`,
|
||||
query: { ...router.query, slug: orgSlug },
|
||||
})
|
||||
},
|
||||
[router]
|
||||
)
|
||||
|
||||
const { mutate, isLoading: isLoadingGitHubIntegrationCreateMutation } =
|
||||
useGitHubIntegrationCreateMutation({
|
||||
onSuccess({ id }) {
|
||||
onInstalled(id, selectedOrg?.slug)
|
||||
},
|
||||
})
|
||||
|
||||
const isAutoInstalling = useGitHubIntegrationAutoInstall(onInstalled)
|
||||
|
||||
const installed = useMemo(
|
||||
() =>
|
||||
integrationData && organizationsData
|
||||
? getHasInstalledObject({
|
||||
integrationName: 'GitHub',
|
||||
integrationData,
|
||||
organizationsData,
|
||||
installationId,
|
||||
})
|
||||
: {},
|
||||
[installationId, integrationData, organizationsData]
|
||||
)
|
||||
|
||||
function onInstall() {
|
||||
const orgSlug = selectedOrg?.slug
|
||||
|
||||
const installedIntegration = integrationData?.find(
|
||||
(x) =>
|
||||
x.organization.slug === orgSlug &&
|
||||
x.metadata !== undefined &&
|
||||
'installation_id' in x.metadata &&
|
||||
String(x.metadata?.installation_id) === String(installationId)
|
||||
)
|
||||
const isIntegrationInstalled = Boolean(installedIntegration)
|
||||
|
||||
if (!orgSlug) {
|
||||
return ui.setNotification({ category: 'error', message: 'Please select an organization' })
|
||||
}
|
||||
|
||||
if (!installationId) {
|
||||
return ui.setNotification({ category: 'error', message: 'GitHub Installation ID is missing' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Only install if Integration hasn't already been installed
|
||||
*/
|
||||
if (!isIntegrationInstalled) {
|
||||
mutate({
|
||||
installationId: Number(installationId),
|
||||
orgSlug,
|
||||
metadata: {
|
||||
supabaseConfig: {
|
||||
supabaseDirectory: '/supabase',
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
router.push({
|
||||
pathname: `/integrations/github/${installedIntegration?.id}/choose-project`,
|
||||
query: router.query,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const disableInstallationForm = Boolean(selectedOrg && installed[selectedOrg.slug])
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="overflow-auto flex flex-col h-full bg">
|
||||
<LoadingLine loading={isLoadingGitHubIntegrationCreateMutation || isAutoInstalling} />
|
||||
<ScaffoldContainer className="flex flex-col gap-6 grow py-8">
|
||||
<ScaffoldColumn className="mx-auto">
|
||||
<h1 className="text-xl text-foreground">Choose organization</h1>
|
||||
<>
|
||||
<Markdown content={`Choose the Supabase organization you wish to install in`} />
|
||||
<OrganizationPicker
|
||||
integrationName="GitHub"
|
||||
selectedOrg={selectedOrg}
|
||||
onSelectedOrgChange={(org) => {
|
||||
router.query.organizationSlug = org.slug
|
||||
setSelectedOrg(org)
|
||||
}}
|
||||
configurationId={installationId}
|
||||
/>
|
||||
{disableInstallationForm && (
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle className="h-4 w-4" strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>GitHub Integration is already installed.</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
You will need to choose another organization to install the integration.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
<div className="flex flex-row w-full justify-end">
|
||||
<Button
|
||||
size="medium"
|
||||
className="self-end"
|
||||
disabled={
|
||||
isLoadingGitHubIntegrationCreateMutation ||
|
||||
isAutoInstalling ||
|
||||
disableInstallationForm
|
||||
}
|
||||
loading={isLoadingGitHubIntegrationCreateMutation || isAutoInstalling}
|
||||
onClick={onInstall}
|
||||
>
|
||||
Install integration
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</ScaffoldColumn>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldContainer className="flex flex-col gap-6 py-3">
|
||||
<Alert withIcon variant="info" title="You can uninstall this Integration at any time.">
|
||||
<Markdown
|
||||
content={`Remove this integration at any time via GitHub or the Supabase dashboard.`}
|
||||
/>
|
||||
</Alert>
|
||||
</ScaffoldContainer>
|
||||
|
||||
<ScaffoldDivider />
|
||||
</main>
|
||||
<ScaffoldContainer className="bg-studio flex flex-row gap-6 py-6">
|
||||
<div className="flex items-center gap-2 text-xs text-foreground-lighter">
|
||||
<IconBook size={16} /> Docs
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-foreground-lighter">
|
||||
<IconLifeBuoy size={16} /> Support
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
GitHubIntegration.getLayout = (page) => (
|
||||
<GitHubIntegrationWindowLayout>{page}</GitHubIntegrationWindowLayout>
|
||||
)
|
||||
|
||||
export default GitHubIntegration
|
||||
@@ -7,6 +7,7 @@ import ShimmeringCard from 'components/interfaces/Home/ProjectList/ShimmeringCar
|
||||
import AppLayout from 'components/layouts/AppLayout/AppLayout'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { useGitHubConnectionsQuery } from 'data/integrations/github-connections-query'
|
||||
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
@@ -27,9 +28,23 @@ const ProjectsPage: NextPageWithLayout = () => {
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
const { data: integrations } = useOrgIntegrationsQuery({ orgSlug: organization?.slug })
|
||||
const githubConnections = integrations
|
||||
?.filter((integration) => integration.integration.name === 'GitHub')
|
||||
.flatMap((integration) => integration.connections)
|
||||
const { data: connections } = useGitHubConnectionsQuery({ organizationId: organization?.id })
|
||||
const githubConnections = connections?.map((connection) => ({
|
||||
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 any,
|
||||
}))
|
||||
const vercelConnections = integrations
|
||||
?.filter((integration) => integration.integration.name === 'Vercel')
|
||||
.flatMap((integration) => integration.connections)
|
||||
|
||||
@@ -23,11 +23,6 @@ export const sidePanelsState = proxy({
|
||||
setGithubConnectionsOpen: (bool: boolean) => {
|
||||
sidePanelsState.githubConnectionsOpen = bool
|
||||
},
|
||||
// ID to determine which github integration installation to use
|
||||
githubConnectionsIntegrationId: undefined as undefined | string,
|
||||
setGithubConnectionsIntegrationId: (id: string) => {
|
||||
sidePanelsState.githubConnectionsIntegrationId = id
|
||||
},
|
||||
})
|
||||
|
||||
export const getSidePanelsState = () => snapshot(sidePanelsState)
|
||||
|
||||
Reference in new issue
Block a user