Publish curated ITWorx-Pulse source from 0e0633c8ac91cb71312a26837f791035a78d81b8
Public source validation / validate (push) Failing after 2m10s

This commit is contained in:
Public Source Publisher committed 2026-09-30 01:14:48 +00:00
1 parent a4956c2af9
commit db8093b608
88 files changed
+3685 -469

No files matched your search

+143 -64
View File
@@ -1,9 +1,9 @@
import { useEffect, useMemo, useState, type CSSProperties } from 'react';
import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react';
import { resolveDashboardScope } from './dashboardScope';
import { LiveChartAdapter, historicalSamplesFromData, type LiveFreshness } from './liveBuffer';
import { LiveClient } from './liveClient';
import { MetricClient, rangeForPreset, type MetricQueryRequest, type MetricRangePreset } from './metricClient';
import { MetricWidget, StatusGridWidget, type MetricWidgetProps, type StatusGridItem } from './MetricWidgets';
import { MetricClient, rangeForSelection, type MetricQueryRequest } from './metricClient';
import { MetricWidget, RankedListWidget, StatusGridWidget, type MetricWidgetProps, type RankedListItem, type StatusGridItem } from './MetricWidgets';
import { buildStorageNodes, type StorageData } from './StoragePage';
import { StorageMapWidget } from './StorageVisuals';
import { aggregateStatus, useSystemStatus } from './systemStatus';
@@ -12,75 +12,115 @@ import { useMetricQuery } from './useMetricQuery';
import { formatDateTime } from './locale';
import { copy } from './copy';
import { presentEventSummary, presentEventType, presentReason, presentStatus } from './presentation';
import { wallboardColumns, wallboardPlacement } from './wallboardLayout';
import { useOperatorContext } from './operatorContext';
import { wallboardPlacement } from './wallboardLayout';
import { spatialStyle } from './dashboardSpatial';
import { isWidgetType, widgetLabel } from './widgetCatalog';
type RecordValue = Record<string, unknown>;
export type RuntimeWidget = { id: string; type: string; title: string; description?: string; data?: RecordValue; visualization?: RecordValue; behavior?: RecordValue; layouts?: RecordValue };
export type RuntimeState = 'loading' | 'usable' | 'empty' | 'error';
export type RuntimeState = 'loading' | 'usable' | 'empty' | 'stale' | 'partial' | 'error' | 'capability-disabled';
type RuntimeRow = { id: string; label: string; status: string; value?: string; detail?: string; occurredAt?: string; severity?: string; acknowledged?: boolean };
type EventItem = { id: string; type: string; severity: string; summary: string; occurredAt: string; sourceId?: string };
type Resource = { state: RuntimeState; rows?: RuntimeRow[]; storage?: StorageData; events?: EventItem[]; text?: string; error?: string };
const metricClient = new MetricClient();
const liveClient = new LiveClient();
const metricTypes = new Set(['stat', 'timeseries', 'gauge', 'query-inspector']);
function field<T>(record: RecordValue | undefined, name: string): T | undefined {
if (!record) return undefined;
const upper = name.charAt(0).toUpperCase() + name.slice(1);
return (record[name] ?? record[upper] ?? record[name.toUpperCase()]) as T | undefined;
}
function safeLimit(widget: RuntimeWidget): number { return Math.min(100, Math.max(1, Math.trunc(Number(field(widget.data, 'limit') ?? 100) || 100))); }
function safeInterval(widget: RuntimeWidget): number { return Math.min(300, Math.max(5, Math.trunc(Number(field(widget.behavior, 'liveIntervalSeconds') ?? 30) || 30))) * 1000; }
function stringValue(value: unknown, fallback = ''): string { return typeof value === 'string' ? value.slice(0, 500) : fallback; }
function Badge({ state }: { state: RuntimeState }) {
const usable = state === 'usable';
const label = usable ? copy.dashboards.runtime.current : state === 'loading' ? copy.dashboards.runtime.loading : state === 'empty' ? copy.dashboards.runtime.empty : copy.dashboards.runtime.error;
return <span className={'status-badge status-badge--' + (usable ? 'ready' : 'unknown')}><span className="status-icon" aria-hidden="true">{usable ? '✓' : '?'}</span>{label}</span>;
const labels: Record<RuntimeState, string> = {
usable: copy.dashboards.runtime.current, loading: copy.dashboards.runtime.loading, empty: copy.dashboards.runtime.empty,
stale: copy.dashboards.runtime.stale, partial: copy.dashboards.runtime.partial, error: copy.dashboards.runtime.error,
'capability-disabled': copy.dashboards.runtime.capabilityDisabled,
};
const ready = state === 'usable';
return <span className={'status-badge status-badge--' + (ready ? 'ready' : 'unknown')}><span className="status-icon" aria-hidden="true">{ready ? '✓' : '?'}</span>{labels[state]}</span>;
}
type Resource = { state: RuntimeState; items?: StatusGridItem[]; storage?: StorageData; events?: EventItem[]; error?: string };
type EventItem = { id: string; type: string; severity: string; summary: string; occurredAt: string; sourceId?: string };
async function readJSON<T>(url: string, signal: AbortSignal): Promise<T> {
const response = await fetch(url, { signal, cache: 'no-store' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
}
function useInventoryResource(widget: RuntimeWidget): Resource {
const [resource, setResource] = useState<Resource>({ state: 'loading' });
function useBoundedResource(widget: RuntimeWidget): Resource {
const sourceType = String(field(widget.data, 'sourceType') ?? '');
const scope = field<RecordValue>(widget.data, 'scope') ?? {};
const entityType = String(field(scope, 'entityType') ?? '');
const isApplications = sourceType === 'inventory' && entityType === 'application';
const isStorage = sourceType === 'inventory' && Array.isArray(field(scope, 'entityTypes'));
const isEvents = sourceType === 'events';
const interval = Math.min(300, Math.max(5, Number(field(widget.behavior, 'liveIntervalSeconds') ?? 15))) * 1000;
const entityTypes = field<unknown[]>(scope, 'entityTypes');
const limit = safeLimit(widget);
const interval = safeInterval(widget);
const rawText = field<unknown>(widget.data, 'text');
const text = typeof rawText === 'string' ? rawText.slice(0, 5000) : '';
const key = JSON.stringify({ sourceType, entityType, entityTypes, limit, interval, type: widget.type, text });
const [resource, setResource] = useState<Resource>(() => sourceType === 'text' ? { state: text.trim() ? 'usable' : 'empty', text } : { state: 'loading' });
useEffect(() => {
if (!isApplications && !isStorage && !isEvents) {
setResource({ state: 'empty' });
return undefined;
}
if (sourceType === 'semantic-metric' || (sourceType === 'inventory' && entityType === 'server')) return undefined;
if (sourceType === 'text') { setResource({ state: text.trim() ? 'usable' : 'empty', text }); return undefined; }
if (!['inventory', 'events', 'alerts', 'incidents'].includes(sourceType)) { setResource({ state: 'capability-disabled' }); return undefined; }
let active = true;
let controller: AbortController | null = null;
const read = async <T,>(url: string, signal: AbortSignal): Promise<T> => {
const response = await fetch(url, { signal, cache: 'no-store' });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<T>;
};
const load = async () => {
controller?.abort();
const current = new AbortController();
controller = current;
try {
if (isApplications) {
const value = await read<{ applications?: Array<{ id: string; name: string; status: string; reasons?: Array<{ message?: string }> }> }>('/api/v1/applications', current.signal);
const items = (value.applications ?? []).slice(0, 50).map((item) => ({ id: item.id, label: item.name, status: presentStatus(item.status), reason: presentReason(item.reasons?.[0]?.message) }));
if (active) setResource({ state: items.length ? 'usable' : 'empty', items });
} else if (isStorage) {
if (sourceType === 'inventory' && Array.isArray(entityTypes)) {
const [array, disks, pools] = await Promise.all([
read<StorageData['array']>('/api/v1/array', current.signal),
read<StorageData['disks']>('/api/v1/disks?limit=100', current.signal),
read<StorageData['pools']>('/api/v1/pools?limit=100', current.signal),
readJSON<StorageData['array']>('/api/v1/array', current.signal),
readJSON<StorageData['disks']>('/api/v1/disks?limit=' + limit, current.signal),
readJSON<StorageData['pools']>('/api/v1/pools?limit=' + limit, current.signal),
]);
const storage = { array, disks, pools };
if (active) setResource({ state: buildStorageNodes(storage).length ? 'usable' : 'empty', storage });
} else {
const limit = Math.min(100, Math.max(1, Number(field(widget.data, 'limit') ?? 100)));
const value = await read<{ items?: EventItem[] }>('/api/v1/events?limit=' + limit, current.signal);
return;
}
if (sourceType === 'inventory' && entityType === 'application') {
const value = await readJSON<{ applications?: Array<RecordValue> }>('/api/v1/applications', current.signal);
const rows = (value.applications ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.name, stringValue(item.id)), status: stringValue(item.status, 'unknown'), detail: presentReason(stringValue(field<Array<RecordValue>>(item, 'reasons')?.[0]?.message)) }));
if (active) setResource({ state: rows.length ? 'usable' : 'empty', rows });
return;
}
if (sourceType === 'inventory' && (entityType === 'service' || widget.type === 'service-matrix')) {
const value = await readJSON<{ services?: Array<RecordValue>; total?: number }>('/api/v1/services?limit=' + limit, current.signal);
const rows = (value.services ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.name, stringValue(item.id)), status: stringValue(item.state, 'unknown'), value: field<number>(item, 'responseTimeMs') == null ? '—' : `${field<number>(item, 'responseTimeMs')} ms`, detail: field<number>(item, 'availabilityPercent') == null ? stringValue(item.reason) : `${field<number>(item, 'availabilityPercent')?.toFixed(2)}%` }));
if (active) setResource({ state: (value.total ?? rows.length) > rows.length ? 'partial' : rows.length ? 'usable' : 'empty', rows });
return;
}
if (sourceType === 'inventory') {
const query = new URLSearchParams({ limit: String(limit) });
if (entityType) query.set('type', entityType);
const value = await readJSON<{ items?: Array<RecordValue>; hasMore?: boolean }>('/api/v1/entities?' + query, current.signal);
const rows = (value.items ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.displayName, stringValue(item.canonicalName, stringValue(item.id))), status: stringValue(item.status, 'unknown'), value: String(field<number>(item, 'factCount') ?? ''), detail: stringValue(item.entityType) }));
if (active) setResource({ state: value.hasMore ? 'partial' : rows.length ? 'usable' : 'empty', rows });
return;
}
if (sourceType === 'events') {
const value = await readJSON<{ items?: EventItem[] }>('/api/v1/events?limit=' + limit, current.signal);
const events = (value.items ?? []).slice(0, limit);
if (active) setResource({ state: events.length ? 'usable' : 'empty', events });
return;
}
if (sourceType === 'alerts') {
const value = await readJSON<{ items?: Array<RecordValue> }>('/api/v1/alerts?limit=' + limit, current.signal);
const rows = (value.items ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.ruleName, stringValue(item.id)), status: stringValue(item.state, 'unknown'), severity: stringValue(item.severity, 'unknown'), value: stringValue(item.entityName, '—'), detail: stringValue(item.reason), occurredAt: stringValue(item.updatedAt), acknowledged: Boolean(item.acknowledgedBy) }));
if (active) setResource({ state: rows.length ? 'usable' : 'empty', rows });
return;
}
const value = await readJSON<{ items?: Array<RecordValue> }>('/api/v1/incidents?limit=' + limit, current.signal);
const rows = (value.items ?? []).slice(0, limit).map((item) => ({ id: stringValue(item.id), label: stringValue(item.title, stringValue(item.id)), status: stringValue(item.status, 'unknown'), severity: stringValue(item.severity, 'unknown'), value: stringValue(item.ownerUserId, '—'), detail: stringValue(item.summary), occurredAt: stringValue(item.updatedAt) }));
if (active) setResource({ state: rows.length ? 'usable' : 'empty', rows });
} catch (error: unknown) {
if (error instanceof DOMException && error.name === 'AbortError') return;
if (active) setResource({ state: 'error', error: error instanceof Error ? error.message : 'bron niet beschikbaar' });
@@ -89,23 +129,55 @@ function useInventoryResource(widget: RuntimeWidget): Resource {
void load();
const timer = window.setInterval(load, interval);
return () => { active = false; controller?.abort(); window.clearInterval(timer); };
}, [isApplications, isStorage, isEvents, interval, widget.data]);
}, [key, sourceType, entityType, entityTypes, limit, interval, text, widget.type]);
return resource;
}
function RuntimeNotice({ state }: { state: RuntimeState }) {
const title = state === 'error' ? copy.dashboards.runtime.sourceUnavailable : state === 'loading' ? copy.dashboards.runtime.telemetryLoading : state === 'capability-disabled' ? copy.dashboards.runtime.capabilityDisabled : copy.dashboards.runtime.noCurrentData;
const detail = state === 'error' ? copy.dashboards.runtime.unavailableDetail : state === 'capability-disabled' ? 'Kies een passend widgettype of een andere datasource.' : copy.dashboards.runtime.retryDetail;
return <div className={'dashboard-runtime-state dashboard-runtime-state--' + state} role={state === 'error' ? 'alert' : 'status'}><strong>{title}</strong><span>{detail}</span></div>;
}
function RuntimeTable({ rows, service = false }: { rows: RuntimeRow[]; service?: boolean }) {
return <div className="dashboard-runtime-table-wrap" tabIndex={0} aria-label={`${rows.length} ${copy.dashboards.runtime.rows}`}><table className="dashboard-runtime-table"><caption className="sr-only">{rows.length} {copy.dashboards.runtime.rows}</caption><thead><tr><th scope="col">Naam</th><th scope="col">Status</th><th scope="col">{service ? 'Respons / beschikbaarheid' : 'Waarde'}</th></tr></thead><tbody>{rows.slice(0, 100).map((row) => <tr key={row.id}><th scope="row">{row.label}<small>{row.detail}</small></th><td>{presentStatus(row.status)}</td><td>{row.value || '—'}</td></tr>)}</tbody></table></div>;
}
function AlertSummary({ rows }: { rows: RuntimeRow[] }) {
const count = (severity: string) => rows.filter((row) => row.severity === severity && row.status !== 'resolved').length;
return <div className="dashboard-alert-summary"><dl><div><dt>Kritiek</dt><dd>{count('critical')}</dd></div><div><dt>Waarschuwing</dt><dd>{count('warning')}</dd></div><div><dt>Actief</dt><dd>{rows.filter((row) => row.status !== 'resolved').length}</dd></div><div><dt>Bevestigd</dt><dd>{rows.filter((row) => row.acknowledged).length}</dd></div></dl><RuntimeTable rows={rows.slice(0, 10)} /></div>;
}
function safeInline(value: string): ReactNode[] {
const parts: ReactNode[] = [];
const pattern = /\[([^\]]{1,120})\]\((\/[^)\s]*|https:\/\/[^)\s]+)\)/g;
let cursor = 0; let match: RegExpExecArray | null;
while ((match = pattern.exec(value)) !== null) { if (match.index > cursor) parts.push(value.slice(cursor, match.index)); parts.push(<a key={match.index} href={match[2]} rel={match[2].startsWith('https://') ? 'noreferrer' : undefined}>{match[1]}</a>); cursor = match.index + match[0].length; }
if (cursor < value.length) parts.push(value.slice(cursor));
return parts;
}
function TextWidget({ text }: { text: string }) { return <div className="dashboard-text-widget">{text.slice(0, 5000).split(/\r?\n/).slice(0, 100).filter(Boolean).map((line, index) => <p key={index}>{safeInline(line.replace(/^#{1,3}\s+/, '').replace(/^[-*]\s+/, '• '))}</p>)}</div>; }
function sourceLabel(sourceType: string): string {
if (sourceType === 'semantic-metric') return copy.dashboards.runtime.semanticMetric;
if (sourceType === 'events') return copy.dashboards.runtime.events;
if (sourceType === 'alerts') return copy.dashboards.runtime.alerts;
if (sourceType === 'incidents') return copy.dashboards.runtime.incidents;
if (sourceType === 'text') return copy.dashboards.runtime.text;
return copy.dashboards.runtime.inventory;
}
export function DashboardRuntimeWidget({ widget, viewport, document, timeRange, onState, onFilter }: { widget: RuntimeWidget; viewport: 'desktop' | 'tablet' | 'mobile' | 'wallboard'; document: RecordValue; timeRange: string; onState: (id: string, state: RuntimeState) => void; onFilter: () => void }) {
const sourceType = String(field(widget.data, 'sourceType') ?? '');
const semantic = sourceType === 'semantic-metric';
const system = useSystemStatus();
const inventory = useInventoryResource(widget);
const range = useMemo(() => rangeForPreset(timeRange as MetricRangePreset), [timeRange]);
const resource = useBoundedResource(widget);
const operator = useOperatorContext();
if (operator.paused && viewport !== 'wallboard' && timeRange === 'live') timeRange = 'paused';
const range = useMemo(() => rangeForSelection(timeRange), [timeRange, operator.refreshVersion]);
const request = useMemo<MetricQueryRequest | null>(() => {
if (!semantic) return null;
if (!semantic || !metricTypes.has(widget.type)) return null;
const metric = String(field(widget.data, 'metric') ?? '');
if (!metric) return null;
const scope = resolveDashboardScope(field<RecordValue>(widget.data, 'scope') ?? {}, field<unknown[]>(document, 'variables') ?? []);
return { metric, scope, range, aggregation: String(field(widget.data, 'aggregation') ?? 'avg') };
}, [semantic, widget.data, document, range]);
return { metric, scope: resolveDashboardScope(field<RecordValue>(widget.data, 'scope') ?? {}, field<unknown[]>(document, 'variables') ?? []), range, aggregation: String(field(widget.data, 'aggregation') ?? 'avg') };
}, [semantic, widget.type, widget.data, document, range]);
const metricState = useMetricQuery(metricClient, request);
const historicalSamples = useMemo(() => metricState.status === 'success' ? historicalSamplesFromData(metricState.response?.data) : [], [metricState.status, metricState.response?.data]);
const freshness: LiveFreshness = metricState.response?.freshness ?? 'unavailable';
@@ -113,29 +185,36 @@ export function DashboardRuntimeWidget({ widget, viewport, document, timeRange,
const live = useLiveMetric(liveClient, semantic && timeRange === 'live' ? request : null, historicalSamples);
const systemWidget = sourceType === 'inventory' && String(field(field<RecordValue>(widget.data, 'scope'), 'entityType') ?? '') === 'server';
const systemStatus = aggregateStatus(system);
const metricRuntime: RuntimeState = metricState.status === 'error' || (timeRange === 'live' && live.state === 'error') ? 'error' : (timeRange === 'live' ? live.series.length : historicalSeries.length) > 0 ? 'usable' : metricState.status === 'loading' || (timeRange === 'live' && live.state === 'connecting') ? 'loading' : 'empty';
const runtime = semantic ? metricRuntime : systemWidget ? (system.state === 'loading' ? 'loading' : system.state === 'ready' ? 'usable' : 'error') : inventory.state;
const metricRuntime: RuntimeState = !metricTypes.has(widget.type) ? 'capability-disabled' : metricState.status === 'error' || (timeRange === 'live' && live.state === 'error') ? 'error' : (timeRange === 'live' ? live.series.length : historicalSeries.length) > 0 ? (freshness === 'stale' ? 'stale' : 'usable') : metricState.status === 'loading' || (timeRange === 'live' && live.state === 'connecting') ? 'loading' : 'empty';
let runtime: RuntimeState = semantic ? metricRuntime : systemWidget ? (system.state === 'loading' ? 'loading' : system.state === 'ready' ? 'usable' : 'error') : resource.state;
if (!isWidgetType(widget.type)) runtime = 'error';
const rows = resource.rows ?? [];
const compatible = semantic ? metricTypes.has(widget.type)
: systemWidget ? ['stat', 'status-grid'].includes(widget.type)
: sourceType === 'text' ? widget.type === 'text'
: resource.storage ? widget.type === 'storage-map'
: sourceType === 'events' ? widget.type === 'event-timeline'
: ['stat', 'ranked-list', 'status-grid', 'table', 'event-timeline', 'service-matrix', 'alert-summary'].includes(widget.type);
if (isWidgetType(widget.type) && !compatible && runtime !== 'error') runtime = 'capability-disabled';
useEffect(() => { onState(widget.id, runtime); }, [onState, runtime, widget.id]);
const activeLayout = field<RecordValue>(widget.layouts, viewport) ?? field<RecordValue>(widget.layouts, 'desktop') ?? {};
const columns = viewport === 'wallboard' ? wallboardColumns : viewport === 'tablet' ? 8 : viewport === 'mobile' ? 1 : 18;
const width = viewport === 'mobile' ? 1 : Math.min(columns, Math.max(1, Number(field(activeLayout, 'w') ?? 6)));
const placement = wallboardPlacement(activeLayout);
const layoutStyle = viewport === 'wallboard' ? { '--widget-span': String(placement.columnSpan), gridColumn: `${placement.columnStart} / span ${placement.columnSpan}`, gridRow: `${placement.rowStart} / span ${placement.rowSpan}` } as CSSProperties : { '--widget-span': String(width) } as CSSProperties;
let content;
if (semantic) {
const metric: MetricWidgetProps = { kind: widget.type as MetricWidgetProps['kind'], series: timeRange === 'live' ? live.series : historicalSeries, freshness, expectedStepSeconds: request?.range.stepSeconds ?? 15, availability: timeRange === 'live' ? live.state : metricState.status, error: timeRange === 'live' ? live.error : metricState.error?.actionable ?? null, visualization: widget.visualization, metricName: request?.metric, sourceObservedAt: metricState.response?.sourceObservedAt, receivedAt: metricState.response?.receivedAt, warnings: metricState.response?.warnings, inspector: metricState.response?.inspector };
content = <MetricWidget {...metric} />;
} else if (systemWidget && system.state === 'ready') {
content = <div className="dashboard-runtime-stat"><strong>{systemStatus.label}</strong><span>{systemStatus.detail}</span><small>{system.status?.components.length ?? 0} {copy.dashboards.runtime.checkedComponents}</small></div>;
} else if (inventory.items?.length) {
content = <StatusGridWidget items={inventory.items} onSelect={onFilter} />;
} else if (inventory.storage) {
content = <StorageMapWidget nodes={buildStorageNodes(inventory.storage)} title={widget.title} description={widget.description ?? ''} idPrefix={'dashboard-storage-' + widget.id} />;
} else if (inventory.events?.length) {
content = <ol className="dashboard-event-list">{inventory.events.slice(0, viewport === 'mobile' ? 6 : 12).map((event) => <li key={event.id}><span className={'event-severity event-severity--' + event.severity} aria-hidden="true" /><span><strong>{presentEventSummary(event.type, event.summary)}</strong><small>{presentEventType(event.type)} · {formatDateTime(event.occurredAt)}</small></span></li>)}</ol>;
} else {
content = <div className={'dashboard-runtime-state dashboard-runtime-state--' + runtime} role={runtime === 'error' ? 'alert' : 'status'}><strong>{runtime === 'error' ? copy.dashboards.runtime.sourceUnavailable : runtime === 'loading' ? copy.dashboards.runtime.telemetryLoading : copy.dashboards.runtime.noCurrentData}</strong><span>{runtime === 'error' ? copy.dashboards.runtime.unavailableDetail : copy.dashboards.runtime.retryDetail}</span></div>;
}
return <article className="widget-card widget-card--runtime" data-runtime-state={runtime} data-viewport={viewport} style={layoutStyle} aria-labelledby={'widget-' + widget.id}><div className="widget-card-heading"><div><p className="card-kicker">{sourceType === 'semantic-metric' ? copy.dashboards.runtime.semanticMetric : sourceType === 'events' ? copy.dashboards.runtime.events : copy.dashboards.runtime.inventory}</p><h3 id={'widget-' + widget.id}>{widget.title}</h3></div><Badge state={runtime} /></div>{widget.description && <p className="widget-description">{widget.description}</p>}{content}</article>;
const layoutStyle = viewport === 'wallboard' ? { '--widget-span': String(placement.columnSpan), gridColumn: `${placement.columnStart} / span ${placement.columnSpan}`, gridRow: `${placement.rowStart} / span ${placement.rowSpan}` } as CSSProperties : spatialStyle(activeLayout, viewport) as CSSProperties;
let content: ReactNode;
if (!isWidgetType(widget.type)) content = <div className="dashboard-runtime-state dashboard-runtime-state--error" role="alert"><strong>{copy.dashboards.runtime.unknownType}</strong><span>{widget.type.slice(0, 80)}</span></div>;
else if (semantic && metricTypes.has(widget.type)) content = <MetricWidget kind={widget.type as MetricWidgetProps['kind']} series={timeRange === 'live' ? live.series : historicalSeries} freshness={freshness} expectedStepSeconds={request?.range.stepSeconds ?? 15} availability={timeRange === 'live' ? live.state : metricState.status} error={timeRange === 'live' ? live.error : metricState.error?.actionable ?? null} visualization={widget.visualization} metricName={request?.metric} sourceObservedAt={metricState.response?.sourceObservedAt} receivedAt={metricState.response?.receivedAt} warnings={metricState.response?.warnings} inspector={metricState.response?.inspector} />;
else if (systemWidget && system.state === 'ready') content = <div className="dashboard-runtime-stat"><strong>{systemStatus.label}</strong><span>{systemStatus.detail}</span><small>{system.status?.components.length ?? 0} {copy.dashboards.runtime.checkedComponents}</small></div>;
else if (resource.storage && widget.type === 'storage-map') content = <StorageMapWidget nodes={buildStorageNodes(resource.storage)} title={widget.title} description={widget.description ?? ''} idPrefix={'dashboard-storage-' + widget.id} />;
else if (widget.type === 'text' && resource.text?.trim()) content = <TextWidget text={resource.text} />;
else if (widget.type === 'alert-summary' && rows.length) content = <AlertSummary rows={rows} />;
else if (widget.type === 'service-matrix' && rows.length) content = <RuntimeTable rows={rows} service />;
else if (widget.type === 'table' && rows.length) content = <RuntimeTable rows={rows} />;
else if (widget.type === 'ranked-list' && rows.length) content = <RankedListWidget items={rows.map((row): RankedListItem => ({ id: row.id, label: row.label, value: row.value || presentStatus(row.status), detail: row.detail }))} onSelect={onFilter} />;
else if (widget.type === 'status-grid' && rows.length) content = <StatusGridWidget items={rows.map((row): StatusGridItem => ({ id: row.id, label: row.label, status: row.status, reason: row.detail }))} onSelect={onFilter} />;
else if (widget.type === 'stat' && rows.length) content = <div className="dashboard-runtime-stat"><strong>{rows.length}</strong><span>{sourceLabel(sourceType)}</span><small>{rows.filter((row) => !['healthy', 'up', 'operational', 'resolved'].includes(row.status)).length} vragen aandacht</small></div>;
else if (widget.type === 'event-timeline' && (resource.events?.length || rows.length)) { const events = resource.events ?? rows.map((row) => ({ id: row.id, type: sourceType, severity: row.severity ?? row.status, summary: row.label, occurredAt: row.occurredAt ?? '', sourceId: row.id })); content = <ol className="dashboard-event-list">{events.slice(0, viewport === 'mobile' ? 6 : 12).map((event) => <li key={event.id}><span className={'event-severity event-severity--' + event.severity} aria-hidden="true" /><span><strong>{sourceType === 'events' ? presentEventSummary(event.type, event.summary) : event.summary}</strong><small>{sourceType === 'events' ? presentEventType(event.type) : sourceLabel(sourceType)}{event.occurredAt ? ' · ' + formatDateTime(event.occurredAt) : ''}</small></span></li>)}</ol>; }
else content = <RuntimeNotice state={runtime === 'usable' || runtime === 'partial' || runtime === 'stale' ? 'capability-disabled' : runtime} />;
return <article className="widget-card widget-card--runtime" data-runtime-state={runtime} data-viewport={viewport} style={layoutStyle} aria-labelledby={'widget-' + widget.id}><div className="widget-card-heading"><div><p className="card-kicker">{sourceLabel(sourceType)} · {widgetLabel(widget.type)}</p><h3 id={'widget-' + widget.id}>{widget.title}</h3></div><Badge state={runtime} /></div>{widget.description && <p className="widget-description">{widget.description}</p>}{content}</article>;
}