feat(fleet): card-only UI with progressive load and restart ops

Drop the redundant advanced table so fleet stays resource-first cards.
Keep multi-select batch service/system restart via prepare→execute, card
restarts, overview system ops, and progressive fleet loading.
This commit is contained in:
chick
2026-07-21 22:56:55 +08:00
parent ebed4c1969
commit 9ea8021cce
20 changed files with 1207 additions and 346 deletions
+148 -18
View File
@@ -1,6 +1,11 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { InstanceContext } from '../app-shell.js';
import {
createOperationClient,
type OperationClient,
} from '../operations/operation-client.js';
import { safeUiError } from '../ui/locale.js';
export type OverviewFieldValue = string | number | boolean | null;
export type OverviewSection = Readonly<Record<string, OverviewFieldValue>>;
@@ -25,6 +30,8 @@ export interface OverviewSystemPageProps {
readonly dataSource?: OverviewDataSource;
/** Change this value when an external owner has requested a refresh. */
readonly refreshSignal?: unknown;
readonly operationClient?: OperationClient;
readonly revision?: number;
}
type ReadState =
@@ -166,10 +173,78 @@ export function OverviewSystemPage({
instance,
dataSource,
refreshSignal,
operationClient,
revision,
}: OverviewSystemPageProps) {
const requestOwner = useRef(0);
const [retry, setRetry] = useState(0);
const [state, setState] = useState<ReadState>({ kind: 'idle' });
const [actionState, setActionState] = useState<{
busy: boolean;
message?: string;
error?: string;
}>({ busy: false });
const client = useMemo(() => operationClient ?? createOperationClient(), [operationClient]);
const effectiveRevision = revision ?? instance.revision;
async function runOverviewAction(kind: 'service' | 'system'): Promise<void> {
const targetRevision = effectiveRevision;
if (!targetRevision || targetRevision < 1) {
setActionState({ busy: false, error: '缺少配置版本,请刷新后重试。' });
return;
}
const op =
kind === 'service'
? {
operationId: 'postServiceRestart',
parameterSchemaId: 'simadmin.58e2204.postServiceRestart.parameters.v1',
title: '重启服务',
fields: [] as const,
}
: {
operationId: 'postSystemReboot',
parameterSchemaId: 'simadmin.58e2204.postSystemReboot.parameters.v1',
title: '系统重启',
fields: [{ fieldId: 'delay_seconds', kind: 'number' as const, value: 3 }],
};
if (
typeof window !== 'undefined' &&
!window.confirm(`将执行「${op.title}」。此操作为高风险,确认继续?`)
)
return;
setActionState({ busy: true, message: `正在${op.title}` });
try {
await client.list({ pageSize: 100 });
const prepared = await client.prepare({
operationId: op.operationId,
targets: [{ instanceId: instance.id, revision: targetRevision }],
parameters: {
parameterSchemaId: op.parameterSchemaId,
fields: [...op.fields],
},
});
const confirmed =
typeof window === 'undefined'
? true
: window.confirm(`${prepared.confirmationPrompt}
风险等级 ${prepared.risk}。确认继续?`);
if (!confirmed) {
setActionState({ busy: false, message: '已取消操作。' });
return;
}
const job = await client.execute(prepared.id);
setActionState({
busy: false,
message: job.status === 'succeeded' ? `${op.title}已提交成功。` : `${op.title}结果未知或失败。`,
...(job.status === 'succeeded' ? {} : { error: `${op.title}结果未知或失败。` }),
});
} catch (error) {
setActionState({
busy: false,
error: safeUiError(error, `${op.title}失败,请稍后重试。`),
});
}
}
useEffect(() => {
const request = ++requestOwner.current;
@@ -212,37 +287,95 @@ export function OverviewSystemPage({
return () => controller.abort();
}, [dataSource, instance.authentication, instance.id, refreshSignal, retry]);
const systemOperations = (
<section className="overview-operations" aria-label="系统操作">
<h2></h2>
<p></p>
<div className="fleet-card-actions" role="group" aria-label="重启操作">
<button
type="button"
disabled={actionState.busy || !effectiveRevision}
onClick={() => void runOverviewAction('service')}
>
</button>
<button
type="button"
className="danger-button"
disabled={actionState.busy || !effectiveRevision}
onClick={() => void runOverviewAction('system')}
>
</button>
</div>
{!effectiveRevision ? (
<p role="status"></p>
) : null}
{actionState.error ? (
<p role="alert" className="card-operation-error">
{actionState.error}
</p>
) : null}
{actionState.message ? (
<p role="status" className="card-operation-success">
{actionState.message}
</p>
) : null}
</section>
);
if (instance.authentication === 'auth-required')
return (
<div className="state-panel state-error" role="alert">
<div className="overview-system">
<div className="state-panel state-error" role="alert">
</div>
{systemOperations}
</div>
);
if (!dataSource) return <ResourceSummary instance={instance} />;
if (!dataSource)
return (
<div className="overview-system">
<ResourceSummary instance={instance} />
<p role="status" aria-label="概览加载状态">
使
</p>
{systemOperations}
</div>
);
const retainedSnapshot =
state.kind === 'loading' || state.kind === 'error' ? state.snapshot : undefined;
const snapshot = state.kind === 'ready' ? state.snapshot : retainedSnapshot;
if ((state.kind === 'loading' || state.kind === 'idle') && !retainedSnapshot)
return (
<p role="status" aria-label="概览加载状态">
</p>
<div className="overview-system">
<ResourceSummary instance={instance} />
<p role="status" aria-label="概览加载状态">
</p>
{systemOperations}
</div>
);
if (state.kind === 'error' && !state.snapshot)
return (
<div className="state-panel state-error" role="alert">
<p> {state.message}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
</button>
<div className="overview-system">
<ResourceSummary instance={instance} />
<div className="state-panel state-error" role="alert">
<p> {state.message}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
</button>
</div>
{systemOperations}
</div>
);
const snapshot = state.kind === 'ready' ? state.snapshot : retainedSnapshot;
if (!snapshot) return null;
if (!snapshot) return <div className="overview-system">{systemOperations}</div>;
return (
<div className="overview-system">
@@ -267,10 +400,7 @@ export function OverviewSystemPage({
<StructuredSection key={key} label={label} values={snapshot[key]} />
))}
</div>
<details className="system-operation-note">
<summary></summary>
<p> R3 </p>
</details>
{systemOperations}
</div>
);
}