Files
fusion/packages/dashboard/app/components/DockerProvisioningStatus.tsx
gsxdsm 1e49494bac feat(i18n): full-sweep string migration — 5,930 keys across 5 locales (#1352)
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>
2026-06-03 19:06:53 -07:00

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>
);
}