Files
multi-simadmin/apps/web/src/fleet/fleet-api-data-source.ts
T
chick 9ea8021cce 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.
2026-07-21 22:56:55 +08:00

143 lines
4.7 KiB
TypeScript

import type { FleetDataSource, FleetSnapshot } from './fleet-page.js';
import type { FleetInstance, FleetStatus } from './fleet-table-view-model.js';
const integer = (value: unknown): value is number =>
typeof value === 'number' && Number.isSafeInteger(value);
const finite = (value: unknown): value is number =>
typeof value === 'number' && Number.isFinite(value);
const string = (value: unknown): value is string => typeof value === 'string' && value.length > 0;
const record = (value: unknown): Record<string, unknown> | undefined =>
typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: undefined;
function parseInstance(value: unknown): FleetInstance | undefined {
const item = record(value);
if (
!item ||
!string(item.id) ||
!string(item.name) ||
!string(item.origin) ||
!Array.isArray(item.tags) ||
!item.tags.every((tag) => typeof tag === 'string')
)
return undefined;
const revision =
integer(item.revision) && item.revision > 0 ? (item.revision as number) : undefined;
return {
id: item.id,
name: item.name,
url: item.origin,
tags: item.tags as string[],
...(revision === undefined ? {} : { revision }),
};
}
function parseResources(value: unknown): NonNullable<FleetStatus['summary']> {
const body = record(value) ?? {};
const resources: {
cpuPercent?: number;
memoryPercent?: number;
maxTemperatureCelsius?: number;
phoneNumbers?: string[];
} = {};
if (finite(body.cpuPercent)) resources.cpuPercent = body.cpuPercent;
if (finite(body.memoryPercent)) resources.memoryPercent = body.memoryPercent;
if (finite(body.maxTemperatureCelsius))
resources.maxTemperatureCelsius = body.maxTemperatureCelsius;
if (
Array.isArray(body.phoneNumbers) &&
body.phoneNumbers.every((item) => typeof item === 'string' && item.length > 0)
)
resources.phoneNumbers = body.phoneNumbers as string[];
return {
freshness: 'fresh',
...(Object.keys(resources).length > 0 ? { resources } : {}),
};
}
async function readJson(response: Response): Promise<unknown> {
try {
return await response.json();
} catch {
return undefined;
}
}
function skeletonStatuses(instances: readonly FleetInstance[]): Map<string, FleetStatus> {
return new Map(
instances.map((instance) => [
instance.id,
{
reachable: true,
authenticated: true,
summary: { freshness: 'unknown' },
},
]),
);
}
export function createFleetApiDataSource(fetcher: typeof fetch = fetch): FleetDataSource {
return {
async load(signal, onPartial) {
const listResponse = await fetcher('/api/v1/instances', {
method: 'GET',
credentials: 'same-origin',
headers: { accept: 'application/json' },
...(signal ? { signal } : {}),
});
const listBody = record(await readJson(listResponse));
if (
!listResponse.ok ||
!listBody ||
!Array.isArray(listBody.items) ||
!record(listBody.page)
)
throw new Error('Fleet response is invalid.');
const instances = listBody.items.map(parseInstance);
if (instances.some((item) => !item)) throw new Error('Fleet response is invalid.');
const readyInstances = instances as FleetInstance[];
const partial: FleetSnapshot = {
instances: readyInstances,
statuses: skeletonStatuses(readyInstances),
};
onPartial?.(partial);
const statuses = new Map(
await Promise.all(
readyInstances.map(async (instance) => {
try {
const response = await fetcher(
`/api/v1/instances/${encodeURIComponent(instance.id)}/resources`,
{
method: 'GET',
credentials: 'same-origin',
headers: { accept: 'application/json' },
...(signal ? { signal } : {}),
},
);
if (!response.ok) throw new Error('resource unavailable');
const status: FleetStatus = {
reachable: true,
authenticated: true,
summary: parseResources(await readJson(response)),
};
return [instance.id, status] as const;
} catch {
const status: FleetStatus = {
reachable: true,
authenticated: true,
summary: { freshness: 'unknown' },
};
return [instance.id, status] as const;
}
}),
),
);
const complete: FleetSnapshot = { instances: readyInstances, statuses };
onPartial?.(complete);
return complete;
},
};
}