feat(web): add runnable fleet console foundation
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
export type FleetStatusKind = 'online' | 'auth' | 'offline' | 'unknown';
|
||||
export type FleetFilter = 'all' | FleetStatusKind;
|
||||
export type FleetSortColumn = 'name' | 'status' | 'latency';
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
export type EmptyFleetReason = 'config' | 'search' | 'filter';
|
||||
|
||||
export interface FleetInstance {
|
||||
readonly id: string;
|
||||
readonly name?: string;
|
||||
readonly url: string;
|
||||
readonly description?: string;
|
||||
readonly tags?: readonly string[];
|
||||
}
|
||||
|
||||
export interface FleetStatus {
|
||||
readonly reachable: boolean;
|
||||
readonly authenticated?: boolean;
|
||||
readonly latencyMs?: number;
|
||||
readonly summary?: Readonly<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface FleetTableOptions {
|
||||
readonly query?: string;
|
||||
readonly filter?: FleetFilter;
|
||||
readonly sort?: Readonly<{
|
||||
column: FleetSortColumn;
|
||||
direction: SortDirection;
|
||||
}>;
|
||||
readonly selectedIds?: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
export interface FleetTableRow {
|
||||
readonly id: string;
|
||||
readonly displayName: string;
|
||||
readonly instance: FleetInstance;
|
||||
readonly status?: FleetStatus;
|
||||
readonly statusKind: FleetStatusKind;
|
||||
readonly latencyMs?: number;
|
||||
readonly selected: boolean;
|
||||
}
|
||||
|
||||
export interface VisibleSelection {
|
||||
readonly checked: boolean;
|
||||
readonly indeterminate: boolean;
|
||||
readonly selectedCount: number;
|
||||
readonly rowCount: number;
|
||||
}
|
||||
|
||||
export interface FleetTableViewModel {
|
||||
readonly rows: readonly FleetTableRow[];
|
||||
readonly totalCount: number;
|
||||
readonly visibleCount: number;
|
||||
readonly selectedIds: readonly string[];
|
||||
readonly visibleSelection: VisibleSelection;
|
||||
readonly emptyReason: EmptyFleetReason | null;
|
||||
}
|
||||
|
||||
const STATUS_RANK: Readonly<Record<FleetStatusKind, number>> = {
|
||||
online: 0,
|
||||
auth: 1,
|
||||
offline: 2,
|
||||
unknown: 3,
|
||||
};
|
||||
|
||||
export function fleetStatusKind(status: FleetStatus | undefined): FleetStatusKind {
|
||||
if (status === undefined) return 'unknown';
|
||||
if (!status.reachable) return 'offline';
|
||||
if (status.authenticated === false) return 'auth';
|
||||
return 'online';
|
||||
}
|
||||
|
||||
function displayName(instance: FleetInstance): string {
|
||||
const name = instance.name?.trim();
|
||||
return name ? name : instance.id;
|
||||
}
|
||||
|
||||
function searchableText(instance: FleetInstance, status: FleetStatus | undefined): string {
|
||||
return [
|
||||
instance.id,
|
||||
instance.name ?? '',
|
||||
instance.url,
|
||||
instance.description ?? '',
|
||||
...(instance.tags ?? []),
|
||||
status?.summary === undefined ? '' : JSON.stringify(status.summary),
|
||||
]
|
||||
.join(' ')
|
||||
.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
function compareNames(left: FleetInstance, right: FleetInstance): number {
|
||||
return displayName(left).localeCompare(displayName(right), undefined, { sensitivity: 'base' });
|
||||
}
|
||||
|
||||
function compareInstances(
|
||||
left: FleetInstance,
|
||||
right: FleetInstance,
|
||||
statuses: ReadonlyMap<string, FleetStatus>,
|
||||
column: FleetSortColumn,
|
||||
direction: SortDirection,
|
||||
): number {
|
||||
const leftStatus = statuses.get(left.id);
|
||||
const rightStatus = statuses.get(right.id);
|
||||
let comparison: number;
|
||||
|
||||
if (column === 'status') {
|
||||
comparison =
|
||||
STATUS_RANK[fleetStatusKind(leftStatus)] - STATUS_RANK[fleetStatusKind(rightStatus)];
|
||||
} else if (column === 'latency') {
|
||||
const leftMissing = leftStatus?.latencyMs === undefined;
|
||||
const rightMissing = rightStatus?.latencyMs === undefined;
|
||||
if (leftMissing !== rightMissing) return leftMissing ? 1 : -1;
|
||||
comparison = (leftStatus?.latencyMs ?? 0) - (rightStatus?.latencyMs ?? 0);
|
||||
} else {
|
||||
comparison = compareNames(left, right);
|
||||
}
|
||||
|
||||
if (comparison !== 0) return direction === 'asc' ? comparison : -comparison;
|
||||
return compareNames(left, right) || left.id.localeCompare(right.id);
|
||||
}
|
||||
|
||||
export function buildFleetTableViewModel(
|
||||
instances: readonly FleetInstance[],
|
||||
statuses: ReadonlyMap<string, FleetStatus>,
|
||||
options: FleetTableOptions = {},
|
||||
): FleetTableViewModel {
|
||||
const query = options.query?.trim().toLocaleLowerCase() ?? '';
|
||||
const filter = options.filter ?? 'all';
|
||||
const sort = options.sort ?? { column: 'name', direction: 'asc' };
|
||||
const knownIds = new Set(instances.map((instance) => instance.id));
|
||||
const selectedIds = [...(options.selectedIds ?? [])]
|
||||
.filter((id) => knownIds.has(id))
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
const selectedIdSet = new Set(selectedIds);
|
||||
|
||||
const visibleInstances = instances
|
||||
.filter((instance) => {
|
||||
const status = statuses.get(instance.id);
|
||||
if (filter !== 'all' && fleetStatusKind(status) !== filter) return false;
|
||||
return !query || searchableText(instance, status).includes(query);
|
||||
})
|
||||
.slice()
|
||||
.sort((left, right) => compareInstances(left, right, statuses, sort.column, sort.direction));
|
||||
|
||||
const rows: FleetTableRow[] = visibleInstances.map((instance) => {
|
||||
const status = statuses.get(instance.id);
|
||||
return {
|
||||
id: instance.id,
|
||||
displayName: displayName(instance),
|
||||
instance,
|
||||
...(status === undefined ? {} : { status }),
|
||||
statusKind: fleetStatusKind(status),
|
||||
...(status?.latencyMs === undefined ? {} : { latencyMs: status.latencyMs }),
|
||||
selected: selectedIdSet.has(instance.id),
|
||||
};
|
||||
});
|
||||
const visibleSelectedCount = rows.reduce((count, row) => count + (row.selected ? 1 : 0), 0);
|
||||
const rowCount = rows.length;
|
||||
const emptyReason: EmptyFleetReason | null =
|
||||
instances.length === 0
|
||||
? 'config'
|
||||
: rowCount > 0
|
||||
? null
|
||||
: query
|
||||
? 'search'
|
||||
: filter !== 'all'
|
||||
? 'filter'
|
||||
: null;
|
||||
|
||||
return {
|
||||
rows,
|
||||
totalCount: instances.length,
|
||||
visibleCount: rowCount,
|
||||
selectedIds,
|
||||
visibleSelection: {
|
||||
checked: rowCount > 0 && visibleSelectedCount === rowCount,
|
||||
indeterminate: visibleSelectedCount > 0 && visibleSelectedCount < rowCount,
|
||||
selectedCount: visibleSelectedCount,
|
||||
rowCount,
|
||||
},
|
||||
emptyReason,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user