Files
forgetting/awg-ui/src/components/ServerBar.tsx
T
maeneko 53d5a08bbd Add shaping, Prometheus, monitor tab and peer audit
Baseline TypeScript version before the Go rewrite: CAKE fair sharing with
automatic deprioritisation of heavy devices, per-key priority, /metrics with
a metrics token, the built-in Prometheus install/remove, the Monitor tab,
grouped sidebar tabs and the dead-peer audit.
2026-10-14 13:27:23 +00:00

150 lines
8.0 KiB
TypeScript

// Copyright (c) 2026 Ivan Vasilev
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
import { type NodeInfo, genLabel, cmpVersion } from '../lib/shared';
import { IcoRefresh, IcoPlus, IcoTrash, IcoCopy, IcoSettings } from './icons';
// Сервер-бар: карточки серверов (core-локальный и ноды) + «Добавить сервер» и
// (на мобильной) перезапуск AWG под ними. Общий для вкладок — показывает
// серверы, между которыми переключается содержимое вкладки (пиры / абоненты).
// Версия Forgetting на сервере. Нода, которая отстаёт от панели или не сообщает версию
// (агент старше 0.3.4), подсвечена: её стоит обновить — install.sh, «Обновить».
function versionChip(n: NodeInfo, panel: string | null) {
if (n.pending) return null;
let tip = '', warn = false;
if (!n.version) {
warn = true;
tip = 'Версия неизвестна: нода старше 0.3.4 — обнови её (install.sh → «Обновить»)';
} else if (panel && !n.local) {
const d = cmpVersion(n.version, panel);
if (d < 0) { warn = true; tip = `Панель на ${panel} — обнови ноду (install.sh → «Обновить»)`; }
else if (d > 0) { warn = true; tip = `Нода новее панели (${panel}) — обнови панель`; }
}
const chip = (
<span className={`chip server-ver-chip${warn ? ' server-ver-chip--warn' : ''}`}>
{n.version ? `v${n.version}` : 'v?.?.?'}
</span>
);
return tip ? <span className="tip-wrap server-gen-wrap" data-tip={tip}>{chip}</span> : chip;
}
export default function ServerBar({
nodes, serverId, hubEnabled, panelVersion, onSelect, onAdd, onJoin, onDelete, onSettings,
onRestartAwg, restarting, showRestart = true,
}: {
nodes: NodeInfo[];
panelVersion: string | null;
serverId: number | null;
hubEnabled: boolean;
onSelect: (id: number) => void;
onAdd: () => void;
onJoin: (n: NodeInfo) => void;
onDelete: (n: NodeInfo) => void;
onSettings: (n: NodeInfo) => void;
onRestartAwg: () => void;
restarting: boolean;
showRestart?: boolean;
}) {
return (
<div className="server-bar">
{nodes.map(n => {
const h = n.health;
const active = n.id === serverId;
return (
<div
key={n.id}
className={`server-card server-card--${active ? 'active' : 'inactive'}`}
role="button"
tabIndex={0}
aria-pressed={active}
onClick={() => onSelect(n.id)}
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(n.id); } }}
>
{/* toggle — виден на мобильном вместо чипа */}
<div className={`server-toggle${n.online ? ' server-toggle--on' : ''}`} aria-hidden="true">
<div className="server-toggle-thumb" />
</div>
{/* чип онлайн/офлайн — виден на десктопе */}
<span className={`chip chip--${n.online ? 'online' : 'offline'} server-online-chip`}>
<span className="chip-dot" />
</span>
<div className="server-card-info">
<span className="server-card-name">{h?.server || n.name}</span>
{n.pending ? (
<span className="server-card-ip">ждёт подключения</span>
) : !n.online ? (
<span className="server-card-ip">нет связи</span>
) : h && (
<span className="server-card-ip">{h.ip}</span>
)}
{/* Плашки: поколение AWG (только на связи) и версия Forgetting (известна и без связи) */}
{!n.pending && (
<span className="server-chips">
{h && n.online && ((h.module || h.tools) ? (
<span
className="tip-wrap server-gen-wrap"
data-tip={`модуль ${h.module || '?'} · tools ${h.tools || '?'}`}
>
<span className="chip chip--offline server-gen-chip">{genLabel(h.gen)}</span>
</span>
) : (
<span className="chip chip--offline server-gen-chip">{genLabel(h.gen)}</span>
))}
{versionChip(n, panelVersion)}
</span>
)}
</div>
{h && n.online && <span className="server-card-peers">{h.peers} peers</span>}
{n.pending && (
<button
className="btn-icon btn-icon--neutral"
aria-label="Новая ссылка подключения"
onClick={e => { e.stopPropagation(); onJoin(n); }}
>
<IcoCopy />
</button>
)}
{/* Настройки сервера: у ноды, которая ещё не подключалась, настраивать нечего */}
{!n.pending && n.online && (
<button
className="btn-icon btn-icon--neutral"
aria-label={`Настройки сервера ${n.name}`}
onClick={e => { e.stopPropagation(); onSettings(n); }}
>
<IcoSettings />
</button>
)}
{!n.local && (
<button
className="btn-icon btn-icon--danger"
aria-label={`Удалить сервер ${n.name}`}
onClick={e => { e.stopPropagation(); onDelete(n); }}
>
<IcoTrash />
</button>
)}
</div>
);
})}
<span
className="tip-wrap"
data-tip={hubEnabled ? '' : 'Приём нод выключен: задайте NODE_PORT'}
>
<button className="btn server-add" onClick={onAdd} disabled={!hubEnabled}>
<IcoPlus /> Добавить сервер
</button>
</span>
{/* Перезапуск AWG — только на мобильной (на десктопе кнопка в углу). */}
{showRestart && serverId !== null && (
<button
className={`btn btn--tonal awg-restart-mobile${restarting ? ' spinning' : ''}`}
onClick={onRestartAwg}
disabled={restarting}
>
<IcoRefresh /> {restarting ? 'Перезапуск…' : 'Перезапустить AWG'}
</button>
)}
</div>
);
}