diff --git a/ui/src/components/DeploymentProgress.module.css b/ui/src/components/DeploymentProgress.module.css new file mode 100644 index 00000000..846dccba --- /dev/null +++ b/ui/src/components/DeploymentProgress.module.css @@ -0,0 +1,74 @@ +.container { + display: flex; + align-items: center; + gap: 0; + padding: 8px 0; +} + +.step { + display: flex; + align-items: center; + gap: 0; +} + +.dot { + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid var(--border-subtle); + background: transparent; + flex-shrink: 0; +} + +.dotCompleted { + background: var(--success); + border-color: var(--success); +} + +.dotActive { + background: var(--accent, #6c7aff); + border-color: var(--accent, #6c7aff); + animation: pulse 1.5s ease-in-out infinite; +} + +.dotFailed { + background: var(--error); + border-color: var(--error); +} + +.line { + width: 32px; + height: 2px; + background: var(--border-subtle); +} + +.lineCompleted { + background: var(--success); +} + +.label { + font-size: 10px; + color: var(--text-muted); + text-align: center; + margin-top: 4px; +} + +.labelActive { + color: var(--accent, #6c7aff); + font-weight: 600; +} + +.labelFailed { + color: var(--error); +} + +.stepColumn { + display: flex; + flex-direction: column; + align-items: center; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} diff --git a/ui/src/components/DeploymentProgress.tsx b/ui/src/components/DeploymentProgress.tsx new file mode 100644 index 00000000..963a4867 --- /dev/null +++ b/ui/src/components/DeploymentProgress.tsx @@ -0,0 +1,46 @@ +import styles from './DeploymentProgress.module.css'; + +const STAGES = [ + { key: 'PRE_FLIGHT', label: 'Pre-flight' }, + { key: 'PULL_IMAGE', label: 'Pull' }, + { key: 'CREATE_NETWORK', label: 'Network' }, + { key: 'START_REPLICAS', label: 'Start' }, + { key: 'HEALTH_CHECK', label: 'Health' }, + { key: 'SWAP_TRAFFIC', label: 'Swap' }, + { key: 'COMPLETE', label: 'Done' }, +]; + +interface DeploymentProgressProps { + currentStage: string | null; + failed?: boolean; +} + +export function DeploymentProgress({ currentStage, failed }: DeploymentProgressProps) { + if (!currentStage) return null; + + const currentIndex = STAGES.findIndex((s) => s.key === currentStage); + + return ( +
+ {STAGES.map((stage, i) => { + const isCompleted = i < currentIndex; + const isActive = i === currentIndex && !failed; + const isFailed = i === currentIndex && failed; + + return ( +
+ {i > 0 && ( +
+ )} +
+
+ + {stage.label} + +
+
+ ); + })} +
+ ); +}