0.2.78
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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`;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { matchPath, useLocation } from 'react-router-dom';
|
||||
import { Glyph, Icon, type IconName } from './Icon';
|
||||
import type { Tone } from '../lib/format';
|
||||
import { allNavItems } from '../nav';
|
||||
import { useDocumentTitle } from '../lib/hooks';
|
||||
|
||||
/* The component vocabulary. Every page is built from what is below and adds nothing of its
|
||||
own: a screen that needs a look it cannot get from here is a missing component, not a
|
||||
@@ -27,6 +28,9 @@ export function PageHead({
|
||||
const pageIcon = icon ?? allNavItems.find((item) =>
|
||||
matchPath({ path: item.path, end: true }, location.pathname),
|
||||
)?.icon;
|
||||
// The tab is named after the heading, so the two can never disagree — see
|
||||
// useDocumentTitle for why this hangs off PageHead rather than off the router.
|
||||
useDocumentTitle(title);
|
||||
return (
|
||||
<header className="page-head">
|
||||
{crumbs ? <nav className="crumbs">{crumbs}</nav> : null}
|
||||
@@ -480,6 +484,18 @@ export function KeyValue({ rows }: { rows: { label: string; value: ReactNode }[]
|
||||
);
|
||||
}
|
||||
|
||||
/** Subhead titles a section inside a card. Two pages had already hand-rolled an h4 with
|
||||
* their own rules; a screen that needs a look of its own is a missing component here, not
|
||||
* a licence for a style attribute. */
|
||||
export function Subhead({ children, aside }: { children: ReactNode; aside?: ReactNode }) {
|
||||
return (
|
||||
<h4 className="subhead">
|
||||
<span>{children}</span>
|
||||
{aside ? <small>{aside}</small> : null}
|
||||
</h4>
|
||||
);
|
||||
}
|
||||
|
||||
/** PlainTiles is a tile strip with no card around it, for a group of numbers inside one. */
|
||||
export function PlainTiles({ tiles }: { tiles: TileSpec[] }) {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user