feat(web): дашборд инстанса в настройках с живыми метриками
Вкладка «Инстанс» сверху показывает нагрузку сервера, ниже — диск и базу, внизу healthcheck с пингом (AGENT.md 7.19). - `Gauge` — полукруглый индикатор: заполняется по значению, меняет цвет на порогах 70 % и 90 %, доступен как `role="meter"`; - процессор и память обновляются раз в секунду (`useInstanceMetrics`), для простоя вкладки опрос останавливается; - два блока диска: «занято / всего» и вес базы (файл + WAL); видимых подписей нет — они раскрываются в тултипе при наведении на значение; - healthcheck: состояние каждой проверки, общий статус, техническая деталь и живой пинг — время ответа инстанса на запрос дашборда в миллисекундах; - переводы ru/en, тесты на индикаторы, формат «занято / всего», тултипы, healthcheck и отсутствие раздела у обычного пользователя.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
interface GaugeProps {
|
||||
/** Заполнение в процентах (0…100). */
|
||||
value: number;
|
||||
/** Подпись для скринридера и тултипа. */
|
||||
label: string;
|
||||
/** Крупное значение в центре (например, «23 %»). */
|
||||
display: string;
|
||||
/** Уточнение под значением (например, «1,2 / 3,4 ГБ»). */
|
||||
caption?: string | undefined;
|
||||
/** Порог, после которого индикатор считается нагруженным. */
|
||||
warnAt?: number;
|
||||
dangerAt?: number;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
/** Радиус и параметры полукруга: дуга 180° от левого края к правому. */
|
||||
const RADIUS = 62;
|
||||
const CENTER_X = 80;
|
||||
const CENTER_Y = 74;
|
||||
const STROKE = 12;
|
||||
const LENGTH = Math.PI * RADIUS;
|
||||
|
||||
/** describeArc собирает путь полукруга (180°) в координатах SVG. */
|
||||
function describeArc(): string {
|
||||
const startX = CENTER_X - RADIUS;
|
||||
const endX = CENTER_X + RADIUS;
|
||||
return `M ${String(startX)} ${String(CENTER_Y)} A ${String(RADIUS)} ${String(RADIUS)} 0 0 1 ${String(endX)} ${String(CENTER_Y)}`;
|
||||
}
|
||||
|
||||
function toneClass(value: number, warnAt: number, dangerAt: number): string {
|
||||
if (value >= dangerAt) {
|
||||
return 'text-danger';
|
||||
}
|
||||
if (value >= warnAt) {
|
||||
return 'text-warning';
|
||||
}
|
||||
return 'text-accent';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gauge — полукруглый индикатор нагрузки: заполняется по значению и меняет
|
||||
* цвет при высокой загрузке. Длина дуги считается через `stroke-dasharray`,
|
||||
* поэтому анимация — обычный CSS-переход (при `prefers-reduced-motion`
|
||||
* анимации отключаются глобально).
|
||||
*/
|
||||
export function Gauge({
|
||||
value,
|
||||
label,
|
||||
display,
|
||||
caption,
|
||||
warnAt = 70,
|
||||
dangerAt = 90,
|
||||
testId,
|
||||
}: GaugeProps) {
|
||||
const clamped = Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0));
|
||||
const filled = (clamped / 100) * LENGTH;
|
||||
const tone = toneClass(clamped, warnAt, dangerAt);
|
||||
|
||||
return (
|
||||
<figure
|
||||
className="flex flex-col items-center gap-1"
|
||||
data-testid={testId}
|
||||
role="meter"
|
||||
aria-label={label}
|
||||
aria-valuenow={Math.round(clamped)}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
title={`${label}: ${display}`}
|
||||
>
|
||||
<svg viewBox="0 0 160 88" className="h-[88px] w-[160px]" aria-hidden="true">
|
||||
<path
|
||||
d={describeArc()}
|
||||
className="stroke-surface-3"
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d={describeArc()}
|
||||
className={`${tone} transition-[stroke-dasharray] duration-[var(--duration-base)] ease-out`}
|
||||
stroke="currentColor"
|
||||
strokeWidth={STROKE}
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
strokeDasharray={`${String(filled)} ${String(LENGTH)}`}
|
||||
data-testid={testId === undefined ? undefined : `${testId}-arc`}
|
||||
/>
|
||||
</svg>
|
||||
<figcaption className="-mt-8 flex flex-col items-center">
|
||||
<span
|
||||
className={`text-xl font-semibold tabular-nums ${tone}`}
|
||||
data-testid={testId === undefined ? undefined : `${testId}-value`}
|
||||
>
|
||||
{display}
|
||||
</span>
|
||||
{caption === undefined ? null : (
|
||||
<span className="text-xs text-fg-muted tabular-nums">{caption}</span>
|
||||
)}
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user