Public source validation / validate (push) Failing after 3m8s
48 lines
5.9 KiB
TypeScript
48 lines
5.9 KiB
TypeScript
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></>;
|
||
}
|