feat(web): add runnable fleet console foundation

This commit is contained in:
chick
2026-07-17 16:02:15 +08:00
parent b4ae28c8f0
commit 4093634e13
16 changed files with 2730 additions and 7 deletions
@@ -0,0 +1,126 @@
import { describe, expect, it } from 'vitest';
import {
buildFleetTableViewModel,
fleetStatusKind,
type FleetInstance,
type FleetStatus,
} from './fleet-table-view-model.js';
const instances: readonly FleetInstance[] = [
{
id: 'charlie',
name: 'Charlie',
url: 'https://charlie.example',
description: 'Warehouse router',
tags: ['west'],
},
{ id: 'alpha', name: 'Alpha', url: 'https://alpha.example', tags: ['east'] },
{ id: 'bravo', name: 'Bravo', url: 'https://bravo.example' },
{ id: 'delta', name: '', url: 'https://delta.example' },
];
const statuses: ReadonlyMap<string, FleetStatus> = new Map([
[
'charlie',
{ reachable: true, authenticated: true, latencyMs: 55, summary: { carrier: 'Acme' } },
],
['alpha', { reachable: false, latencyMs: 8 }],
['bravo', { reachable: true, authenticated: false, latencyMs: 20 }],
]);
describe('fleet table view model', () => {
it('classifies missing, unreachable, authentication-required, and online status', () => {
expect(fleetStatusKind(undefined)).toBe('unknown');
expect(fleetStatusKind({ reachable: false })).toBe('offline');
expect(fleetStatusKind({ reachable: true, authenticated: false })).toBe('auth');
expect(fleetStatusKind({ reachable: true, authenticated: true })).toBe('online');
});
it('builds stable rows and defaults to ascending display-name order', () => {
const model = buildFleetTableViewModel(instances, statuses);
expect(model.rows.map((row) => [row.id, row.displayName, row.statusKind])).toEqual([
['alpha', 'Alpha', 'offline'],
['bravo', 'Bravo', 'auth'],
['charlie', 'Charlie', 'online'],
['delta', 'delta', 'unknown'],
]);
expect(model.totalCount).toBe(4);
expect(model.visibleCount).toBe(4);
expect(model.emptyReason).toBeNull();
});
it('searches user-visible instance and status fields case-insensitively', () => {
expect(
buildFleetTableViewModel(instances, statuses, { query: ' ACME ' }).rows.map((row) => row.id),
).toEqual(['charlie']);
expect(
buildFleetTableViewModel(instances, statuses, { query: 'WEST' }).rows.map((row) => row.id),
).toEqual(['charlie']);
});
it('filters by status and reports a useful empty reason', () => {
const filtered = buildFleetTableViewModel(instances, statuses, { filter: 'online' });
expect(filtered.rows.map((row) => row.id)).toEqual(['charlie']);
expect(buildFleetTableViewModel([], new Map()).emptyReason).toBe('config');
expect(
buildFleetTableViewModel(instances, statuses, { query: 'not present' }).emptyReason,
).toBe('search');
expect(
buildFleetTableViewModel(instances, statuses, { filter: 'online', query: 'Alpha' })
.emptyReason,
).toBe('search');
expect(
buildFleetTableViewModel([instances[1]!], statuses, { filter: 'online' }).emptyReason,
).toBe('filter');
expect(
buildFleetTableViewModel(instances, statuses, { filter: 'online', query: ' ' }).emptyReason,
).toBeNull();
expect(
buildFleetTableViewModel(instances, statuses, { filter: 'offline', query: 'Bravo' })
.emptyReason,
).toBe('search');
});
it('sorts latency with missing latency last and status by operational priority', () => {
expect(
buildFleetTableViewModel(instances, statuses, {
sort: { column: 'latency', direction: 'asc' },
}).rows.map((row) => row.id),
).toEqual(['alpha', 'bravo', 'charlie', 'delta']);
expect(
buildFleetTableViewModel(instances, statuses, {
sort: { column: 'latency', direction: 'desc' },
}).rows.map((row) => row.id),
).toEqual(['charlie', 'bravo', 'alpha', 'delta']);
expect(
buildFleetTableViewModel(instances, statuses, {
sort: { column: 'status', direction: 'asc' },
}).rows.map((row) => row.id),
).toEqual(['charlie', 'bravo', 'alpha', 'delta']);
});
it('normalizes selection to known instances and exposes visible selection state', () => {
const model = buildFleetTableViewModel(instances, statuses, {
filter: 'online',
selectedIds: new Set(['charlie', 'alpha', 'missing']),
});
expect(model.rows[0]?.selected).toBe(true);
expect(model.selectedIds).toEqual(['alpha', 'charlie']);
expect(model.visibleSelection).toEqual({
checked: true,
indeterminate: false,
selectedCount: 1,
rowCount: 1,
});
});
it('does not mutate input arrays while sorting', () => {
const originalOrder = instances.map((instance) => instance.id);
buildFleetTableViewModel(instances, statuses, { sort: { column: 'name', direction: 'desc' } });
expect(instances.map((instance) => instance.id)).toEqual(originalOrder);
});
});