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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user