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
+51 -4
View File
@@ -78,6 +78,8 @@ export interface InstanceContext {
status: 'online' | 'offline' | 'auth-required' | 'degraded' | 'unknown';
authentication: 'authenticated' | 'auth-required' | 'unknown';
freshness: 'fresh' | 'stale' | 'expired' | 'unknown';
/** Control-plane config revision for prepare/execute CAS when known. */
revision?: number;
resources?: Readonly<{
cpuPercent?: number;
memoryPercent?: number;
@@ -283,6 +285,7 @@ function Page({
instance={instance}
{...(overviewDataSource ? { dataSource: overviewDataSource } : {})}
refreshSignal={detailRefreshSignal}
{...(instance.revision !== undefined ? { revision: instance.revision } : {})}
/>
),
}
@@ -450,10 +453,51 @@ export function AppShell({
const controller = new AbortController();
let active = true;
setInstanceLoading(true);
const fleetLoad = (fleetDataSource ?? defaultFleetDataSource).load(controller.signal).then(
(fleet) => ({ ok: true as const, fleet }),
() => ({ ok: false as const }),
);
const fleetLoad = (fleetDataSource ?? defaultFleetDataSource)
.load(controller.signal, (partial) => {
if (!active) return;
const fleetOwner = partial.instances.find((candidate) => candidate.id === routeInstanceId);
const status = partial.statuses.get(routeInstanceId);
if (!fleetOwner && !status) return;
setLoadedInstance((current) => {
if (!current || current.id !== routeInstanceId) return current;
const routeStatus = !status
? current.status
: !status.reachable
? 'offline'
: status.authenticated === false
? 'auth-required'
: 'online';
const freshness = status?.summary?.freshness;
const nextRevision = fleetOwner?.revision ?? current.revision;
return {
...current,
origin: fleetOwner?.url ?? current.origin,
status: routeStatus,
authentication:
status?.authenticated === true
? 'authenticated'
: status?.authenticated === false
? 'auth-required'
: current.authentication,
freshness:
freshness === 'fresh' || freshness === 'stale' || freshness === 'expired'
? freshness
: current.freshness,
...(nextRevision === undefined ? {} : { revision: nextRevision }),
...(status?.summary?.resources ? { resources: status.summary.resources } : {}),
resourceSummaryState: status?.summary?.resources
? 'ready'
: current.resourceSummaryState === 'ready'
? 'ready'
: 'loading',
};
});
})
.then(
(fleet) => ({ ok: true as const, fleet }),
() => ({ ok: false as const }),
);
void resolvedInstanceDataSource.get(routeInstanceId).then(
(owner) => {
if (!active) return;
@@ -469,6 +513,7 @@ export function AppShell({
status: 'unknown',
authentication: 'unknown',
freshness: 'unknown',
...(owner.revision === undefined ? {} : { revision: owner.revision }),
resourceSummaryState: 'loading',
});
setInstanceLoading(false);
@@ -494,6 +539,7 @@ export function AppShell({
? 'auth-required'
: 'online';
const freshness = status?.summary?.freshness;
const nextRevision = fleetOwner?.revision ?? owner.revision;
setLoadedInstance({
id: owner.id,
name: owner.name,
@@ -509,6 +555,7 @@ export function AppShell({
freshness === 'fresh' || freshness === 'stale' || freshness === 'expired'
? freshness
: 'unknown',
...(nextRevision === undefined ? {} : { revision: nextRevision }),
...(status?.summary?.resources ? { resources: status.summary.resources } : {}),
resourceSummaryState: status ? 'ready' : 'unavailable',
});