123 lines
7.5 KiB
TypeScript
123 lines
7.5 KiB
TypeScript
import { PackageCheck, RotateCw } from "lucide-react";
|
||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
|
||
import { platformApiClient } from "../api/client";
|
||
import type { PluginLifecycleInstallationResponse, PluginLifecycleOperation, ServerInstanceResponse } from "../api/types";
|
||
import { ConfirmDialog } from "./OperationControls";
|
||
import { ErrorState, LoadingState, ResultBadge } from "./StateViews";
|
||
|
||
interface PluginLifecycleWorkbenchProps {
|
||
pluginId: string;
|
||
pluginName: string;
|
||
operations?: PluginLifecycleOperation[];
|
||
serverId?: string;
|
||
disabled?: boolean;
|
||
}
|
||
|
||
export function PluginLifecycleWorkbench({ pluginId, pluginName, operations = lifecycleOperations, serverId, disabled = false }: PluginLifecycleWorkbenchProps) {
|
||
const [servers, setServers] = useState<ServerInstanceResponse[]>([]);
|
||
const [installations, setInstallations] = useState<PluginLifecycleInstallationResponse[]>([]);
|
||
const [selectedServerId, setSelectedServerId] = useState(serverId ?? "");
|
||
const [operation, setOperation] = useState<PluginLifecycleOperation>(operations[0] ?? "install");
|
||
const [targetVersion, setTargetVersion] = useState("");
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState("");
|
||
const [confirming, setConfirming] = useState(false);
|
||
const [busy, setBusy] = useState(false);
|
||
const [result, setResult] = useState<{ status: "succeeded" | "failed" | "pending"; label: string } | null>(null);
|
||
|
||
const refresh = useCallback(async () => {
|
||
setLoading(true);
|
||
setError("");
|
||
try {
|
||
const [serverResponse, lifecycleResponse] = await Promise.all([
|
||
platformApiClient.listServerInstances(),
|
||
platformApiClient.listPluginLifecycles({ pluginId, ...(serverId ? { serverInstanceId: serverId } : {}) })
|
||
]);
|
||
const compatibleServers = serverResponse.items.filter((server) => server.pluginId === pluginId && (!serverId || server.id === serverId));
|
||
setServers(compatibleServers);
|
||
setInstallations(lifecycleResponse.items);
|
||
setSelectedServerId((current) => current || compatibleServers[0]?.id || "");
|
||
} catch (caught) {
|
||
setError(caught instanceof Error ? caught.message : "插件生命周期加载失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [pluginId, serverId]);
|
||
|
||
useEffect(() => {
|
||
void refresh();
|
||
}, [refresh]);
|
||
|
||
const installation = useMemo(() => installations.find((item) => item.serverInstanceId === selectedServerId), [installations, selectedServerId]);
|
||
|
||
async function submit() {
|
||
if (!selectedServerId || busy) return;
|
||
setBusy(true);
|
||
setResult({ status: "pending", label: `${lifecycleOperationLabel(operation)}提交中…` });
|
||
try {
|
||
const response = await platformApiClient.runPluginLifecycle(pluginId, {
|
||
serverInstanceId: selectedServerId,
|
||
operation,
|
||
targetVersion: targetVersion.trim() || undefined,
|
||
idempotencyKey: `web:plugin.lifecycle:${pluginId}:${selectedServerId}:${operation}:${Date.now()}`,
|
||
confirmed: disruptiveOperations.includes(operation)
|
||
});
|
||
const evidence = [response.job?.id && `任务 ${response.job.id}`].filter(Boolean).join(" · ");
|
||
setResult({ status: response.status === "queued" || response.status === "accepted" ? "succeeded" : response.status === "deferred" ? "pending" : "failed", label: `${lifecycleOperationLabel(operation)}:${response.status}${evidence ? ` · ${evidence}` : ""}` });
|
||
setConfirming(false);
|
||
await refresh();
|
||
} catch (caught) {
|
||
setResult({ status: "failed", label: caught instanceof Error ? caught.message : "插件生命周期操作失败" });
|
||
setConfirming(false);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="plugin-lifecycle-workbench" aria-label={`${pluginName} production lifecycle`}>
|
||
<div className="panel-header">
|
||
<h3><PackageCheck size={15} /> 生产生命周期</h3>
|
||
<button type="button" className="icon-command" disabled={loading || busy} onClick={() => void refresh()} title="刷新插件生命周期"><RotateCw size={14} /><span>刷新</span></button>
|
||
</div>
|
||
{result && <ResultBadge status={result.status} label={result.label} />}
|
||
{loading && <LoadingState label="正在同步插件生命周期…" compact />}
|
||
{!loading && error && <ErrorState title="插件生命周期不可用" reason={error} diagnosticId={`plugin-lifecycle:${pluginId}`} onRetry={() => void refresh()} compact />}
|
||
{!loading && !error && (
|
||
<>
|
||
<div className="server-toolbar plugin-lifecycle-controls">
|
||
<select aria-label="生命周期服务器" value={selectedServerId} disabled={Boolean(serverId) || busy} onChange={(event) => setSelectedServerId(event.target.value)}>
|
||
{servers.length === 0 && <option value="">暂无匹配服务器</option>}
|
||
{servers.map((server) => <option key={server.id} value={server.id}>{server.name} · {server.id}</option>)}
|
||
</select>
|
||
<select aria-label="生命周期操作" value={operation} disabled={busy} onChange={(event) => setOperation(event.target.value as PluginLifecycleOperation)}>
|
||
{operations.map((item) => <option key={item} value={item}>{lifecycleOperationLabel(item)}</option>)}
|
||
</select>
|
||
{(operation === "install" || operation === "upgrade") && <input aria-label="目标版本" placeholder="目标版本" value={targetVersion} disabled={busy} onChange={(event) => setTargetVersion(event.target.value)} />}
|
||
<button type="button" className="primary-command" disabled={disabled || busy || !selectedServerId} onClick={() => setConfirming(true)}>{busy ? "提交中…" : "执行"}</button>
|
||
</div>
|
||
{installation ? (
|
||
<div className="console-record plugin-lifecycle-state">
|
||
<div className="console-record-head"><strong>{installation.currentState} → {installation.desiredState}</strong><span className="status-pill status-active">{installation.compatibility || "pending"}</span></div>
|
||
<div className="console-record-meta">
|
||
<span>当前 {installation.currentVersion || "--"}</span><span>目标 {installation.targetVersion || "--"}</span><span>依赖 {installation.dependencyState || "unknown"}</span>
|
||
{installation.jobId && <span>任务 {installation.jobId}</span>}
|
||
</div>
|
||
{installation.failureReason && <p className="operation-error">{installation.failureReason}</p>}
|
||
</div>
|
||
) : <p className="console-empty-note">该服务器尚无插件生命周期记录。</p>}
|
||
</>
|
||
)}
|
||
<ConfirmDialog open={confirming} title={`确认${lifecycleOperationLabel(operation)}`} description={`插件 ${pluginName},服务器 ${selectedServerId || "--"}${targetVersion ? `,目标版本 ${targetVersion}` : ""}。`} confirmLabel={lifecycleOperationLabel(operation)} danger={disruptiveOperations.includes(operation)} busy={busy} onCancel={() => { if (!busy) setConfirming(false); }} onConfirm={() => void submit()} />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const lifecycleOperations: PluginLifecycleOperation[] = ["install", "enable", "disable", "upgrade", "rollback", "retire", "dependency-check"];
|
||
const disruptiveOperations: PluginLifecycleOperation[] = ["disable", "rollback", "retire"];
|
||
|
||
function lifecycleOperationLabel(operation: PluginLifecycleOperation) {
|
||
return ({ install: "安装", enable: "启用", disable: "停用", upgrade: "升级", rollback: "回滚", retire: "退役", "dependency-check": "依赖检查" } as Record<PluginLifecycleOperation, string>)[operation];
|
||
}
|