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:
authored and GitHub committed 2024-03-04 18:17:26 +11:00
1 parent 5c9ab30902
commit 74563fbeba
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_>
@@ -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>
@@ -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>
@@ -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_>
)}
/>
@@ -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>
@@ -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
@@ -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 })
+121 -143
View File
@@ -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,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,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)
@@ -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 = {
+10 -14
View File
@@ -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,
}
-74
View File
@@ -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
}
+53
View File
@@ -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
+18 -3
View File
@@ -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)
-5
View File
@@ -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)