mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 21:59:16 +03:00
workflow refactor
This commit is contained in:
1 parent
5523b3c695
commit
cbd86af632
5 files changed
+240
-224
No files matched your search
@@ -17,10 +17,10 @@ import { Admonition } from 'ui-patterns'
|
||||
interface OutOfDateNoticeProps {
|
||||
isBranchOutOfDateMigrations: boolean
|
||||
missingMigrationsCount: number
|
||||
hasNewerRemovedFunctions: boolean
|
||||
newerRemovedFunctionsCount: number
|
||||
hasNewerModifiedFunctions: boolean
|
||||
newerModifiedFunctionsCount: number
|
||||
hasMissingFunctions: boolean
|
||||
missingFunctionsCount: number
|
||||
hasOutOfDateFunctions: boolean
|
||||
outOfDateFunctionsCount: number
|
||||
hasEdgeFunctionModifications: boolean
|
||||
modifiedFunctionsCount: number
|
||||
isPushing: boolean
|
||||
@@ -30,10 +30,10 @@ interface OutOfDateNoticeProps {
|
||||
export const OutOfDateNotice = ({
|
||||
isBranchOutOfDateMigrations,
|
||||
missingMigrationsCount,
|
||||
hasNewerRemovedFunctions,
|
||||
newerRemovedFunctionsCount,
|
||||
hasNewerModifiedFunctions,
|
||||
newerModifiedFunctionsCount,
|
||||
hasMissingFunctions,
|
||||
missingFunctionsCount,
|
||||
hasOutOfDateFunctions,
|
||||
outOfDateFunctionsCount,
|
||||
hasEdgeFunctionModifications,
|
||||
modifiedFunctionsCount,
|
||||
isPushing,
|
||||
@@ -43,29 +43,18 @@ export const OutOfDateNotice = ({
|
||||
const hasOutdatedMigrations = isBranchOutOfDateMigrations && missingMigrationsCount > 0
|
||||
|
||||
const getTitle = () => {
|
||||
if (hasOutdatedMigrations && (hasNewerRemovedFunctions || hasNewerModifiedFunctions)) {
|
||||
const functionCount = newerRemovedFunctionsCount + newerModifiedFunctionsCount
|
||||
return `Missing ${missingMigrationsCount} migration${missingMigrationsCount !== 1 ? 's' : ''} and ${functionCount} updated function${functionCount !== 1 ? 's' : ''} from main branch`
|
||||
if (hasOutdatedMigrations && (hasMissingFunctions || hasOutOfDateFunctions)) {
|
||||
return 'Your database schema and edge functions are out of date'
|
||||
} else if (hasOutdatedMigrations) {
|
||||
return `Missing ${missingMigrationsCount} migration${missingMigrationsCount !== 1 ? 's' : ''} from main branch`
|
||||
} else if (hasNewerRemovedFunctions && hasNewerModifiedFunctions) {
|
||||
const functionCount = newerRemovedFunctionsCount + newerModifiedFunctionsCount
|
||||
return `Main branch has ${functionCount} updated function${functionCount !== 1 ? 's' : ''}`
|
||||
} else if (hasNewerRemovedFunctions) {
|
||||
return `Main branch has ${newerRemovedFunctionsCount} updated function${newerRemovedFunctionsCount !== 1 ? 's' : ''} that were removed in this branch`
|
||||
} else if (hasNewerModifiedFunctions) {
|
||||
return `Main branch has ${newerModifiedFunctionsCount} updated function${newerModifiedFunctionsCount !== 1 ? 's' : ''}`
|
||||
return 'Your database schema is out of date'
|
||||
} else if (hasMissingFunctions || hasOutOfDateFunctions) {
|
||||
return 'Your functions are out of date'
|
||||
}
|
||||
return 'Branch is out of date'
|
||||
}
|
||||
|
||||
const getDescription = () => {
|
||||
if (hasOutdatedMigrations && (hasNewerRemovedFunctions || hasNewerModifiedFunctions)) {
|
||||
return `The main branch has migrations and functions that were updated after this branch was created.`
|
||||
} else if (hasNewerRemovedFunctions || hasNewerModifiedFunctions) {
|
||||
return `The main branch has functions that were created or updated after this branch was created.`
|
||||
}
|
||||
return 'Update this branch to get the latest changes from the main branch.'
|
||||
return 'Update this branch to get the latest changes from the production branch.'
|
||||
}
|
||||
|
||||
const handleUpdateClick = () => {
|
||||
@@ -103,7 +92,8 @@ export const OutOfDateNotice = ({
|
||||
<AlertDialogDescription>
|
||||
This branch has {modifiedFunctionsCount} modified edge function
|
||||
{modifiedFunctionsCount !== 1 ? 's' : ''} that will be overwritten when updating
|
||||
with the latest functions from the main branch, and this action cannot be undone.
|
||||
with the latest functions from the production branch. This action cannot be
|
||||
undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -2,128 +2,97 @@ import React from 'react'
|
||||
import { Card, CardHeader, CardContent, CardTitle, Button } from 'ui'
|
||||
import { motion } from 'framer-motion'
|
||||
import { CircleDotDashed, GitMerge, X, AlertTriangle } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useWorkflowRunQuery } from 'data/workflow-runs/workflow-run-query'
|
||||
import { useWorkflowRunsQuery } from 'data/workflow-runs/workflow-runs-query'
|
||||
|
||||
interface WorkflowRun {
|
||||
id: string
|
||||
status: string
|
||||
branch_id?: string
|
||||
check_run_id?: number | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
workdir?: string | null
|
||||
git_config?: unknown
|
||||
}
|
||||
|
||||
interface WorkflowLogsCardProps {
|
||||
workflowRunId: string
|
||||
projectRef: string
|
||||
workflowRun: WorkflowRun | null | undefined
|
||||
logs: string | undefined
|
||||
isLoading?: boolean
|
||||
onClose?: () => void
|
||||
onStatusChange?: (status: string, workflowRunId: string) => void
|
||||
statusComplete?: string // Status that indicates completion (e.g., 'FUNCTIONS_DEPLOYED')
|
||||
statusFailed?: string[] // Statuses that indicate failure (e.g., ['MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'])
|
||||
headerTitle?: string // Additional header title
|
||||
headerDescription?: string // Additional header description
|
||||
headerIcon?: React.ReactNode // Additional header icon
|
||||
headerAction?: React.ReactNode // Additional header action button
|
||||
// Override props for failed workflows
|
||||
overrideTitle?: string
|
||||
overrideDescription?: string
|
||||
overrideIcon?: React.ReactNode
|
||||
overrideAction?: React.ReactNode
|
||||
}
|
||||
|
||||
const WorkflowLogsCard: React.FC<WorkflowLogsCardProps> = ({
|
||||
workflowRunId,
|
||||
projectRef,
|
||||
workflowRun,
|
||||
logs,
|
||||
isLoading = false,
|
||||
onClose,
|
||||
onStatusChange,
|
||||
statusComplete = 'FUNCTIONS_DEPLOYED',
|
||||
statusFailed = ['MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'],
|
||||
headerTitle,
|
||||
headerDescription,
|
||||
headerIcon,
|
||||
headerAction,
|
||||
overrideTitle,
|
||||
overrideDescription,
|
||||
overrideIcon,
|
||||
overrideAction,
|
||||
}) => {
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
// Get workflow runs for the specified project to find the workflow run
|
||||
const { data: workflowRuns } = useWorkflowRunsQuery(
|
||||
{ projectRef },
|
||||
{
|
||||
enabled: !!projectRef,
|
||||
refetchInterval: 3000, // Always poll
|
||||
refetchOnMount: 'always',
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: 0,
|
||||
}
|
||||
)
|
||||
|
||||
// Find the current workflow run
|
||||
const currentWorkflowRun = React.useMemo(() => {
|
||||
if (!workflowRunId) return null
|
||||
return workflowRuns?.find((run) => run.id === workflowRunId)
|
||||
}, [workflowRuns, workflowRunId])
|
||||
|
||||
// Query for workflow run logs with continuous polling
|
||||
const { data: workflowRunLogs } = useWorkflowRunQuery(
|
||||
{ workflowRunId },
|
||||
{
|
||||
enabled: !!workflowRunId,
|
||||
refetchInterval: 2000, // Always poll every 2 seconds
|
||||
refetchOnMount: 'always',
|
||||
refetchOnWindowFocus: true,
|
||||
staleTime: 0, // Always fetch fresh data
|
||||
}
|
||||
)
|
||||
|
||||
// Auto-scroll to bottom when logs change
|
||||
React.useEffect(() => {
|
||||
if (scrollRef.current && workflowRunLogs?.logs) {
|
||||
if (scrollRef.current && logs) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
||||
}
|
||||
}, [workflowRunLogs?.logs])
|
||||
}, [logs])
|
||||
|
||||
// Call onStatusChange when status changes
|
||||
React.useEffect(() => {
|
||||
if (currentWorkflowRun?.status && onStatusChange) {
|
||||
onStatusChange(currentWorkflowRun.status, workflowRunId)
|
||||
}
|
||||
}, [currentWorkflowRun?.status, onStatusChange, workflowRunId])
|
||||
|
||||
const showSuccessIcon = currentWorkflowRun?.status === statusComplete
|
||||
const isFailed = currentWorkflowRun?.status && statusFailed.includes(currentWorkflowRun.status)
|
||||
const showSuccessIcon = workflowRun?.status === 'FUNCTIONS_DEPLOYED'
|
||||
const isFailed =
|
||||
workflowRun?.status && ['MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'].includes(workflowRun.status)
|
||||
const isPolling =
|
||||
currentWorkflowRun?.status !== statusComplete &&
|
||||
(!currentWorkflowRun?.status || !statusFailed.includes(currentWorkflowRun.status))
|
||||
workflowRun?.status !== 'FUNCTIONS_DEPLOYED' &&
|
||||
(!workflowRun?.status ||
|
||||
!['MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'].includes(workflowRun.status))
|
||||
|
||||
const displayTitle =
|
||||
overrideTitle ||
|
||||
(isPolling
|
||||
? 'Processing...'
|
||||
: showSuccessIcon
|
||||
? 'Workflow completed successfully'
|
||||
: isFailed
|
||||
? 'Workflow failed'
|
||||
: 'Workflow completed')
|
||||
|
||||
const displayIcon =
|
||||
overrideIcon ||
|
||||
(isPolling ? (
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
|
||||
>
|
||||
<CircleDotDashed size={16} strokeWidth={1.5} className="text-warning" />
|
||||
</motion.div>
|
||||
) : showSuccessIcon ? (
|
||||
<GitMerge size={16} strokeWidth={1.5} className="text-brand" />
|
||||
) : null)
|
||||
|
||||
return (
|
||||
<Card className="bg-background overflow-hidden h-64 flex flex-col">
|
||||
<CardHeader className={showSuccessIcon ? 'text-brand' : isFailed ? 'text-destructive' : ''}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Custom icon or activity / success indicator */}
|
||||
{headerIcon ? (
|
||||
headerIcon
|
||||
) : isPolling ? (
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
|
||||
>
|
||||
<CircleDotDashed size={16} strokeWidth={1.5} className="text-warning" />
|
||||
</motion.div>
|
||||
) : (
|
||||
showSuccessIcon && <GitMerge size={16} strokeWidth={1.5} className="text-brand" />
|
||||
)}
|
||||
{displayIcon}
|
||||
<div>
|
||||
{/* Custom title or default status text */}
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{headerTitle ||
|
||||
(isPolling
|
||||
? 'Processing...'
|
||||
: showSuccessIcon
|
||||
? 'Workflow completed successfully'
|
||||
: isFailed
|
||||
? 'Workflow failed'
|
||||
: 'Workflow completed')}
|
||||
</CardTitle>
|
||||
{/* Custom description */}
|
||||
{headerDescription && (
|
||||
<CardTitle className="text-sm font-medium">{displayTitle}</CardTitle>
|
||||
{overrideDescription && (
|
||||
<div className="text-sm text-foreground-light font-normal mt-0">
|
||||
{headerDescription}
|
||||
{overrideDescription}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Custom action or default close button */}
|
||||
{headerAction && headerAction}
|
||||
{overrideAction && overrideAction}
|
||||
{onClose && (
|
||||
<Button
|
||||
type="text"
|
||||
@@ -142,13 +111,11 @@ const WorkflowLogsCard: React.FC<WorkflowLogsCardProps> = ({
|
||||
>
|
||||
{/* sticky gradient overlay */}
|
||||
<div className="sticky top-0 -mb-8 h-8 bg-gradient-to-b from-background to-transparent pointer-events-none z-10" />
|
||||
{workflowRunLogs?.logs ? (
|
||||
<pre className="p-6 text-xs text-foreground-light p-0 rounded">
|
||||
{workflowRunLogs.logs}
|
||||
</pre>
|
||||
{logs ? (
|
||||
<pre className="p-6 text-xs text-foreground-light p-0 rounded">{logs}</pre>
|
||||
) : (
|
||||
<pre className="p-6 text-sm text-foreground-light rounded">
|
||||
{isPolling ? 'Initializing workflow...' : 'Waiting for logs...'}
|
||||
{isLoading || isPolling ? 'Initializing workflow...' : 'Waiting for logs...'}
|
||||
</pre>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -32,10 +32,10 @@ export interface BranchMergeDiffResult {
|
||||
// Branch state
|
||||
isBranchOutOfDateMigrations: boolean
|
||||
hasEdgeFunctionModifications: boolean
|
||||
newerRemovedFunctionsCount: number
|
||||
hasNewerRemovedFunctions: boolean
|
||||
newerModifiedFunctionsCount: number
|
||||
hasNewerModifiedFunctions: boolean
|
||||
missingFunctionsCount: number
|
||||
hasMissingFunctions: boolean
|
||||
outOfDateFunctionsCount: number
|
||||
hasOutOfDateFunctions: boolean
|
||||
isBranchOutOfDateOverall: boolean
|
||||
missingMigrationsCount: number
|
||||
modifiedFunctionsCount: number
|
||||
@@ -120,7 +120,7 @@ export const useBranchMergeDiff = ({
|
||||
}, [currentBranchMigrations, mainBranchMigrations])
|
||||
|
||||
// Check if main branch has functions that are newer than the current branch versions
|
||||
const newerModifiedFunctionsCount = useMemo(() => {
|
||||
const outOfDateFunctionsCount = useMemo(() => {
|
||||
if (!edgeFunctionsDiff.modifiedSlugs.length) return 0
|
||||
|
||||
return edgeFunctionsDiff.modifiedSlugs.filter((slug) => {
|
||||
@@ -145,16 +145,16 @@ export const useBranchMergeDiff = ({
|
||||
])
|
||||
|
||||
// Check if main branch has functions that are newer than the current branch versions
|
||||
const hasNewerModifiedFunctions = newerModifiedFunctionsCount > 0
|
||||
const hasOutOfDateFunctions = outOfDateFunctionsCount > 0
|
||||
|
||||
// Check if current branch has any edge function modifications (not additions/removals)
|
||||
// This only includes functions where the current branch version is newer than the main branch version
|
||||
const hasEdgeFunctionModifications =
|
||||
edgeFunctionsDiff.modifiedSlugs.length > newerModifiedFunctionsCount
|
||||
edgeFunctionsDiff.modifiedSlugs.length > outOfDateFunctionsCount
|
||||
|
||||
// Count of removed functions that were updated on main branch after this branch was created
|
||||
// Note: For removed functions, we use branch creation date since there's no current branch version to compare to
|
||||
const newerRemovedFunctionsCount = useMemo(() => {
|
||||
const missingFunctionsCount = useMemo(() => {
|
||||
if (!currentBranchCreatedAt || !edgeFunctionsDiff.removedSlugs.length) return 0
|
||||
|
||||
const branchCreatedAt = new Date(currentBranchCreatedAt).getTime()
|
||||
@@ -175,11 +175,11 @@ export const useBranchMergeDiff = ({
|
||||
])
|
||||
|
||||
// Check if main branch has functions removed from current branch that were updated after branch creation
|
||||
const hasNewerRemovedFunctions = newerRemovedFunctionsCount > 0
|
||||
const hasMissingFunctions = missingFunctionsCount > 0
|
||||
|
||||
// Update overall out-of-date check to include newer removed functions and newer modified functions
|
||||
const isBranchOutOfDateOverall =
|
||||
isBranchOutOfDateMigrations || hasNewerRemovedFunctions || hasNewerModifiedFunctions
|
||||
isBranchOutOfDateMigrations || hasMissingFunctions || hasOutOfDateFunctions
|
||||
|
||||
// Get the count of migrations that the branch is missing
|
||||
const missingMigrationsCount = useMemo(() => {
|
||||
@@ -225,10 +225,10 @@ export const useBranchMergeDiff = ({
|
||||
// Branch state
|
||||
isBranchOutOfDateMigrations,
|
||||
hasEdgeFunctionModifications,
|
||||
newerRemovedFunctionsCount,
|
||||
hasNewerRemovedFunctions,
|
||||
newerModifiedFunctionsCount,
|
||||
hasNewerModifiedFunctions,
|
||||
missingFunctionsCount,
|
||||
hasMissingFunctions,
|
||||
outOfDateFunctionsCount,
|
||||
hasOutOfDateFunctions,
|
||||
isBranchOutOfDateOverall,
|
||||
missingMigrationsCount,
|
||||
modifiedFunctionsCount,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useWorkflowRunsQuery } from 'data/workflow-runs/workflow-runs-query'
|
||||
import { useWorkflowRunQuery } from 'data/workflow-runs/workflow-run-query'
|
||||
|
||||
interface UseWorkflowManagementProps {
|
||||
workflowRunId?: string
|
||||
projectRef?: string
|
||||
onWorkflowComplete?: (status: string) => void
|
||||
}
|
||||
|
||||
export const useWorkflowManagement = ({
|
||||
workflowRunId,
|
||||
projectRef,
|
||||
onWorkflowComplete,
|
||||
}: UseWorkflowManagementProps) => {
|
||||
const [hasRefetched, setHasRefetched] = useState(false)
|
||||
|
||||
// Get workflow runs and logs
|
||||
const { data: workflowRuns } = useWorkflowRunsQuery(
|
||||
{ projectRef },
|
||||
{
|
||||
enabled: !!projectRef && !!workflowRunId,
|
||||
refetchInterval: 3000,
|
||||
refetchOnMount: 'always',
|
||||
staleTime: 0,
|
||||
}
|
||||
)
|
||||
|
||||
const { data: workflowRunLogs } = useWorkflowRunQuery(
|
||||
{ workflowRunId },
|
||||
{
|
||||
enabled: !!workflowRunId,
|
||||
refetchInterval: 2000,
|
||||
refetchOnMount: 'always',
|
||||
staleTime: 0,
|
||||
}
|
||||
)
|
||||
|
||||
// Find current workflow run
|
||||
const currentWorkflowRun = workflowRuns?.find((run) => run.id === workflowRunId)
|
||||
|
||||
// Handle workflow completion
|
||||
useEffect(() => {
|
||||
if (!currentWorkflowRun?.status || !workflowRunId) return
|
||||
|
||||
const isComplete = ['FUNCTIONS_DEPLOYED', 'MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'].includes(
|
||||
currentWorkflowRun.status
|
||||
)
|
||||
|
||||
// Only refetch once per workflow completion
|
||||
if (isComplete && !hasRefetched) {
|
||||
setHasRefetched(true)
|
||||
onWorkflowComplete?.(currentWorkflowRun.status)
|
||||
}
|
||||
}, [currentWorkflowRun?.status, workflowRunId, hasRefetched, onWorkflowComplete])
|
||||
|
||||
// Reset refetch flag when workflow ID changes
|
||||
useEffect(() => {
|
||||
setHasRefetched(false)
|
||||
}, [workflowRunId])
|
||||
|
||||
return {
|
||||
currentWorkflowRun,
|
||||
workflowRunLogs: workflowRunLogs?.logs,
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ import { useBranchesQuery } from 'data/branches/branches-query'
|
||||
import { useBranchMergeMutation } from 'data/branches/branch-merge-mutation'
|
||||
import { useBranchPushMutation } from 'data/branches/branch-push-mutation'
|
||||
import { useBranchMergeDiff } from 'hooks/misc/useBranchMergeDiff'
|
||||
import { useWorkflowManagement } from 'hooks/misc/useWorkflowManagement'
|
||||
import DatabaseDiffPanel from 'components/interfaces/BranchManagement/DatabaseDiffPanel'
|
||||
import EdgeFunctionsDiffPanel from 'components/interfaces/BranchManagement/EdgeFunctionsDiffPanel'
|
||||
import { OutOfDateNotice } from 'components/interfaces/BranchManagement/OutOfDateNotice'
|
||||
import { Badge, Button, NavMenu, NavMenuItem, cn, Alert } from 'ui'
|
||||
import { toast } from 'sonner'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
import { ScaffoldContainer } from 'components/layouts/Scaffold'
|
||||
import { GitBranchIcon, GitMerge, Shield, ExternalLink, AlertTriangle } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
@@ -34,12 +36,7 @@ const MergePage: NextPageWithLayout = () => {
|
||||
|
||||
const project = useSelectedProject()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
// State to track related workflow runs with their project refs
|
||||
const [relatedWorkflowRunIds, setRelatedWorkflowRunIds] = useState<Record<string, string>>({})
|
||||
|
||||
// State to track failed workflows
|
||||
const [failedWorkflowIds, setFailedWorkflowIds] = useState<Set<string>>(new Set())
|
||||
const [workflowFinalStatus, setWorkflowFinalStatus] = useState<string | null>(null)
|
||||
|
||||
const isBranch = project?.parent_project_ref !== undefined
|
||||
const parentProjectRef = project?.parent_project_ref
|
||||
@@ -59,18 +56,6 @@ const MergePage: NextPageWithLayout = () => {
|
||||
const currentBranch = branches?.find((branch) => branch.project_ref === ref)
|
||||
const mainBranch = branches?.find((branch) => branch.is_default)
|
||||
|
||||
// Get workflow run ID from URL query parameter and initialize state
|
||||
useEffect(() => {
|
||||
const workflowRunId = router.query.workflow_run_id as string | undefined
|
||||
if (workflowRunId && !relatedWorkflowRunIds[workflowRunId]) {
|
||||
// Default to current branch project ref if not specified
|
||||
setRelatedWorkflowRunIds((prev) => ({
|
||||
...prev,
|
||||
[workflowRunId]: ref || '',
|
||||
}))
|
||||
}
|
||||
}, [router.query.workflow_run_id, ref, relatedWorkflowRunIds])
|
||||
|
||||
// Get combined diff data (database, edge functions, migrations, and branch state)
|
||||
const {
|
||||
diffContent,
|
||||
@@ -81,10 +66,10 @@ const MergePage: NextPageWithLayout = () => {
|
||||
edgeFunctionsDiff,
|
||||
isBranchOutOfDateMigrations,
|
||||
hasEdgeFunctionModifications,
|
||||
newerRemovedFunctionsCount,
|
||||
hasNewerRemovedFunctions,
|
||||
newerModifiedFunctionsCount,
|
||||
hasNewerModifiedFunctions,
|
||||
missingFunctionsCount,
|
||||
hasMissingFunctions,
|
||||
outOfDateFunctionsCount,
|
||||
hasOutOfDateFunctions,
|
||||
isBranchOutOfDateOverall,
|
||||
missingMigrationsCount,
|
||||
modifiedFunctionsCount,
|
||||
@@ -99,43 +84,74 @@ const MergePage: NextPageWithLayout = () => {
|
||||
currentBranchCreatedAt: currentBranch?.created_at,
|
||||
})
|
||||
|
||||
// Handle workflow completion - refetch diff once when workflow completes or fails
|
||||
const handleWorkflowStatusChange = useCallback(
|
||||
(status: string, workflowRunId: string) => {
|
||||
const isComplete =
|
||||
status === 'FUNCTIONS_DEPLOYED' ||
|
||||
status === 'MIGRATIONS_FAILED' ||
|
||||
status === 'FUNCTIONS_FAILED'
|
||||
// Get workflow run ID from URL
|
||||
const currentWorkflowRunId = router.query.workflow_run_id as string | undefined
|
||||
|
||||
const isFailed = status === 'MIGRATIONS_FAILED' || status === 'FUNCTIONS_FAILED'
|
||||
// Reset workflow status when workflow ID changes
|
||||
useEffect(() => {
|
||||
setWorkflowFinalStatus(null)
|
||||
}, [currentWorkflowRunId])
|
||||
|
||||
if (isFailed) {
|
||||
setFailedWorkflowIds((prev) => new Set(prev).add(workflowRunId))
|
||||
}
|
||||
|
||||
if (isComplete) {
|
||||
// Refetch diff and edge functions once when workflow completes
|
||||
// Try current branch first, then parent branch
|
||||
const { currentWorkflowRun: currentBranchWorkflow, workflowRunLogs: currentBranchLogs } =
|
||||
useWorkflowManagement({
|
||||
workflowRunId: currentWorkflowRunId,
|
||||
projectRef: ref,
|
||||
onWorkflowComplete: (status) => {
|
||||
setWorkflowFinalStatus(status)
|
||||
refetchDiff()
|
||||
edgeFunctionsDiff.refetchCurrentBranchFunctions()
|
||||
edgeFunctionsDiff.refetchMainBranchFunctions()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { currentWorkflowRun: parentBranchWorkflow, workflowRunLogs: parentBranchLogs } =
|
||||
useWorkflowManagement({
|
||||
workflowRunId: currentWorkflowRunId,
|
||||
projectRef: parentProjectRef,
|
||||
onWorkflowComplete: (status) => {
|
||||
setWorkflowFinalStatus(status)
|
||||
refetchDiff()
|
||||
edgeFunctionsDiff.refetchCurrentBranchFunctions()
|
||||
edgeFunctionsDiff.refetchMainBranchFunctions()
|
||||
},
|
||||
})
|
||||
|
||||
// Use whichever workflow run was found
|
||||
const currentWorkflowRun = currentBranchWorkflow || parentBranchWorkflow
|
||||
const workflowRunLogs = currentBranchLogs || parentBranchLogs
|
||||
|
||||
// Check if workflow failed based on final status or current status
|
||||
const hasCurrentWorkflowFailed = workflowFinalStatus
|
||||
? ['MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'].includes(workflowFinalStatus)
|
||||
: currentWorkflowRun?.status &&
|
||||
['MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'].includes(currentWorkflowRun.status)
|
||||
|
||||
// Helper functions for URL management
|
||||
const addWorkflowRun = useCallback(
|
||||
(workflowRunId: string) => {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, workflow_run_id: workflowRunId },
|
||||
})
|
||||
},
|
||||
[refetchDiff, edgeFunctionsDiff]
|
||||
[router]
|
||||
)
|
||||
|
||||
const clearWorkflowRun = useCallback(() => {
|
||||
const { workflow_run_id, ...queryWithoutWorkflowId } = router.query
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: queryWithoutWorkflowId,
|
||||
})
|
||||
}, [router])
|
||||
|
||||
const { mutate: pushBranch, isLoading: isPushing } = useBranchPushMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success('Branch update initiated!')
|
||||
// Add workflow run ID to URL for persistence and track it (push workflows run on current branch)
|
||||
// Add workflow run ID to URL for persistence
|
||||
if (data?.workflow_run_id) {
|
||||
setRelatedWorkflowRunIds((prev) => ({
|
||||
...prev,
|
||||
[data.workflow_run_id]: ref || '',
|
||||
}))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, workflow_run_id: data.workflow_run_id },
|
||||
})
|
||||
addWorkflowRun(data.workflow_run_id)
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -152,16 +168,9 @@ const MergePage: NextPageWithLayout = () => {
|
||||
} else {
|
||||
toast.success('Branch merge initiated!')
|
||||
}
|
||||
// Add workflow run ID to URL for persistence and track it (merge workflows run on parent branch)
|
||||
// Add workflow run ID to URL for persistence
|
||||
if (data.workflowRunId) {
|
||||
setRelatedWorkflowRunIds((prev) => ({
|
||||
...prev,
|
||||
[data.workflowRunId]: parentProjectRef || '',
|
||||
}))
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: { ...router.query, workflow_run_id: data.workflowRunId },
|
||||
})
|
||||
addWorkflowRun(data.workflowRunId)
|
||||
}
|
||||
} else {
|
||||
toast.info('No changes to merge - branch merged successfully!')
|
||||
@@ -205,13 +214,6 @@ const MergePage: NextPageWithLayout = () => {
|
||||
// Determine current active tab via query param (defaults to 'database')
|
||||
const currentTab = (router.query.tab as string) || 'database'
|
||||
|
||||
// Get the current workflow run ID from URL
|
||||
const currentWorkflowRunId = router.query.workflow_run_id as string | undefined
|
||||
|
||||
// Check if current workflow run has failed
|
||||
const hasCurrentWorkflowFailed =
|
||||
currentWorkflowRunId && failedWorkflowIds.has(currentWorkflowRunId)
|
||||
|
||||
// Navigation items for PageLayout - updates the `tab` query param
|
||||
const navigationItems = useMemo(() => {
|
||||
const buildHref = (tab: string) => {
|
||||
@@ -274,8 +276,7 @@ const MergePage: NextPageWithLayout = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const isDataLoaded = !isCombinedDiffLoading
|
||||
const isMergeDisabled = combinedHasChanges && isDataLoaded
|
||||
const isMergeDisabled = !combinedHasChanges || isCombinedDiffLoading || isBranchOutOfDateOverall
|
||||
|
||||
// Update primary actions - remove push button if branch is out of date (it will be in the notice)
|
||||
const primaryActions = (
|
||||
@@ -284,7 +285,7 @@ const MergePage: NextPageWithLayout = () => {
|
||||
<ButtonTooltip
|
||||
tooltip={{
|
||||
content: {
|
||||
text: 'No changes to merge',
|
||||
text: !combinedHasChanges ? 'No changes to merge' : 'Branch is out of date',
|
||||
},
|
||||
}}
|
||||
type="primary"
|
||||
@@ -354,10 +355,10 @@ const MergePage: NextPageWithLayout = () => {
|
||||
<OutOfDateNotice
|
||||
isBranchOutOfDateMigrations={isBranchOutOfDateMigrations}
|
||||
missingMigrationsCount={missingMigrationsCount}
|
||||
hasNewerRemovedFunctions={hasNewerRemovedFunctions}
|
||||
newerRemovedFunctionsCount={newerRemovedFunctionsCount}
|
||||
hasNewerModifiedFunctions={hasNewerModifiedFunctions}
|
||||
newerModifiedFunctionsCount={newerModifiedFunctionsCount}
|
||||
hasMissingFunctions={hasMissingFunctions}
|
||||
missingFunctionsCount={missingFunctionsCount}
|
||||
hasOutOfDateFunctions={hasOutOfDateFunctions}
|
||||
outOfDateFunctionsCount={outOfDateFunctionsCount}
|
||||
hasEdgeFunctionModifications={hasEdgeFunctionModifications}
|
||||
modifiedFunctionsCount={modifiedFunctionsCount}
|
||||
isPushing={isPushing}
|
||||
@@ -366,25 +367,17 @@ const MergePage: NextPageWithLayout = () => {
|
||||
) : currentWorkflowRunId ? (
|
||||
<div className="my-6">
|
||||
<WorkflowLogsCard
|
||||
workflowRunId={currentWorkflowRunId}
|
||||
projectRef={relatedWorkflowRunIds[currentWorkflowRunId] || ref || ''}
|
||||
onClose={() => {
|
||||
const { workflow_run_id, ...queryWithoutWorkflowId } = router.query
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: queryWithoutWorkflowId,
|
||||
})
|
||||
}}
|
||||
onStatusChange={handleWorkflowStatusChange}
|
||||
statusComplete="FUNCTIONS_DEPLOYED"
|
||||
statusFailed={['MIGRATIONS_FAILED', 'FUNCTIONS_FAILED']}
|
||||
headerTitle={hasCurrentWorkflowFailed ? 'Workflow failed' : undefined}
|
||||
headerDescription={
|
||||
workflowRun={currentWorkflowRun}
|
||||
logs={workflowRunLogs}
|
||||
isLoading={!workflowRunLogs && !!currentWorkflowRunId}
|
||||
onClose={clearWorkflowRun}
|
||||
overrideTitle={hasCurrentWorkflowFailed ? 'Workflow failed' : undefined}
|
||||
overrideDescription={
|
||||
hasCurrentWorkflowFailed
|
||||
? 'Consider creating a fresh branch from the latest production branch to resolve potential conflicts.'
|
||||
: undefined
|
||||
}
|
||||
headerIcon={
|
||||
overrideIcon={
|
||||
hasCurrentWorkflowFailed ? (
|
||||
<AlertTriangle
|
||||
size={16}
|
||||
@@ -393,7 +386,7 @@ const MergePage: NextPageWithLayout = () => {
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
headerAction={
|
||||
overrideAction={
|
||||
hasCurrentWorkflowFailed ? (
|
||||
<Button
|
||||
type="default"
|
||||
|
||||
Reference in new issue
Block a user