// 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 · подсветка портов в реальном времени
{/* Корпус */} {/* Power разъём + подпись */} Power DC10-28V {/* hAPaclite лого */} hAP ac lite {/* WiFi-дуга над лого */} {/* RES (кнопка с кругом и подписью WPS) */} RES WPS {/* PWR кнопка (квадрат) */} PWR {/* USR светодиод */} USR {/* Тёмная полоса фоны для верхних/нижних лейблов */} {/* Оранжевая зона PoE out над портом 5 */} {/* Оранжевая зона PoE out внизу */} {/* Порты */} {ports.map((p, i) => { const x = firstPortX + i * (portW + portGap); const it = findPort(interfaces, p.name); const col = portColor(it); return ( {/* Верхний лейбл (Internet / 2 / 3 / 4 / 5) */} {p.label} {/* Корпус порта (металлический ободок) */} {/* Внутренний экран порта */} {/* RJ45 «зубчики» */} {/* LED-индикатор (точка) */} {/* Имя интерфейса под портом для понятности */} {p.name} {/* Тултип через */} {p.name} ({p.label}){p.poe === 'in' ? ' · PoE in' : p.poe === 'out' ? ' · PoE out' : ''} {'\n'}статус: {col.label} {it?.comment ? `\ncomment: ${it.comment}` : ''} {it?.mac_address ? `\nmac: ${it.mac_address}` : ''} ); })} {/* Нижние подписи: PoE in / LAN / PoE out */} PoE in LAN PoE out
{/* Легенда */}
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² · подсветка портов в реальном времени
{/* Чёрный пластиковый корпус */} {/* Утопленная плашка отсека (чуть темнее, со внутренней тенью обводки) */} {/* DC разъём */} DC 12-28V {/* res/wps — утопленная кнопка */} res/wps {/* pwr LED */} pwr {/* usr LED */} usr {/* Цифры над портами */} {ports.map((p, i) => { const x = firstPortX + i * (portW + portGap); return ( {p.label} ); })} {/* Порты */} {ports.map((p, i) => { const x = firstPortX + i * (portW + portGap); const it = findPort(interfaces, p.name); const col = portColor(it); return ( {/* Металлический ободок RJ45 */} {/* Внутренний экран — закрашивается под статус */} {/* RJ45 «зубчики» */} {/* LED-индикатор линка */} {/* Имя интерфейса под портом — мелким шрифтом, чтобы не сливалось с подписями групп */} {p.name} {p.name} (порт {p.label}){p.accent === 'poe-in' ? ' · Internet / PoE in' : ' · LAN'} {'\n'}статус: {col.label} {it?.comment ? `\ncomment: ${it.comment}` : ''} {it?.mac_address ? `\nmac: ${it.mac_address}` : ''} ); })} {/* Группа Internet/PoE in под портом 1 */} Internet/PoE in {/* Группа LAN под портами 2-5 */} LAN
); } // --------- 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+ · подсветка портов в реальном времени
{/* Чёрный корпус */} {/* DC jack */} 12-57V DC DC IN {/* RES */} R RES {/* USB 3.0 */} USB USB 3.0 {/* PWR/USR LED */} PWR USR {/* Лейблы цифр над портами + полоса акцента (PoE/2.5G) */} {ports.map((p, i) => { const x = portsStartX + i * (portW + gap); const accent = accentColor(p.accent); return ( {accent && ( )} {p.label} ); })} {/* Порты */} {ports.map((p, i) => { const x = portsStartX + i * (portW + gap); const it = findPort(interfaces, p.name); const col = portColor(it); return ( {p.name} (порт {p.label}) {p.accent === 'poe' ? ' · PoE in' : ''} {p.accent === '2g5' ? ' · 2.5 GbE' : ''} {'\n'}статус: {col.label} {it?.comment ? `\ncomment: ${it.comment}` : ''} {it?.mac_address ? `\nmac: ${it.mac_address}` : ''} ); })} {/* SFP+ слот */} {(() => { const col = portColor(sfp); return ( SFP+ 10G SFP+ sfp-sfpplus1 · 10 GbE SFP+ {'\n'}статус: {col.label} {sfp?.comment ? `\ncomment: ${sfp.comment}` : ''} ); })()} {/* Подписи акцентов снизу */} PoE in 2.5G
); } // --------- 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+ · подсветка портов в реальном времени
{/* Чёрный корпус */} {/* RESET кнопка */} RESET {/* PWR LED */} PWR {/* SFP+ слот */} {(() => { const col = portColor(sfp); return ( SFP+ SFP+ 10G sfp-sfpplus1 · 10 GbE SFP+ {'\n'}статус: {col.label} {sfp?.comment ? `\ncomment: ${sfp.comment}` : ''} ); })()} {/* Акцентная полоска PoE-in над ether1 */} {/* Лейблы цифр над портами 1-5 */} {portsLeft.map((p, i) => { const x = group1StartX + i * (portW + gap); return ( {p.label} ); })} {/* Порты 1-5 */} {portsLeft.map((p, i) => { const x = group1StartX + i * (portW + gap); const it = findPort(interfaces, p.name); const col = portColor(it); const isPoeIn = i === 0; return ( {p.name} (порт {p.label}){isPoeIn ? ' · PoE in 18-57V' : ''} {'\n'}статус: {col.label} {it?.comment ? `\ncomment: ${it.comment}` : ''} {it?.mac_address ? `\nmac: ${it.mac_address}` : ''} ); })} {/* Подпись группы 1-5 снизу */} PoE in 18-57V {/* Центральная LED-матрица статусов */} {(() => { const lx = group1StartX + 5 * (portW + gap) - gap + ledBlockGap; const cy1 = portsY + 9; const cy2 = portsY + portH - 9; return ( {[0, 1, 2, 3, 4].map((i) => { const cx = lx + 3.5 + i * 4.2; const top = findPort(interfaces, `ether${i + 1}`); const bot = findPort(interfaces, `ether${i + 6}`); return ( {top ? `ether${i + 1}: ${top.running ? 'up' : top.disabled ? 'disabled' : 'down'}` : `ether${i + 1}: нет данных`} {bot ? `ether${i + 6}: ${bot.running ? 'up' : bot.disabled ? 'disabled' : 'down'}` : `ether${i + 6}: нет данных`} ); })} ); })()} {/* Акцентная полоска PoE-out над ether10 */} {/* Лейблы цифр над портами 6-10 */} {portsRight.map((p, i) => { const x = group2StartX + i * (portW + gap); return ( {p.label} ); })} {/* Порты 6-10 */} {portsRight.map((p, i) => { const x = group2StartX + i * (portW + gap); const it = findPort(interfaces, p.name); const col = portColor(it); const isPoeOut = i === 4; return ( {p.name} (порт {p.label}){isPoeOut ? ' · PoE out' : ''} {'\n'}статус: {col.label} {it?.comment ? `\ncomment: ${it.comment}` : ''} {it?.mac_address ? `\nmac: ${it.mac_address}` : ''} ); })} {/* Подпись группы 6-10 снизу */} PoE out
); } // --------- 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 · подсветка портов в реальном времени
{/* Белый фон-корпус */} {/* Лейбл mikrotik слева (шрифт в 2 раза мельче) */} mikrotik Cloud Hosted Router {/* Разделитель */} {/* Порты */} {ports.length === 0 && ( нет интерфейсов ether* )} {ports.map((it, i) => { const x = portsStartX + i * (portW + gap); const col = portColor(it); // Короткий лейбл — только номер порта (ether7 → "7"). const num = (it.name.match(/(\d+)$/) || [, it.name])[1]; return ( {/* Корпус виртуального порта */} {/* Номер порта внутри */} {num} {/* Имя интерфейса под портом */} {it.name} {it.name} {it.type ? ` · ${it.type}` : ''} {'\n'}статус: {col.label} {it.comment ? `\ncomment: ${it.comment}` : ''} {it.mac_address ? `\nmac: ${it.mac_address}` : ''} ); })}
{/* Легенда */}
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 · подсветка портов в реальном времени
{/* Корпус тёмно-серый */} {/* Power разъём + подпись */} Power 12-57V DC {/* hEX s лого */} hEX s {/* SFP слот */} {(() => { const col = portColor(sfp); return {sfp ? `${sfp.name} · SFP\nстатус: ${col.label}` : 'SFP · нет данных'} ; })()} SFP INTERNET {/* Passive/af/at подпись над портом 1 */} Passive/af/at {/* Оранжевая зона над/под портом 5 (PoE out) */} {/* Лейблы цифр над портами 2-5 */} {ports.slice(1).map((p, idx) => { const i = idx + 1; const x = portsStartX + i * (portW + gap); return ( {p.label} ); })} {/* Порты */} {ports.map((p, i) => { const x = portsStartX + i * (portW + gap); const it = findPort(interfaces, p.name); const col = portColor(it); return ( {p.name} (порт {p.label}){p.accent === 'poe-in' ? ' · PoE in' : p.accent === 'poe-out' ? ' · PoE out' : ''}{'\n'}статус: {col.label}{it?.comment ? `\ncomment: ${it.comment}` : ''} ); })} {/* Нижние подписи */} PoE in LAN PoE out
); } // --------- 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 · подсветка портов в реальном времени
{/* Красный корпус */} {/* RES кнопка */} RES {/* power led */} {/* DC разъём */} 24-56 V DC ⊖-⊙-⊕ {/* SFP слот */} SFP {(() => { const col = portColor(sfp); return {sfp ? `${sfp.name} · SFP\nстатус: ${col.label}` : 'SFP · нет данных'} ; })()} SFP {/* USB 3.0 */} USB USB 3.0 {/* Оранжевая зона над/под портом 8 (PoE out) */} {/* Лейблы цифр над портами */} {ports.map((p, i) => ( {p.label} ))} {/* Порты */} {ports.map((p, i) => { const x = xOf(i); const it = findPort(interfaces, p.name); const col = portColor(it); return ( {p.name} (порт {p.label}){p.accent === 'poe-in' ? ' · PoE in' : p.accent === 'poe-out' ? ' · PoE out' : ''}{'\n'}статус: {col.label}{it?.comment ? `\ncomment: ${it.comment}` : ''} ); })} {/* Нижние подписи скоростей */} PoE in PoE out
); } // Общая мини-легенда для физических мокапов. function MockupLegend() { return (
up down disabled
); }