Files
fusion/packages/dashboard/app/components/DockerProvisioningStatus.tsx
Fusion 873a30141a feat(FN-3110): add docker provisioning service, API routes, and status UI c
Merges FN-3110 to add Docker provisioning support across the stack: TypeScript types and DockerProvisioningService in core, API routes in dashboard for provisioning operations, a useDockerProvisioning hook for frontend integration, and a DockerProvisioningStatus UI component — all covered by unit an

Fusion-Task-Id: FN-3110
2026-05-03 14:10:11 -07:00

126 lines
3.6 KiB
TypeScript

import { useState, useEffect } from "react";
import { CheckCircle, AlertCircle, ExternalLink, RefreshCw, Terminal } from "lucide-react";
import type { DockerProvisionResult } from "@fusion/core";
import "./DockerProvisioningStatus.css";
const STAGES = [
"Pulling image...",
"Creating container...",
"Starting container...",
"Registering node...",
];
const STAGE_INTERVAL_MS = 2000;
export interface DockerProvisioningStatusProps {
result?: DockerProvisionResult;
isProvisioning: boolean;
error?: string | null;
onRetry?: () => void;
onViewNode?: (nodeId: string) => void;
}
export function DockerProvisioningStatus({
result,
isProvisioning,
error,
onRetry,
onViewNode,
}: DockerProvisioningStatusProps) {
const [stageIndex, setStageIndex] = useState(0);
// Animate stages during provisioning
useEffect(() => {
if (!isProvisioning || result) {
return;
}
setStageIndex(0);
const timer = setInterval(() => {
setStageIndex((prev) => (prev < STAGES.length - 1 ? prev + 1 : prev));
}, STAGE_INTERVAL_MS);
return () => clearInterval(timer);
}, [isProvisioning, result]);
// Provisioning state
if (isProvisioning && !result) {
return (
<div className="provisioning-status provisioning-status--loading">
<div className="provisioning-status__spinner">
<span className="provisioning-status__dot" />
<span className="provisioning-status__dot" />
<span className="provisioning-status__dot" />
</div>
<div className="provisioning-status__text">Creating Docker node...</div>
<div className="provisioning-status__stage">{STAGES[stageIndex]}</div>
</div>
);
}
// Success state
if (result?.success) {
const durationSec = result.durationMs ? (result.durationMs / 1000).toFixed(1) : null;
return (
<div className="provisioning-status provisioning-status--success">
<div className="provisioning-status__icon provisioning-status__icon--success">
<CheckCircle size={24} />
</div>
<div className="provisioning-status__text">Node created successfully!</div>
{result.containerId && (
<div className="provisioning-status__detail">
Container: <code>{result.containerId.slice(0, 12)}</code>
</div>
)}
{durationSec && (
<div className="provisioning-status__detail">
Provisioned in {durationSec}s
</div>
)}
{result.nodeId && onViewNode && (
<button
className="btn btn-primary btn-sm"
onClick={() => onViewNode(result.nodeId!)}
>
<ExternalLink size={14} />
View Node
</button>
)}
</div>
);
}
// Failure state
const displayError = error ?? result?.error;
const failedStage = result?.failedStage;
return (
<div className="provisioning-status provisioning-status--error">
<div className="provisioning-status__icon provisioning-status__icon--error">
<AlertCircle size={24} />
</div>
<div className="provisioning-status__text">
{displayError ?? "Provisioning failed"}
</div>
{failedStage && (
<div className="provisioning-status__detail">
Failed at: {failedStage}
</div>
)}
{result?.containerName && (
<div className="provisioning-status__hint">
<Terminal size={14} />
<code>docker logs {result.containerName}</code>
</div>
)}
{onRetry && (
<button className="btn btn-sm" onClick={onRetry}>
<RefreshCw size={14} />
Retry
</button>
)}
</div>
);
}