mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Merge pull request #16052 from supabase/feat/branch-management
feat/branch management part 1
This commit is contained in:
30 files changed
+1395
-84
No files matched your search
@@ -79,7 +79,7 @@ const DropdownMenuItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-overlay-hover focus:text-strong data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-overlay-hover focus:text-strong data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer',
|
||||
inset && 'pl-8',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -68,8 +68,9 @@ const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
|
||||
// Check validity of project that the user is trying to access
|
||||
const projects = projectsRef.current ?? []
|
||||
const isValidProject = projects.some((project) => project.ref === ref)
|
||||
const isValidBranch = projects.some((project) => project.preview_branch_refs.includes(ref))
|
||||
|
||||
if (!isValidProject) {
|
||||
if (!isValidProject && !isValidBranch) {
|
||||
ui.setNotification({ category: 'error', message: 'This project does not exist' })
|
||||
router.push(navLayoutV2 ? `/org/${organizations?.[0].slug}` : '/projects')
|
||||
return
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { partition } from 'lodash'
|
||||
import { useState } from 'react'
|
||||
import { Button, IconSearch, Input } from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import TextConfirmModal from 'components/ui/Modals/TextConfirmModal'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useBranchDeleteMutation } from 'data/branches/branch-delete-mutation'
|
||||
import { Branch, useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useSelectedProject, useStore } from 'hooks'
|
||||
import { MainBranchPanel } from './BranchPanels'
|
||||
import CreateBranchSidePanel from './CreateBranchSidePanel'
|
||||
import PreviewBranches from './PreviewBranches'
|
||||
import PullRequests from './PullRequests'
|
||||
import UpdateBranchSidePanel from './UpdateBranchSidePanel'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const BranchManagement = () => {
|
||||
const { ui } = useStore()
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const projectDetails = useSelectedProject()
|
||||
|
||||
const isBranch = projectDetails?.parent_project_ref !== undefined
|
||||
const projectRef =
|
||||
projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
|
||||
|
||||
const [showCreateBranch, setShowCreateBranch] = useState(false)
|
||||
const [selectedBranchToUpdate, setSelectedBranchToUpdate] = useState<Branch>()
|
||||
const [selectedBranchToDelete, setSelectedBranchToDelete] = useState<Branch>()
|
||||
|
||||
const { data: branches, error, isLoading, isError, isSuccess } = useBranchesQuery({ projectRef })
|
||||
const [[mainBranch], previewBranches] = partition(branches, (branch) => branch.is_default)
|
||||
|
||||
const { mutate: deleteBranch, isLoading: isDeleting } = useBranchDeleteMutation({
|
||||
onSuccess: () => {
|
||||
if (selectedBranchToDelete?.project_ref === ref) {
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message:
|
||||
'Successfully deleted branch. You are now currently on the main branch of your project.',
|
||||
})
|
||||
router.push(`/project/${projectRef}/branches`)
|
||||
} else {
|
||||
ui.setNotification({ category: 'success', message: 'Successfully deleted branch' })
|
||||
}
|
||||
setSelectedBranchToDelete(undefined)
|
||||
},
|
||||
})
|
||||
|
||||
const onConfirmDeleteBranch = () => {
|
||||
if (selectedBranchToDelete == undefined) return console.error('No branch selected')
|
||||
if (projectRef == undefined) return console.error('Project ref is required')
|
||||
deleteBranch({ id: selectedBranchToDelete?.id, projectRef })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<div className="col-span-12">
|
||||
<h3 className="text-xl mb-8">Branch Manager</h3>
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input placeholder="Search branch" size="small" icon={<IconSearch />} />
|
||||
</div>
|
||||
<Button onClick={() => setShowCreateBranch(true)}>Create preview branch</Button>
|
||||
</div>
|
||||
<div className="">
|
||||
{isLoading && <GenericSkeletonLoader />}
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve branches" />}
|
||||
{isSuccess && (
|
||||
<>
|
||||
<MainBranchPanel branch={mainBranch} onSelectUpdate={() => {}} />
|
||||
<PullRequests previewBranches={previewBranches} />
|
||||
<PreviewBranches
|
||||
previewBranches={previewBranches}
|
||||
onSelectUpdateBranch={setSelectedBranchToUpdate}
|
||||
onSelectDeleteBranch={setSelectedBranchToDelete}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
|
||||
<TextConfirmModal
|
||||
size="medium"
|
||||
visible={selectedBranchToDelete !== undefined}
|
||||
onCancel={() => setSelectedBranchToDelete(undefined)}
|
||||
onConfirm={() => onConfirmDeleteBranch()}
|
||||
title="Delete branch"
|
||||
loading={isDeleting}
|
||||
confirmLabel={`Delete branch`}
|
||||
confirmPlaceholder="Type in name of branch"
|
||||
confirmString={selectedBranchToDelete?.name ?? ''}
|
||||
text={`This will delete your branch "${selectedBranchToDelete?.name}"`}
|
||||
alert="You cannot recover this branch once it is deleted!"
|
||||
/>
|
||||
|
||||
<CreateBranchSidePanel
|
||||
visible={showCreateBranch}
|
||||
onClose={() => setShowCreateBranch(false)}
|
||||
/>
|
||||
|
||||
<UpdateBranchSidePanel
|
||||
selectedBranch={selectedBranchToUpdate}
|
||||
onClose={() => setSelectedBranchToUpdate(undefined)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BranchManagement
|
||||
@@ -0,0 +1,160 @@
|
||||
import clsx from 'clsx'
|
||||
import dayjs from 'dayjs'
|
||||
import { noop } from 'lodash'
|
||||
import { PropsWithChildren, forwardRef, useState } from 'react'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenuContent_Shadcn_,
|
||||
DropdownMenuItem_Shadcn_,
|
||||
DropdownMenuTrigger_Shadcn_,
|
||||
DropdownMenu_Shadcn_,
|
||||
IconEdit,
|
||||
IconGitBranch,
|
||||
IconGitHub,
|
||||
IconMoreVertical,
|
||||
IconShield,
|
||||
IconTrash,
|
||||
cn,
|
||||
} from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
|
||||
interface BranchPanelProps {
|
||||
branch: Branch
|
||||
onSelectUpdate?: () => void
|
||||
onSelectDelete?: () => void
|
||||
}
|
||||
|
||||
const MainBranchPanel = ({ branch, onSelectUpdate = noop }: BranchPanelProps) => {
|
||||
const { ref } = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
const isActive = ref === branch.project_ref
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg">
|
||||
<div className="bg-surface-200 shadow-sm flex justify-between items-center pl-8 pr-6 py-3 rounded-t-lg text-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-8 h-8 bg-scale-200 rounded-md flex items-center justify-center">
|
||||
<IconGitHub size={18} strokeWidth={2} />
|
||||
</div>
|
||||
<p>Github branch workflow</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-surface-100 border-t shadow-sm flex justify-between items-center pl-8 pr-6 py-3 rounded-b-lg text-sm">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-8 h-8 bg-amber-300 rounded-md flex items-center justify-center">
|
||||
<IconShield size={18} strokeWidth={2} className="text-amber-900" />
|
||||
</div>
|
||||
<p>{branch.name}</p>
|
||||
<Badge color="amber">Production</Badge>
|
||||
{isActive && <Badge color="green">Selected</Badge>}
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<DropdownMenu_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
||||
<DropdownMenuTrigger_Shadcn_>
|
||||
<Button asChild type="text" className="px-1" icon={<IconMoreVertical size={14} />}>
|
||||
<span></span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger_Shadcn_>
|
||||
<DropdownMenuContent_Shadcn_ side="bottom" align="end">
|
||||
<DropdownMenuItem_Shadcn_ className="flex gap-2" onSelect={() => onSelectUpdate()}>
|
||||
<IconEdit size={14} />
|
||||
Edit branch
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</DropdownMenuContent_Shadcn_>
|
||||
</DropdownMenu_Shadcn_>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BranchContainer = ({ className, children }: PropsWithChildren<{ className?: string }>) => {
|
||||
return (
|
||||
<div className="list-none ml-6 pl-8 pb-4 border-l border-scale-600 dark:border-scale-400 relative last:!border-transparent">
|
||||
<div className="absolute w-[33px] rounded-bl-full border-b border-l border-scale-600 dark:border-scale-400 h-10 -left-px" />
|
||||
<div
|
||||
className={clsx(
|
||||
'border shadow-sm flex justify-between items-center pl-8 pr-6 py-4 rounded-lg text-sm',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const BranchPanel = ({
|
||||
branch,
|
||||
onSelectUpdate = noop,
|
||||
onSelectDelete = noop,
|
||||
}: BranchPanelProps) => {
|
||||
const { ref } = useParams()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const isActive = ref === branch.project_ref
|
||||
const daysFromNow = dayjs().diff(dayjs(branch.created_at), 'day')
|
||||
const formattedTimeFromNow = dayjs(branch.created_at).fromNow()
|
||||
const formattedCreatedAt = dayjs(branch.created_at).format('DD MMM YYYY, HH:mm:ss (ZZ)')
|
||||
|
||||
return (
|
||||
<BranchContainer className="bg-surface-100">
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconGitBranch className="text-brand-900" size={16} strokeWidth={2} />
|
||||
<p>{branch.name}</p>
|
||||
{isActive && <Badge color="green">Selected</Badge>}
|
||||
<p className="text-scale-1000">
|
||||
{daysFromNow > 1 ? `Created on ${formattedCreatedAt}` : `Created ${formattedTimeFromNow}`}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<DropdownMenu_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
||||
<DropdownMenuTrigger_Shadcn_>
|
||||
<Button asChild type="text" className="px-1" icon={<IconMoreVertical size={14} />}>
|
||||
<span></span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger_Shadcn_>
|
||||
<DropdownMenuContent_Shadcn_ side="bottom" align="end">
|
||||
<DropdownMenuItem_Shadcn_ className="flex gap-2" onSelect={() => onSelectUpdate()}>
|
||||
<IconEdit size={14} />
|
||||
Edit branch
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
<DropdownMenuItem_Shadcn_ className="flex gap-2" onSelect={() => onSelectDelete()}>
|
||||
<IconTrash size={14} />
|
||||
Delete branch
|
||||
</DropdownMenuItem_Shadcn_>
|
||||
</DropdownMenuContent_Shadcn_>
|
||||
</DropdownMenu_Shadcn_>
|
||||
</div>
|
||||
</BranchContainer>
|
||||
)
|
||||
}
|
||||
|
||||
interface BranchHeader extends React.HTMLAttributes<HTMLDivElement> {
|
||||
name?: string
|
||||
markdown?: string
|
||||
}
|
||||
|
||||
const BranchHeader = forwardRef<HTMLDivElement, BranchHeader>(
|
||||
({ className, name, markdown = '', ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-l border-scale-600 dark:border-scale-400 ml-6 pl-8 pt-6 pb-3',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Markdown content={markdown} className="" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
BranchHeader.displayName = 'BranchHeader'
|
||||
export { BranchHeader, BranchContainer, BranchPanel, MainBranchPanel }
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useRef } from 'react'
|
||||
import { Button, Form, Input, Listbox, SidePanel } from 'ui'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useBranchCreateMutation } from 'data/branches/branch-create-mutation'
|
||||
import { useSelectedProject, useStore } from 'hooks'
|
||||
|
||||
interface CreateBranchSidePanelProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const MOCK_BRANCHES = [
|
||||
{ id: 1, name: 'feat/branch-01' },
|
||||
{ id: 2, name: 'feat/branch-02' },
|
||||
]
|
||||
|
||||
const CreateBranchSidePanel = ({ visible, onClose }: CreateBranchSidePanelProps) => {
|
||||
const { ui } = useStore()
|
||||
const submitRef: any = useRef()
|
||||
const { ref } = useParams()
|
||||
const projectDetails = useSelectedProject()
|
||||
|
||||
const isBranch = projectDetails?.parent_project_ref !== undefined
|
||||
const projectRef =
|
||||
projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
|
||||
|
||||
const formId = 'create-branch-form'
|
||||
const initialValues = { branchName: '', gitBranch: 'no-selection' }
|
||||
const { mutate: createBranch, isLoading: isCreating } = useBranchCreateMutation({
|
||||
onSuccess: () => {
|
||||
ui.setNotification({ category: 'success', message: `Successfully created new branch` })
|
||||
onClose()
|
||||
},
|
||||
})
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
if (values.branchName.length === 0)
|
||||
errors.branchName = 'Please provide a name for your preview branch'
|
||||
if (values.gitBranch.length === 0 || values.gitBranch === 'no-selection')
|
||||
errors.gitBranch = 'Please select a Git branch to link to this database preview branch'
|
||||
return errors
|
||||
}
|
||||
|
||||
const onConfirmCreate = (values: any) => {
|
||||
if (!projectRef) return console.error('Project ref is required')
|
||||
createBranch({ projectRef, ...values })
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
visible={visible}
|
||||
loading={isCreating}
|
||||
onCancel={onClose}
|
||||
header="Create a new database preview branch"
|
||||
customFooter={
|
||||
<div className="flex w-full justify-end space-x-3 border-t border-scale-500 px-3 py-4">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClose}
|
||||
disabled={isCreating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
htmlType="button"
|
||||
disabled={isCreating}
|
||||
loading={isCreating}
|
||||
onClick={() => submitRef?.current?.click()}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="py-6">
|
||||
<SidePanel.Content>
|
||||
<Form
|
||||
validateOnBlur
|
||||
id={formId}
|
||||
initialValues={initialValues}
|
||||
validate={validate}
|
||||
onSubmit={onConfirmCreate}
|
||||
>
|
||||
{() => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Input id="branchName" label="Branch name" />
|
||||
<Listbox size="medium" id="gitBranch" name="gitBranch" label="Git branch">
|
||||
<Listbox.Option
|
||||
label="---"
|
||||
key="no-selection"
|
||||
id="no-selection"
|
||||
value="no-selection"
|
||||
>
|
||||
---
|
||||
</Listbox.Option>
|
||||
{MOCK_BRANCHES.map((branch) => (
|
||||
<Listbox.Option
|
||||
key={branch.id}
|
||||
id={branch.id.toString()}
|
||||
value={branch.name}
|
||||
label={branch.name}
|
||||
>
|
||||
<p className="text-scale-1200">{branch.name}</p>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
|
||||
<button ref={submitRef} type="submit" className="hidden" />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateBranchSidePanel
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
import { BranchContainer, BranchHeader, BranchPanel } from './BranchPanels'
|
||||
import { Button, IconExternalLink, IconGitBranch } from 'ui'
|
||||
|
||||
interface PreviewBranchesProps {
|
||||
previewBranches: Branch[]
|
||||
onSelectUpdateBranch: (branch: Branch) => void
|
||||
onSelectDeleteBranch: (branch: Branch) => void
|
||||
}
|
||||
|
||||
const PreviewBranches = ({
|
||||
previewBranches,
|
||||
onSelectUpdateBranch,
|
||||
onSelectDeleteBranch,
|
||||
}: PreviewBranchesProps) => {
|
||||
return (
|
||||
<>
|
||||
<BranchHeader markdown={previewBranches.length > 0 ? `#### Preview branches` : undefined} />
|
||||
{previewBranches.length === 0 ? (
|
||||
<BranchContainer>
|
||||
<div className="flex items-center flex-col justify-center w-full py-8">
|
||||
<p>No database preview branches</p>
|
||||
<p className="text-scale-1000">Database preview branches will be shown here</p>
|
||||
<div className="w-[500px] border rounded-md mt-4">
|
||||
<div className="px-5 py-3 bg-surface-100 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconGitBranch strokeWidth={2} className="text-scale-1100" />
|
||||
<div>
|
||||
<p>Create a preview branch</p>
|
||||
<p className="text-scale-1000">Start developing in preview</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="default">Create preview branch</Button>
|
||||
</div>
|
||||
<div className="px-5 py-3 border-t flex items-center justify-between">
|
||||
<div>
|
||||
<p>Not sure what to do?</p>
|
||||
<p className="text-scale-1000">Browse our documentation</p>
|
||||
</div>
|
||||
<Button type="default" iconRight={<IconExternalLink />}>
|
||||
Docs
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BranchContainer>
|
||||
) : (
|
||||
previewBranches.map((branch) => (
|
||||
<BranchPanel
|
||||
key={branch.id}
|
||||
branch={branch}
|
||||
onSelectUpdate={() => onSelectUpdateBranch(branch)}
|
||||
onSelectDelete={() => onSelectDeleteBranch(branch)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreviewBranches
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
import { BranchContainer, BranchHeader } from './BranchPanels'
|
||||
import { Button, IconExternalLink, IconGitBranch } from 'ui'
|
||||
|
||||
interface PullRequestsProps {
|
||||
previewBranches: Branch[]
|
||||
}
|
||||
|
||||
const PullRequests = ({ previewBranches }: PullRequestsProps) => {
|
||||
const pullRequests = []
|
||||
|
||||
return (
|
||||
<>
|
||||
<BranchHeader
|
||||
markdown={
|
||||
pullRequests.length > 0 && previewBranches.length > 0
|
||||
? `#### Open pull requests`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{previewBranches.length === 0 || pullRequests.length === 0 ? (
|
||||
<BranchContainer>
|
||||
<div className="flex items-center flex-col justify-center w-full py-8">
|
||||
<p>No pull requests made yet for this repository</p>
|
||||
<p className="text-scale-1000">
|
||||
Only pull requests with the ./migration directory changes will show here.
|
||||
</p>
|
||||
{previewBranches.length > 0 && (
|
||||
<div className="w-96 border rounded-md mt-4">
|
||||
<div className="px-5 py-3 bg-surface-100 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconGitBranch strokeWidth={2} className="text-scale-1100" />
|
||||
<p>Create a pull request</p>
|
||||
</div>
|
||||
<Button type="default" iconRight={<IconExternalLink />}>
|
||||
Github
|
||||
</Button>
|
||||
</div>
|
||||
<div className="px-5 py-3 border-t flex items-center justify-between">
|
||||
<div>
|
||||
<p>Not sure what to do?</p>
|
||||
<p className="text-scale-1000">Browse our documentation</p>
|
||||
</div>
|
||||
<Button type="default" iconRight={<IconExternalLink />}>
|
||||
Docs
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BranchContainer>
|
||||
) : (
|
||||
<BranchContainer>State 3</BranchContainer>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PullRequests
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useParams } from 'common'
|
||||
import { useBranchUpdateMutation } from 'data/branches/branch-update-mutation'
|
||||
import { Branch } from 'data/branches/branches-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { useRef } from 'react'
|
||||
import { Button, Form, Input, SidePanel } from 'ui'
|
||||
|
||||
interface UpdateBranchSidePanelProps {
|
||||
selectedBranch?: Branch
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const UpdateBranchSidePanel = ({ selectedBranch, onClose }: UpdateBranchSidePanelProps) => {
|
||||
const { ui } = useStore()
|
||||
const submitRef: any = useRef()
|
||||
const { ref: projectRef } = useParams()
|
||||
const { mutate: updateBranch, isLoading: isUpdating } = useBranchUpdateMutation({
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
ui.setNotification({ category: 'success', message: 'Successfully updated branch' })
|
||||
},
|
||||
})
|
||||
|
||||
const formId = 'update-branch-form'
|
||||
const initialValues = {
|
||||
branchName: selectedBranch?.name,
|
||||
gitBranch: selectedBranch?.git_branch,
|
||||
}
|
||||
|
||||
const validate = (values: any) => {
|
||||
const errors: any = {}
|
||||
if (values.branchName.length === 0) errors.branchName = 'Please provide a branch name'
|
||||
if (values.gitBranch.length === 0) errors.gitBranch = 'Please provide a Git branch'
|
||||
return errors
|
||||
}
|
||||
|
||||
const onConfirmUpdate = (values: any) => {
|
||||
if (selectedBranch === undefined) return console.error('No branch selected')
|
||||
if (projectRef === undefined) return console.error('Project ref is required')
|
||||
updateBranch({ ...values, id: selectedBranch.id, projectRef })
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
visible={selectedBranch !== undefined}
|
||||
header={`Update branch: ${selectedBranch?.name}`}
|
||||
onCancel={onClose}
|
||||
customFooter={
|
||||
<div className="flex w-full justify-end space-x-3 border-t border-scale-500 px-3 py-4">
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
htmlType="button"
|
||||
onClick={onClose}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="primary"
|
||||
htmlType="button"
|
||||
disabled={isUpdating}
|
||||
loading={isUpdating}
|
||||
onClick={() => submitRef?.current?.click()}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="py-6">
|
||||
<SidePanel.Content>
|
||||
<Form
|
||||
validateOnBlur
|
||||
id={formId}
|
||||
initialValues={initialValues}
|
||||
validate={validate}
|
||||
onSubmit={onConfirmUpdate}
|
||||
>
|
||||
{() => {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Input id="branchName" label="Branch name" />
|
||||
<Input disabled id="gitBranch" label="Git branch" />
|
||||
<button ref={submitRef} type="submit" className="hidden" />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</SidePanel.Content>
|
||||
</div>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateBranchSidePanel
|
||||
@@ -9,7 +9,11 @@ interface Props extends Omit<ReactMarkdownProps, 'children' | 'node'> {
|
||||
|
||||
const Markdown = ({ className, content, ...props }: Props) => {
|
||||
return (
|
||||
<ReactMarkdown {...props} className={cn('prose text-sm', className)}>
|
||||
<ReactMarkdown
|
||||
components={{ h3: ({ children }) => <h3 className="mb-1">{children}</h3> }}
|
||||
{...props}
|
||||
className={cn('prose text-sm', className)}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
)
|
||||
|
||||
@@ -20,11 +20,20 @@ import AlertError from 'components/ui/AlertError'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useFlag } from 'hooks'
|
||||
import { useFlag, useProjectByRef } from 'hooks'
|
||||
import { getCloudProviderArchitecture } from 'lib/cloudprovider-utils'
|
||||
import { BASE_PATH, PROJECT_STATUS } from 'lib/constants'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { SUBSCRIPTION_PANEL_KEYS, useSubscriptionPageStateSnapshot } from 'state/subscription-page'
|
||||
import { Alert, Button, IconChevronRight, IconExternalLink } from 'ui'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
IconAlertCircle,
|
||||
IconChevronRight,
|
||||
IconExternalLink,
|
||||
} from 'ui'
|
||||
import { ComputeInstanceSidePanel, CustomDomainSidePanel, PITRSidePanel } from './'
|
||||
|
||||
const Addons = () => {
|
||||
@@ -34,6 +43,8 @@ const Addons = () => {
|
||||
const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate')
|
||||
const { project: selectedProject } = useProjectContext()
|
||||
|
||||
const parentProject = useProjectByRef(selectedProject?.parent_project_ref)
|
||||
const isBranch = parentProject !== undefined
|
||||
const isProjectActive = useIsProjectActive()
|
||||
const allowedPanelValues = ['computeInstance', 'pitr', 'customDomain']
|
||||
if (panel && typeof panel === 'string' && allowedPanelValues.includes(panel)) {
|
||||
@@ -78,6 +89,25 @@ const Addons = () => {
|
||||
|
||||
<ScaffoldDivider />
|
||||
|
||||
{isBranch && (
|
||||
<ScaffoldContainer>
|
||||
<Alert_Shadcn_ variant="default" className="mt-6">
|
||||
<IconAlertCircle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>
|
||||
You are currently on a preview branch of your project
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Updating addons are not available while you're on a preview branch. To manage your
|
||||
addons, you may return to your{' '}
|
||||
<Link passHref href={`/project/${parentProject.ref}/settings/general`}>
|
||||
<a className="text-brand-900">main branch</a>
|
||||
</Link>
|
||||
.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
</ScaffoldContainer>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
@@ -161,7 +191,7 @@ const Addons = () => {
|
||||
type="default"
|
||||
className="mt-2 pointer-events-auto"
|
||||
onClick={() => snap.setPanelKey('computeInstance')}
|
||||
disabled={!isProjectActive || projectUpdateDisabled}
|
||||
disabled={isBranch || !isProjectActive || projectUpdateDisabled}
|
||||
>
|
||||
Change optimized compute
|
||||
</Button>
|
||||
@@ -356,7 +386,7 @@ const Addons = () => {
|
||||
type="default"
|
||||
className="mt-2 pointer-events-auto"
|
||||
onClick={() => snap.setPanelKey('pitr')}
|
||||
disabled={!isProjectActive || projectUpdateDisabled}
|
||||
disabled={isBranch || !isProjectActive || projectUpdateDisabled}
|
||||
>
|
||||
Change point in time recovery
|
||||
</Button>
|
||||
@@ -420,7 +450,7 @@ const Addons = () => {
|
||||
type="default"
|
||||
className="mt-2 pointer-events-auto"
|
||||
onClick={() => snap.setPanelKey('customDomain')}
|
||||
disabled={!isProjectActive || projectUpdateDisabled}
|
||||
disabled={isBranch || !isProjectActive || projectUpdateDisabled}
|
||||
>
|
||||
Change custom domain
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import Link from 'next/link'
|
||||
import { Button, Form, IconBarChart2, Input } from 'ui'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
Form,
|
||||
IconAlertCircle,
|
||||
IconBarChart2,
|
||||
Input,
|
||||
} from 'ui'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import {
|
||||
@@ -14,7 +23,7 @@ import {
|
||||
import Panel from 'components/ui/Panel'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useProjectUpdateMutation } from 'data/projects/project-update-mutation'
|
||||
import { useCheckPermissions, useSelectedOrganization, useStore } from 'hooks'
|
||||
import { useCheckPermissions, useProjectByRef, useSelectedOrganization, useStore } from 'hooks'
|
||||
import PauseProjectButton from './Infrastructure/PauseProjectButton'
|
||||
import RestartServerButton from './Infrastructure/RestartServerButton'
|
||||
|
||||
@@ -23,6 +32,9 @@ const General = () => {
|
||||
const { project } = useProjectContext()
|
||||
const organization = useSelectedOrganization()
|
||||
|
||||
const parentProject = useProjectByRef(project?.parent_project_ref)
|
||||
const isBranch = parentProject !== undefined
|
||||
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
const formId = 'project-general-settings'
|
||||
const initialValues = { name: project?.name ?? '', ref: project?.ref ?? '' }
|
||||
@@ -41,6 +53,24 @@ const General = () => {
|
||||
return (
|
||||
<div>
|
||||
<FormHeader title="Project Settings" description="" />
|
||||
|
||||
{isBranch && (
|
||||
<Alert_Shadcn_ variant="default" className="mb-6">
|
||||
<IconAlertCircle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>
|
||||
You are currently on a preview branch of your project
|
||||
</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
Certain settings are not available while you're on a preview branch. To adjust your
|
||||
project settings, you may return to your{' '}
|
||||
<Link passHref href={`/project/${parentProject.ref}/settings/general`}>
|
||||
<a className="text-brand-900">main branch</a>
|
||||
</Link>
|
||||
.
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
|
||||
{project === undefined ? (
|
||||
<GenericSkeletonLoader />
|
||||
) : (
|
||||
@@ -72,7 +102,7 @@ const General = () => {
|
||||
id="name"
|
||||
size="small"
|
||||
label="Project name"
|
||||
disabled={!canUpdateProject}
|
||||
disabled={isBranch || !canUpdateProject}
|
||||
/>
|
||||
<Input copy disabled id="ref" size="small" label="Reference ID" />
|
||||
</FormSectionContent>
|
||||
@@ -82,7 +112,7 @@ const General = () => {
|
||||
}}
|
||||
</Form>
|
||||
)}
|
||||
{isOrgBilling && (
|
||||
{!isBranch && isOrgBilling && (
|
||||
<>
|
||||
<div className="mt-6">
|
||||
<FormPanel>
|
||||
|
||||
@@ -2,7 +2,7 @@ import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { useFlag, useSelectedOrganization } from 'hooks'
|
||||
import FeedbackDropdown from '../ProjectLayout/LayoutHeader/FeedbackDropdown'
|
||||
import HelpPopover from '../ProjectLayout/LayoutHeader/HelpPopover'
|
||||
import NotificationsPopover from '../ProjectLayout/LayoutHeader/NotificationsPopover'
|
||||
@@ -10,11 +10,13 @@ import OrganizationDropdown from './OrganizationDropdown'
|
||||
import ProjectDropdown from './ProjectDropdown'
|
||||
import SettingsButton from './SettingsButton'
|
||||
import UserSettingsDropdown from './UserSettingsDropdown'
|
||||
import BranchDropdown from './BranchDropdown'
|
||||
|
||||
const AppHeader = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const organization = useSelectedOrganization()
|
||||
const enableBranchManagement = useFlag('branchManagement')
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-4 py-1 bg-scale-200 border-b">
|
||||
@@ -30,6 +32,7 @@ const AppHeader = () => {
|
||||
</Link>
|
||||
<OrganizationDropdown />
|
||||
{ref !== undefined && <ProjectDropdown />}
|
||||
{ref !== undefined && enableBranchManagement && <BranchDropdown />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useRef, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { Branch, useBranchesQuery } from 'data/branches/branches-query'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
IconAlertCircle,
|
||||
IconCheck,
|
||||
IconCode,
|
||||
IconGitBranch,
|
||||
IconPlus,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
import { useSelectedProject } from 'hooks'
|
||||
import { sanitizeRoute } from './ProjectDropdown'
|
||||
|
||||
const BranchLink = ({
|
||||
branch,
|
||||
isSelected,
|
||||
setOpen,
|
||||
}: {
|
||||
branch: Branch
|
||||
isSelected: boolean
|
||||
setOpen: (value: boolean) => void
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const sanitizedRoute = sanitizeRoute(router.route, router.query)
|
||||
const href =
|
||||
sanitizedRoute?.replace('[ref]', branch.project_ref) ?? `/project/${branch.project_ref}`
|
||||
|
||||
return (
|
||||
<Link passHref href={href}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
value={branch.name}
|
||||
className="cursor-pointer w-full flex items-center justify-between"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
router.push(href)
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<a>
|
||||
{branch.name}
|
||||
{isSelected && <IconCheck />}
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const BranchDropdown = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const projectDetails = useSelectedProject()
|
||||
const branchNameRef = useRef<HTMLAnchorElement>(null)
|
||||
|
||||
const isBranch = projectDetails?.parent_project_ref !== undefined
|
||||
const projectRef =
|
||||
projectDetails !== undefined ? (isBranch ? projectDetails.parent_project_ref : ref) : undefined
|
||||
const { data: branches, isLoading, isError, isSuccess } = useBranchesQuery({ projectRef })
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const popoverOffset = (branchNameRef.current?.offsetWidth ?? 0) + 12
|
||||
const selectedBranch = branches?.find((branch) => branch.project_ref === ref)
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <ShimmeringLoader className="w-[90px]" />}
|
||||
|
||||
{isError && (
|
||||
<div className="flex items-center space-x-2 text-amber-900">
|
||||
<IconAlertCircle size={16} strokeWidth={2} />
|
||||
<p className="text-sm">Failed to load branches</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSuccess && branches.length > 0 && (
|
||||
<div className="flex items-center space-x-2 px-2">
|
||||
<Link passHref href={`/project/${ref}`}>
|
||||
<a ref={branchNameRef} className="flex items-center space-x-2 text-sm">
|
||||
{selectedBranch?.name}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Link passHref href={`/project/${ref}/branches`}>
|
||||
<a ref={branchNameRef} className="flex items-center space-x-2">
|
||||
{selectedBranch?.is_default ? (
|
||||
<Badge color="amber">Production</Badge>
|
||||
) : (
|
||||
<Badge color="green">Preview Branch</Badge>
|
||||
)}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={<IconCode className="text-scale-1100 rotate-90" strokeWidth={2} size={12} />}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0"
|
||||
side="bottom"
|
||||
align="start"
|
||||
style={{ marginLeft: `-${popoverOffset}px` }}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Find branch..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No branches found</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{branches?.map((branch) => (
|
||||
<BranchLink
|
||||
key={branch.id}
|
||||
branch={branch}
|
||||
isSelected={branch.id === selectedBranch?.id}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
<CommandGroup_Shadcn_ className="border-t">
|
||||
<Link passHref href={`/project/${ref}/branches`}>
|
||||
<CommandItem_Shadcn_
|
||||
asChild
|
||||
className="cursor-pointer flex items-center space-x-2 w-full"
|
||||
onSelect={(e) => {
|
||||
setOpen(false)
|
||||
router.push(`/project/${ref}/branches`)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<a>
|
||||
<IconGitBranch size={14} strokeWidth={1.5} />
|
||||
<p>Manage branches</p>
|
||||
</a>
|
||||
</CommandItem_Shadcn_>
|
||||
</Link>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BranchDropdown
|
||||
@@ -76,8 +76,6 @@ const OrganizationDropdown = () => {
|
||||
{organizations?.map((org) => {
|
||||
const href = router.pathname.includes('[slug]')
|
||||
? router.pathname.replace('[slug]', org.slug)
|
||||
: router.pathname.includes('[ref]/settings')
|
||||
? `/org/${org.slug}/general`
|
||||
: `/org/${org.slug}`
|
||||
return (
|
||||
<Link passHref href={href} key={org.slug}>
|
||||
|
||||
@@ -106,6 +106,11 @@ const ProjectDropdown = () => {
|
||||
const projectNameRef = useRef<HTMLAnchorElement>(null)
|
||||
const { data: allProjects, isLoading: isLoadingProjects } = useProjectsQuery()
|
||||
|
||||
const isBranch = projectDetails?.parent_project_ref !== undefined
|
||||
const parentProject = isBranch
|
||||
? allProjects?.find((project) => project.ref === projectDetails.parent_project_ref)
|
||||
: undefined
|
||||
|
||||
const isOrgBilling = !!selectedOrganization?.subscription_id
|
||||
const { data: subscription, isSuccess } = useProjectSubscriptionV2Query(
|
||||
{ projectRef: ref },
|
||||
@@ -115,7 +120,9 @@ const ProjectDropdown = () => {
|
||||
?.filter((x) => x.status !== PROJECT_STATUS.INACTIVE)
|
||||
.filter((x) => x.organization_id === selectedOrganization?.id)
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
const selectedProject = projectDetails || projects?.find((project) => project.ref === ref)
|
||||
const selectedProject = isBranch
|
||||
? parentProject
|
||||
: projectDetails || projects?.find((project) => project.ref === ref)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const popoverOffset = (projectNameRef.current?.offsetWidth ?? 0) + 12
|
||||
@@ -126,7 +133,7 @@ const ProjectDropdown = () => {
|
||||
|
||||
return IS_PLATFORM ? (
|
||||
<div className="flex items-center space-x-2 px-2">
|
||||
<Link href={`/project/${selectedProject?.ref}`}>
|
||||
<Link href={`/project/${ref}`}>
|
||||
<a ref={projectNameRef} className="flex items-center space-x-2">
|
||||
<p className="text-sm">{selectedProject?.name}</p>
|
||||
{isSuccess && !isOrgBilling && <Badge color="slate">{subscription?.plan.name}</Badge>}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import {
|
||||
DropdownMenuCheckboxItem_Shadcn_,
|
||||
DropdownMenuContent_Shadcn_,
|
||||
DropdownMenuGroup_Shadcn_,
|
||||
DropdownMenuItem_Shadcn_,
|
||||
@@ -10,12 +9,8 @@ import {
|
||||
DropdownMenuRadioItem_Shadcn_,
|
||||
DropdownMenuSeparator_Shadcn_,
|
||||
DropdownMenuShortcut_Shadcn_,
|
||||
DropdownMenuSubContent_Shadcn_,
|
||||
DropdownMenuSubTrigger_Shadcn_,
|
||||
DropdownMenuSub_Shadcn_,
|
||||
DropdownMenuTrigger_Shadcn_,
|
||||
DropdownMenu_Shadcn_,
|
||||
IconCheck,
|
||||
IconUser,
|
||||
useCommandMenu,
|
||||
} from 'ui'
|
||||
|
||||
@@ -29,6 +29,8 @@ const routesToIgnoreProjectDetailsRequest = [
|
||||
'/project/[ref]/settings/billing/invoices',
|
||||
]
|
||||
|
||||
const routesToIgnoreDBConnection = ['/project/[ref]/branches']
|
||||
|
||||
const routesToIgnorePostgrestConnection = [
|
||||
'/project/[ref]/reports',
|
||||
'/project/[ref]/settings/general',
|
||||
@@ -161,8 +163,9 @@ const ContentWrapper = ({ isLoading, children }: ContentWrapperProps) => {
|
||||
const router = useRouter()
|
||||
|
||||
const requiresDbConnection: boolean =
|
||||
!router.pathname.includes('/project/[ref]/settings') ||
|
||||
router.pathname.includes('/project/[ref]/settings/vault')
|
||||
(!router.pathname.includes('/project/[ref]/settings') &&
|
||||
!routesToIgnoreDBConnection.includes(router.pathname)) ||
|
||||
router.pathname === '/project/[ref]/settings/vault'
|
||||
const requiresPostgrestConnection = !routesToIgnorePostgrestConnection.includes(router.pathname)
|
||||
const requiresProjectDetails = !routesToIgnoreProjectDetailsRequest.includes(router.pathname)
|
||||
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get, isResponseOk } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { get } from 'data/fetchers'
|
||||
import { useCallback } from 'react'
|
||||
import { resourceKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type ResourcesVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export type ResourcesResponse = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export async function getResources({ projectRef }: ResourcesVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) {
|
||||
throw new Error('projectRef is required')
|
||||
}
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
|
||||
const response = await get<ResourcesResponse[]>(`${API_URL}/projects/${projectRef}/resources`, {
|
||||
// Use any of the API endpoints as the parameter here, TS will check if its valid - "branches" here is just an example
|
||||
const { data, error } = await get(`/v1/projects/{ref}/branches`, {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
if (error) throw error
|
||||
|
||||
if (!isResponseOk(response)) {
|
||||
throw response.error
|
||||
}
|
||||
|
||||
return response
|
||||
return data
|
||||
}
|
||||
|
||||
export type ResourcesData = Awaited<ReturnType<typeof getResources>>
|
||||
export type ResourcesError = unknown
|
||||
export type ResourcesError = ResponseError
|
||||
|
||||
export const useResourcesQuery = <TData = ResourcesData>(
|
||||
{ projectRef }: ResourcesVariables,
|
||||
@@ -43,6 +36,7 @@ export const useResourcesQuery = <TData = ResourcesData>(
|
||||
|
||||
/**
|
||||
* useResourcesPrefetch is used for prefetching data. For example, starting a query loading before a page is navigated to.
|
||||
* Feel free to omit if not required
|
||||
*
|
||||
* @example
|
||||
* const prefetch = useResourcesPrefetch({ projectRef })
|
||||
|
||||
Vendored
+205
-33
@@ -396,6 +396,8 @@ export interface paths {
|
||||
"/platform/projects/{ref}/content": {
|
||||
/** Gets project's content */
|
||||
get: operations["ContentController_getContent"];
|
||||
/** Updates project's content */
|
||||
put: operations["ContentController_updateWholeContent"];
|
||||
/** Creates project's content */
|
||||
post: operations["ContentController_createContent"];
|
||||
/** Deletes project's content */
|
||||
@@ -1056,6 +1058,8 @@ export interface paths {
|
||||
"/v0/projects/{ref}/content": {
|
||||
/** Gets project's content */
|
||||
get: operations["ContentController_getContent"];
|
||||
/** Updates project's content */
|
||||
put: operations["ContentController_updateWholeContent"];
|
||||
/** Creates project's content */
|
||||
post: operations["ContentController_createContent"];
|
||||
/** Deletes project's content */
|
||||
@@ -1247,7 +1251,7 @@ export interface paths {
|
||||
/** Deletes objects */
|
||||
delete: operations["StorageObjectsController_deleteObjects"];
|
||||
};
|
||||
"/v1/branch/{branch_id}": {
|
||||
"/v1/branches/{branch_id}": {
|
||||
/**
|
||||
* Get database branch config
|
||||
* @description Fetches configurations of the specified database branch
|
||||
@@ -1339,7 +1343,7 @@ export interface paths {
|
||||
/** Updates project's postgrest config */
|
||||
patch: operations["PostgrestConfigController_updatePostgRESTConfig"];
|
||||
};
|
||||
"/v1/projects/{ref}/query": {
|
||||
"/v1/projects/{ref}/database/query": {
|
||||
/** Run sql query */
|
||||
post: operations["QueryController_runQuery"];
|
||||
};
|
||||
@@ -1485,7 +1489,6 @@ export interface paths {
|
||||
post: operations["OAuthAppsController_createOAuthApp"];
|
||||
};
|
||||
"/v1/organizations/{slug}/oauth/apps/{id}": {
|
||||
get: operations["OAuthAppsController_getOAuthApp"];
|
||||
/** Update an oauth app */
|
||||
put: operations["OAuthAppsController_updateOAuthApp"];
|
||||
/** Remove a published oauth app */
|
||||
@@ -1609,7 +1612,8 @@ export interface components {
|
||||
SMTP_HOST: string;
|
||||
SMTP_PORT: string;
|
||||
SMTP_USER: string;
|
||||
SMTP_PASS: string;
|
||||
SMTP_PASS?: string | null;
|
||||
SMTP_PASS_ENCRYPTED?: string | null;
|
||||
SMTP_MAX_FREQUENCY: number;
|
||||
MAILER_AUTOCONFIRM: boolean;
|
||||
MAILER_URLPATHS_INVITE: string;
|
||||
@@ -1684,7 +1688,6 @@ export interface components {
|
||||
EXTERNAL_SLACK_SECRET: string;
|
||||
SMS_MESSAGEBIRD_ACCESS_KEY: string;
|
||||
SMS_MESSAGEBIRD_ORIGINATOR: string;
|
||||
SMTP_PASS_ENCRYPTED: string;
|
||||
REFRESH_TOKEN_ROTATION_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_CLIENT_ID: string;
|
||||
@@ -1749,7 +1752,8 @@ export interface components {
|
||||
SMTP_HOST: string;
|
||||
SMTP_PORT: string;
|
||||
SMTP_USER: string;
|
||||
SMTP_PASS: string;
|
||||
SMTP_PASS?: string | null;
|
||||
SMTP_PASS_ENCRYPTED?: string | null;
|
||||
SMTP_MAX_FREQUENCY: number;
|
||||
MAILER_AUTOCONFIRM: boolean;
|
||||
MAILER_URLPATHS_INVITE: string;
|
||||
@@ -1824,7 +1828,6 @@ export interface components {
|
||||
EXTERNAL_SLACK_SECRET: string;
|
||||
SMS_MESSAGEBIRD_ACCESS_KEY: string;
|
||||
SMS_MESSAGEBIRD_ORIGINATOR: string;
|
||||
SMTP_PASS_ENCRYPTED: string;
|
||||
REFRESH_TOKEN_ROTATION_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_CLIENT_ID: string;
|
||||
@@ -1889,7 +1892,8 @@ export interface components {
|
||||
SMTP_HOST: string;
|
||||
SMTP_PORT: string;
|
||||
SMTP_USER: string;
|
||||
SMTP_PASS: string;
|
||||
SMTP_PASS?: string | null;
|
||||
SMTP_PASS_ENCRYPTED?: string | null;
|
||||
SMTP_MAX_FREQUENCY: number;
|
||||
MAILER_AUTOCONFIRM: boolean;
|
||||
MAILER_URLPATHS_INVITE: string;
|
||||
@@ -1964,7 +1968,6 @@ export interface components {
|
||||
EXTERNAL_SLACK_SECRET: string;
|
||||
SMS_MESSAGEBIRD_ACCESS_KEY: string;
|
||||
SMS_MESSAGEBIRD_ORIGINATOR: string;
|
||||
SMTP_PASS_ENCRYPTED: string;
|
||||
REFRESH_TOKEN_ROTATION_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_ENABLED: boolean;
|
||||
EXTERNAL_LINKEDIN_CLIENT_ID: string;
|
||||
@@ -3038,6 +3041,29 @@ export interface components {
|
||||
anon_key: string;
|
||||
service_key: string;
|
||||
};
|
||||
GetUserContentObject: {
|
||||
owner: {
|
||||
id?: number;
|
||||
username?: string;
|
||||
};
|
||||
updated_by: {
|
||||
id?: number;
|
||||
username?: string;
|
||||
};
|
||||
id: string;
|
||||
inserted_at: string;
|
||||
updated_at: string;
|
||||
type: Record<string, never>;
|
||||
visibility: Record<string, never>;
|
||||
name: string;
|
||||
description?: string;
|
||||
project_id: number;
|
||||
owner_id: number;
|
||||
last_updated_by: number;
|
||||
};
|
||||
GetUserContentResponse: {
|
||||
data: (components["schemas"]["GetUserContentObject"])[];
|
||||
};
|
||||
CreateContentParams: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -3049,6 +3075,30 @@ export interface components {
|
||||
content?: Record<string, never>;
|
||||
owner_id?: number;
|
||||
};
|
||||
UserContentObject: {
|
||||
id: string;
|
||||
inserted_at: string;
|
||||
updated_at: string;
|
||||
type: Record<string, never>;
|
||||
visibility: Record<string, never>;
|
||||
name: string;
|
||||
description?: string;
|
||||
project_id: number;
|
||||
owner_id: number;
|
||||
last_updated_by: number;
|
||||
};
|
||||
UpsertContentParams: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
/** @enum {string} */
|
||||
type: "sql" | "report" | "log_sql";
|
||||
/** @enum {string} */
|
||||
visibility: "user" | "project" | "org" | "public";
|
||||
content?: Record<string, never>;
|
||||
owner_id?: number;
|
||||
project_id: number;
|
||||
};
|
||||
UpdateContentParams: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
@@ -3076,6 +3126,7 @@ export interface components {
|
||||
id: string;
|
||||
name: string;
|
||||
project_ref: string;
|
||||
parent_project_ref: string;
|
||||
is_default: boolean;
|
||||
git_branch: string;
|
||||
created_at: string;
|
||||
@@ -3102,6 +3153,7 @@ export interface components {
|
||||
maxDatabasePreprovisionGb?: number;
|
||||
lastDatabaseResizeAt?: string;
|
||||
preview_branches: (components["schemas"]["BranchResponse"])[];
|
||||
parent_project_ref?: string;
|
||||
};
|
||||
ProjectRefResponse: {
|
||||
id: number;
|
||||
@@ -3191,6 +3243,11 @@ export interface components {
|
||||
name: string;
|
||||
limit: number;
|
||||
};
|
||||
PreviewTransferInvoiceItem: {
|
||||
description: string;
|
||||
quantity: number;
|
||||
amount: number;
|
||||
};
|
||||
PreviewProjectTransferResponse: {
|
||||
valid: boolean;
|
||||
warnings: (components["schemas"]["PreviewTransferInfo"])[];
|
||||
@@ -3206,6 +3263,8 @@ export interface components {
|
||||
charge_on_target_organization: number;
|
||||
source_subscription_plan: Record<string, never>;
|
||||
target_subscription_plan: Record<string, unknown> | null;
|
||||
source_invoice_items: (components["schemas"]["PreviewTransferInvoiceItem"])[];
|
||||
target_invoice_items: (components["schemas"]["PreviewTransferInvoiceItem"])[];
|
||||
};
|
||||
AnalyticsResponse: {
|
||||
error?: OneOf<[{
|
||||
@@ -4087,19 +4146,56 @@ export interface components {
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
};
|
||||
OAuthAppResponse: {
|
||||
id: string;
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
authorized_at?: string;
|
||||
created_at?: string;
|
||||
client_id?: string;
|
||||
client_secret_alias?: string;
|
||||
redirect_uris?: (string)[];
|
||||
};
|
||||
CreateOAuthAppBody: {
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
redirect_uris: (string)[];
|
||||
};
|
||||
UpdateOAuthAppBody: {
|
||||
CreateOAuthAppResponse: {
|
||||
id: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
};
|
||||
PutOAuthAppResponse: {
|
||||
id: string;
|
||||
client_id: string;
|
||||
client_secret_alias: string;
|
||||
created_at: string;
|
||||
name: string;
|
||||
website: string;
|
||||
redirect_uris: (string)[];
|
||||
icon?: string;
|
||||
redirect_uris: (string)[];
|
||||
};
|
||||
TokenDTO: {
|
||||
RevokeAuthorizedOAuthAppResponse: {
|
||||
id: string;
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
authorized_at: string;
|
||||
};
|
||||
DeleteOAuthAppResponse: {
|
||||
id: string;
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
created_at: string;
|
||||
client_id: string;
|
||||
client_secret_alias: string;
|
||||
redirect_uris: (string)[];
|
||||
};
|
||||
OAuthTokenBody: {
|
||||
/** @enum {string} */
|
||||
grant_type: "authorization_code" | "refresh_token";
|
||||
client_id: string;
|
||||
@@ -4109,9 +4205,31 @@ export interface components {
|
||||
redirect_uri?: string;
|
||||
refresh_token?: string;
|
||||
};
|
||||
OAuthTokenResponse: {
|
||||
/** @enum {string} */
|
||||
token_type: "Bearer";
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
};
|
||||
GetAuthorizationResponse: {
|
||||
name: string;
|
||||
website: string;
|
||||
icon?: string;
|
||||
domain: string;
|
||||
expires_at: string;
|
||||
approved_at?: string;
|
||||
approved_organization_slug?: string;
|
||||
};
|
||||
AuthorizationsApproveBody: {
|
||||
organization_id: string;
|
||||
};
|
||||
ApproveAuthorizationResponse: {
|
||||
url: string;
|
||||
};
|
||||
DeclineAuthorizationResponse: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@@ -6738,11 +6856,38 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["GetUserContentResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Failed to retrieve project's content */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Updates project's content */
|
||||
ContentController_updateWholeContent: {
|
||||
parameters: {
|
||||
path: {
|
||||
/** @description Project ref */
|
||||
ref: string;
|
||||
};
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpsertContentParams"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UserContentObject"];
|
||||
};
|
||||
};
|
||||
/** @description Failed to update project's content */
|
||||
500: never;
|
||||
};
|
||||
};
|
||||
/** Creates project's content */
|
||||
ContentController_createContent: {
|
||||
parameters: {
|
||||
@@ -6759,7 +6904,7 @@ export interface operations {
|
||||
responses: {
|
||||
201: {
|
||||
content: {
|
||||
"application/json": (Record<string, never>)[];
|
||||
"application/json": (components["schemas"]["UserContentObject"])[];
|
||||
};
|
||||
};
|
||||
/** @description Failed to create project's content */
|
||||
@@ -6776,7 +6921,7 @@ export interface operations {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": Record<string, never>;
|
||||
"application/json": components["schemas"]["UserContentObject"];
|
||||
};
|
||||
};
|
||||
/** @description Failed to delete project's content */
|
||||
@@ -6798,7 +6943,7 @@ export interface operations {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": (Record<string, never>)[];
|
||||
"application/json": (components["schemas"]["UserContentObject"])[];
|
||||
};
|
||||
};
|
||||
/** @description Failed to update project's content */
|
||||
@@ -9668,7 +9813,7 @@ export interface operations {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": Record<string, never>;
|
||||
"application/json": (components["schemas"]["OAuthAppResponse"])[];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -9686,12 +9831,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
};
|
||||
};
|
||||
OAuthAppsController_getOAuthApp: {
|
||||
responses: {
|
||||
200: never;
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateOAuthAppResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Update an oauth app */
|
||||
@@ -9704,11 +9848,15 @@ export interface operations {
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdateOAuthAppBody"];
|
||||
"application/json": components["schemas"]["CreateOAuthAppBody"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["PutOAuthAppResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Remove a published oauth app */
|
||||
@@ -9720,7 +9868,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeleteOAuthAppResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Revoke an authorized oauth app */
|
||||
@@ -9732,7 +9884,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["RevokeAuthorizedOAuthAppResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Authorize user through oauth */
|
||||
@@ -9750,18 +9906,22 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
303: never;
|
||||
};
|
||||
};
|
||||
/** Exchange auth code for user's access and refresh token */
|
||||
OAuthController_token: {
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/x-www-form-urlencoded": components["schemas"]["TokenDTO"];
|
||||
"application/x-www-form-urlencoded": components["schemas"]["OAuthTokenBody"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["OAuthTokenResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
AuthorizationsController_getAuthorizationRequest: {
|
||||
@@ -9771,7 +9931,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["GetAuthorizationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Approve oauth app authorization request */
|
||||
@@ -9787,7 +9951,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
201: never;
|
||||
201: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApproveAuthorizationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/** Decline oauth app authorization request */
|
||||
@@ -9798,7 +9966,11 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: never;
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["DeclineAuthorizationResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { post } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { branchKeys } from './keys'
|
||||
|
||||
export type BranchCreateVariables = {
|
||||
projectRef: string
|
||||
branchName: string
|
||||
gitBranch?: string
|
||||
region?: string
|
||||
}
|
||||
|
||||
export async function createBranch({
|
||||
projectRef,
|
||||
branchName,
|
||||
gitBranch,
|
||||
region,
|
||||
}: BranchCreateVariables) {
|
||||
const { data, error } = await post('/v1/projects/{ref}/branches', {
|
||||
params: {
|
||||
path: { ref: projectRef },
|
||||
},
|
||||
body: {
|
||||
branch_name: branchName,
|
||||
git_branch: gitBranch,
|
||||
region: region,
|
||||
},
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
type BranchCreateData = Awaited<ReturnType<typeof createBranch>>
|
||||
|
||||
export const useBranchCreateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<BranchCreateData, ResponseError, BranchCreateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<BranchCreateData, ResponseError, BranchCreateVariables>(
|
||||
(vars) => createBranch(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(branchKeys.list(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to create branch: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { del } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { branchKeys } from './keys'
|
||||
|
||||
export type BranchDeleteVariables = {
|
||||
id: string
|
||||
projectRef: string
|
||||
}
|
||||
|
||||
export async function deleteBranch({ id }: BranchDeleteVariables) {
|
||||
const { data, error } = await del('/v1/branches/{branch_id}', {
|
||||
params: { path: { branch_id: id } },
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
type BranchDeleteData = Awaited<ReturnType<typeof deleteBranch>>
|
||||
|
||||
export const useBranchDeleteMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<BranchDeleteData, ResponseError, BranchDeleteVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<BranchDeleteData, ResponseError, BranchDeleteVariables>(
|
||||
(vars) => deleteBranch(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(branchKeys.list(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to delete branch: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'data/fetchers'
|
||||
import { branchKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type BranchVariables = {
|
||||
id: string
|
||||
}
|
||||
|
||||
export async function getBranch({ id }: BranchVariables, signal?: AbortSignal) {
|
||||
const { data, error } = await get(`/v1/branches/{branch_id}`, {
|
||||
params: { path: { branch_id: id } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
export type BranchData = Awaited<ReturnType<typeof getBranch>>
|
||||
export type BranchError = ResponseError
|
||||
|
||||
export const useBranchQuery = <TData = BranchData>(
|
||||
{ id }: BranchVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<BranchData, BranchError, TData> = {}
|
||||
) =>
|
||||
useQuery<BranchData, BranchError, TData>(
|
||||
branchKeys.detail(id),
|
||||
({ signal }) => getBranch({ id }, signal),
|
||||
{ enabled: enabled && typeof id !== 'undefined', ...options }
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
import { patch } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { branchKeys } from './keys'
|
||||
|
||||
export type BranchUpdateVariables = {
|
||||
id: string
|
||||
projectRef: string
|
||||
branchName?: string
|
||||
gitBranch?: string
|
||||
}
|
||||
|
||||
export async function updateBranch({ id, branchName, gitBranch }: BranchUpdateVariables) {
|
||||
const { data, error } = await patch('/v1/branches/{branch_id}', {
|
||||
params: {
|
||||
path: { branch_id: id },
|
||||
},
|
||||
body: {
|
||||
branch_name: branchName,
|
||||
git_branch: gitBranch,
|
||||
},
|
||||
})
|
||||
|
||||
if (error) throw error
|
||||
return data
|
||||
}
|
||||
|
||||
type BranchUpdateData = Awaited<ReturnType<typeof updateBranch>>
|
||||
|
||||
export const useBranchUpdateMutation = ({
|
||||
onSuccess,
|
||||
onError,
|
||||
...options
|
||||
}: Omit<
|
||||
UseMutationOptions<BranchUpdateData, ResponseError, BranchUpdateVariables>,
|
||||
'mutationFn'
|
||||
> = {}) => {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation<BranchUpdateData, ResponseError, BranchUpdateVariables>(
|
||||
(vars) => updateBranch(vars),
|
||||
{
|
||||
async onSuccess(data, variables, context) {
|
||||
const { projectRef } = variables
|
||||
await queryClient.invalidateQueries(branchKeys.list(projectRef))
|
||||
await onSuccess?.(data, variables, context)
|
||||
},
|
||||
async onError(data, variables, context) {
|
||||
if (onError === undefined) {
|
||||
toast.error(`Failed to update branch: ${data.message}`)
|
||||
} else {
|
||||
onError(data, variables, context)
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
|
||||
import { get } from 'data/fetchers'
|
||||
import { branchKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
import { components } from 'data/api'
|
||||
|
||||
export type BranchesVariables = {
|
||||
projectRef?: string
|
||||
}
|
||||
|
||||
export type Branch = components['schemas']['BranchResponse']
|
||||
|
||||
export async function getBranches({ projectRef }: BranchesVariables, signal?: AbortSignal) {
|
||||
if (!projectRef) throw new Error('Project ref is required')
|
||||
|
||||
const { data, error } = await get(`/v1/projects/{ref}/branches`, {
|
||||
params: { path: { ref: projectRef } },
|
||||
signal,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
if ((error as ResponseError).message === 'Preview branching is not enabled for this project.') {
|
||||
return []
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export type BranchesData = Awaited<ReturnType<typeof getBranches>>
|
||||
export type BranchesError = ResponseError
|
||||
|
||||
export const useBranchesQuery = <TData = BranchesData>(
|
||||
{ projectRef }: BranchesVariables,
|
||||
{ enabled = true, ...options }: UseQueryOptions<BranchesData, BranchesError, TData> = {}
|
||||
) =>
|
||||
useQuery<BranchesData, BranchesError, TData>(
|
||||
branchKeys.list(projectRef),
|
||||
({ signal }) => getBranches({ projectRef }, signal),
|
||||
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
export const branchKeys = {
|
||||
list: (projectRef: string | undefined) => ['projects', projectRef, 'branches'] as const,
|
||||
detail: (id: string | undefined) => ['branches', id] as const,
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
import { useParams } from 'common'
|
||||
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
|
||||
import { useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
export function useSelectedProject() {
|
||||
const { ref } = useParams()
|
||||
const { data } = useProjectDetailQuery({ ref })
|
||||
return data
|
||||
}
|
||||
|
||||
export function useProjectByRef(ref?: string) {
|
||||
const { data: projects } = useProjectsQuery()
|
||||
return useMemo(() => {
|
||||
if (!ref) return undefined
|
||||
return projects?.find((project) => project.ref === ref)
|
||||
}, [projects, ref])
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import BranchManagement from 'components/interfaces/BranchManagement/BranchManagement'
|
||||
import { ProjectLayoutWithAuth } from 'components/layouts'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const BranchManagementPage: NextPageWithLayout = () => {
|
||||
return <BranchManagement />
|
||||
}
|
||||
|
||||
BranchManagementPage.getLayout = (page) => (
|
||||
<ProjectLayoutWithAuth>
|
||||
<main style={{ maxHeight: '100vh' }} className="flex-1 overflow-y-auto">
|
||||
{page}
|
||||
</main>
|
||||
</ProjectLayoutWithAuth>
|
||||
)
|
||||
|
||||
export default BranchManagementPage
|
||||
@@ -2,14 +2,14 @@ import { ClientLibrary, ExampleProject } from 'components/interfaces/Home'
|
||||
import { CLIENT_LIBRARIES, EXAMPLE_PROJECTS } from 'components/interfaces/Home/Home.constants'
|
||||
import ProjectUsageSection from 'components/interfaces/Home/ProjectUsageSection'
|
||||
import { ProjectLayoutWithAuth } from 'components/layouts'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import ProjectPausedState from 'components/layouts/ProjectLayout/ProjectPausedState'
|
||||
import ProjectUpgradeFailedBanner from 'components/ui/ProjectUpgradeFailedBanner'
|
||||
import { useSelectedProject } from 'hooks'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const Home: NextPageWithLayout = () => {
|
||||
const { project } = useProjectContext()
|
||||
const project = useSelectedProject()
|
||||
|
||||
const projectName =
|
||||
project?.ref !== 'default' && project?.name !== undefined
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { NextPageWithLayout } from 'types'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import {
|
||||
General,
|
||||
Infrastructure,
|
||||
CustomDomainConfig,
|
||||
DeleteProjectPanel,
|
||||
General,
|
||||
Infrastructure,
|
||||
TransferProjectPanel,
|
||||
} from 'components/interfaces/Settings/General'
|
||||
import { SettingsLayout } from 'components/layouts'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useFlag, useSelectedOrganization } from 'hooks'
|
||||
import { NextPageWithLayout } from 'types'
|
||||
|
||||
const ProjectSettings: NextPageWithLayout = () => {
|
||||
const { project } = useProjectContext()
|
||||
const organization = useSelectedOrganization()
|
||||
const isBranch = !!project?.parent_project_ref
|
||||
const isOrgBilling = !!organization?.subscription_id
|
||||
const transferProjectEnabled = useFlag('transferProject')
|
||||
|
||||
@@ -18,10 +21,14 @@ const ProjectSettings: NextPageWithLayout = () => {
|
||||
return (
|
||||
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32 ">
|
||||
<General />
|
||||
{!isOrgBilling && <Infrastructure />}
|
||||
<CustomDomainConfig />
|
||||
{transferProjectEnabled && <TransferProjectPanel />}
|
||||
<DeleteProjectPanel />
|
||||
{!isBranch ? (
|
||||
<>
|
||||
{!isOrgBilling && <Infrastructure />}
|
||||
{<CustomDomainConfig />}
|
||||
{transferProjectEnabled && <TransferProjectPanel />}
|
||||
<DeleteProjectPanel />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ProjectBase {
|
||||
region: string
|
||||
inserted_at: string
|
||||
subscription_id: string
|
||||
preview_branch_refs: string[]
|
||||
}
|
||||
|
||||
export interface Project extends ProjectBase {
|
||||
@@ -32,6 +33,7 @@ export interface Project extends ProjectBase {
|
||||
restUrl?: string
|
||||
lastDatabaseResizeAt?: string | null
|
||||
maxDatabasePreprovisionGb?: string | null
|
||||
parent_project_ref?: string
|
||||
|
||||
/**
|
||||
* postgrestStatus is available on client side only.
|
||||
|
||||
Reference in new issue
Block a user