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
@@ -22,7 +22,7 @@ describe('Fleet API data source', () => {
name: 'Alpha',
origin: 'https://alpha.example/admin',
tags: ['lab'],
revision: 1,
revision: 4,
capabilityStatus: 'unknown',
freshness: 'unknown',
credentialConfigured: false,
@@ -36,7 +36,7 @@ describe('Fleet API data source', () => {
);
const snapshot = await createFleetApiDataSource(fetcher as typeof fetch).load();
expect(snapshot.instances).toEqual([
{ id: 'alpha', name: 'Alpha', url: 'https://alpha.example/admin', tags: ['lab'] },
{ id: 'alpha', name: 'Alpha', url: 'https://alpha.example/admin', tags: ['lab'], revision: 4 },
]);
expect(snapshot.statuses.get('alpha')?.summary?.resources).toEqual({
cpuPercent: 23.4,
@@ -54,6 +54,54 @@ describe('Fleet API data source', () => {
);
});
it('emits a partial fleet snapshot before resource enrichment completes', async () => {
let resolveResources!: (value: Response) => void;
const resourcesPromise = new Promise<Response>((resolve) => {
resolveResources = resolve;
});
const fetcher = vi.fn(async (input: RequestInfo | URL) => {
if (String(input).endsWith('/resources')) return resourcesPromise;
return new Response(
JSON.stringify({
items: [
{
id: 'alpha',
name: 'Alpha',
origin: 'https://alpha.example/admin',
tags: [],
revision: 1,
capabilityStatus: 'unknown',
freshness: 'unknown',
credentialConfigured: false,
},
],
page: { page: 1, pageSize: 20, total: 1 },
}),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
const partials: unknown[] = [];
const pending = createFleetApiDataSource(fetcher as typeof fetch).load(undefined, (snapshot) => {
partials.push({
resources: snapshot.statuses.get('alpha')?.summary?.resources,
freshness: snapshot.statuses.get('alpha')?.summary?.freshness,
});
});
await vi.waitFor(() => expect(partials.length).toBe(1));
expect(partials[0]).toEqual({ resources: undefined, freshness: 'unknown' });
resolveResources(
new Response(JSON.stringify({ cpuPercent: 11, memoryPercent: 22 }), {
status: 200,
headers: { 'content-type': 'application/json' },
}),
);
await pending;
expect(partials.at(-1)).toEqual({
resources: { cpuPercent: 11, memoryPercent: 22 },
freshness: 'fresh',
});
});
it('rejects legacy or malformed envelopes instead of rendering a false empty fleet', async () => {
const fetcher = vi.fn(async () => new Response(JSON.stringify({ data: [] }), { status: 200 }));
await expect(createFleetApiDataSource(fetcher as typeof fetch).load()).rejects.toThrow(