This commit is contained in:
commit
bd774932d5
614 files changed
+77116
No files matched your search
@@ -0,0 +1,15 @@
|
||||
import { formatDateTime } from './locale';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { copy } from './copy';
|
||||
import { presentArrayRole, presentReason, presentStatus } from './presentation';
|
||||
|
||||
type SmartAttribute = { id:string; name:string; rawValue:number; normalizedValue?:number; unit?:string; status:string; critical:boolean; reason?:string }; type Smart = { state:string; overall:string; observedAt?:string; attributes:SmartAttribute[]; selfTest?:{supported:boolean;result:string;completedAt?:string;ageSeconds?:number}; reasons?:string[] }; type Sample = { observedAt:string; readBytesPerSecond?:number; writeBytesPerSecond?:number; readIops?:number; writeIops?:number; readLatencyMs?:number; writeLatencyMs?:number }; type Disk = { id:string; name:string; role:string; state:string; model?:string; serialDisplay?:string; filesystem?:string; sizeBytes:number; usedBytes:number; freeBytes:number; utilizationPercent:number; capacitySeverity?:string; thermalSeverity?:string; inodes?:{total:number;used:number;free:number;utilizationPercent:number}; smart?:Smart; performance?:{state:string;current:Sample;history:Sample[]}; temperature?:{state:string;celsius?:number;status:string;observedAt?:string}; spin?:{state:string} };
|
||||
type Snapshot = { source:{id:string;state:string;freshness:string;observedAt?:string;reason?:string}; disks:Disk[]; total:number; missingHistory?:Array<{diskId:string;name:string;role:string;observedAt:string;reason:string}> };
|
||||
type Detail = {source:Snapshot['source'];disk:Disk};
|
||||
function bytes(value:number):string{if(!Number.isFinite(value)||value<0)return'—';const units=['B','KB','MB','GB','TB'];let scaled=value;let index=0;while(scaled>=1024&&index<units.length-1){scaled/=1024;index+=1}return scaled.toLocaleString('nl-BE',{maximumFractionDigits:1})+' '+units[index]}
|
||||
function date(value?:string):string{return formatDateTime(value)}
|
||||
function Badge({label,ready}:{label:string;ready:boolean}){return <span className={'status-badge status-badge--'+(ready?'ready':'unknown')}><span className="status-icon" aria-hidden="true">{ready?'✓':'?'}</span>{label}</span>}
|
||||
function severityLabel(value?:string):string{return value==='normal'?'Normaal':value==='attention'?'Aandacht':value==='critical'?'Kritiek':'Onbekend'}
|
||||
function DiskCard({disk}:{disk:Disk}){return <article className="card"><div className="card-heading"><div><p className="card-kicker">{presentArrayRole(disk.role)}</p><h2><a className="entity-link" href={'/disks/'+encodeURIComponent(disk.id)}>{disk.name}</a></h2></div><Badge label={presentStatus(disk.state)} ready={disk.state==='online'}/></div><p className="card-copy">{disk.filesystem||copy.disks.noFilesystem} · {bytes(disk.sizeBytes)} · {disk.utilizationPercent.toLocaleString('nl-BE',{maximumFractionDigits:1})}% {copy.disks.used}</p><p className="card-copy">Capaciteit: {severityLabel(disk.capacitySeverity)} · Temperatuur: {severityLabel(disk.thermalSeverity)}</p><p className="card-copy">{disk.model||copy.disks.noModel} · {disk.serialDisplay||copy.disks.noSerial}</p></article>}
|
||||
export function DiskPage(){const[state,setState]=useState<'loading'|'ready'|'error'>('loading');const[snapshot,setSnapshot]=useState<Snapshot|null>(null);useEffect(()=>{const controller=new AbortController();fetch('/api/v1/disks?limit=100',{signal:controller.signal}).then(response=>{if(!response.ok)throw new Error('disks');return response.json() as Promise<Snapshot>}).then(data=>{setSnapshot(data);setState('ready')}).catch(error=>{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.disks.loading}</h1></section>;if(state==='error'||!snapshot)return <section className="state-page" role="alert"><span className="state-icon" aria-hidden="true">×</span><h1>{copy.disks.errorTitle}</h1><p>{copy.disks.errorDetail}</p></section>;const fresh=snapshot.source?.freshness==='fresh'&&snapshot.source?.state!=='unknown';return <><header className="page-intro"><p className="eyebrow">{copy.disks.eyebrow}</p><h1>{copy.disks.title}</h1><p className="intro">{copy.disks.intro}</p></header><section className="card container-summary" aria-labelledby="disks-summary-title"><div className="card-heading"><div><p className="card-kicker">{copy.disks.source}</p><h2 id="disks-summary-title">{snapshot.source?.id||copy.disks.unknown}</h2><p className="card-copy">{snapshot.source?.reason?presentReason(snapshot.source.reason):(fresh?copy.disks.fresh:copy.disks.stale)}</p></div><Badge label={fresh?copy.disks.available:copy.disks.unknown} ready={fresh}/></div><p className="container-provenance">{copy.disks.observed}: {date(snapshot.source?.observedAt)} · {snapshot.total} {copy.disks.rows}</p></section><section className="card-grid" aria-label={copy.disks.cards}>{snapshot.disks.length?snapshot.disks.map(disk=><DiskCard key={disk.id} disk={disk}/>):<article className="card"><p className="card-copy">{copy.disks.empty}</p></article>}</section><section className="card" aria-labelledby="missing-disk-title"><div className="card-heading"><div><p className="card-kicker">{copy.disks.history}</p><h2 id="missing-disk-title">{copy.disks.missingTitle}</h2></div></div>{snapshot.missingHistory?.length?<ul className="inventory-list">{snapshot.missingHistory.map(item=><li key={item.diskId+'-'+item.observedAt}><span><strong>{item.name}</strong><small>{item.role} · {presentReason(item.reason)} · {date(item.observedAt)}</small></span><Badge label={copy.disks.missing} ready={false}/></li>)}</ul>:<p className="card-copy">{copy.disks.noMissingHistory}</p>}</section></>}
|
||||
function TelemetrySection({performance,temperature,spin}:{performance?:Disk['performance'];temperature?:Disk['temperature'];spin?:Disk['spin']}){return <section className="card" aria-labelledby="disk-telemetry-title"><div className="card-heading"><div><p className="card-kicker">{copy.disks.telemetry}</p><h2 id="disk-telemetry-title">{temperature?.celsius == null ? '—' : temperature.celsius.toLocaleString('nl-BE',{maximumFractionDigits:1})+' °C'}</h2></div><Badge label={presentStatus(temperature?.status)} ready={temperature?.status==='normal'}/></div><p className="card-copy">{copy.disks.spin}: {spin?.state==='unsupported'?copy.disks.unsupported:presentStatus(spin?.state)} · {copy.disks.performance}: {performance?.state==='unsupported'?copy.disks.unsupported:presentStatus(performance?.state)}</p>{performance?.state==='available'&&<p className="card-copy">{copy.disks.read}: {Math.round(performance.current.readBytesPerSecond||0).toLocaleString('nl-BE')} B/s · {copy.disks.write}: {Math.round(performance.current.writeBytesPerSecond||0).toLocaleString('nl-BE')} B/s · {performance.history.length} {copy.disks.historyPoints}</p>}</section>}function SmartSection({smart}:{smart?:Smart}){if(!smart)return <section className="card" aria-labelledby="smart-title"><div className="card-heading"><h2 id="smart-title">SMART</h2><Badge label={copy.disks.smartUnknown} ready={false}/></div><p className="card-copy">{copy.disks.smartUnavailable}</p></section>;return <section className="card" aria-labelledby="smart-title"><div className="card-heading"><div><p className="card-kicker">SMART</p><h2 id="smart-title">{presentStatus(smart.overall)}</h2></div><Badge label={smart.state=== 'available'?copy.disks.smartAvailable:copy.disks.smartUnknown} ready={smart.state==='available'&&smart.overall==='passed'}/></div>{smart.reasons?.length?<ul className="reason-list">{smart.reasons.map(reason=><li key={reason}>{reason}</li>)}</ul>:<p className="card-copy">{copy.disks.smartNoReasons}</p>}{smart.selfTest&&<p className="card-copy">{copy.disks.selfTest}: {smart.selfTest.result} · {smart.selfTest.ageSeconds == null ? '—' : Math.floor(smart.selfTest.ageSeconds/3600)+' u'}</p>}{smart.attributes.length>0&&<div className="host-table-wrap"><table className="host-table"><thead><tr><th>{copy.disks.attribute}</th><th>{copy.disks.value}</th><th>{copy.disks.state}</th></tr></thead><tbody>{smart.attributes.map(attribute=><tr key={attribute.id}><th scope="row">{attribute.name}<small>{attribute.id}</small></th><td>{attribute.rawValue}{attribute.unit?' '+attribute.unit:''}</td><td><Badge label={presentStatus(attribute.status)} ready={!attribute.critical}/></td></tr>)}</tbody></table></div>}</section>}export function DiskDetailPage({id}:{id:string}){const[state,setState]=useState<'loading'|'ready'|'error'>('loading');const[detail,setDetail]=useState<Detail|null>(null);useEffect(()=>{const controller=new AbortController();fetch('/api/v1/disks/'+encodeURIComponent(id),{signal:controller.signal}).then(response=>{if(!response.ok)throw new Error('disk-detail');return response.json() as Promise<Detail>}).then(data=>{setDetail(data);setState('ready')}).catch(error=>{if(error instanceof DOMException&&error.name==='AbortError')return;setState('error')});return()=>controller.abort()},[id]);if(state==='loading')return <section className="state-page" role="status"><span className="loader" aria-hidden="true"/><h1>{copy.disks.loading}</h1></section>;if(state==='error'||!detail)return <section className="state-page" role="alert"><span className="state-icon" aria-hidden="true">×</span><h1>{copy.disks.errorTitle}</h1><p>{copy.disks.errorDetail}</p><a className="button" href="/disks">{copy.disks.back}</a></section>;const disk=detail.disk;return <><header className="page-intro"><p className="eyebrow">{copy.disks.detailEyebrow}</p><h1>{disk.name}</h1><p className="intro">{copy.disks.detailIntro}</p><div className="detail-actions"><a className="button button--secondary" href="/disks">{copy.disks.back}</a></div></header><section className="card container-summary" aria-labelledby="disk-detail-summary"><div className="card-heading"><div><p className="card-kicker">{copy.disks.source}</p><h2 id="disk-detail-summary">{detail.source?.id||copy.disks.unknown}</h2><p className="card-copy">{detail.source?.reason||copy.disks.readOnly}</p></div><Badge label={presentStatus(disk.state)} ready={disk.state==='online'}/></div><p className="container-provenance">{copy.disks.observed}: {date(detail.source?.observedAt)} · {detail.source?.freshness=== 'fresh'?copy.disks.fresh:copy.disks.stale}</p></section><SmartSection smart={disk.smart}/><TelemetrySection performance={disk.performance} temperature={disk.temperature} spin={disk.spin}/><section className="card-grid" aria-label={copy.disks.metrics}><article className="card"><p className="card-kicker">{copy.disks.capacity}</p><h2>{disk.utilizationPercent.toLocaleString('nl-BE',{maximumFractionDigits:1})}%</h2><p className="card-copy">{bytes(diskLine truncated
|
||||
Reference in new issue
Block a user