// SVG-мокапы лицевых панелей MikroTik. Подсвечивают живые порты по InterfaceInfo[].
// Сейчас реализован hAP ac lite (RB952Ui-5ac2nD): синий корпус, 5 ethernet,
// первый — PoE in (Internet), 2–4 LAN, 5 — PoE out (оранжевая обводка).
import { InterfaceInfo } from '@/api/client';
export interface DeviceMockupProps {
/** Имя модели из RouterOS (board-name), например "hAP ac lite". */
boardName: string | null | undefined;
/** Текущий снимок интерфейсов с устройства. */
interfaces: InterfaceInfo[];
}
const isHapAcLite = (b?: string | null): boolean =>
!!b && /h\s*A\s*P\s*ac\s*lite/i.test(b);
// hAP ac² (RBD52G-5HacD2HnD): отличаем по цифре «2» / «²» после «ac»,
// чтобы случайно не перехватить hAP ac lite.
const isHapAc2 = (b?: string | null): boolean =>
!!b && (/h\s*A\s*P\s*ac[\s\^]*[²2]/i.test(b) || /RBD52G/i.test(b));
const isHapLike = (b?: string | null): boolean => !!b && /\bh\s*A\s*P\b/i.test(b);
const isRb5009 = (b?: string | null): boolean =>
!!b && /RB?\s*5009/i.test(b);
const isChr = (b?: string | null): boolean =>
!!b && /\bCHR\b/i.test(b);
const isHexS = (b?: string | null): boolean =>
!!b && /h\s*EX\s*S|RB?\s*760/i.test(b);
const isL009 = (b?: string | null): boolean =>
!!b && /\bL\s*009/i.test(b);
const isRb4011 = (b?: string | null): boolean =>
!!b && /RB?\s*4011/i.test(b);
// Найти интерфейс по базовому имени, допуская суффиксы вида `ether1-Uztelecom`,
// `ether2_LAN`, `ether3 description` и т.п. Сначала пробуем точное совпадение, потом по префиксу.
function findPort(interfaces: InterfaceInfo[], baseName: string): InterfaceInfo | undefined {
const exact = interfaces.find((x) => x.name === baseName);
if (exact) return exact;
const re = new RegExp(`^${baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\-_.:]|$)`, 'i');
return interfaces.find((x) => re.test(x.name));
}
// Цвета порта по статусу.
function portColor(it: InterfaceInfo | undefined): { fill: string; stroke: string; label: string } {
if (!it) return { fill: '#0c0c0c', stroke: '#3a3a3a', label: 'нет данных' };
if (it.disabled) return { fill: '#1a1a1a', stroke: '#5b5b5b', label: 'отключён' };
if (it.running) return { fill: '#0a3a14', stroke: '#22c55e', label: 'up' };
return { fill: '#1a1a1a', stroke: '#ef4444', label: 'down' };
}
export default function DeviceMockup({ boardName, interfaces }: DeviceMockupProps) {
if (isHapAcLite(boardName)) {
return ;
}
if (isHapAc2(boardName)) {
return ;
}
if (isHapLike(boardName) && interfaces.filter((it) => /^ether/.test(it.name)).length === 5) {
return ;
}
if (isRb5009(boardName)) {
return ;
}
if (isRb4011(boardName)) {
return ;
}
if (isHexS(boardName)) {
return ;
}
if (isL009(boardName)) {
return ;
}
if (isChr(boardName)) {
return ;
}
return (
Мокап для модели {boardName || '—'} ещё не подготовлен.
Статусы интерфейсов смотрите во вкладке «Интерфейсы».
);
}
// --------- hAP ac lite ---------
function HapAcLiteMockup({ interfaces }: { interfaces: InterfaceInfo[] }) {
const byName = new Map(interfaces.map((it) => [it.name, it]));
// Раскладка портов: ether1 = Internet/PoE in, ether2..ether4 = LAN, ether5 = PoE out.
const ports = [
{ name: 'ether1', label: 'Internet', poe: 'in' as const },
{ name: 'ether2', label: '2', poe: null as const },
{ name: 'ether3', label: '3', poe: null as const },
{ name: 'ether4', label: '4', poe: null as const },
{ name: 'ether5', label: '5', poe: 'out' as const },
];
// Размеры в условных единицах — масштабируются через viewBox.
const W = 1180, H = 230;
const bodyR = 14;
const portW = 130, portH = 110;
const firstPortX = 360;
const portGap = 12;
const portsTopY = 50;
return (
Лицевая панель hAP ac lite · подсветка портов в реальном времени
{/* Легенда */}
up (running)
down
disabled / нет данных
);
}
// --------- hAP ac² ---------
// Чёрный пластиковый корпус (RBD52G-5HacD2HnD).
// Слева: DC 12-28V, утопленная кнопка res/wps, индикаторы pwr / usr.
// Справа: 5 GigE портов — ether1 «Internet/PoE in», ether2..ether5 «LAN».
// PoE-out нет (в отличие от hAP ac lite).
function HapAc2Mockup({ interfaces }: { interfaces: InterfaceInfo[] }) {
const ports = [
{ name: 'ether1', label: '1', accent: 'poe-in' as const },
{ name: 'ether2', label: '2', accent: null as const },
{ name: 'ether3', label: '3', accent: null as const },
{ name: 'ether4', label: '4', accent: null as const },
{ name: 'ether5', label: '5', accent: null as const },
];
// Соотношение фото задней панели ~4.3:1. При height: 62px ширина ≈ 268px.
const W = 1180, H = 274;
const bodyR = 20;
const portW = 130, portH = 130;
const portGap = 14;
const firstPortX = 410;
const portsTopY = 60;
const lanStartX = firstPortX + portW + portGap;
const lanSpanW = 4 * portW + 3 * portGap;
return (
Лицевая панель hAP ac² · подсветка портов в реальном времени
);
}
// --------- RB5009UG+S+ ---------
// Чёрный корпус, 8 GigE портов (ether1..ether8) + 1 SFP+ (sfp-sfpplus1).
// Слева: DC jack 12-57V, кнопка R (reset), USB 3.0 порт.
// ether1 — PoE in (жёлтая обводка), ether8 — 2.5GbE (синяя обводка), sfp-sfpplus1 — 10G.
function Rb5009Mockup({ interfaces }: { interfaces: InterfaceInfo[] }) {
const byName = new Map(interfaces.map((it) => [it.name, it]));
const W = 520, H = 66;
const portW = 32, portH = 32, gap = 3;
const portsY = (H - portH) / 2 - 1;
const portsStartX = 132;
const sfpW = 60;
const sfp = findPort(interfaces, 'sfp-sfpplus1') || findPort(interfaces, 'sfpplus1');
const ports = [
{ name: 'ether1', label: '1', accent: 'poe' as const },
{ name: 'ether2', label: '2', accent: null as const },
{ name: 'ether3', label: '3', accent: null as const },
{ name: 'ether4', label: '4', accent: null as const },
{ name: 'ether5', label: '5', accent: null as const },
{ name: 'ether6', label: '6', accent: null as const },
{ name: 'ether7', label: '7', accent: null as const },
{ name: 'ether8', label: '8', accent: '2g5' as const },
];
const accentColor = (a: 'poe' | '2g5' | null) =>
a === 'poe' ? '#f0851a' : a === '2g5' ? '#2563eb' : null;
const sfpX = portsStartX + ports.length * (portW + gap) + 6;
return (
Лицевая панель RB5009UG+S+ · подсветка портов в реальном времени
);
}
// --------- RB4011iGS+ ---------
// Чёрный корпус 1U: слева RESET + PWR LED, затем SFP+ слот, 5 GigE портов (1-5, PoE-in 18-57V на ether1),
// центральная LED-матрица статусов (1-5 сверху, 6-10 снизу) и 5 GigE портов (6-10, PoE-out на ether10).
function Rb4011Mockup({ interfaces }: { interfaces: InterfaceInfo[] }) {
const W = 500, H = 66;
const portW = 32, portH = 32, gap = 3;
const portsY = (H - portH) / 2 - 1;
const sfpW = 50;
const sfpX = 30;
const group1StartX = sfpX + sfpW + 4;
const ledBlockW = 24;
const ledBlockGap = 4;
const group2StartX =
group1StartX + 5 * (portW + gap) - gap + ledBlockGap + ledBlockW + ledBlockGap;
const sfp = findPort(interfaces, 'sfp-sfpplus1') || findPort(interfaces, 'sfpplus1');
const portsLeft = [
{ name: 'ether1', label: '1' },
{ name: 'ether2', label: '2' },
{ name: 'ether3', label: '3' },
{ name: 'ether4', label: '4' },
{ name: 'ether5', label: '5' },
];
const portsRight = [
{ name: 'ether6', label: '6' },
{ name: 'ether7', label: '7' },
{ name: 'ether8', label: '8' },
{ name: 'ether9', label: '9' },
{ name: 'ether10', label: '10' },
];
return (
Лицевая панель RB4011iGS+ · подсветка портов в реальном времени
);
}
// --------- CHR (Cloud Hosted Router) ---------
// Виртуальная машина MikroTik — нет физической панели.
// Простой белый прямоугольник: слева лейбл «CHR», справа порты ether* в ряд.
// Количество портов — динамическое (сколько отдало устройство).
function ChrMockup({ interfaces }: { interfaces: InterfaceInfo[] }) {
const ports = interfaces
.filter((it) => /^ether/i.test(it.name))
.sort((a, b) => {
const ai = parseInt(a.name.replace(/\D/g, ''), 10) || 0;
const bi = parseInt(b.name.replace(/\D/g, ''), 10) || 0;
return ai - bi;
});
// Фиксированные размеры: 500×66 px. SVG в viewBox 1:1 пикселям, scale=1.
// Порты 30×32 px начинаются после блока «mikrotik» слева, если все не помещаются —
// их можно прокрутить горизонтально через overflow-x-auto обёртки.
const W = 500;
const H = 66;
const padX = 6;
const labelW = 92;
const gap = 4;
const portW = 30;
const portH = 32;
const portsY = (H - portH) / 2 - 2;
const portsStartX = padX + labelW + 6;
return (
Виртуальный роутер MikroTik CHR · подсветка портов в реальном времени
{/* Легенда */}
up (running)
down
disabled / нет данных
);
}
// --------- hEX S (RB760iGS) ---------
// Тёмно-серый корпус, Power DC + лого, SFP, 5 GigE портов.
// ether1 = INTERNET / PoE in, ether2-4 = LAN, ether5 = PoE out (оранжевый), sfp1.
function HexSMockup({ interfaces }: { interfaces: InterfaceInfo[] }) {
const byName = new Map(interfaces.map((it) => [it.name, it]));
const W = 320, H = 66;
const padX = 4;
const portW = 32, portH = 32, gap = 3;
const portsY = (H - portH) / 2 - 1;
const portsStartX = 96;
const sfp = findPort(interfaces, 'sfp1') || findPort(interfaces, 'sfp-sfpplus1');
const ports = [
{ name: 'ether1', label: '1', accent: 'poe-in' as const },
{ name: 'ether2', label: '2', accent: null as const },
{ name: 'ether3', label: '3', accent: null as const },
{ name: 'ether4', label: '4', accent: null as const },
{ name: 'ether5', label: '5', accent: 'poe-out' as const },
];
return (
Лицевая панель hEX S · подсветка портов в реальном времени
);
}
// --------- L009 (L009UiGS-RM) ---------
// Красный 19" rack: RES, DC 24-56V, SFP, USB 3.0, 8 GigE портов.
// ether1 = PoE in, ether8 = PoE out (оранжевый), sfp1.
function L009Mockup({ interfaces }: { interfaces: InterfaceInfo[] }) {
const byName = new Map(interfaces.map((it) => [it.name, it]));
const W = 480, H = 66;
const portW = 36, portH = 32, gap = 3;
const portsY = (H - portH) / 2 - 1;
// Слева до портов: RES + DC + SFP + USB ≈ 110px
const portsStartX = 116;
// Между ether4 и ether5 — небольшой визуальный разрыв
const groupGap = 8;
const sfp = findPort(interfaces, 'sfp1');
const ports = [
{ name: 'ether1', label: '1', accent: 'poe-in' as const },
{ name: 'ether2', label: '2', accent: null as const },
{ name: 'ether3', label: '3', accent: null as const },
{ name: 'ether4', label: '4', accent: null as const },
{ name: 'ether5', label: '5', accent: null as const },
{ name: 'ether6', label: '6', accent: null as const },
{ name: 'ether7', label: '7', accent: null as const },
{ name: 'ether8', label: '8', accent: 'poe-out' as const },
];
const xOf = (i: number) => portsStartX + i * (portW + gap) + (i >= 4 ? groupGap : 0);
return (
Лицевая панель L009UiGS · подсветка портов в реальном времени
);
}
// Общая мини-легенда для физических мокапов.
function MockupLegend() {
return (
up
down
disabled
);
}