Files
ITWorx-Pulse-Public/apps/web/src/ApplicationPage.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

48 lines
5.9 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 { presentReason } from './presentation';
import { SourceStatusDetails } from './SourceStatusDetails';
type ComponentItem = { id: string; name: string; kind: string; critical: boolean; containerState: string; serviceState: string; status: string; reason?: string };
type ApplicationItem = { id: string; name: string; status: string; overridden: boolean; components: ComponentItem[]; reasons?: Array<{ code: string; message: string; componentId?: string; critical: boolean }> };
type ApplicationSnapshot = { source: { id: string; state: string; freshness: string; observedAt?: string; reason?: string }; applications: ApplicationItem[]; total: number };
type ApplicationDetail = { source: ApplicationSnapshot['source']; application: ApplicationItem };
function Badge({ state }: { state: string }) {
const normalized = state.toLowerCase();
const tone = normalized === 'healthy' ? 'ready' : normalized === 'degraded' || normalized === 'down' ? 'attention' : 'unknown';
return <span className={'status-badge status-badge--' + tone}><span className="status-icon" aria-hidden="true">{tone === 'ready' ? '✓' : tone === 'attention' ? '!' : '?'}</span>{normalized === 'healthy' ? copy.applications.healthy : normalized === 'degraded' || normalized === 'down' ? copy.applications.degraded : copy.applications.unknown}</span>;
}
function Source({ source }: { source: ApplicationSnapshot['source'] }) {
return <SourceStatusDetails source={source ?? {}} fallbackReason={source?.freshness === 'fresh' ? copy.applications.fresh : copy.applications.stale} />;
}
function Components({ items }: { items: ComponentItem[] }) {
return <details className="card technical-details"><summary>{copy.applications.components} ({items.length})</summary><div className="application-components">{items.map((item) => <article className="application-component" key={item.id}><div><strong>{item.name}</strong><small>{item.kind} · {item.critical ? copy.applications.critical : copy.applications.optional}</small></div><div><Badge state={item.status} /><small>{item.reason || item.id}</small></div></article>)}</div></details>;
}
export function ApplicationPage({ id }: { id?: string }) {
const [state, setState] = useState<'loading' | 'ready' | 'error'>('loading');
const [data, setData] = useState<ApplicationSnapshot | ApplicationDetail | null>(null);
useEffect(() => {
const controller = new AbortController();
const url = id ? '/api/v1/applications/' + encodeURIComponent(id) : '/api/v1/applications';
fetch(url, { signal: controller.signal }).then((response) => {
if (!response.ok) throw new Error('applications');
return response.json() as Promise<ApplicationSnapshot | ApplicationDetail>;
}).then((value) => { setData(value); setState('ready'); }).catch((error: unknown) => {
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.applications.loading}</h1></section>;
if (state === 'error' || !data) return <section className="state-page" role="alert"><span className="state-icon" aria-hidden="true">×</span><h1>{copy.applications.errorTitle}</h1><p>{copy.applications.errorDetail}</p><a className="button" href="/applications">{copy.applications.backToList}</a></section>;
if (id) {
const detail = data as ApplicationDetail;
const app = detail.application;
return <><header className="page-intro"><p className="eyebrow">{copy.applications.detailEyebrow}</p><h1>{app.name}</h1><p className="intro">{copy.applications.detailIntro}</p><div className="detail-actions"><a className="button button--secondary" href="/applications">{copy.applications.backToList}</a><a className="button button--secondary" href="/dashboards?filterKey=sourceType&filterValue=inventory">{copy.applications.openDashboard}</a></div></header><section className="card container-summary" aria-labelledby="application-summary-title"><div className="card-heading"><div><p className="card-kicker">{copy.applications.source}</p><h2 id="application-summary-title">{detail.source?.id || copy.applications.unknown}</h2><Source source={detail.source} /></div><Badge state={app.status} /></div>{app.reasons?.map((reason) => <p className="host-reason" key={reason.componentId + '-' + reason.code}>{presentReason(reason.code)}</p>)}</section><Components items={app.components} /></>;
}
const snapshot = data as ApplicationSnapshot;
return <><header className="page-intro"><p className="eyebrow">{copy.applications.eyebrow}</p><h1>{copy.applications.title}</h1><p className="intro">{copy.applications.intro}</p></header><section className="card container-summary" aria-labelledby="applications-source-title"><div className="card-heading"><div><p className="card-kicker">{copy.applications.source}</p><h2 id="applications-source-title">{snapshot.source?.id || copy.applications.unknown}</h2><Source source={snapshot.source} /></div><Badge state={snapshot.source?.state || 'unknown'} /></div><p className="container-count">{snapshot.total} {copy.applications.rows}</p></section><section className="card container-panel" aria-labelledby="application-list-title"><div className="card-heading"><div><p className="card-kicker">{copy.applications.list}</p><h2 id="application-list-title">{copy.applications.overview}</h2></div></div>{snapshot.applications.length === 0 ? <p className="card-copy">{copy.applications.empty}</p> : <ul className="inventory-list">{snapshot.applications.map((item) => <li key={item.id}><a className="entity-link" href={"/applications/" + encodeURIComponent(item.id)}><strong>{item.name}</strong><small>{item.components.length} {copy.applications.components} · {item.overridden ? copy.applications.overridden : copy.applications.discovered}</small></a><Badge state={item.status} /></li>)}</ul>}</section></>;
}