83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
import { Icon } from './Icon';
|
|
import { Tag } from './ui';
|
|
import type { Tone } from '../lib/format';
|
|
import type { RuntimeHealth, RuntimeLevel, RuntimeTrend } from '../api/types';
|
|
|
|
/* The two places the process figures are read — the overview's Process card and the
|
|
Runtime page — say the same things about them, so the wording lives here rather than
|
|
twice. None of it decides anything: the verdict, its sentences and the areas to
|
|
investigate all arrive from the gateway, and this renders what it was handed. */
|
|
|
|
export const levelTone: Record<RuntimeLevel, Tone> = {
|
|
ok: 'ok',
|
|
watch: 'warn',
|
|
bad: 'bad',
|
|
};
|
|
|
|
const levelWord: Record<RuntimeLevel, string> = {
|
|
ok: 'Healthy',
|
|
watch: 'Worth watching',
|
|
bad: 'Needs attention',
|
|
};
|
|
|
|
/** RuntimeVerdict is the headline. It replaces a bare goroutine count with the answer that
|
|
* count was standing in for, and when there is something to look at it names the area
|
|
* rather than leaving an operator to work out which number matters. */
|
|
export function RuntimeVerdict({
|
|
health,
|
|
compact,
|
|
}: {
|
|
health: RuntimeHealth;
|
|
compact?: boolean;
|
|
}) {
|
|
const notes = health.notes ?? [];
|
|
return (
|
|
<div className="verdict" data-tone={levelTone[health.level] ?? 'ok'}>
|
|
<div className="verdict-head">
|
|
<Icon name={health.level === 'ok' ? 'check' : 'alert'} />
|
|
<b>{levelWord[health.level] ?? 'Unknown'}</b>
|
|
<span>{health.summary}</span>
|
|
</div>
|
|
{/* On the overview the summary is the whole of it — the card is a signpost, and the
|
|
notes belong on the page it points at. */}
|
|
{compact || notes.length === 0 ? null : (
|
|
<ul className="verdict-notes">
|
|
{notes.map((note, index) => (
|
|
<li key={`${note.area}-${index}`}>
|
|
<Tag tone={levelTone[note.level] ?? 'note'}>{note.area}</Tag>
|
|
<span>{note.message}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** trendWord turns a trend into the half-sentence that sits under a figure. "Steady" is
|
|
* worth printing: it is the answer to the question the figure raises, and a tile that
|
|
* says nothing when nothing is wrong makes the operator check anyway. */
|
|
export function trendWord(trend: RuntimeTrend, format: (value: number) => string): string {
|
|
switch (trend.direction) {
|
|
case 'rising':
|
|
return `rising ${format(Math.abs(trend.perHour))} an hour`;
|
|
case 'falling':
|
|
return `falling ${format(Math.abs(trend.perHour))} an hour`;
|
|
case 'steady':
|
|
return 'steady';
|
|
default:
|
|
return 'gathering history';
|
|
}
|
|
}
|
|
|
|
/** uptime words how long the process has been up. Days matter here and seconds do not: the
|
|
* question this answers is "has it restarted", not "how long exactly". */
|
|
export function uptime(seconds: number): string {
|
|
if (!seconds || seconds < 60) return `${Math.max(0, Math.round(seconds))}s`;
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) return `${minutes} min`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 48) return `${hours}h ${minutes % 60}m`;
|
|
return `${Math.floor(hours / 24)} days`;
|
|
}
|