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.
143 lines
4.7 KiB
TypeScript
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;
|
|
},
|
|
};
|
|
}
|