/* eslint-disable @nx/enforce-module-boundaries */ import { FileChange } from '@nx/devkit'; import type { MigrationDetailsWithId } from 'nx/src/config/misc-interfaces'; import type { MigrationsJsonMetadata } from 'nx/src/command-line/migrate/migrate-ui-api'; /* eslint-enable @nx/enforce-module-boundaries */ import { ChevronUpIcon, ChevronDownIcon, ExclamationCircleIcon, CheckCircleIcon, ClockIcon, MinusIcon, } from '@heroicons/react/24/outline'; import { useEffect, useState, useRef } from 'react'; import { MigrationCard, MigrationCardHandle } from './migration-card'; import { Collapsible } from '@nx/graph/ui-common'; import { twMerge } from 'tailwind-merge'; export interface MigrationTimelineProps { migrations: MigrationDetailsWithId[]; nxConsoleMetadata: MigrationsJsonMetadata; currentMigrationIndex: number; currentMigrationRunning?: boolean; currentMigrationFailed?: boolean; currentMigrationSuccess?: boolean; currentMigrationHasChanges?: boolean; isDone?: boolean; isInit: boolean; onRunMigration: (migration: MigrationDetailsWithId) => void; onSkipMigration: (migration: MigrationDetailsWithId) => void; onFileClick: ( migration: MigrationDetailsWithId, file: Omit ) => void; onViewImplementation: (migration: MigrationDetailsWithId) => void; onViewDocumentation: (migration: MigrationDetailsWithId) => void; onCancel?: () => void; onReviewMigration: (migrationId: string) => void; } export function MigrationTimeline({ migrations, nxConsoleMetadata, currentMigrationIndex, currentMigrationRunning, currentMigrationFailed, currentMigrationSuccess, currentMigrationHasChanges, onRunMigration, onSkipMigration, onFileClick, onViewImplementation, onViewDocumentation, onCancel, onReviewMigration, }: MigrationTimelineProps) { const [showAllPastMigrations, setShowAllPastMigrations] = useState(false); const [showAllFutureMigrations, setShowAllFutureMigrations] = useState(false); const [expandedMigrations, setExpandedMigrations] = useState<{ [key: string]: boolean; }>({}); const currentMigration = migrations[currentMigrationIndex]; const pastMigrations = migrations.slice(0, currentMigrationIndex); const futureMigrations = migrations.slice(currentMigrationIndex + 1); // Number of visible migrations when collapsed const visiblePastCount = 0; const visibleFutureCount = 2; const visiblePastMigrations = showAllPastMigrations ? pastMigrations : pastMigrations.slice( Math.max(0, pastMigrations.length - visiblePastCount) ); const visibleFutureMigrations = showAllFutureMigrations ? futureMigrations : futureMigrations.slice(0, visibleFutureCount); const hasPastMigrationsHidden = pastMigrations.length > visiblePastCount && !showAllPastMigrations; const hasFutureMigrationsHidden = futureMigrations.length > visibleFutureCount && !showAllFutureMigrations; const currentMigrationRef = useRef(null); // Auto-expand when entering a failed migration or requires review useEffect(() => { if (currentMigrationFailed || currentMigrationHasChanges) { toggleMigrationExpanded(currentMigration.id, true); } }, [currentMigrationHasChanges, currentMigrationFailed, currentMigration]); const toggleMigrationExpanded = (migrationId: string, state?: boolean) => { setExpandedMigrations((prev) => ({ ...prev, [migrationId]: state ?? !prev[migrationId], })); }; return (
{onCancel && ( )}
{/* Timeline lines */} {/* Solid line for visible migrations */}
{/* Dashed line for the section after the last visible migration */} {hasFutureMigrationsHidden && (
)} {/* Timeline container */}
{/* Past migrations section */} {pastMigrations.length > 0 && ( <> {showAllPastMigrations && (
setShowAllPastMigrations(false)} />
setShowAllPastMigrations(false)} > Hide Past Migrations
)} {visiblePastMigrations.map((migration) => (
toggleMigrationExpanded(migration.id)} />
toggleMigrationExpanded(migration.id)} className={`flex-shrink-0 cursor-pointer whitespace-nowrap text-base ${ nxConsoleMetadata.completedMigrations?.[ migration.id ]?.type === 'successful' ? 'text-green-600' : 'text-slate-600' }`} > {migration.name} {!expandedMigrations[migration.id] && ( {' '} {migration.description}{' '} )}
{expandedMigrations[migration.id] && (
{nxConsoleMetadata.completedMigrations?.[migration.id] ?.type === 'failed' && ( )}
)}
onFileClick(migration, file)} onViewImplementation={() => onViewImplementation(migration) } onViewDocumentation={() => onViewDocumentation(migration) } />
))} {hasPastMigrationsHidden && (
setShowAllPastMigrations(true)} />
setShowAllPastMigrations(true)} > Show Past Migrations
)} )} {/* Current migration */}
{/* TODO: Change this to be a clickable element li, button etc... */}
toggleMigrationExpanded(currentMigration.id)} />
toggleMigrationExpanded(currentMigration.id) } > {currentMigration.name} {!expandedMigrations[currentMigration.id] && (

{currentMigration.description}

)}
{expandedMigrations[currentMigration.id] && (
{currentMigrationFailed && ( )} {!currentMigrationSuccess && ( )} {currentMigrationHasChanges && ( )}
)}
{/* Migration Card */} onFileClick(currentMigration, file)} forceIsRunning={currentMigrationRunning} onViewImplementation={() => onViewImplementation(currentMigration) } onViewDocumentation={() => onViewDocumentation(currentMigration) } />
{/* Future migrations */} {futureMigrations.length > 0 && ( <> {visibleFutureMigrations.map((migration) => (
toggleMigrationExpanded(migration.id)} />
toggleMigrationExpanded(migration.id)} > {migration.name} {!expandedMigrations[migration.id] && ( {migration.description}{' '} )}
{/* ONLY SHOW BUTTONS FOR PENDING MIGRATIONS */} {expandedMigrations[migration.id] && !nxConsoleMetadata.completedMigrations?.[ migration.id ] && (
)}
onFileClick(migration, file)} onViewImplementation={() => onViewImplementation(migration) } onViewDocumentation={() => onViewDocumentation(migration) } />
))} {hasFutureMigrationsHidden && (
setShowAllFutureMigrations(true)} />
setShowAllFutureMigrations(true)} > Show more
)} {showAllFutureMigrations && (
setShowAllFutureMigrations(false)} />
setShowAllFutureMigrations(false)} > Show fewer
)} )}
); } interface TimelineButtonProps { icon: React.ElementType; onClick: () => void; } function TimelineButton({ icon: Icon, onClick }: TimelineButtonProps) { return (
); } interface MigrationStateCircleProps { migration: MigrationDetailsWithId; nxConsoleMetadata: MigrationsJsonMetadata; isRunning?: boolean; onClick: () => void; } function MigrationStateCircle({ migration, nxConsoleMetadata, isRunning, onClick, }: MigrationStateCircleProps) { let bgColor = ''; let textColor = ''; let Icon = ClockIcon; // Check if this migration is in the completed migrations const completedMigration = nxConsoleMetadata.completedMigrations?.[migration.id]; const isSkipped = completedMigration?.type === 'skipped'; const isError = completedMigration?.type === 'failed'; const isSuccess = completedMigration?.type === 'successful'; if (isSkipped) { bgColor = 'bg-slate-300'; textColor = 'text-slate-700'; Icon = MinusIcon; } else if (isError) { bgColor = 'bg-red-500'; textColor = 'text-white'; Icon = ExclamationCircleIcon; } else if (isRunning) { bgColor = 'bg-blue-500'; textColor = 'text-white'; Icon = ClockIcon; } else if (isSuccess) { bgColor = 'bg-green-500'; textColor = 'text-white'; Icon = CheckCircleIcon; } else { // Future migration (none of the states above) bgColor = 'bg-slate-300'; textColor = 'text-slate-700'; Icon = ClockIcon; } return (
{isRunning ? ( ) : ( )}
); }