Repair live server operations console
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { AlertTriangle, CakeSlice, Candy, Search, Sparkles, Trash2 } from "lucide-react";
|
||||
import { AlertTriangle, CakeSlice, Candy, ScrollText, Search, Sparkles, Terminal, Trash2 } from "lucide-react";
|
||||
import { type CSSProperties, type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
runtimeUpdateStages,
|
||||
useRuntimeTaskController
|
||||
} from "../components/RuntimeTaskProgress";
|
||||
import { ServerLiveLogDrawer, ServerManagementTerminalDrawer } from "../components/ServerLiveOperations";
|
||||
import { ConfirmDialog, ManagementDialog, UsageMeter } from "../components/OperationControls";
|
||||
import { ServerDeploymentWorkflow } from "../components/ServerDeploymentWorkflow";
|
||||
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
||||
@@ -57,6 +58,9 @@ const statusFilters: Array<{ id: ServerStatusFilter; label: string }> = [
|
||||
{ id: "attention", label: "需关注" }
|
||||
];
|
||||
|
||||
const serverListRefreshMs = 5000;
|
||||
const serverMetricFreshMs = 30000;
|
||||
|
||||
export function ServersPage({ session, operations, onNavigate }: PageComponentProps) {
|
||||
const [listState, setListState] = useState<ListState>("loading");
|
||||
const [listError, setListError] = useState<string>("");
|
||||
@@ -78,9 +82,11 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
const [deletePassword, setDeletePassword] = useState("");
|
||||
const [deleteBusy, setDeleteBusy] = useState(false);
|
||||
const [runTargetSelection, setRunTargetSelection] = useState<RunTargetSelectionState | null>(null);
|
||||
const [liveLogTarget, setLiveLogTarget] = useState<ServerInstanceResponse | null>(null);
|
||||
const [terminalTarget, setTerminalTarget] = useState<ServerInstanceResponse | null>(null);
|
||||
|
||||
const refreshList = useCallback(async () => {
|
||||
setListState("loading");
|
||||
const refreshList = useCallback(async (showLoading = true) => {
|
||||
if (showLoading) setListState("loading");
|
||||
try {
|
||||
const [pluginResponse, endpointResponse, instanceResponse, jobResponse] = await Promise.all([
|
||||
platformApiClient.listGamePlugins(),
|
||||
@@ -92,7 +98,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
setEndpoints(endpointResponse.items);
|
||||
setInstances(instanceResponse.items);
|
||||
setJobs(jobResponse.items);
|
||||
setForm((current) => {
|
||||
if (showLoading) setForm((current) => {
|
||||
const plugin = pluginResponse.items.find((item) => item.id === current.pluginId) ?? pluginResponse.items[0];
|
||||
const profileKey = plugin?.runtimeProfiles?.lifecycleProfiles?.some((profile) => profile.key === current.profileKey)
|
||||
? current.profileKey
|
||||
@@ -107,10 +113,12 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
: endpointResponse.items[0]?.id || ""
|
||||
};
|
||||
});
|
||||
setListState("ready");
|
||||
setListError("");
|
||||
if (showLoading) {
|
||||
setListState("ready");
|
||||
setListError("");
|
||||
}
|
||||
} catch (error) {
|
||||
setListState("error");
|
||||
if (showLoading) setListState("error");
|
||||
setListError(error instanceof Error ? error.message : "加载失败");
|
||||
}
|
||||
}, []);
|
||||
@@ -137,6 +145,14 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setInterval(() => {
|
||||
void refreshList(false);
|
||||
void refreshMetrics();
|
||||
}, serverListRefreshMs);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [refreshList, refreshMetrics]);
|
||||
|
||||
const cards = useMemo<ServerCardView[]>(
|
||||
() =>
|
||||
summarizeServerOperations(instances, metrics, jobs).map((summary) => ({
|
||||
@@ -596,6 +612,8 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
deleteDisabledReason={serverDeleteDisabledReason(session, card.instance)}
|
||||
onOpen={() => onNavigate("serverDetail", { serverId: card.instance.id })}
|
||||
onEdit={() => void openEditDeployment(card.instance)}
|
||||
onOpenLogs={() => setLiveLogTarget(card.instance)}
|
||||
onOpenTerminal={() => setTerminalTarget(card.instance)}
|
||||
onQuickAction={(action) => void handleQuickRuntimeAction(card.instance, action)}
|
||||
onDelete={() => {
|
||||
setDeletePassword("");
|
||||
@@ -630,6 +648,8 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
</label>
|
||||
</ConfirmDialog>
|
||||
<RuntimeTaskProgressDialog task={runtimeTask.task} onClose={runtimeTask.closeTask} actions={runtimeTaskActions} />
|
||||
<ServerLiveLogDrawer open={liveLogTarget !== null} serverId={liveLogTarget?.id ?? ""} serverName={liveLogTarget?.name ?? ""} onClose={() => setLiveLogTarget(null)} />
|
||||
<ServerManagementTerminalDrawer open={terminalTarget !== null} serverId={terminalTarget?.id ?? ""} serverName={terminalTarget?.name ?? ""} pluginId={terminalTarget?.pluginId ?? ""} canManage={canManageServers} onClose={() => setTerminalTarget(null)} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -674,15 +694,19 @@ interface ServerCardProps {
|
||||
deleteDisabledReason: string;
|
||||
onOpen: () => void;
|
||||
onEdit: () => void;
|
||||
onOpenLogs: () => void;
|
||||
onOpenTerminal: () => void;
|
||||
onQuickAction: (action: ServerQuickRuntimeAction) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
function ServerCard({ card, metricsPending, metricsUnavailable, canManage, deleteDisabledReason, onOpen, onEdit, onQuickAction, onDelete }: ServerCardProps) {
|
||||
function ServerCard({ card, metricsPending, metricsUnavailable, canManage, deleteDisabledReason, onOpen, onEdit, onOpenLogs, onOpenTerminal, onQuickAction, onDelete }: ServerCardProps) {
|
||||
const { instance, metrics, pendingJobs, failedJobs = 0 } = card;
|
||||
const online = serverIsOnline(instance.state);
|
||||
const canDelete = deleteDisabledReason === "";
|
||||
const canOpenActions = canManage || canDelete;
|
||||
const metricsWaiting = metrics?.source === "run-metrics-pending";
|
||||
const metricsStale = isMetricsStale(metrics);
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuPanelRef = useRef<HTMLDivElement>(null);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
@@ -793,6 +817,8 @@ function ServerCard({ card, metricsPending, metricsUnavailable, canManage, delet
|
||||
<UsageMeter label="磁盘" percent={metrics?.diskPercent} />
|
||||
</div>
|
||||
{metricsUnavailable && <span className="server-card-warning"><AlertTriangle size={13} />指标不可用</span>}
|
||||
{!metricsUnavailable && metricsWaiting && <span className="server-card-warning"><AlertTriangle size={13} />等待指标上报</span>}
|
||||
{!metricsUnavailable && metricsStale && <span className="server-card-warning"><AlertTriangle size={13} />指标过期</span>}
|
||||
{failedJobs > 0 && <span className="server-card-warning"><AlertTriangle size={13} />存在失败任务,打开详情恢复</span>}
|
||||
<div className="action-strip" style={{ justifyContent: "space-between" }}>
|
||||
<button type="button" className="icon-command" onClick={onOpen}>
|
||||
@@ -800,6 +826,8 @@ function ServerCard({ card, metricsPending, metricsUnavailable, canManage, delet
|
||||
<span>详情</span>
|
||||
</button>
|
||||
<button type="button" className="icon-command" disabled={!canManage || instance.state === "running" || instance.state === "installing"} title={instance.state === "running" || instance.state === "installing" ? "请先停止服务器再编辑部署" : "编辑部署"} onClick={onEdit}><span>编辑部署</span></button>
|
||||
<button type="button" className="icon-command" onClick={onOpenLogs}><ScrollText size={14} /><span>实时日志</span></button>
|
||||
<button type="button" className="icon-command" disabled={!canManage} title={canManage ? "管理终端" : "当前账号没有运行操作权限"} onClick={onOpenTerminal}><Terminal size={14} /><span>管理终端</span></button>
|
||||
<button ref={menuButtonRef} type="button" className="icon-command" disabled={!canOpenActions} title={canOpenActions ? "运行操作" : "当前账号没有运行操作权限"} aria-haspopup="menu" aria-expanded={menuOpen} onClick={toggleMenu}>
|
||||
<span>运行操作</span>
|
||||
</button>
|
||||
@@ -979,6 +1007,12 @@ function formatStat(value: number | undefined, pending: boolean, format: (value:
|
||||
return pending ? "…" : "--";
|
||||
}
|
||||
|
||||
function isMetricsStale(metrics?: ServerMetricsResponse): boolean {
|
||||
if (!metrics || metrics.source === "run-metrics-pending") return false;
|
||||
const collectedAt = new Date(metrics.collectedAt).getTime();
|
||||
return Number.isFinite(collectedAt) && Date.now() - collectedAt > serverMetricFreshMs;
|
||||
}
|
||||
|
||||
export function stateLabel(state: ServerInstanceResponse["state"]): string {
|
||||
switch (state) {
|
||||
case "installing":
|
||||
|
||||
Reference in New Issue
Block a user