Migration (multi-agent sweep over 216 files, 60 batches):
- Every user-visible dashboard + TUI string moved to t() with the exact
English inline default (en rendering byte-identical)
- Catalogs merged from per-batch fragments: en/zh-CN/zh-TW/fr/es now
carry ~5,930 keys each across common/app/errors/cli namespaces;
CLI bundles regenerated (6 locales incl. ko)
Integration fixes:
- 18 type errors: reserved {{count}} interpolations renamed, malformed
plural call, hand-rolled t-param types replaced with TFunction<"app">
- 23 lint errors: superseded label constants/helpers removed
- ExecutorStatusBar hook-order violation (keyboard-open early return
moved below hooks)
- TUI tests wrapped in I18nextProvider (uninitialized fallback renders
literal {{placeholders}}); dashboard vitest.setup boots a minimal en
i18next instance for the same reason
Known WIP (next commits): ~457 residual strings across 50 batches,
Korean drafts for swept keys, and a dashboard test-suite pass that is
still being stabilized (~283 failures under investigation — fake-timer
waitFor interaction, likely stale node_modules vs merged lockfile).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
134 lines
4.3 KiB
TypeScript
134 lines
4.3 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { CheckCircle, AlertCircle, ExternalLink, RefreshCw, Terminal } from "lucide-react";
|
|
import type { DockerProvisionResult } from "@fusion/core";
|
|
import "./DockerProvisioningStatus.css";
|
|
|
|
const STAGE_KEYS = [
|
|
"docker.stage.pullingImage",
|
|
"docker.stage.creatingContainer",
|
|
"docker.stage.startingContainer",
|
|
"docker.stage.registeringNode",
|
|
];
|
|
|
|
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 { t } = useTranslation("app");
|
|
const [stageIndex, setStageIndex] = useState(0);
|
|
const STAGES = [
|
|
t("docker.stage.pullingImage", "Pulling image..."),
|
|
t("docker.stage.creatingContainer", "Creating container..."),
|
|
t("docker.stage.startingContainer", "Starting container..."),
|
|
t("docker.stage.registeringNode", "Registering node..."),
|
|
];
|
|
|
|
// Animate stages during provisioning
|
|
useEffect(() => {
|
|
if (!isProvisioning || result) {
|
|
return;
|
|
}
|
|
|
|
setStageIndex(0);
|
|
const timer = setInterval(() => {
|
|
setStageIndex((prev) => (prev < STAGE_KEYS.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">{t("docker.creatingNode", "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">{t("docker.successMessage", "Node created successfully!")}</div>
|
|
{result.containerId && (
|
|
<div className="provisioning-status__detail">
|
|
{t("docker.container", "Container:")} <code>{result.containerId.slice(0, 12)}</code>
|
|
</div>
|
|
)}
|
|
{durationSec && (
|
|
<div className="provisioning-status__detail">
|
|
{t("docker.provisionedIn", "Provisioned in {{durationSec}}s", { durationSec })}
|
|
</div>
|
|
)}
|
|
{result.nodeId && onViewNode && (
|
|
<button
|
|
className="btn btn-primary btn-sm"
|
|
onClick={() => onViewNode(result.nodeId!)}
|
|
>
|
|
<ExternalLink size={14} />
|
|
{t("docker.viewNode", "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 ?? t("docker.failedMessage", "Provisioning failed")}
|
|
</div>
|
|
{failedStage && (
|
|
<div className="provisioning-status__detail">
|
|
{t("docker.failedAt", "Failed at: {{stage}}", { stage: 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} />
|
|
{t("docker.retry", "Retry")}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|