diff --git a/apps/studio/components/interfaces/Branching/MergeRequest.tsx b/apps/studio/components/interfaces/Branching/MergeRequest.tsx new file mode 100644 index 0000000000..2381bf8cd5 --- /dev/null +++ b/apps/studio/components/interfaces/Branching/MergeRequest.tsx @@ -0,0 +1,248 @@ +import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import dayjs from 'dayjs' +import { GitBranchIcon, GitMerge, MoreVertical, Shield } from 'lucide-react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { useMemo } from 'react' +import { toast } from 'sonner' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from 'ui' +import { TimestampInfo } from 'ui-patterns' + +import { useIsPgDeltaDiffEnabled } from '../App/FeaturePreview/FeaturePreviewContext' +import { ReviewWithAI } from '../BranchManagement/ReviewWithAI' +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge' +import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation' +import { useBranchesQuery } from '@/data/branches/branches-query' +import { useProjectGitHubConnectionQuery } from '@/data/integrations/github-connections-query' +import { useProjectDetailQuery } from '@/data/projects/project-detail-query' +import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' +import { useBranchMergeDiff } from '@/hooks/branches/useBranchMergeDiff' +import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' + +export const MergeTitle = () => { + const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() + const pgDeltaDiffEnabled = useIsPgDeltaDiffEnabled() + + const parentProjectRef = project?.parent_project_ref + + const { data: branches } = useBranchesQuery( + { projectRef: parentProjectRef }, + { + refetchOnMount: 'always', + refetchOnWindowFocus: true, + staleTime: 0, + } + ) + const currentBranch = branches?.find((branch) => branch.project_ref === ref) + const mainBranch = branches?.find((branch) => branch.is_default) + + return ( +
+
+ Merge + + + + {currentBranch?.name} + + + into + + + + + {mainBranch?.name || 'main'} + + +
+ + {pgDeltaDiffEnabled && ( + + )} +
+ ) +} + +export const MergeSubtitle = () => { + const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() + const parentProjectRef = project?.parent_project_ref + + const { data: branches } = useBranchesQuery( + { projectRef: parentProjectRef }, + { + refetchOnMount: 'always', + refetchOnWindowFocus: true, + staleTime: 0, + } + ) + const currentBranch = branches?.find((branch) => branch.project_ref === ref) + + const subtitle = useMemo(() => { + if (!currentBranch?.created_at) return 'Branch information unavailable' + + if (!currentBranch?.review_requested_at) { + return 'Not ready for review' + } + + const reviewRequestedTime = dayjs(currentBranch.review_requested_at).fromNow() + return ( + <> + Request opened{' '} + + + ) + }, [currentBranch?.created_at, currentBranch?.review_requested_at]) + + return

{subtitle}

+} + +export const MergeActions = ({ + isWorkflowRunning, + isSubmitting, + onSelectMerge, +}: { + isWorkflowRunning: boolean + isSubmitting: boolean + onSelectMerge: () => void +}) => { + const router = useRouter() + const { ref } = useParams() + const { data: project } = useSelectedProjectQuery() + const { data: selectedOrg } = useSelectedOrganizationQuery() + + const { mutate: sendEvent } = useSendEventMutation() + const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({ + onError: (error) => { + toast.error(`Failed to update branch: ${error.message}`) + }, + }) + + const parentProjectRef = project?.parent_project_ref + const { data: parentProject } = useProjectDetailQuery({ ref: parentProjectRef }) + const { data: ghConnection } = useProjectGitHubConnectionQuery({ ref: parentProjectRef }) + + const { data: branches } = useBranchesQuery( + { projectRef: parentProjectRef }, + { + refetchOnMount: 'always', + refetchOnWindowFocus: true, + staleTime: 0, + } + ) + const currentBranch = branches?.find((branch) => branch.project_ref === ref) + const mainBranch = branches?.find((branch) => branch.is_default) + + const { + diffContent, + isBranchOutOfDateOverall, + isLoading: isCombinedDiffLoading, + hasChanges: combinedHasChanges, + } = useBranchMergeDiff({ + currentBranchRef: ref, + parentProjectRef, + currentBranchConnectionString: project?.connectionString || undefined, + parentBranchConnectionString: parentProject?.connectionString || undefined, + currentBranchCreatedAt: currentBranch?.created_at, + }) + + const isMergeDisabled = + !combinedHasChanges || + isCombinedDiffLoading || + isBranchOutOfDateOverall || + isWorkflowRunning || + (!!ghConnection && Boolean(mainBranch?.git_branch)) + + return ( +
+ + {isMergeDisabled ? ( + } + > + Merge branch + + ) : ( + + )} + + + +
+ ) +} diff --git a/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx b/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx index 39c6b7b939..fb58bd8ffd 100644 --- a/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx +++ b/apps/studio/components/interfaces/Settings/Integrations/IntegrationsSettings.tsx @@ -47,6 +47,7 @@ export const IntegrationSettings = () => { )} + {showVercelIntegration && ( diff --git a/apps/studio/data/integrations/github-connections-query.ts b/apps/studio/data/integrations/github-connections-query.ts index 336171ef15..cb22cf1916 100644 --- a/apps/studio/data/integrations/github-connections-query.ts +++ b/apps/studio/data/integrations/github-connections-query.ts @@ -1,7 +1,9 @@ import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' import { integrationKeys } from './keys' import { get, handleError } from '@/data/fetchers' +import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import type { ResponseError, UseCustomQueryOptions } from '@/types' export type GitHubConnectionsVariables = { @@ -47,3 +49,17 @@ export const useGitHubConnectionsQuery = ( ...options, }) } + +export const useProjectGitHubConnectionQuery = ({ ref }: { ref?: string }) => { + const { data: organization } = useSelectedOrganizationQuery() + const { data: connections, ...props } = useGitHubConnectionsQuery( + { organizationId: organization?.id }, + { enabled: !!ref && !!organization?.id } + ) + + const existingConnection = useMemo( + () => connections?.find((c) => c.project.ref === ref), + [connections, ref] + ) + return { data: existingConnection, ...props } +} diff --git a/apps/studio/pages/project/[ref]/merge.tsx b/apps/studio/pages/project/[ref]/merge.tsx index 4a823d87f7..98bc528cae 100644 --- a/apps/studio/pages/project/[ref]/merge.tsx +++ b/apps/studio/pages/project/[ref]/merge.tsx @@ -1,41 +1,35 @@ -import { LOCAL_STORAGE_KEYS, useParams } from 'common' -import dayjs from 'dayjs' -import { AlertTriangle, GitBranchIcon, GitMerge, MoreVertical, Shield, X } from 'lucide-react' +import { useParams } from 'common' +import { AlertTriangle, GitBranchIcon, X } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { useCallback, useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' -import { - Badge, - Button, - cn, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - NavMenu, - NavMenuItem, -} from 'ui' +import { Button, cn, NavMenu, NavMenuItem } from 'ui' +import { Admonition } from 'ui-patterns' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' import { useIsPgDeltaDiffEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext' +import { + MergeActions, + MergeSubtitle, + MergeTitle, +} from '@/components/interfaces/Branching/MergeRequest' import { DatabaseDiffPanel } from '@/components/interfaces/BranchManagement/DatabaseDiffPanel' import { EdgeFunctionsDiffPanel } from '@/components/interfaces/BranchManagement/EdgeFunctionsDiffPanel' import { OutOfDateNotice } from '@/components/interfaces/BranchManagement/OutOfDateNotice' -import { ReviewWithAI } from '@/components/interfaces/BranchManagement/ReviewWithAI' import { WorkflowLogsCard } from '@/components/interfaces/BranchManagement/WorkflowLogsCard' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import { PageLayout } from '@/components/layouts/PageLayout/PageLayout' import { ProjectLayoutWithAuth } from '@/components/layouts/ProjectLayout' import { ScaffoldContainer } from '@/components/layouts/Scaffold' import ProductEmptyState from '@/components/to-be-cleaned/ProductEmptyState' -import { ButtonTooltip } from '@/components/ui/ButtonTooltip' -import { FeaturePreviewBadge } from '@/components/ui/FeaturePreviewBadge' +import { InlineLink } from '@/components/ui/InlineLink' import { useBranchDeleteMutation } from '@/data/branches/branch-delete-mutation' import { useBranchMergeMutation } from '@/data/branches/branch-merge-mutation' import { useBranchPushMutation } from '@/data/branches/branch-push-mutation' import { useBranchUpdateMutation } from '@/data/branches/branch-update-mutation' import { useBranchesQuery } from '@/data/branches/branches-query' +import { useProjectGitHubConnectionQuery } from '@/data/integrations/github-connections-query' import { useProjectDetailQuery } from '@/data/projects/project-detail-query' import { useSendEventMutation } from '@/data/telemetry/send-event-mutation' import { useBranchMergeDiff } from '@/hooks/branches/useBranchMergeDiff' @@ -59,6 +53,7 @@ const MergePage: NextPageWithLayout = () => { const parentProjectRef = project?.parent_project_ref const { data: parentProject } = useProjectDetailQuery({ ref: parentProjectRef }) + const { data: ghConnection } = useProjectGitHubConnectionQuery({ ref: parentProjectRef }) const { data: branches } = useBranchesQuery( { projectRef: parentProjectRef }, @@ -87,8 +82,6 @@ const MergePage: NextPageWithLayout = () => { isBranchOutOfDateOverall, missingMigrationsCount, modifiedFunctionsCount, - isLoading: isCombinedDiffLoading, - hasChanges: combinedHasChanges, } = useBranchMergeDiff({ currentBranchRef: ref, parentProjectRef, @@ -97,7 +90,7 @@ const MergePage: NextPageWithLayout = () => { currentBranchCreatedAt: currentBranch?.created_at, }) - const { mutate: updateBranch, isPending: isUpdating } = useBranchUpdateMutation({ + const { mutate: updateBranch } = useBranchUpdateMutation({ onError: (error) => { toast.error(`Failed to update branch: ${error.message}`) }, @@ -359,143 +352,42 @@ const MergePage: NextPageWithLayout = () => { ) } - const isMergeDisabled = - !combinedHasChanges || - isCombinedDiffLoading || - isBranchOutOfDateOverall || - isWorkflowRunning || - Boolean(mainBranch?.git_branch) - - const primaryActions = ( -
- - {isMergeDisabled ? ( - setShowConfirmDialog(true)} - icon={} - > - Merge branch - - ) : ( - - )} - - -
- ) - - const pageTitle = () => ( -
- Merge - - - - - {currentBranch.name} - - - - into - - - - - {mainBranch?.name || 'main'} - - - - {pgDeltaDiffEnabled && ( - - )} -
- ) - - const pageSubtitle = () => { - if (!currentBranch?.created_at) return 'Branch information unavailable' - - if (!currentBranch?.review_requested_at) { - return 'Not ready for review' - } - - const reviewRequestedTime = dayjs(currentBranch.review_requested_at).fromNow() - return `Request opened ${reviewRequestedTime}` - } + const hasGHProductionDeployEnabled = !!ghConnection && Boolean(mainBranch?.git_branch) return ( } + subtitle={} breadcrumbs={breadcrumbs} - primaryActions={primaryActions} + primaryActions={ + setShowConfirmDialog(true)} + /> + } size="full" className="h-full border-b-0 pb-0" >
- {isBranchOutOfDateOverall && !currentWorkflowRunId ? ( + {hasGHProductionDeployEnabled ? ( + +

+ Branches should be managed via GitHub to prevent drifts in migrations from your + repository's state. You may either move your schema changes to a GitHub pull + request, or disable "Deploy to production" in the{' '} + + GitHub integration settings + + . +

+
+ ) : isBranchOutOfDateOverall && !currentWorkflowRunId ? ( {
+
{currentTab === 'database' ? (