feat(web): rebuild the console around the fused control plane
Render the Hub feature set in the existing Animal Island styling: fleet messages, notification centre, log centre, organisation panel, per-instance device module panels and the settings backup, connection and maintenance surfaces.
This commit is contained in:
@@ -3,39 +3,46 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
import { createFleetApiDataSource } from './fleet-api-data-source.js';
|
||||
|
||||
describe('Fleet API data source', () => {
|
||||
it('loads instances and their allowlisted resource summaries', async () => {
|
||||
it('loads the aggregate fleet overview and its resource summaries', async () => {
|
||||
const fetcher = vi.fn(
|
||||
async (input: RequestInfo | URL) =>
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify(
|
||||
String(input).endsWith('/resources')
|
||||
? {
|
||||
JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
id: 'alpha',
|
||||
name: 'Alpha',
|
||||
origin: 'https://alpha.example/admin',
|
||||
tags: ['lab'],
|
||||
revision: 4,
|
||||
capabilityStatus: 'unknown',
|
||||
freshness: 'unknown',
|
||||
credentialConfigured: false,
|
||||
resources: {
|
||||
cpuPercent: 23.4,
|
||||
memoryPercent: 67.8,
|
||||
maxTemperatureCelsius: 52.6,
|
||||
phoneNumbers: ['13800000000'],
|
||||
hardwareOnline: true,
|
||||
controlOnline: true,
|
||||
simPresent: true,
|
||||
carrier: 'China Mobile',
|
||||
cellularRegistration: 'registered_home',
|
||||
accessTechnology: 'LTE',
|
||||
cellularOnline: true,
|
||||
signalPercent: 72,
|
||||
uptimeSeconds: 93784.9,
|
||||
version: '1.9.4',
|
||||
}
|
||||
: {
|
||||
items: [
|
||||
{
|
||||
id: 'alpha',
|
||||
name: 'Alpha',
|
||||
origin: 'https://alpha.example/admin',
|
||||
tags: ['lab'],
|
||||
revision: 4,
|
||||
capabilityStatus: 'unknown',
|
||||
freshness: 'unknown',
|
||||
credentialConfigured: false,
|
||||
},
|
||||
],
|
||||
page: { page: 1, pageSize: 20, total: 1 },
|
||||
},
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { 'content-type': 'application/json' } },
|
||||
),
|
||||
);
|
||||
|
||||
const snapshot = await createFleetApiDataSource(fetcher as typeof fetch).load();
|
||||
|
||||
expect(snapshot.instances).toEqual([
|
||||
{
|
||||
id: 'alpha',
|
||||
@@ -50,63 +57,57 @@ describe('Fleet API data source', () => {
|
||||
memoryPercent: 67.8,
|
||||
maxTemperatureCelsius: 52.6,
|
||||
phoneNumbers: ['13800000000'],
|
||||
hardwareOnline: true,
|
||||
controlOnline: true,
|
||||
simPresent: true,
|
||||
carrier: 'China Mobile',
|
||||
cellularRegistration: 'registered_home',
|
||||
accessTechnology: 'LTE',
|
||||
cellularOnline: true,
|
||||
signalPercent: 72,
|
||||
uptimeSeconds: 93784,
|
||||
});
|
||||
expect(snapshot.statuses.get('alpha')?.summary?.version).toBe('1.9.4');
|
||||
expect(fetcher).toHaveBeenCalledTimes(1);
|
||||
expect(fetcher).toHaveBeenCalledWith(
|
||||
'/api/v1/instances',
|
||||
expect.objectContaining({ credentials: 'same-origin' }),
|
||||
);
|
||||
expect(fetcher).toHaveBeenCalledWith(
|
||||
'/api/v1/instances/alpha/resources',
|
||||
'/api/v1/fleet/overview',
|
||||
expect.objectContaining({ credentials: 'same-origin' }),
|
||||
);
|
||||
});
|
||||
|
||||
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' } },
|
||||
);
|
||||
});
|
||||
it('emits skeleton and complete snapshots from the aggregate overview', async () => {
|
||||
const fetcher = vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
id: 'alpha',
|
||||
name: 'Alpha',
|
||||
origin: 'https://alpha.example/admin',
|
||||
tags: [],
|
||||
revision: 1,
|
||||
capabilityStatus: 'unknown',
|
||||
freshness: 'unknown',
|
||||
credentialConfigured: false,
|
||||
resources: { cpuPercent: 11, memoryPercent: 22 },
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ 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));
|
||||
|
||||
await createFleetApiDataSource(fetcher as typeof fetch).load(undefined, (snapshot) => {
|
||||
partials.push({
|
||||
resources: snapshot.statuses.get('alpha')?.summary?.resources,
|
||||
freshness: snapshot.statuses.get('alpha')?.summary?.freshness,
|
||||
});
|
||||
});
|
||||
|
||||
expect(partials.length).toBe(2);
|
||||
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',
|
||||
@@ -120,3 +121,148 @@ describe('Fleet API data source', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function overviewWith(connection: unknown) {
|
||||
return vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
id: 'alpha',
|
||||
name: 'Alpha',
|
||||
origin: 'https://alpha.example/admin',
|
||||
tags: [],
|
||||
revision: 1,
|
||||
capabilityStatus: 'unknown',
|
||||
freshness: 'unknown',
|
||||
credentialConfigured: false,
|
||||
connection,
|
||||
resources: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { 'content-type': 'application/json' } },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe('Fleet resource bounds', () => {
|
||||
const overviewWithResources = (resources: Record<string, unknown>) =>
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
id: 'alpha',
|
||||
name: 'Alpha',
|
||||
origin: 'https://alpha.example/admin',
|
||||
tags: [],
|
||||
revision: 1,
|
||||
capabilityStatus: 'unknown',
|
||||
freshness: 'unknown',
|
||||
credentialConfigured: false,
|
||||
resources,
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200, headers: { 'content-type': 'application/json' } },
|
||||
),
|
||||
) as unknown as typeof fetch;
|
||||
|
||||
it('drops a negative or non-numeric uptime instead of rendering a bogus clock', async () => {
|
||||
const negative = await createFleetApiDataSource(
|
||||
overviewWithResources({ uptimeSeconds: -1 }),
|
||||
).load();
|
||||
const text = await createFleetApiDataSource(
|
||||
overviewWithResources({ uptimeSeconds: '93784' }),
|
||||
).load();
|
||||
expect(negative.statuses.get('alpha')?.summary?.resources?.uptimeSeconds).toBeUndefined();
|
||||
expect(text.statuses.get('alpha')?.summary?.resources?.uptimeSeconds).toBeUndefined();
|
||||
});
|
||||
|
||||
it('keeps a zero uptime, which means the device just booted', async () => {
|
||||
const snapshot = await createFleetApiDataSource(
|
||||
overviewWithResources({ uptimeSeconds: 0 }),
|
||||
).load();
|
||||
expect(snapshot.statuses.get('alpha')?.summary?.resources?.uptimeSeconds).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fleet heartbeat connection state', () => {
|
||||
it('carries a live heartbeat through to the row status', async () => {
|
||||
const snapshot = await createFleetApiDataSource(
|
||||
overviewWith({
|
||||
probed: true,
|
||||
reachable: true,
|
||||
authenticated: true,
|
||||
checkedAt: '2026-09-05T00:00:00.000Z',
|
||||
}) as typeof fetch,
|
||||
).load();
|
||||
expect(snapshot.statuses.get('alpha')).toMatchObject({
|
||||
probed: true,
|
||||
reachable: true,
|
||||
authenticated: true,
|
||||
checkedAt: '2026-09-05T00:00:00.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('marks a device the heartbeat lost as offline but still checked', async () => {
|
||||
const snapshot = await createFleetApiDataSource(
|
||||
overviewWith({
|
||||
probed: true,
|
||||
reachable: false,
|
||||
authenticated: false,
|
||||
checkedAt: '2026-09-05T00:00:00.000Z',
|
||||
}) as typeof fetch,
|
||||
).load();
|
||||
expect(snapshot.statuses.get('alpha')).toMatchObject({
|
||||
probed: true,
|
||||
reachable: false,
|
||||
authenticated: false,
|
||||
checkedAt: '2026-09-05T00:00:00.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('reports a device that has never been probed as unknown', async () => {
|
||||
const snapshot = await createFleetApiDataSource(
|
||||
overviewWith({
|
||||
probed: false,
|
||||
reachable: false,
|
||||
authenticated: false,
|
||||
checkedAt: null,
|
||||
}) as typeof fetch,
|
||||
).load();
|
||||
expect(snapshot.statuses.get('alpha')).toMatchObject({
|
||||
probed: false,
|
||||
checkedAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the optimistic shape for a server without the heartbeat journal', async () => {
|
||||
const snapshot = await createFleetApiDataSource(overviewWith(undefined) as typeof fetch).load();
|
||||
expect(snapshot.statuses.get('alpha')).toMatchObject({
|
||||
reachable: true,
|
||||
authenticated: true,
|
||||
});
|
||||
expect(snapshot.statuses.get('alpha')?.probed).toBeUndefined();
|
||||
expect(snapshot.statuses.get('alpha')?.checkedAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it('applies the heartbeat state to the skeleton snapshot too', async () => {
|
||||
const partials: unknown[] = [];
|
||||
await createFleetApiDataSource(
|
||||
overviewWith({
|
||||
probed: true,
|
||||
reachable: false,
|
||||
authenticated: false,
|
||||
checkedAt: null,
|
||||
}) as typeof fetch,
|
||||
).load(undefined, (snapshot) => {
|
||||
partials.push(snapshot.statuses.get('alpha'));
|
||||
});
|
||||
expect(partials[0]).toMatchObject({ probed: true, reachable: false });
|
||||
expect(partials.at(-1)).toMatchObject({ probed: true, reachable: false });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user