feat(web): второй ряд дашборда и пинг участников в голосовой комнате
- дашборд инстанса: два ряда полукругов с подписями сверху — «Контейнер инстанса» (лимиты профиля) и «Сервер целиком» (вся машина); - голосовая комната: рядом с качеством связи видно пинг участника («Отличное соединение, 42 мс») и он обновляется раз в секунду. Свой RTT измеряется по статистике WebRTC, чужой приходит по data-каналу LiveKit (`glchat.latency`, lossy), поэтому пинг виден для каждого участника; - статистика берётся с любой своей дорожки: в аудио-сессии RTT раньше был недоступен (отчёт читался только с видео); - в «Аудио-видео» появился тумблер «Показывать пинг в голосовой комнате»: выключенный не опрашивает SDK и не публикует своё значение; - тесты: ряд хоста и подписи, пинг в плитке своего и чужого участника, публикация по служебной теме, выключенный пинг не дёргает SDK.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { InstanceMetrics } from '@/api/metrics';
|
||||
import { Card } from '@/components/ui/primitives';
|
||||
import { Gauge } from '@/components/ui/Gauge';
|
||||
import { formatBytes } from '@/lib/format';
|
||||
@@ -10,16 +11,15 @@ interface InstanceDashboardProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Дашборд инстанса: полукруглые индикаторы процессора и памяти, ниже — два
|
||||
* блока про диск (занято / всего) и вес базы данных. Значения обновляются раз
|
||||
* в секунду, подписи у блоков диска показываются только при наведении.
|
||||
* Дашборд инстанса: два ряда полукруглых индикаторов (контейнер и вся машина),
|
||||
* ниже — два блока про диск (занято / всего) и вес базы данных. Значения
|
||||
* обновляются раз в секунду, подписи у блоков диска показываются только при
|
||||
* наведении, а у рядов подпись стоит сверху.
|
||||
*/
|
||||
export function InstanceDashboard({ state }: InstanceDashboardProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const locale = i18n.resolvedLanguage ?? 'ru';
|
||||
const metrics = state.metrics;
|
||||
// До первого ответа показываем прочерк, а не нули: ноль — это тоже значение.
|
||||
const pending = metrics === undefined ? '—' : undefined;
|
||||
|
||||
return (
|
||||
<Card data-testid="instance-dashboard">
|
||||
@@ -30,33 +30,20 @@ export function InstanceDashboard({ state }: InstanceDashboardProps) {
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center justify-around gap-6">
|
||||
<Gauge
|
||||
testId="gauge-cpu"
|
||||
value={metrics?.cpu.percent ?? 0}
|
||||
label={t('settings.instance.dashboard.cpu')}
|
||||
display={pending ?? formatPercent(metrics?.cpu.percent ?? 0, locale)}
|
||||
caption={
|
||||
metrics === undefined
|
||||
? undefined
|
||||
: t('settings.instance.dashboard.cpuCaption', {
|
||||
count: Math.round(metrics.cpu.cores),
|
||||
})
|
||||
}
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
<GaugeRow
|
||||
testId="row-container"
|
||||
title={t('settings.instance.dashboard.containerRow')}
|
||||
hint={t('settings.instance.dashboard.containerHint')}
|
||||
cpu={metrics?.cpu}
|
||||
memory={metrics?.memory}
|
||||
/>
|
||||
<Gauge
|
||||
testId="gauge-memory"
|
||||
value={metrics?.memory.percent ?? 0}
|
||||
label={t('settings.instance.dashboard.memory')}
|
||||
display={pending ?? formatPercent(metrics?.memory.percent ?? 0, locale)}
|
||||
caption={
|
||||
metrics === undefined
|
||||
? undefined
|
||||
: `${formatBytes(metrics.memory.used_bytes, locale)} / ${formatBytes(
|
||||
metrics.memory.total_bytes,
|
||||
locale,
|
||||
)}`
|
||||
}
|
||||
<GaugeRow
|
||||
testId="row-host"
|
||||
title={t('settings.instance.dashboard.hostRow')}
|
||||
hint={t('settings.instance.dashboard.hostHint')}
|
||||
cpu={metrics?.host_cpu}
|
||||
memory={metrics?.host_memory}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -91,6 +78,58 @@ export function InstanceDashboard({ state }: InstanceDashboardProps) {
|
||||
);
|
||||
}
|
||||
|
||||
interface GaugeRowProps {
|
||||
testId: string;
|
||||
/** Подпись ряда: к чему относятся индикаторы (контейнер или вся машина). */
|
||||
title: string;
|
||||
hint: string;
|
||||
cpu: InstanceMetrics['cpu'] | undefined;
|
||||
memory: InstanceMetrics['memory'] | undefined;
|
||||
}
|
||||
|
||||
/** GaugeRow — ряд из двух полукругов с подписью, к чему он относится. */
|
||||
function GaugeRow({ testId, title, hint, cpu, memory }: GaugeRowProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const locale = i18n.resolvedLanguage ?? 'ru';
|
||||
const pending = cpu === undefined && memory === undefined ? '—' : undefined;
|
||||
|
||||
return (
|
||||
<section data-testid={testId}>
|
||||
<header className="flex items-baseline justify-between gap-2">
|
||||
<h4 className="text-sm font-medium">{title}</h4>
|
||||
<span className="text-xs text-fg-muted">{hint}</span>
|
||||
</header>
|
||||
<div className="mt-1 flex flex-wrap items-center justify-around gap-6">
|
||||
<Gauge
|
||||
testId={`${testId}-cpu`}
|
||||
value={cpu?.percent ?? 0}
|
||||
label={t('settings.instance.dashboard.cpu')}
|
||||
display={pending ?? formatPercent(cpu?.percent ?? 0, locale)}
|
||||
caption={
|
||||
cpu === undefined
|
||||
? undefined
|
||||
: t('settings.instance.dashboard.cpuCaption', { count: Math.round(cpu.cores) })
|
||||
}
|
||||
/>
|
||||
<Gauge
|
||||
testId={`${testId}-memory`}
|
||||
value={memory?.percent ?? 0}
|
||||
label={t('settings.instance.dashboard.memory')}
|
||||
display={pending ?? formatPercent(memory?.percent ?? 0, locale)}
|
||||
caption={
|
||||
memory === undefined
|
||||
? undefined
|
||||
: `${formatBytes(memory.used_bytes, locale)} / ${formatBytes(
|
||||
memory.total_bytes,
|
||||
locale,
|
||||
)}`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface DiskStatProps {
|
||||
value: string;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user