功能修改
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { CakeSlice, Candy, Search, Sparkles } from "lucide-react";
|
||||
import { AlertTriangle, CakeSlice, Candy, Search, Sparkles } from "lucide-react";
|
||||
import { type CSSProperties, type ChangeEvent, type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
@@ -14,17 +14,17 @@ import {
|
||||
runtimeUpdateStages,
|
||||
useRuntimeTaskController
|
||||
} from "../components/RuntimeTaskProgress";
|
||||
import { UsageMeter } from "../components/OperationControls";
|
||||
import { ManagementDialog, UsageMeter } from "../components/OperationControls";
|
||||
import { EmptyState, ErrorState, LoadingState, ResultBadge } from "../components/StateViews";
|
||||
import type { PageComponentProps } from "../contracts/page";
|
||||
import {
|
||||
defaultServerCreateForm,
|
||||
endpointLabel,
|
||||
pendingJobsForServer,
|
||||
pluginLabel,
|
||||
runtimeBindingFields,
|
||||
type ServerCreateFormState
|
||||
} from "../contracts/serverManagement";
|
||||
import { summarizeServerOperations } from "../contracts/operationsConsole";
|
||||
import { filterServerCards, serverIsOnline, type ServerCardView, type ServerStatusFilter } from "../contracts/workspace";
|
||||
import {
|
||||
clientManagerBuildRequest,
|
||||
@@ -56,6 +56,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
const [jobs, setJobs] = useState<JobResponse[]>([]);
|
||||
const [metrics, setMetrics] = useState<Map<string, ServerMetricsResponse>>(new Map());
|
||||
const [metricsPending, setMetricsPending] = useState(true);
|
||||
const [metricsError, setMetricsError] = useState("");
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<ServerStatusFilter>("all");
|
||||
const [form, setForm] = useState<ServerCreateFormState>(() => defaultServerCreateForm([], []));
|
||||
@@ -63,7 +64,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
const runtimeTask = useRuntimeTaskController();
|
||||
const [runtimeTaskActions, setRuntimeTaskActions] = useState<RuntimeTaskDialogAction[]>([]);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const refreshList = useCallback(async () => {
|
||||
setListState("loading");
|
||||
try {
|
||||
const [pluginResponse, endpointResponse, instanceResponse, jobResponse] = await Promise.all([
|
||||
@@ -97,33 +98,46 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
setListState("error");
|
||||
setListError(error instanceof Error ? error.message : "加载失败");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshMetrics = useCallback(async () => {
|
||||
setMetricsPending(true);
|
||||
try {
|
||||
const metricsResponse = await platformApiClient.listServerMetrics();
|
||||
setMetrics(new Map(metricsResponse.items.map((item) => [item.serverInstanceId, item])));
|
||||
} catch {
|
||||
setMetricsError("");
|
||||
} catch (error) {
|
||||
setMetrics(new Map());
|
||||
setMetricsError(error instanceof Error ? error.message : "服务器指标加载失败");
|
||||
} finally {
|
||||
setMetricsPending(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
await Promise.all([refreshList(), refreshMetrics()]);
|
||||
}, [refreshList, refreshMetrics]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const cards = useMemo<ServerCardView[]>(
|
||||
() =>
|
||||
instances.map((instance) => ({
|
||||
instance,
|
||||
metrics: metrics.get(instance.id),
|
||||
pendingJobs: pendingJobsForServer(jobs, instance.id).length
|
||||
summarizeServerOperations(instances, metrics, jobs).map((summary) => ({
|
||||
instance: summary.instance,
|
||||
metrics: summary.metrics,
|
||||
pendingJobs: summary.activeJobs,
|
||||
activeJobs: summary.activeJobs,
|
||||
failedJobs: summary.failedJobs,
|
||||
latestJob: summary.latestJob
|
||||
})),
|
||||
[instances, jobs, metrics]
|
||||
);
|
||||
|
||||
const visibleCards = useMemo(() => filterServerCards(cards, keyword, statusFilter), [cards, keyword, statusFilter]);
|
||||
const createPending = operations.isPending("platform", "创建服务器");
|
||||
const canManageServers = session.capabilities.includes("servers.manage");
|
||||
const selectedCreatePlugin = plugins.find((plugin) => plugin.id === form.pluginId);
|
||||
const createBindingFields = runtimeBindingFields(selectedCreatePlugin, form.profileKey);
|
||||
|
||||
@@ -356,13 +370,23 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
<Sparkles size={16} />
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
<button type="button" className="icon-command" title="创建服务器" onClick={() => setShowCreate((current) => !current)}>
|
||||
<button type="button" className="icon-command" title={canManageServers ? "创建服务器" : "当前账号没有创建权限"} disabled={!canManageServers} onClick={() => setShowCreate((current) => !current)}>
|
||||
<Candy size={16} />
|
||||
<span>创建服务器</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{metricsError && (
|
||||
<div className="operations-inline-warning" role="status">
|
||||
<AlertTriangle size={14} />
|
||||
<span>服务器指标不可用:{metricsError}</span>
|
||||
<button type="button" className="inline-link-command" onClick={() => void refreshMetrics()}>
|
||||
重试指标
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{latestCreate && (
|
||||
<div className="inline-result-strip" aria-live="polite">
|
||||
<ResultBadge
|
||||
@@ -378,11 +402,14 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCreate && (
|
||||
<form className="provider-form" onSubmit={(event) => void handleCreate(event)} aria-label="创建服务器">
|
||||
<div className="form-header">
|
||||
<h2>创建服务器</h2>
|
||||
</div>
|
||||
<ManagementDialog
|
||||
open={showCreate && canManageServers}
|
||||
title="创建服务器"
|
||||
description="选择插件声明的运行配置和安全逻辑绑定。提交后以 Platform 返回的实例与安装任务为准。"
|
||||
wide
|
||||
onClose={() => { if (!createPending) setShowCreate(false); }}
|
||||
>
|
||||
<form className="provider-form dialog-form" onSubmit={(event) => void handleCreate(event)} aria-label="创建服务器">
|
||||
<div className="form-grid">
|
||||
<label>
|
||||
实例 ID
|
||||
@@ -436,12 +463,15 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button type="submit" className="primary-command" disabled={createPending || !form.profileKey} title="创建服务器">
|
||||
<Sparkles size={16} />
|
||||
<span>{createPending ? "创建中…" : "创建并安装"}</span>
|
||||
</button>
|
||||
<div className="confirm-actions">
|
||||
<button type="button" disabled={createPending} onClick={() => setShowCreate(false)}>取消</button>
|
||||
<button type="submit" className="confirm-primary" disabled={createPending || !form.profileKey} title="创建服务器">
|
||||
<Sparkles size={16} />
|
||||
<span>{createPending ? "创建中…" : "创建并安装"}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</ManagementDialog>
|
||||
|
||||
<div className="server-toolbar" role="search">
|
||||
<Search size={16} aria-hidden="true" />
|
||||
@@ -465,7 +495,7 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
</div>
|
||||
|
||||
{listState === "loading" && <LoadingState label="正在加载服务器列表…" />}
|
||||
{listState === "error" && <ErrorState title="服务器列表加载失败" reason={listError} diagnosticId="server-list" onRetry={() => void refresh()} />}
|
||||
{listState === "error" && <ErrorState title="服务器列表加载失败" reason={listError} diagnosticId="server-list" onRetry={() => void refreshList()} />}
|
||||
{listState === "ready" && cards.length === 0 && (
|
||||
<EmptyState
|
||||
icon={<CakeSlice size={26} />}
|
||||
@@ -493,6 +523,8 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
||||
key={card.instance.id}
|
||||
card={card}
|
||||
metricsPending={metricsPending}
|
||||
metricsUnavailable={Boolean(metricsError)}
|
||||
canManage={canManageServers}
|
||||
onOpen={() => onNavigate("serverDetail", { serverId: card.instance.id })}
|
||||
onQuickAction={(action) => void handleQuickRuntimeAction(card.instance, action)}
|
||||
/>
|
||||
@@ -517,12 +549,14 @@ type ServerQuickRuntimeAction =
|
||||
interface ServerCardProps {
|
||||
card: ServerCardView;
|
||||
metricsPending: boolean;
|
||||
metricsUnavailable: boolean;
|
||||
canManage: boolean;
|
||||
onOpen: () => void;
|
||||
onQuickAction: (action: ServerQuickRuntimeAction) => void;
|
||||
}
|
||||
|
||||
function ServerCard({ card, metricsPending, onOpen, onQuickAction }: ServerCardProps) {
|
||||
const { instance, metrics, pendingJobs } = card;
|
||||
function ServerCard({ card, metricsPending, metricsUnavailable, canManage, onOpen, onQuickAction }: ServerCardProps) {
|
||||
const { instance, metrics, pendingJobs, failedJobs = 0 } = card;
|
||||
const online = serverIsOnline(instance.state);
|
||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuPanelRef = useRef<HTMLDivElement>(null);
|
||||
@@ -622,7 +656,7 @@ function ServerCard({ card, metricsPending, onOpen, onQuickAction }: ServerCardP
|
||||
</span>
|
||||
<span className="server-card-stat">
|
||||
<span>任务</span>
|
||||
<strong>{pendingJobs > 0 ? `${pendingJobs} 进行中` : online ? "空闲" : "--"}</strong>
|
||||
<strong>{failedJobs > 0 ? `${failedJobs} 失败` : pendingJobs > 0 ? `${pendingJobs} 进行中` : online ? "空闲" : "--"}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div className="server-card-meters">
|
||||
@@ -630,12 +664,14 @@ function ServerCard({ card, metricsPending, onOpen, onQuickAction }: ServerCardP
|
||||
<UsageMeter label="内存" percent={metrics?.memoryPercent} />
|
||||
<UsageMeter label="磁盘" percent={metrics?.diskPercent} />
|
||||
</div>
|
||||
{metricsUnavailable && <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}>
|
||||
<Sparkles size={14} />
|
||||
<span>详情</span>
|
||||
</button>
|
||||
<button ref={menuButtonRef} type="button" className="icon-command" aria-haspopup="menu" aria-expanded={menuOpen} onClick={toggleMenu}>
|
||||
<button ref={menuButtonRef} type="button" className="icon-command" disabled={!canManage} title={canManage ? "运行操作" : "当前账号没有运行操作权限"} aria-haspopup="menu" aria-expanded={menuOpen} onClick={toggleMenu}>
|
||||
<span>运行操作</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user