Chore/update branch selection UI (#18402)

* Set up branch selection UI in EnableBranchingModal to be text input

* Scaffold new CreateBranchModal

* Refactor Enable Branching Modal to use RHF and implement async validation

* midway updating create branch modal

* Update create branch modal with branch checking through text input

* Deprecate create branch side panel

* Remove unused import

* Small improvement to validation

* Remove console log
This commit is contained in:
Joshen Lim authored and GitHub committed 2023-10-24 18:29:42 +08:00
1 parent c4b20e114a
commit 237fdb8db0
9 files changed
+557 -475

No files matched your search

@@ -27,9 +27,9 @@ import { useSelectedOrganization, useSelectedProject, useStore } from 'hooks'
import { OPT_IN_TAGS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { MainBranchPanel } from './BranchPanels'
import CreateBranchSidePanel from './CreateBranchSidePanel'
import PreviewBranches from './PreviewBranches'
import PullRequests from './PullRequests'
import CreateBranchModal from './CreateBranchModal'
const BranchManagement = () => {
const { ui } = useStore()
@@ -247,10 +247,12 @@ const BranchManagement = () => {
</Modal.Content>
</ConfirmationModal>
<CreateBranchSidePanel
{/* <CreateBranchSidePanel
visible={showCreateBranch}
onClose={() => setShowCreateBranch(false)}
/>
/> */}
<CreateBranchModal visible={showCreateBranch} onClose={() => setShowCreateBranch(false)} />
</>
)
}
@@ -4,11 +4,6 @@ import dayjs from 'dayjs'
import { noop } from 'lodash'
import Link from 'next/link'
import { forwardRef, PropsWithChildren, useState } from 'react'
import { Markdown } from 'components/interfaces/Markdown'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { Branch } from 'data/branches/branches-query'
import { GitHubPullRequest } from 'data/integrations/integrations-github-pull-requests-query'
import {
Badge,
Button,
@@ -26,6 +21,11 @@ import {
IconTrash,
} from 'ui'
import { Markdown } from 'components/interfaces/Markdown'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { Branch } from 'data/branches/branches-query'
import { GitHubPullRequest } from 'data/integrations/integrations-github-pull-requests-query'
interface BranchPanelProps {
repo?: string
branch?: Branch
@@ -99,7 +99,7 @@ const MainBranchPanel = ({
<span></span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end">
<DropdownMenuContent side="bottom" align="end" className="w-48">
<Link passHref href={`/project/${ref}/settings/integrations`}>
<DropdownMenuItem
asChild
@@ -0,0 +1,233 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useParams } from 'common'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import {
Button,
FormControl_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormMessage_Shadcn_,
Form_Shadcn_,
IconCheck,
IconExternalLink,
IconLoader,
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
onClose: () => void
}
const CreateBranchModal = ({ visible, onClose }: CreateBranchModalProps) => {
const { ref } = useParams()
const projectDetails = useSelectedProject()
const selectedOrg = useSelectedOrganization()
// [Joshen] There's something weird with RHF that I can't figure out atm
// but calling form.formState.isValid somehow removes the onBlur check,
// and makes the validation run onChange instead. This is a workaround
const [isValid, setIsValid] = useState(false)
const isBranch = projectDetails?.parent_project_ref !== undefined
const projectRef =
projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
const {
data: integrations,
error: integrationsError,
isLoading: isLoadingIntegrations,
isSuccess: isSuccessIntegrations,
isError: isErrorIntegrations,
} = useOrgIntegrationsQuery({
orgSlug: selectedOrg?.slug,
})
const { data: branches } = useBranchesQuery({ projectRef })
const { mutateAsync: checkGithubBranchValidity, isLoading: isChecking } =
useCheckGithubBranchValidity({
onError: () => {},
})
const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({
onSuccess: () => {
toast.success('Successfully created new branch')
onClose()
},
})
const githubIntegration = integrations?.find(
(integration) =>
integration.integration.name === 'GitHub' &&
integration.connections.some(
(connection) => connection.supabase_project_ref === projectDetails?.parentRef
)
)
const githubConnection = githubIntegration?.connections?.find(
(connection) => connection.supabase_project_ref === projectDetails?.parentRef
)
const [repoOwner, repoName] = githubConnection?.metadata.name.split('/') || []
const formId = 'create-branch-form'
const FormSchema = z.object({
branchName: z.string().superRefine(async (val, ctx) => {
if ((branches ?? []).some((branch) => branch.git_branch === val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'This branch already has a Preview Branch',
})
return
}
if (val.length > 0) {
try {
await checkGithubBranchValidity({
organizationIntegrationId: githubIntegration?.id,
repoOwner,
repoName,
branchName: val,
})
setIsValid(true)
} catch (error) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Unable to find branch from ${repoOwner}/${repoName}`,
})
setIsValid(false)
return
}
}
}),
})
const form = useForm<z.infer<typeof FormSchema>>({
mode: 'onBlur',
reValidateMode: 'onBlur',
resolver: zodResolver(FormSchema),
defaultValues: { branchName: '' },
})
const canSubmit = form.getValues('branchName').length > 0 && !isChecking && isValid
const onSubmit = (data: z.infer<typeof FormSchema>) => {
if (!projectRef) return console.error('Project ref is required')
createBranch({ projectRef, branchName: data.branchName, gitBranch: data.branchName })
}
useEffect(() => {
if (form && visible) {
setIsValid(false)
form.reset()
}
}, [form, visible])
return (
<Form_Shadcn_ {...form}>
<form
id={formId}
className="space-y-4"
onSubmit={form.handleSubmit(onSubmit)}
onChange={() => setIsValid(false)}
>
<Modal
hideFooter
size="medium"
modal={false}
visible={visible}
onCancel={onClose}
header="Create a new preview branch"
confirmText="Create Preview Branch"
>
<Modal.Content className="pt-3 pb-1">
{isLoadingIntegrations && <GenericSkeletonLoader />}
{isErrorIntegrations && (
<AlertError
error={integrationsError}
subject="Failed to retrieve Github repository information"
/>
)}
{isSuccessIntegrations && (
<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>
<Link passHref href={`https://github.com/${repoOwner}/${repoName}`}>
<a target="_blank" rel="noreferrer">
<IconExternalLink size={14} strokeWidth={1.5} />
</a>
</Link>
</div>
</div>
)}
</Modal.Content>
<Modal.Separator />
<Modal.Content className="pt-1 pb-3 space-y-3">
<p className="text-sm">
Choose a Git Branch to base your Preview Branch on. Any migration changes added to
this Git Branch will be run on this new Preview Branch.
</p>
<FormField_Shadcn_
control={form.control}
name="branchName"
render={({ field }) => (
<FormItem_Shadcn_ className="relative">
<label className="text-sm text-foreground-light">
Choose your branch to create a preview from
</label>
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} placeholder="e.g feat/some-feature" />
</FormControl_Shadcn_>
<div className="absolute top-9 right-3">
{isChecking ? (
<IconLoader className="animate-spin" />
) : isValid ? (
<IconCheck className="text-brand" strokeWidth={2} />
) : null}
</div>
<FormMessage_Shadcn_ />
</FormItem_Shadcn_>
)}
/>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<div className="flex items-center justify-end space-x-2 py-2 pb-4">
<Button disabled={isCreating} type="default" onClick={() => onClose()}>
Cancel
</Button>
<Button
form={formId}
disabled={isCreating || !canSubmit}
loading={isCreating}
type="primary"
htmlType="submit"
>
Create Preview branch
</Button>
</div>
</Modal.Content>
</Modal>
</form>
</Form_Shadcn_>
)
}
export default CreateBranchModal
@@ -1,223 +0,0 @@
import { useParams } from 'common'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
IconExternalLink,
IconGitBranch,
IconSearch,
Input,
SidePanel,
} from 'ui'
import AlertError from 'components/ui/AlertError'
import NoSearchResults from 'components/ui/NoSearchResults'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
import { useBranchesQuery } from 'data/branches/branches-query'
import { useGithubBranchesQuery } from 'data/integrations/integrations-github-branches-query'
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
import { useSelectedOrganization, useSelectedProject, useStore } from 'hooks'
interface CreateBranchSidePanelProps {
visible: boolean
onClose: () => void
}
// [Joshen] Optimization: Remove Form component, just use Combobox
const CreateBranchSidePanel = ({ visible, onClose }: CreateBranchSidePanelProps) => {
const { ui } = useStore()
const { ref } = useParams()
const projectDetails = useSelectedProject()
const selectedOrg = useSelectedOrganization()
const [searchValue, setSearchValue] = useState('')
const [selectedBranch, setSelectedBranch] = useState<string>()
useEffect(() => {
if (visible) {
setSelectedBranch(undefined)
}
}, [visible])
const isBranch = projectDetails?.parent_project_ref !== undefined
const projectRef =
projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
const { data: branches } = useBranchesQuery({ projectRef })
const {
data: integrations,
error: integrationsError,
isLoading: isLoadingIntegrations,
isSuccess: isSuccessIntegrations,
isError: isErrorIntegrations,
} = useOrgIntegrationsQuery({
orgSlug: selectedOrg?.slug,
})
const githubIntegration = integrations?.find(
(integration) =>
integration.integration.name === 'GitHub' &&
integration.connections.some(
(connection) => connection.supabase_project_ref === projectDetails?.parentRef
)
)
const githubConnection = githubIntegration?.connections?.find(
(connection) => connection.supabase_project_ref === projectDetails?.parentRef
)
const [repoOwner, repoName] = githubConnection?.metadata.name.split('/') || []
const {
data: githubBranches,
error: githubBranchesError,
isLoading: isLoadingBranches,
isSuccess: isSuccessBranches,
isError: isErrorBranches,
} = useGithubBranchesQuery({
organizationIntegrationId: githubIntegration?.id,
repoOwner,
repoName,
})
const branchOptions =
searchValue.length > 0
? (githubBranches || [])?.filter((branch) => branch.name.includes(searchValue))
: githubBranches || []
const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({
onSuccess: () => {
ui.setNotification({ category: 'success', message: `Successfully created new branch` })
onClose()
},
})
const onCreateBranch = (branch: string) => {
if (!projectRef) return console.error('Project ref is required')
setSelectedBranch(branch)
createBranch({ projectRef, branchName: branch, gitBranch: branch })
}
return (
<SidePanel
hideFooter
size="large"
visible={visible}
loading={isCreating}
onCancel={onClose}
header="Create a new database preview branch"
>
<div className="py-6 space-y-4 ">
<SidePanel.Content>
{isLoadingIntegrations && <GenericSkeletonLoader />}
{isSuccessIntegrations && (
<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>
<Link passHref href={`https://github.com/${repoOwner}/${repoName}`}>
<a target="_blank" rel="noreferrer">
<IconExternalLink size={14} strokeWidth={1.5} />
</a>
</Link>
</div>
</div>
)}
</SidePanel.Content>
<SidePanel.Separator />
<SidePanel.Content>
<div>
<p className="text-sm prose mb-2">
Select a Git branch to create a database preview from
</p>
{(isLoadingIntegrations || isLoadingBranches) && <GenericSkeletonLoader />}
{(isErrorIntegrations || isErrorBranches) && (
<AlertError
error={integrationsError || githubBranchesError}
subject="Failed to retrieve Github branches"
/>
)}
{isSuccessIntegrations && isSuccessBranches && (
<>
<Input
placeholder="Search branch..."
className="mb-3"
value={searchValue}
icon={<IconSearch />}
onChange={(event) => setSearchValue(event.target.value)}
/>
{branchOptions.length === 0 && searchValue.length === 0 && (
<Alert_Shadcn_ variant="default" className="!pl-4">
<AlertTitle_Shadcn_>No branches available in repository</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<p>Create a branch in the {repoName} repository on Github first</p>
<Link passHref href={`https://github.com/${repoOwner}/${repoName}`}>
<a target="_blank" rel="noreferrer">
<Button type="default" iconRight={<IconExternalLink />} className="mt-2">
Create a new branch on Github
</Button>
</a>
</Link>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
{branchOptions.length === 0 && searchValue.length > 0 && (
<NoSearchResults
searchString={searchValue}
onResetFilter={() => setSearchValue('')}
/>
)}
<div className="[&>*:first-child]:rounded-t-md">
{branchOptions?.map((branch) => {
const isProductionBranch = branches?.find(
(b) => b.git_branch === branch.name && b.is_default
)
const alreadyHasDatabaseBranch =
branches?.some((b) => b.git_branch === branch.name) ?? false
return (
<div
key={branch.name}
className="px-6 py-4 bg-surface-200 last:rounded-b-md border border-b-0 last:border-b"
>
<div className="w-full flex items-center justify-between">
<div className="flex items-center space-x-3">
<IconGitBranch size={14} strokeWidth={1.5} />
<p className="text-sm">{branch.name}</p>
</div>
<Button
type={isProductionBranch ? 'warning' : 'default'}
loading={isCreating}
disabled={isCreating || alreadyHasDatabaseBranch}
onClick={() => onCreateBranch(branch.name)}
className={
selectedBranch !== undefined && selectedBranch !== branch.name
? 'opacity-0'
: ''
}
>
{isProductionBranch
? 'Production branch'
: alreadyHasDatabaseBranch
? 'Branch already created'
: 'Create branch'}
</Button>
</div>
</div>
)
})}
</div>
</>
)}
</div>
</SidePanel.Content>
</div>
</SidePanel>
)
}
export default CreateBranchSidePanel
@@ -43,10 +43,10 @@ const FreeProjectLimitWarning = ({
{orgLevelBilling && (
<div>
<Link href={`/org/${orgSlug}/billing?panel=subscriptionPlan`} passHref>
<a target="_blank">
<Button type="primary">Upgrade plan</Button>
</a>
<Link passHref href={`/org/${orgSlug}/billing?panel=subscriptionPlan`}>
<Button asChild type="primary">
<a>Upgrade plan</a>
</Button>
</Link>
</div>
)}
@@ -1,21 +1,26 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useParams } from 'common'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useForm } from 'react-hook-form'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
Form_Shadcn_,
IconAlertCircle,
IconFileText,
IconGitBranch,
Modal,
} from 'ui'
import * as z from 'zod'
import SidePanelGitHubRepoLinker from 'components/interfaces/Organization/IntegrationSettings/SidePanelGitHubRepoLinker'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
import { useCheckGithubBranchValidity } from 'data/integrations/integrations-github-branch-check'
import { useOrgIntegrationsQuery } from 'data/integrations/integrations-query-org-only'
import { useSelectedOrganization, useStore } from 'hooks'
import { useAppStateSnapshot } from 'state/app-state'
@@ -24,17 +29,15 @@ import GithubRepositorySelection from './GithubRepositorySelection'
const EnableBranchingModal = () => {
const { ui } = useStore()
const { ref } = useParams()
const snap = useAppStateSnapshot()
const selectedOrg = useSelectedOrganization()
const snap = useAppStateSnapshot()
const [selectedBranch, setSelectedBranch] = useState<string>()
// [Joshen] There's something weird with RHF that I can't figure out atm
// but calling form.formState.isValid somehow removes the onBlur check,
// and makes the validation run onChange instead. This is a workaround
const [isValid, setIsValid] = useState(false)
const isOrgBilling = !!selectedOrg?.subscription_id
useEffect(() => {
if (snap.showEnableBranchingModal) setSelectedBranch(undefined)
}, [snap.showEnableBranchingModal])
const {
data: integrations,
error: integrationsError,
@@ -45,13 +48,6 @@ const EnableBranchingModal = () => {
orgSlug: selectedOrg?.slug,
})
const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({
onSuccess: () => {
ui.setNotification({ category: 'success', message: `Successfully created new branch` })
snap.setShowEnableBranchingModal(false)
},
})
const hasGithubIntegrationInstalled =
integrations?.some((integration) => integration.integration.name === 'GitHub') ?? false
const githubIntegration = integrations?.find(
@@ -59,13 +55,64 @@ const EnableBranchingModal = () => {
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 onEnableBranching = () => {
const { mutateAsync: checkGithubBranchValidity, isLoading: isChecking } =
useCheckGithubBranchValidity({ onError: () => {} })
const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({
onSuccess: () => {
ui.setNotification({ category: 'success', message: `Successfully created new branch` })
snap.setShowEnableBranchingModal(false)
},
})
const formId = 'enable-branching-form'
const FormSchema = z.object({
branchName: z
.string()
.refine((val) => val.length > 1, `Please enter a branch name from ${repoOwner}/${repoName}`)
.refine(async (val) => {
try {
if (val.length > 0) {
await checkGithubBranchValidity({
organizationIntegrationId: githubIntegration?.id,
repoOwner,
repoName,
branchName: val,
})
setIsValid(true)
}
return true
} catch (error) {
setIsValid(false)
return false
}
}, `Unable to find branch from ${repoOwner}/${repoName}`),
})
const form = useForm<z.infer<typeof FormSchema>>({
mode: 'onBlur',
reValidateMode: 'onChange',
resolver: zodResolver(FormSchema),
defaultValues: { branchName: '' },
})
const canSubmit = form.getValues('branchName').length > 0 && !isChecking && isValid
const onSubmit = (data: z.infer<typeof FormSchema>) => {
if (!ref) return console.error('Project ref is required')
if (!selectedBranch) return console.error('No branch selected')
createBranch({ projectRef: ref, branchName: selectedBranch, gitBranch: selectedBranch })
createBranch({ projectRef: ref, branchName: data.branchName, gitBranch: data.branchName })
}
useEffect(() => {
if (form && snap.showEnableBranchingModal) {
setIsValid(false)
form.reset()
}
}, [form, snap.showEnableBranchingModal])
return (
<>
<Modal
@@ -75,131 +122,145 @@ const EnableBranchingModal = () => {
className="!bg"
size="medium"
>
<Modal.Content className="px-7 py-5 flex items-center space-x-4">
<IconGitBranch strokeWidth={2} size={20} />
<div>
<p className="text-foreground">Enable database branching</p>
<p className="text-sm text-light">Manage environments in Supabase</p>
</div>
</Modal.Content>
{!isOrgBilling ? (
<>
<Modal.Content className="px-3 border-y bg-alternative">
<Alert_Shadcn_ variant="default" className="rounded-none border-0">
<IconAlertCircle strokeWidth={2} />
<AlertTitle_Shadcn_>
Organization-based billing migration required
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<p className="!leading-normal">
Your organization will first need to be migrated to use our new
organization-based billing before you can enable Branching.
</p>
<p className="!leading-normal mt-1">
You may do so under the billing settings of your organization.
</p>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</Modal.Content>
<Modal.Content className="px-4 pt-3">
<div className="flex items-center justify-end space-x-2 py-2 pb-4">
<Button
disabled={isCreating}
type="default"
onClick={() => snap.setShowEnableBranchingModal(false)}
>
Understood
</Button>
<Link passHref href={`/org/${selectedOrg?.slug}/billing`}>
<Button asChild>
<a>Organization settings</a>
</Button>
</Link>
<Form_Shadcn_ {...form}>
<form
id={formId}
className="space-y-4"
onSubmit={form.handleSubmit(onSubmit)}
onChange={() => setIsValid(false)}
>
<Modal.Content className="px-7 py-5 flex items-center space-x-4">
<IconGitBranch strokeWidth={2} size={20} />
<div>
<p className="text-foreground">Enable database branching</p>
<p className="text-sm text-light">Manage environments in Supabase</p>
</div>
</Modal.Content>
</>
) : (
<>
{isLoadingIntegrations && (
{!isOrgBilling ? (
<>
<Modal.Separator />
<Modal.Content className="px-7 py-6">
<GenericSkeletonLoader />
<Modal.Content className="px-3 border-y bg-alternative">
<Alert_Shadcn_ variant="default" className="rounded-none border-0">
<IconAlertCircle strokeWidth={2} />
<AlertTitle_Shadcn_>
Organization-based billing migration required
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<p className="!leading-normal">
Your organization will first need to be migrated to use our new
organization-based billing before you can enable Branching.
</p>
<p className="!leading-normal mt-1">
You may do so under the billing settings of your organization.
</p>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</Modal.Content>
<Modal.Separator />
</>
)}
{isErrorIntegrations && (
<>
<Modal.Separator />
<Modal.Content className="px-7 py-6">
<AlertError error={integrationsError} subject="Failed to retrieve integrations" />
<Modal.Content className="px-4 pt-3">
<div className="flex items-center justify-end space-x-2 py-2 pb-4">
<Button
disabled={isCreating}
type="default"
onClick={() => snap.setShowEnableBranchingModal(false)}
>
Understood
</Button>
<Link passHref href={`/org/${selectedOrg?.slug}/billing`}>
<Button asChild>
<a>Organization settings</a>
</Button>
</Link>
</div>
</Modal.Content>
<Modal.Separator />
</>
)}
{isSuccessIntegrations && (
) : (
<>
<GithubRepositorySelection
integration={githubIntegration}
selectedBranch={selectedBranch}
hasGithubIntegrationInstalled={hasGithubIntegrationInstalled}
setSelectedBranch={setSelectedBranch}
/>
{isLoadingIntegrations && (
<>
<Modal.Separator />
<Modal.Content className="px-7 py-6">
<GenericSkeletonLoader />
</Modal.Content>
<Modal.Separator />
</>
)}
{isErrorIntegrations && (
<>
<Modal.Separator />
<Modal.Content className="px-7 py-6">
<AlertError
error={integrationsError}
subject="Failed to retrieve integrations"
/>
</Modal.Content>
<Modal.Separator />
</>
)}
{isSuccessIntegrations && (
<>
<GithubRepositorySelection
form={form}
isChecking={isChecking}
isValid={canSubmit}
integration={githubIntegration}
hasGithubIntegrationInstalled={hasGithubIntegrationInstalled}
/>
</>
)}
<Modal.Content className="px-7 py-6 flex flex-col gap-3">
<p className="text-sm text-light">Please keep in mind the following:</p>
<div className="flex flex-row gap-4">
<div>
<figure className="w-10 h-10 rounded-md bg-warning-200 border border-warning-300 flex items-center justify-center">
<IconFileText className="text-amber-900" size={20} strokeWidth={2} />
</figure>
</div>
<div>
<p className="text-sm text-foreground">
You will not be able to use the dashboard to make changes to the database
</p>
<p className="text-sm text-light">
Schema changes for database preview branches must be done via Git. We are
nonetheless working on allowing the dashboard to make schema changes for
preview branches.
</p>
</div>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content className="px-7">
<div className="flex items-center space-x-2 py-2 pb-4">
<Button
size="medium"
block
disabled={isCreating}
type="default"
onClick={() => snap.setShowEnableBranchingModal(false)}
>
Cancel
</Button>
<Button
block
size="medium"
form={formId}
disabled={isCreating || !canSubmit}
loading={isCreating}
type="primary"
htmlType="submit"
>
I understand, enable branching
</Button>
</div>
</Modal.Content>
</>
)}
<Modal.Content className="px-7 py-6 flex flex-col gap-3">
<p className="text-sm text-light">Please keep in mind the following:</p>
<div className="flex flex-row gap-4">
<div>
<figure className="w-10 h-10 rounded-md bg-warning-200 border border-warning-300 flex items-center justify-center">
<IconFileText className="text-amber-900" size={20} strokeWidth={2} />
</figure>
</div>
<div>
<p className="text-sm text-foreground">
You will not be able to use the dashboard to make changes to the database
</p>
<p className="text-sm text-light">
Schema changes for database preview branches must be done via Git. We are
nonetheless working on allowing the dashboard to make schema changes for preview
branches.
</p>
</div>
</div>
</Modal.Content>
<Modal.Separator />
<Modal.Content className="px-7">
<div className="flex items-center space-x-2 py-2 pb-4">
<Button
size="medium"
block
disabled={isCreating}
type="default"
onClick={() => snap.setShowEnableBranchingModal(false)}
>
Cancel
</Button>
<Button
size="medium"
block
disabled={selectedBranch === undefined || isCreating}
loading={isCreating}
type="primary"
onClick={() => onEnableBranching()}
>
I understand, enable branching
</Button>
</div>
</Modal.Content>
</>
)}
</form>
</Form_Shadcn_>
</Modal>
<SidePanelGitHubRepoLinker projectRef={ref} />
@@ -1,57 +1,48 @@
import clsx from 'clsx'
import { useParams } from 'common'
import Link from 'next/link'
import { useRef, useState } from 'react'
import {
Badge,
Button,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
FormMessage_Shadcn_,
IconCheck,
IconChevronDown,
IconLoader,
Input_Shadcn_,
Modal,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
} from 'ui'
import {
EmptyIntegrationConnection,
IntegrationConnection,
} from 'components/interfaces/Integrations/IntegrationPanels'
import AlertError from 'components/ui/AlertError'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { useGithubBranchesQuery } from 'data/integrations/integrations-github-branches-query'
import { Integration } from 'data/integrations/integrations.types'
import { useSidePanelsStateSnapshot } from 'state/side-panels'
import { useSelectedOrganization } from 'hooks'
import { useSidePanelsStateSnapshot } from 'state/side-panels'
interface GithubRepositorySelectionProps {
form: any
isChecking: boolean
isValid: boolean
integration?: Integration
selectedBranch?: string
hasGithubIntegrationInstalled: boolean
setSelectedBranch: (name: string) => void
}
const GithubRepositorySelection = ({
form,
isChecking,
isValid,
integration,
selectedBranch,
hasGithubIntegrationInstalled,
setSelectedBranch,
}: GithubRepositorySelectionProps) => {
const org = useSelectedOrganization()
const { ref } = useParams()
const [open, setOpen] = useState(false)
const comboBoxRef = useRef<HTMLButtonElement>(null)
const org = useSelectedOrganization()
const githubConnection = integration?.connections.find(
(connection) => connection.supabase_project_ref === ref
)
const [repoOwner, repoName] = githubConnection?.metadata.name.split('/') ?? []
const sidePanels = useSidePanelsStateSnapshot()
const githubIntegrationAppUrl =
@@ -61,18 +52,6 @@ const GithubRepositorySelection = ({
? `https://github.com/apps/supabase-staging/installations/new?state=${ref}`
: `https://github.com/apps/supabase-local-testing/installations/new?state=${ref}`
const {
data: githubBranches,
error: githubBranchesError,
isLoading: isLoadingBranches,
isSuccess: isSuccessBranches,
isError: isErrorBranches,
} = useGithubBranchesQuery({
organizationIntegrationId: integration?.id,
repoOwner,
repoName,
})
function onSelectConnectRepo() {
if (integration) {
sidePanels.setGithubConnectionsOpen(true)
@@ -130,77 +109,29 @@ const GithubRepositorySelection = ({
/>
</ul>
<div>
<label className="block text-sm text-light mb-2" htmlFor="branch-selector">
Select your production branch:
</label>
{isLoadingBranches && <ShimmeringLoader />}
{isErrorBranches && (
<AlertError
error={githubBranchesError}
subject="Failed to retrieve Github branches"
/>
<FormField_Shadcn_
control={form.control}
name="branchName"
render={({ field }) => (
<FormItem_Shadcn_ className="relative">
<label className="text-sm text-foreground-light">
Choose your production branch
</label>
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} placeholder="e.g main" />
</FormControl_Shadcn_>
<div className="absolute top-9 right-3">
{isChecking ? (
<IconLoader className="animate-spin" />
) : isValid ? (
<IconCheck className="text-brand" strokeWidth={2} />
) : null}
</div>
<FormMessage_Shadcn_ />
</FormItem_Shadcn_>
)}
{isSuccessBranches && (
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild name="branch-selector">
<Button
block
type="default"
size="medium"
ref={comboBoxRef}
className={clsx(
'justify-start',
selectedBranch === undefined ? 'text-light' : 'text'
)}
iconRight={
<span className="grow flex justify-end">
<IconChevronDown className={''} />
</span>
}
>
{selectedBranch || '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) => (
<CommandItem_Shadcn_
asChild
key={branch.name}
value={branch.name}
className="cursor-pointer w-full flex items-center justify-between"
onSelect={() => {
setOpen(false)
setSelectedBranch(branch.name)
}}
onClick={() => {
setOpen(false)
setSelectedBranch(branch.name)
}}
>
<a>
{branch.name}
{branch.name === selectedBranch && <IconCheck />}
</a>
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
</div>
/>
</>
)}
</div>
@@ -0,0 +1,65 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { get } from 'data/fetchers'
import { ResponseError } from 'types'
export type GithubBranchVariables = {
organizationIntegrationId?: string
repoOwner: string
repoName: string
branchName: string
}
export async function checkGithubBranchValidity(
{ organizationIntegrationId, repoOwner, repoName, 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}',
{
params: {
path: {
organization_integration_id: organizationIntegrationId,
repo_owner: repoOwner,
repo_name: repoName,
branch_name: branchName,
},
},
signal,
}
)
if (error) throw error
return data
}
type GitHubIntegrationCreateData = Awaited<ReturnType<typeof checkGithubBranchValidity>>
export const useCheckGithubBranchValidity = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<GitHubIntegrationCreateData, ResponseError, GithubBranchVariables>,
'mutationFn'
> = {}) => {
return useMutation<GitHubIntegrationCreateData, ResponseError, GithubBranchVariables>(
(vars) => checkGithubBranchValidity(vars),
{
async onSuccess(data, variables, context) {
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to check Github branch: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
+13
View File
@@ -8,6 +8,19 @@ export const integrationKeys = {
['organizations', organization_integration_id, 'github-repos'] as const,
vercelConnectionsList: (organization_integration_id: string | undefined) =>
['organizations', organization_integration_id, 'vercel-connections'] as const,
githubBranch: (
organization_integration_id: string | undefined,
repo_owner: string,
repo_name: string,
branch_name: string
) => [
'organizations',
organization_integration_id,
'branches',
repo_owner,
repo_name,
branch_name,
],
githubBranchesList: (
organization_integration_id: string | undefined,
repo_owner: string,