功能修改
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import { Activity, Ban, KeyRound, PackageCheck, Play, RefreshCw, RotateCcw, ShieldAlert, Square, Trash2, UploadCloud } from "lucide-react";
|
||||
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { platformApiClient } from "../api/client";
|
||||
import type { ClientManagerInstallationResponse, ClientManagerLifecycleOperation } from "../api/types";
|
||||
import type { CurrentUserView } from "../contracts/workspace";
|
||||
import type { OperationTracker } from "../stores/operations";
|
||||
import { cx } from "../utils/classes";
|
||||
import { ConfirmDialog } from "./OperationControls";
|
||||
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "./StateViews";
|
||||
|
||||
type LoadState = { status: "loading" } | { status: "error"; reason: string } | { status: "ready"; items: ClientManagerInstallationResponse[] };
|
||||
|
||||
interface ClientManagerLifecyclePanelProps {
|
||||
serverId: string;
|
||||
serverName: string;
|
||||
session: CurrentUserView;
|
||||
operations: OperationTracker;
|
||||
}
|
||||
|
||||
interface PendingConfirmation {
|
||||
title: string;
|
||||
description: string;
|
||||
danger?: boolean;
|
||||
execute: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function ClientManagerLifecyclePanel({ serverId, serverName, session, operations }: ClientManagerLifecyclePanelProps) {
|
||||
const [state, setState] = useState<LoadState>({ status: "loading" });
|
||||
const [result, setResult] = useState<{ status: "pending" | "succeeded" | "failed"; label: string } | null>(null);
|
||||
const [confirmation, setConfirmation] = useState<PendingConfirmation | null>(null);
|
||||
const [confirmBusy, setConfirmBusy] = useState(false);
|
||||
|
||||
const refresh = useCallback(async (showLoading = false) => {
|
||||
if (showLoading) setState({ status: "loading" });
|
||||
try {
|
||||
const response = await platformApiClient.listClientManagerLifecycles(serverId);
|
||||
setState({ status: "ready", items: response.items });
|
||||
} catch (error) {
|
||||
setState({ status: "error", reason: safeError(error, "Client Manager 状态加载失败") });
|
||||
}
|
||||
}, [serverId]);
|
||||
|
||||
useEffect(() => { void refresh(true); }, [refresh]);
|
||||
|
||||
const hasActiveJob = state.status === "ready" && state.items.some((item) => item.job && ["queued", "accepted", "running", "retrying"].includes(item.job.state));
|
||||
useEffect(() => {
|
||||
if (!hasActiveJob) return undefined;
|
||||
const timer = window.setInterval(() => void refresh(), 2500);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [hasActiveJob, refresh]);
|
||||
|
||||
async function runCommand(item: ClientManagerInstallationResponse, intent: string, execute: () => Promise<ClientManagerInstallationResponse>) {
|
||||
const operationId = operations.begin({ intent, targetKind: "server", targetId: `${serverId}:client-manager:${item.profileKey}`, requester: session.displayName });
|
||||
setResult({ status: "pending", label: `${intent} 已提交,等待 Platform/Run 返回真实状态` });
|
||||
try {
|
||||
const next = await execute();
|
||||
setState((current) => current.status === "ready" ? { status: "ready", items: current.items.map((entry) => entry.id === next.id ? next : entry) } : current);
|
||||
const label = next.job ? `${intent} 已排队,job ${next.job.id}` : `${intent} 已完成状态更新`;
|
||||
operations.succeed(operationId, label);
|
||||
setResult({ status: "succeeded", label });
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
const reason = safeError(error, `${intent} 失败`);
|
||||
operations.fail(operationId, reason, operationId);
|
||||
setResult({ status: "failed", label: reason });
|
||||
}
|
||||
}
|
||||
|
||||
function confirmCommand(config: PendingConfirmation) {
|
||||
setConfirmation(config);
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="console-panel client-manager-lifecycle-panel" aria-label="Client Manager 生命周期">
|
||||
<div className="panel-header">
|
||||
<h2><PackageCheck size={17} /> Client Manager 生命周期</h2>
|
||||
<div className="action-strip">
|
||||
{result && <ResultBadge status={result.status} label={result.label} />}
|
||||
<button type="button" className="icon-command" title="刷新 Client Manager 状态" onClick={() => void refresh()}>
|
||||
<RefreshCw size={15} /><span>刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{state.status === "loading" && <LoadingState label="正在读取 Client Manager 部署与组件健康状态…" />}
|
||||
{state.status === "error" && <ErrorState title="Client Manager 状态不可用" reason={state.reason} diagnosticId={`client-manager:${serverId}`} onRetry={() => void refresh(true)} />}
|
||||
{state.status === "ready" && state.items.length === 0 && <EmptyState title="尚无 Client Manager 生命周期记录" description="先在运行分发区按插件声明构建 Client Manager;可用 artifact 会在这里进入部署闭环。" />}
|
||||
{state.status === "ready" && state.items.length > 0 && (
|
||||
<div className="client-manager-lifecycle-list">
|
||||
{state.items.map((item) => (
|
||||
<ClientManagerLifecycleRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
serverName={serverName}
|
||||
runCommand={runCommand}
|
||||
confirmCommand={confirmCommand}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmation !== null}
|
||||
title={confirmation?.title ?? ""}
|
||||
description={confirmation?.description ?? ""}
|
||||
confirmLabel="确认执行"
|
||||
danger={confirmation?.danger}
|
||||
busy={confirmBusy}
|
||||
onCancel={() => setConfirmation(null)}
|
||||
onConfirm={() => {
|
||||
if (!confirmation) return;
|
||||
setConfirmBusy(true);
|
||||
void confirmation.execute().finally(() => {
|
||||
setConfirmBusy(false);
|
||||
setConfirmation(null);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
interface ClientManagerLifecycleRowProps {
|
||||
item: ClientManagerInstallationResponse;
|
||||
serverName: string;
|
||||
runCommand: (item: ClientManagerInstallationResponse, intent: string, execute: () => Promise<ClientManagerInstallationResponse>) => Promise<void>;
|
||||
confirmCommand: (config: PendingConfirmation) => void;
|
||||
}
|
||||
|
||||
function ClientManagerLifecycleRow({ item, serverName, runCommand, confirmCommand }: ClientManagerLifecycleRowProps) {
|
||||
const actionMap = useMemo(() => new Map(item.actions.map((action) => [action.operation, action])), [item.actions]);
|
||||
const available = (operation: ClientManagerLifecycleOperation) => actionMap.get(operation)?.available ?? false;
|
||||
const reason = (operation: ClientManagerLifecycleOperation) => actionMap.get(operation)?.reason ?? "Platform 当前状态不允许此操作";
|
||||
const distributionId = item.distribution?.id ?? "";
|
||||
const idempotency = (operation: string) => `client-manager.${operation}:${item.serverInstanceId}:${item.profileKey}:${Date.now()}`;
|
||||
const control = (operation: "start" | "stop" | "restart" | "status" | "rollback") =>
|
||||
platformApiClient.controlClientManager(item.serverInstanceId, { profileKey: item.profileKey, operation, expectedDeploymentGeneration: item.deploymentGeneration, idempotencyKey: idempotency(operation) });
|
||||
|
||||
const deploy = () => runCommand(item, item.requiresRedeploy ? "重新部署 Client Manager" : "部署 Client Manager", () => platformApiClient.deployClientManager(item.serverInstanceId, {
|
||||
profileKey: item.profileKey, distributionId, expectedDeploymentGeneration: item.deploymentGeneration, idempotencyKey: idempotency("deploy")
|
||||
}));
|
||||
const update = () => runCommand(item, "更新 Client Manager", () => platformApiClient.updateClientManager(item.serverInstanceId, {
|
||||
profileKey: item.profileKey, distributionId, expectedDeploymentGeneration: item.deploymentGeneration, approved: true, idempotencyKey: idempotency("update")
|
||||
}));
|
||||
|
||||
return (
|
||||
<section className="client-manager-lifecycle-row" aria-label={`${item.profileKey} lifecycle`}>
|
||||
<div className="client-manager-lifecycle-head">
|
||||
<div>
|
||||
<strong>{item.profileKey}</strong>
|
||||
<span className="provider-id">{item.targetOs}/{item.targetArch} · deployment generation {item.deploymentGeneration} · key generation {item.keyGeneration}</span>
|
||||
</div>
|
||||
<div className="tag-list">
|
||||
<span className={cx("status-pill", lifecycleTone(item.status))}>{lifecycleLabel(item.status)}</span>
|
||||
<span className={cx("status-pill", healthTone(item.health))}><Activity size={12} /> {healthLabel(item.health)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="client-manager-version-grid">
|
||||
<VersionCell label="目标版本" version={item.desiredVersion} revision={item.desiredRevision} artifact={item.desiredArtifactId} />
|
||||
<VersionCell label="当前版本" version={item.activeVersion} revision={item.activeRevision} artifact={item.activeArtifactId} />
|
||||
<VersionCell label="回滚版本" version={item.previousVersion} revision={item.previousRevision} artifact={item.previousArtifactId} />
|
||||
<div className="client-manager-version-cell"><span>组件身份</span><strong>{item.lastSeenAt ? `最后心跳 ${formatTime(item.lastSeenAt)}` : "等待独立注册"}</strong><small>{item.healthReason || "未收到安全健康原因"}</small></div>
|
||||
</div>
|
||||
|
||||
<div className="client-manager-phase-line">
|
||||
<span><ShieldAlert size={14} /> {item.phase || "等待生命周期事件"}</span>
|
||||
{item.lastOperation && <span>最近操作 {item.lastOperation}</span>}
|
||||
{item.lastSuccessfulJobId && <span>最近成功 job {item.lastSuccessfulJobId}</span>}
|
||||
</div>
|
||||
|
||||
{item.job && (
|
||||
<div className="client-manager-job-progress" aria-label="Client Manager job progress">
|
||||
<div><span>job {item.job.id} · attempt {item.job.attempt} · {item.job.state}</span><strong>{item.job.progress.percent}%</strong></div>
|
||||
<progress max={100} value={item.job.progress.percent} />
|
||||
<small>{item.job.progress.message || "等待 Run 回报真实阶段"}</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(item.retryable || item.requiresRedeploy || item.status === "failed") && (
|
||||
<div className="client-manager-recovery">
|
||||
<ShieldAlert size={16} />
|
||||
<span>{item.requiresRedeploy ? "组件密钥 generation 已变化:旧 artifact/session 已被围栏。请重新构建当前 generation,再执行重新部署。" : item.retryable ? "Run 保留了可恢复状态,可重试当前 intent;界面不会在 job 成功前推进阶段。" : "检查 Platform 审计与 job 失败原因后选择重新部署、回滚或卸载。"}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="client-manager-command-grid">
|
||||
<LifecycleButton icon={<UploadCloud size={14} />} label={item.activeArtifactId ? "重新部署" : "部署"} disabled={!available("deploy") || !distributionId} reason={!distributionId ? "没有可用 distribution" : reason("deploy")} onClick={deploy} />
|
||||
<LifecycleButton icon={<Play size={14} />} label="启动" disabled={!available("start")} reason={reason("start")} onClick={() => void runCommand(item, "启动 Client Manager", () => control("start"))} />
|
||||
<LifecycleButton icon={<Square size={14} />} label="停止" disabled={!available("stop")} reason={reason("stop")} onClick={() => void runCommand(item, "停止 Client Manager", () => control("stop"))} />
|
||||
<LifecycleButton icon={<RefreshCw size={14} />} label="重启" disabled={!available("restart")} reason={reason("restart")} onClick={() => void runCommand(item, "重启 Client Manager", () => control("restart"))} />
|
||||
<LifecycleButton icon={<Activity size={14} />} label="检查状态" disabled={!available("status")} reason={reason("status")} onClick={() => void runCommand(item, "检查 Client Manager 状态", () => control("status"))} />
|
||||
<LifecycleButton icon={<UploadCloud size={14} />} label="更新" disabled={!available("update") || !distributionId} reason={!distributionId ? "没有兼容的可用 distribution" : reason("update")} onClick={() => confirmCommand({ title: "批准 Client Manager 更新", description: `将 ${serverName} 的 ${item.profileKey} 从 ${item.activeVersion || "未安装"} 更新到 ${item.desiredVersion || "目标版本"}。Run 将 staged activate、健康确认,并在失败时恢复 previous slot。`, execute: update })} />
|
||||
<LifecycleButton icon={<RotateCcw size={14} />} label="回滚" disabled={!available("rollback")} reason={reason("rollback")} onClick={() => confirmCommand({ title: "回滚 Client Manager", description: `确认将 ${item.profileKey} 回滚到 ${item.previousVersion || "previous slot"}?当前组件 session 将被撤销并需要重新注册。`, danger: true, execute: () => runCommand(item, "回滚 Client Manager", () => control("rollback")) })} />
|
||||
<LifecycleButton icon={<RefreshCw size={14} />} label="重试" disabled={!item.retryable} reason="当前失败不可重试" onClick={() => void runCommand(item, "重试 Client Manager", () => platformApiClient.retryClientManagerLifecycle(item.serverInstanceId, { profileKey: item.profileKey, expectedDeploymentGeneration: item.deploymentGeneration, idempotencyKey: idempotency("retry") }))} />
|
||||
<LifecycleButton icon={<Ban size={14} />} label="撤销会话" disabled={!item.activeArtifactId || item.status === "uninstalled"} reason="组件尚未安装" onClick={() => confirmCommand({ title: "撤销 Client Manager 会话", description: `撤销 ${item.profileKey} 的独立组件 session。Run session 与 job lease 不受影响,组件必须使用当前 key generation 重新注册。`, danger: true, execute: () => runCommand(item, "撤销 Client Manager 会话", () => platformApiClient.revokeClientManagerSession(item.serverInstanceId, { profileKey: item.profileKey, reason: "operator revoked component session" })) })} />
|
||||
<LifecycleButton icon={<KeyRound size={14} />} label="重置密钥" disabled={item.status === "uninstalled"} reason="已卸载" onClick={() => confirmCommand({ title: "重置 Client Manager 密钥", description: `重置 ${item.profileKey} 的 component key 会撤销旧 session/artifact generation。必须重新构建并重新部署,不会显示或导出原始密钥。`, danger: true, execute: async () => { await platformApiClient.resetClientManagerKey(item.serverInstanceId, { componentKind: "client-manager", componentKey: item.profileKey }); await runCommand(item, "刷新密钥重置状态", () => platformApiClient.getClientManagerLifecycle(item.serverInstanceId, item.profileKey)); } })} />
|
||||
<LifecycleButton icon={<Trash2 size={14} />} label="卸载" danger disabled={!available("uninstall")} reason={reason("uninstall")} onClick={() => confirmCommand({ title: "卸载 Client Manager", description: `确认停止并卸载 ${serverName} 的 ${item.profileKey}?Run 只会清理受控 Client Manager workspace,Platform 保留 build、artifact 与审计历史。`, danger: true, execute: () => runCommand(item, "卸载 Client Manager", () => platformApiClient.uninstallClientManager(item.serverInstanceId, { profileKey: item.profileKey, expectedDeploymentGeneration: item.deploymentGeneration, confirmed: true, idempotencyKey: idempotency("uninstall") })) })} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function VersionCell({ label, version, revision, artifact }: { label: string; version?: string; revision?: string; artifact?: string }) {
|
||||
return <div className="client-manager-version-cell"><span>{label}</span><strong>{version || "--"}</strong><small>{revision ? `revision ${shortRef(revision)}` : "revision --"}{artifact ? ` · artifact ${shortRef(artifact)}` : ""}</small></div>;
|
||||
}
|
||||
|
||||
function LifecycleButton({ icon, label, disabled, reason, danger, onClick }: { icon: ReactNode; label: string; disabled: boolean; reason: string; danger?: boolean; onClick: () => void }) {
|
||||
return <button type="button" className={cx("icon-command", danger && "danger-command")} disabled={disabled} title={disabled ? reason : label} onClick={onClick}>{icon}<span>{label}</span></button>;
|
||||
}
|
||||
|
||||
function lifecycleLabel(status: ClientManagerInstallationResponse["status"]): string {
|
||||
return ({ requested: "已请求", building: "构建中", available: "可部署", deploying: "部署中", installed: "已安装", registering: "等待注册", online: "在线", degraded: "降级", offline: "离线", updating: "更新中", rolling_back: "回滚中", stopping: "停止中", uninstalled: "已卸载", failed: "失败" })[status];
|
||||
}
|
||||
function lifecycleTone(status: ClientManagerInstallationResponse["status"]): string { return ["online", "installed"].includes(status) ? "status-active" : ["failed", "offline", "uninstalled"].includes(status) ? "status-disabled" : "status-pending"; }
|
||||
function healthLabel(health: ClientManagerInstallationResponse["health"]): string { return ({ unknown: "健康未知", healthy: "健康", degraded: "健康降级", unhealthy: "不健康", offline: "心跳离线" })[health]; }
|
||||
function healthTone(health: ClientManagerInstallationResponse["health"]): string { return health === "healthy" ? "status-active" : health === "unknown" || health === "degraded" ? "status-pending" : "status-disabled"; }
|
||||
function shortRef(value: string): string { return value.length > 18 ? `${value.slice(0, 18)}…` : value; }
|
||||
function formatTime(value: string): string { const time = new Date(value); return Number.isNaN(time.getTime()) ? "未知" : time.toLocaleString(); }
|
||||
function safeError(error: unknown, fallback: string): string { const message = error instanceof Error ? error.message : fallback; return message.replace(/Bearer\s+\S+/gi, "[token]").replace(/sk-[A-Za-z0-9_-]+/g, "[secret]").slice(0, 240); }
|
||||
Reference in New Issue
Block a user