Files
ITWorx-Pulse-Public/apps/web/src/StoragePage.tsx
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

51 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { copy } from './copy';
import { StorageMapWidget, TemperatureHeatmap, type HeatmapPoint, type StorageMapNode } from './StorageVisuals';
import { SourceStatusDetails, type SourceStatus } from './SourceStatusDetails';
type Severity = 'normal' | 'attention' | 'critical' | 'unknown';
type Disk = { id: string; name: string; role: string; state: string; utilizationPercent: number; capacitySeverity?: Severity; thermalSeverity?: Severity; temperature?: { celsius?: number; status: string; observedAt?: string } };
type DiskSnapshot = { source: SourceStatus; disks: Disk[] };
type Pool = { id: string; name: string; filesystem: string; state: string; utilizationPercent: number; capacitySeverity?: Severity };
type PoolSnapshot = { source: SourceStatus; pools: Pool[] };
type ArrayMember = { id: string; name: string; role: string; state: string };
type ArraySnapshot = { source: SourceStatus; state: string; members: ArrayMember[] };
export type StorageData = { array: ArraySnapshot; disks: DiskSnapshot; pools: PoolSnapshot };
function availability(value: string): string { return value === 'online' || value === 'healthy' || value === 'operational' ? 'healthy' : value === 'unknown' ? 'unknown' : 'degraded'; }
function visualSeverity(...values: Array<string | undefined>): string {
if (values.includes('critical')) return 'critical';
if (values.some((value) => value === 'attention' || value === 'degraded' || value === 'faulted')) return 'degraded';
if (values.includes('unknown')) return 'unknown';
return 'healthy';
}
function signalLabel(value: string): string { return value === 'normal' || value === 'healthy' || value === 'online' ? 'normaal' : value === 'critical' ? 'kritiek' : value === 'attention' || value === 'degraded' ? 'aandacht' : 'onbekend'; }
export function buildStorageNodes(data: StorageData): StorageMapNode[] {
const members = new Map((data.array.members ?? []).map((member) => [member.id.toLowerCase(), member]));
const diskNodes = (data.disks.disks ?? []).slice(0, 64).map((disk) => {
const member = members.get(disk.id.toLowerCase());
if (member) members.delete(disk.id.toLowerCase());
const capacity = disk.capacitySeverity ?? 'unknown';
const thermal = disk.thermalSeverity ?? disk.temperature?.status ?? 'unknown';
return { id: 'disk-' + disk.id, label: disk.name, kind: member?.role ?? disk.role, state: visualSeverity(availability(disk.state), capacity, thermal), detail: `Beschikbaarheid ${signalLabel(disk.state)} · capaciteit ${signalLabel(capacity)} · temperatuur ${signalLabel(thermal)}`, href: '/disks/' + encodeURIComponent(disk.id) };
});
const unmatchedMembers = [...members.values()].slice(0, 64).map((member) => ({ id: 'array-' + member.id, label: member.name, kind: member.role, state: availability(member.state), detail: `Beschikbaarheid ${signalLabel(member.state)} · disktelemetrie onbekend`, href: '/array' }));
const poolNodes = (data.pools.pools ?? []).slice(0, 64).map((pool) => {
const capacity = pool.capacitySeverity ?? 'unknown';
return { id: 'pool-' + pool.id, label: pool.name, kind: `pool · ${pool.filesystem}`, state: visualSeverity(availability(pool.state), capacity), detail: `Device-health ${signalLabel(pool.state)} · capaciteit ${signalLabel(capacity)} (${pool.utilizationPercent.toLocaleString('nl-BE', { maximumFractionDigits: 1 })}% gebruikt)`, href: '/pools/' + encodeURIComponent(pool.id) };
});
return [...unmatchedMembers, ...poolNodes, ...diskNodes];
}
export function StoragePage() {
const [state, setState] = useState<'loading' | 'ready' | 'error'>('loading');
const [data, setData] = useState<StorageData | null>(null);
useEffect(() => { const controller = new AbortController(); Promise.all([fetch('/api/v1/array', { signal: controller.signal }).then((response) => response.json() as Promise<ArraySnapshot>), fetch('/api/v1/disks?limit=100', { signal: controller.signal }).then((response) => response.json() as Promise<DiskSnapshot>), fetch('/api/v1/pools?limit=100', { signal: controller.signal }).then((response) => response.json() as Promise<PoolSnapshot>)]).then(([array, disks, pools]) => { setData({ array, disks, pools }); setState('ready'); }).catch((error: unknown) => { if (error instanceof DOMException && error.name === 'AbortError') return; setState('error'); }); return () => controller.abort(); }, []);
if (state === 'loading') return <section className="state-page" role="status"><span className="loader" aria-hidden="true" /><h1>{copy.storage.loading}</h1></section>;
if (state === 'error' || !data) return <section className="state-page" role="alert"><span className="state-icon" aria-hidden="true">×</span><h1>{copy.storage.errorTitle}</h1><p>{copy.storage.errorDetail}</p></section>;
const nodes = buildStorageNodes(data);
const heatmap: HeatmapPoint[] = (data.disks.disks ?? []).slice(0, 64).map((disk) => ({ id: disk.id, label: disk.name, observedAt: disk.temperature?.observedAt || '', value: disk.temperature?.celsius ?? null, status: visualSeverity(disk.thermalSeverity ?? disk.temperature?.status ?? 'unknown'), href: '/disks/' + encodeURIComponent(disk.id) }));
return <><header className="page-intro"><p className="eyebrow">{copy.storage.eyebrow}</p><h1>{copy.storage.title}</h1><p className="intro">{copy.storage.intro}</p></header><section className="card container-summary" aria-labelledby="storage-summary-title"><div className="card-heading"><div><p className="card-kicker">{copy.storage.source}</p><h2 id="storage-summary-title">{copy.storage.sourceTitle}</h2><p className="card-copy">{copy.storage.accessible}</p></div></div><div className="storage-source-grid"><article><h3>{copy.storage.arraySource}</h3><SourceStatusDetails source={{ ...data.array.source, id: data.array.source?.id || 'array' }} /></article><article><h3>{copy.storage.diskSource}</h3><SourceStatusDetails source={{ ...data.disks.source, id: data.disks.source?.id || 'disks' }} /></article><article><h3>{copy.storage.poolSource}</h3><SourceStatusDetails source={{ ...data.pools.source, id: data.pools.source?.id || 'pools' }} /></article></div><p className="container-provenance">{nodes.length} {copy.storage.mapNodes} · {heatmap.length} {copy.storage.heatmapPoints}</p></section><StorageMapWidget nodes={nodes} title={copy.storage.mapTitle} description={copy.storage.mapDescription} /><TemperatureHeatmap points={heatmap} title={copy.storage.heatmapTitle} description={copy.storage.heatmapDescription} /></>;
}