Згортання меню, лого з фавіконом, вирівнювання полів із підказкою
МЕНЮ. Шість груп, разом 1246 px при вікні 720 — довше за екран. Тепер згортаються, стан живе в localStorage як перелік ЗГОРНУТИХ (не розгорнутих: інакше кожна нова група приїхала б до наявних користувачів схованою). Ключ групи окремо від заголовка — підписи тут уже переписували, і якби ключем був заголовок, кожне переписування слова тихо скидало б налаштування всім. Група з активною сторінкою розгортається сама, але рівно одна — вибір для решти не чіпається. Правка стану на ЗМІНІ шляху, а не на кожному рендері, тож згорнути активну групу теж можна. Дві наявні поломки мобільного вигляду, знайдені дорогою: рейка вмикалась класами md:, а розгалуження заголовок/риска було на JS і про md: не знало — людина згортала панель на моніторі, і в шухляді на телефоні ЗНИКАЛИ ВСІ назви розділів. ЛОГО. Пульс на лінії зв'язку: вузол → сплеск → провал → норма → вузол. Два вузли по краях — ключова деталь: без них це іконка «activity» з будь-якого безкоштовного набору, з ними кардіограма стає лінією між двома точками мережі. SVG кодом, не растр: слідує темі через currentColor, важить сотні байтів. Три РІЗНІ малюнки, а не один масштабований: повне лого, знак, і спрощений для 16 px усередині favicon.ico (зібраний власним кодувальником на zlib, без сторонніх інструментів). Пульсація вузла — опційна й типово вимкнена: у шапці вона конкурувала б зі справжнім показником зв'язку, який стоїть поруч. ПОЛЯ З ПІДКАЗКОЮ. Field малює підказку під контролем, тож у ряду з вирівнюванням по низу колонка з підказкою вища — її підпис і поле спливають угору, а гола кнопка з'їжджає вниз. Видно було в словнику трапів; та сама вада знайшлась у редакторі фільтра портів. Ряди вирівняно по верху, а кнопки без підпису загорнуто у FieldGap — порожній підпис такої самої висоти. Відступ у пікселях полагодив би один ряд і роз'їхався б від зміни розміру шрифту. Перевірено очима в живому Chrome: іконки віддаються (4/4, правильні типи й розміри), лого 64×64 на сторінці входу, праве коло справді sky-400 і пульсує лише там.
This commit is contained in:
parent
08801f631c
commit
c7fecfba0b
14 changed files with 286 additions and 77 deletions
|
|
@ -3,7 +3,63 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NetPulse</title>
|
||||
|
||||
<title>NetPulse — моніторинг мережі</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="NetPulse — моніторинг мережі та керування конфігураціями мережевого обладнання."
|
||||
/>
|
||||
|
||||
<!--
|
||||
Іконки. Порядок у розмітці не має значення — браузер обирає сам, —
|
||||
але набір має: SVG для тих, хто вміє, ICO для решти.
|
||||
|
||||
favicon.svg перший і без sizes="any" не обійтися: без цієї
|
||||
підказки Chrome вважає, що ICO придатніший, і масштабує растр там,
|
||||
де міг би намалювати вектор.
|
||||
-->
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" sizes="any" />
|
||||
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
|
||||
<!-- ICO лежить ще й у корені під власним іменем: за ним браузери й
|
||||
агрегатори стукають наосліп, не читаючи розмітки. -->
|
||||
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
|
||||
|
||||
<!--
|
||||
Колір смуги браузера й системних елементів. Один на обидві схеми
|
||||
навмисно: інтерфейс NetPulse темний завжди (див. web/src/index.css),
|
||||
тож світла смуга над темною сторінкою була б швом, а не темою.
|
||||
-->
|
||||
<meta name="theme-color" content="#020617" />
|
||||
<!--
|
||||
Сторінка темна — і браузер має знати про це до того, як застосує
|
||||
власні стилі. Інакше поля вводу, випадні списки й смуги прокрутки
|
||||
малюються світлими доти, доки не завантажиться CSS.
|
||||
-->
|
||||
<meta name="color-scheme" content="dark" />
|
||||
|
||||
<!--
|
||||
Збережено на робочий стіл iPad — типовий сценарій для чергового в
|
||||
NOC. status-bar-style саме black, а не black-translucent: другий
|
||||
пускає вміст під годинник, а верстка про це не знає й ховає під
|
||||
ним шапку.
|
||||
-->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<meta name="apple-mobile-web-app-title" content="NetPulse" />
|
||||
|
||||
<!--
|
||||
Тло тут, а не лише в index.css: CSS приїжджає модулем разом із JS,
|
||||
і до того браузер показує білий аркуш. На вході це білий спалах на
|
||||
весь екран — рівно там, де людина бачить продукт уперше.
|
||||
-->
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
background: #020617;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="uk">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>NetPulse — огляд меню</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/preview-nav.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
web/public/apple-touch-icon.png
Normal file
BIN
web/public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
web/public/favicon-32.png
Normal file
BIN
web/public/favicon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
26
web/public/favicon.svg
Normal file
26
web/public/favicon.svg
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="NetPulse">
|
||||
<!--
|
||||
Знак NetPulse: лінія зв'язку між двома вузлами, на якій б'ється пульс.
|
||||
|
||||
Кольори задані стилями, а не атрибутами, бо окремий файл фавікона
|
||||
не має контексту сторінки: currentColor тут нема від чого успадкувати.
|
||||
Тому тему читаємо напряму з браузера — на світлій смузі вкладок штрих
|
||||
темний, на темній світлий. Без цього знак на одній із тем зникає.
|
||||
|
||||
Заливки навмисно немає: прозоре тло лишає всі 32 одиниці малюнку,
|
||||
а не плашці. На 16 px це різниця між читабельним знаком і плямою.
|
||||
-->
|
||||
<style>
|
||||
.line { fill: none; stroke: #0f172a; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round }
|
||||
.node { fill: #0f172a }
|
||||
.live { fill: #0284c7 }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.line { stroke: #e2e8f0 }
|
||||
.node { fill: #e2e8f0 }
|
||||
.live { fill: #38bdf8 }
|
||||
}
|
||||
</style>
|
||||
<path class="line" d="M3.5 18.5h7l3.5-11.5 4.5 18 3-6.5h7" />
|
||||
<circle class="node" cx="3.5" cy="18.5" r="3" />
|
||||
<circle class="live" cx="28.5" cy="18.5" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -5,6 +5,7 @@ import { session } from '../api/session'
|
|||
import { live } from '../api/ws'
|
||||
import { useAlerts } from '../hooks/useAlerts'
|
||||
import type { Permission } from '../types'
|
||||
import { LogoMark } from './Logo'
|
||||
import { plural, useWideScreen } from './ui'
|
||||
|
||||
interface NavItem {
|
||||
|
|
@ -399,7 +400,10 @@ export function AppShell() {
|
|||
☰
|
||||
</button>
|
||||
|
||||
<span className="flex items-center gap-2">
|
||||
<LogoMark className="h-6 w-6 shrink-0 text-slate-100" />
|
||||
<span className="text-base font-semibold tracking-tight text-slate-100">NetPulse</span>
|
||||
</span>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 text-xs">
|
||||
{/* Замерзлий інтерфейс виглядає точно так само, як справний.
|
||||
|
|
@ -516,10 +520,10 @@ export function AppShell() {
|
|||
onClick={() => toggleGroup(g.id)}
|
||||
aria-expanded={open}
|
||||
aria-controls={panelID}
|
||||
// py-3 на телефоні, а не таке, як на моніторі: там у
|
||||
// заголовок цілять пальцем, і 26 px висоти — це
|
||||
// промах через раз. Поруч пункти по 40 px, тож
|
||||
// однакова висота ще й вирівнює список.
|
||||
// На телефоні заголовок вищий, ніж на моніторі: там у
|
||||
// нього цілять пальцем, а спільні 31 px — це промах
|
||||
// через раз. Поруч пункти по 40 px, тож py-3 ще й
|
||||
// вирівнює список.
|
||||
//
|
||||
// Фокус — outline, а не ring: ring малюється тінню,
|
||||
// а тіні зникають у режимі високої контрастності
|
||||
|
|
@ -567,7 +571,10 @@ export function AppShell() {
|
|||
</span>
|
||||
) : (
|
||||
<span
|
||||
className="tabular-nums text-slate-700"
|
||||
// Той самий колір, що й у назви групи: число —
|
||||
// не окрема новина, а частина заголовка, і
|
||||
// світліше воно перетягувало б увагу на себе.
|
||||
className="tabular-nums"
|
||||
title={`${g.items.length} ${plural(g.items.length, ['пункт', 'пункти', 'пунктів'])}`}
|
||||
>
|
||||
{g.items.length}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { api } from '../api/client'
|
||||
import { Button, ErrorNote, Field, formatBps, inputClass, plural } from './ui'
|
||||
import { Button, ErrorNote, Field, FieldGap, formatBps, inputClass, plural } from './ui'
|
||||
import type {
|
||||
IfFilterField,
|
||||
IfFilterOp,
|
||||
|
|
@ -344,7 +344,7 @@ export function InterfaceFilterEditor({
|
|||
{/* ---- Перевірка на справжньому хості ---- */}
|
||||
|
||||
<div className="space-y-2 rounded border border-slate-800 bg-slate-900/40 p-3">
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<div className="flex flex-wrap items-start gap-2">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Field
|
||||
label="Перевірити на хості"
|
||||
|
|
@ -368,9 +368,11 @@ export function InterfaceFilterEditor({
|
|||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
<FieldGap>
|
||||
<Button kind="primary" disabled={busy || !deviceID} onClick={run}>
|
||||
{busy ? 'Рахуємо…' : 'Перевірити'}
|
||||
</Button>
|
||||
</FieldGap>
|
||||
</div>
|
||||
|
||||
<ErrorNote>{err}</ErrorNote>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useState } from 'react'
|
|||
import { ApiError, api } from '../api/client'
|
||||
import { meFromLogin, session } from '../api/session'
|
||||
import type { TenantMembership } from '../types'
|
||||
import { LogoMark } from './Logo'
|
||||
|
||||
/**
|
||||
* Сторінка входу.
|
||||
|
|
@ -57,15 +58,29 @@ export function LoginPage({ onDone }: { onDone: () => void }) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-slate-950 px-4 py-8">
|
||||
<div className="netpulse-auth flex min-h-screen items-center justify-center px-4 py-8">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="mb-6 text-center">
|
||||
<div className="text-2xl font-semibold tracking-tight text-slate-100">NetPulse</div>
|
||||
<div className="mt-1 text-sm text-slate-500">моніторинг мережі</div>
|
||||
{/* Вхід — єдиний екран, який людина бачить до того, як щось
|
||||
запрацює. Тому знак тут великий і стоїть над карткою, а не
|
||||
рядком у кутку: за ці кілька секунд продукт або виглядає
|
||||
зробленим, або не виглядає ніяк.
|
||||
|
||||
text-slate-100 на самому знаку обов'язково: штрих малюється
|
||||
через currentColor, а на цій сторінці колір тексту ніде не
|
||||
заданий — знак успадкував би типовий чорний і зник би на
|
||||
темному тлі. */}
|
||||
<div className="mb-7 flex flex-col items-center text-center">
|
||||
{/* Квадрат тут більший, ніж здається потрібним: малюнок займає
|
||||
лише середню смугу поля зору (пульс не сягає ані верху, ані
|
||||
низу сітки 32×32), тож знак «на 56 px» насправді має висоту
|
||||
37. Розмір підібрано за шириною слова під ним. */}
|
||||
<LogoMark className="h-16 w-16 text-slate-100" pulse />
|
||||
<div className="mt-3 text-2xl font-semibold tracking-tight text-slate-100">NetPulse</div>
|
||||
<div className="mt-1 text-sm text-slate-500">моніторинг мережі та конфігурацій</div>
|
||||
</div>
|
||||
|
||||
{tenants ? (
|
||||
<div className="rounded-lg border border-slate-800 bg-slate-900 p-4">
|
||||
<div className="rounded-xl border border-slate-800 bg-slate-900 p-5 shadow-2xl shadow-black/50">
|
||||
<h2 className="mb-3 text-sm text-slate-300">Оберіть організацію</h2>
|
||||
<ul className="space-y-2">
|
||||
{tenants.map((t) => (
|
||||
|
|
@ -85,7 +100,7 @@ export function LoginPage({ onDone }: { onDone: () => void }) {
|
|||
</div>
|
||||
) : (
|
||||
<form
|
||||
className="space-y-3 rounded-lg border border-slate-800 bg-slate-900 p-4"
|
||||
className="space-y-3 rounded-xl border border-slate-800 bg-slate-900 p-5 shadow-2xl shadow-black/50"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
void submit()
|
||||
|
|
|
|||
114
web/src/components/Logo.tsx
Normal file
114
web/src/components/Logo.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* Лого NetPulse.
|
||||
*
|
||||
* Намальоване кодом, а не покладене растром, і причина не в вазі файла
|
||||
* (хоч знак і важить менше кілобайта). Растр має вшиті кольори: на
|
||||
* темному тлі світлий PNG сидить світлою плямою, ніби наліпка, а
|
||||
* підігнати його під тему можна лише другим файлом. Тут штрих іде
|
||||
* через currentColor — знак бере колір тексту від батька й тому
|
||||
* правильний скрізь, куди його поставлять, без жодної домовленості.
|
||||
*
|
||||
* Знак і повне лого — різні експорти навмисно. У згорнутому меню й у
|
||||
* фавіконі місця рівно на квадрат; тягнути туди слово «NetPulse»
|
||||
* означало б показувати його завширшки в чотири пікселі на літеру.
|
||||
*
|
||||
* Той самий малюнок повторено у web/public/favicon.svg. Дублювання
|
||||
* свідоме: фавікон вантажиться браузером окремим файлом ще до того, як
|
||||
* виконається бодай рядок JS, тож поділитися з ним компонентом нема як.
|
||||
* Якщо правитимете геометрію — правте в обох місцях.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Пульс на лінії зв'язку, сітка 32×32.
|
||||
*
|
||||
* Читається зліва направо: вузол, спокійна ділянка, різкий сплеск із
|
||||
* провалом, вихід на норму, другий вузол. Це не абстракція — це те, що
|
||||
* NetPulse показує на графіку відгуку, коли пристрій моргнув.
|
||||
*
|
||||
* Координати кратні 0.5 навмисно: на 16 px чверть одиниці лягає між
|
||||
* пікселями, і замість лінії виходить сіра смуга.
|
||||
*/
|
||||
const TRACE = 'M3.5 18.5h7l3.5-11.5 4.5 18 3-6.5h7'
|
||||
|
||||
interface MarkProps {
|
||||
/** Розмір задається класами висоти й ширини: у різних місцях він різний. */
|
||||
className?: string
|
||||
/**
|
||||
* Підпис для читача з екрана. Потрібен лише там, де знак стоїть сам:
|
||||
* поруч зі словом «NetPulse» він озвучив би назву двічі.
|
||||
*/
|
||||
label?: string
|
||||
/**
|
||||
* Повільне блимання живого вузла. Вмикаємо тільки на вході — там знак
|
||||
* великий і людина на нього дивиться. У шапці те саме блимання за
|
||||
* робочий день перетворюється на подразник.
|
||||
*/
|
||||
pulse?: boolean
|
||||
}
|
||||
|
||||
export function LogoMark({ className = 'h-7 w-7', label, pulse = false }: MarkProps) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
className={className}
|
||||
fill="none"
|
||||
// Або підпис, або повне приховання: знак без тексту, який ніяк не
|
||||
// названий, читач з екрана оголосить як «graphic» — тобто нічим.
|
||||
{...(label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true })}
|
||||
>
|
||||
<path
|
||||
d={TRACE}
|
||||
stroke="currentColor"
|
||||
strokeWidth={3}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<circle cx="3.5" cy="18.5" r="3" fill="currentColor" />
|
||||
{/* Другий вузол — блакитний: він означає «звідси щойно прийшла
|
||||
відповідь». Той самий колір, яким у списках позначено живий
|
||||
стан, тож знак і інтерфейс говорять одне й те саме. */}
|
||||
<circle
|
||||
cx="28.5"
|
||||
cy="18.5"
|
||||
r="3"
|
||||
className={`text-sky-400 ${pulse ? 'animate-pulse motion-reduce:animate-none' : ''}`}
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const SIZES = {
|
||||
sm: { mark: 'h-5 w-5', text: 'text-sm', gap: 'gap-2' },
|
||||
md: { mark: 'h-7 w-7', text: 'text-lg', gap: 'gap-2.5' },
|
||||
lg: { mark: 'h-12 w-12', text: 'text-3xl', gap: 'gap-3' },
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Повне лого: знак плюс назва.
|
||||
*
|
||||
* Назва — справжній текст, а не обведений контур. Контур виглядав би
|
||||
* однаково скрізь, але його не прочитає ні пошук, ні читач з екрана, і
|
||||
* він не успадкує шрифт інтерфейсу — тобто на сторінці, зробленій
|
||||
* системним шрифтом, стояв би чужий.
|
||||
*/
|
||||
export function Logo({
|
||||
size = 'md',
|
||||
className = '',
|
||||
pulse = false,
|
||||
}: {
|
||||
size?: keyof typeof SIZES
|
||||
className?: string
|
||||
pulse?: boolean
|
||||
}) {
|
||||
const s = SIZES[size]
|
||||
return (
|
||||
<span className={`inline-flex items-center ${s.gap} ${className}`}>
|
||||
<LogoMark className={`${s.mark} shrink-0`} pulse={pulse} />
|
||||
{/* tracking-tight — та сама щільність, що й у решті заголовків
|
||||
проєкту: лого, набране ширше за сусідній текст, читається як
|
||||
картинка, вставлена з іншого макета. */}
|
||||
<span className={`${s.text} font-semibold tracking-tight text-slate-100`}>NetPulse</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
@ -136,6 +136,29 @@ export function Field({
|
|||
)
|
||||
}
|
||||
|
||||
// FieldGap — порожній підпис заввишки зі справжній.
|
||||
//
|
||||
// Потрібен там, де в одному ряду сітки стоять поля й гола кнопка. Поле
|
||||
// складається з підпису та контролю, кнопка — лише з себе; при
|
||||
// вирівнюванні по ВЕРХУ кнопка з'їжджає на висоту підпису вгору, а при
|
||||
// вирівнюванні по НИЗУ вгору з'їжджає вже поле з підказкою, бо підказка
|
||||
// малюється під контролем і робить колонку вищою.
|
||||
//
|
||||
// Саме це й побачили в словнику трапів: «Пояснення» стояло вище за
|
||||
// сусідів, а кнопка — нижче. Магічний відступ у пікселях полагодив би
|
||||
// один ряд і роз'їхався б від зміни розміру шрифту; порожній підпис
|
||||
// займає рівно ту саму коробку, що й справжній, тому не роз'їдеться.
|
||||
export function FieldGap({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="block">
|
||||
<span aria-hidden className="mb-1 block text-xs font-medium">
|
||||
|
||||
</span>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const inputClass =
|
||||
'w-full rounded border border-slate-700 bg-slate-800 px-3 py-2 text-sm text-slate-100 ' +
|
||||
'outline-none placeholder:text-slate-600 focus:border-sky-600'
|
||||
|
|
|
|||
|
|
@ -27,6 +27,24 @@ body {
|
|||
color: #334155 !important;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Сторінка входу
|
||||
---------------------------------------------------------------------
|
||||
Та сама радіальна підсвітка, що й на полотні мапи, тільки слабша й
|
||||
зміщена вгору — під знак і картку. Рівне чорне тло технічно не гірше,
|
||||
але вхід — єдиний екран, який бачать до того, як продукт хоч щось
|
||||
зробив, і плаский прямокутник на ньому читається як незавершена
|
||||
сторінка. Градієнт тут не прикраса, а єдиний доступний спосіб сказати
|
||||
«це закінчена річ» до першого корисного пікселя.
|
||||
|
||||
Клас, а не правило на body: після входу тло малює вже AppShell, і
|
||||
градієнт, лишений глобально, проступав би крізь усі сторінки.
|
||||
*/
|
||||
.netpulse-auth {
|
||||
background:
|
||||
radial-gradient(900px 520px at 50% 28%, #16233a 0%, #0a1120 55%, #020617 100%);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Кнопки — це кнопки, а не текст
|
||||
------------------------------------------------------------------ */
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
Card,
|
||||
ErrorNote,
|
||||
Field,
|
||||
FieldGap,
|
||||
PageBody,
|
||||
PageHeader,
|
||||
Spinner,
|
||||
|
|
@ -629,7 +630,7 @@ function Dictionary({
|
|||
</p>
|
||||
|
||||
{canEdit && (
|
||||
<div className="mt-3 grid gap-2 md:grid-cols-[1fr_1fr_1fr_auto] md:items-end">
|
||||
<div className="mt-3 grid gap-2 md:grid-cols-[1fr_1fr_1fr_auto] md:items-start">
|
||||
<Field label="OID трапа">
|
||||
<input
|
||||
className={inputClass}
|
||||
|
|
@ -654,9 +655,11 @@ function Dictionary({
|
|||
onChange={(e) => setDesc(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<FieldGap>
|
||||
<Button kind="primary" disabled={busy || !oid || !name} onClick={save}>
|
||||
Додати
|
||||
</Button>
|
||||
</FieldGap>
|
||||
</div>
|
||||
)}
|
||||
<ErrorNote>{err}</ErrorNote>
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
// ТИМЧАСОВИЙ стенд для огляду меню очима. Видаляється після перевірки.
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom'
|
||||
import { AppShell } from './components/AppShell'
|
||||
import { session } from './api/session'
|
||||
import type { Permission } from './types'
|
||||
import './index.css'
|
||||
|
||||
const counts = { firing: 3, acknowledged: 0, suppressed: 0, disaster: 1, high: 2 }
|
||||
|
||||
window.fetch = (async (url: RequestInfo | URL) => {
|
||||
const u = String(url)
|
||||
const body = u.includes('/alerts') ? { alerts: [], counts } : {}
|
||||
return {
|
||||
status: 200,
|
||||
ok: true,
|
||||
statusText: '',
|
||||
text: async () => JSON.stringify(body),
|
||||
} as unknown as Response
|
||||
}) as typeof window.fetch
|
||||
|
||||
class Silent {
|
||||
close() {}
|
||||
send() {}
|
||||
}
|
||||
;(window as unknown as { WebSocket: unknown }).WebSocket = Silent
|
||||
|
||||
const perms = (new URLSearchParams(location.search).get('perms') ?? '*').split(
|
||||
',',
|
||||
) as Permission[]
|
||||
const path = new URLSearchParams(location.search).get('path') ?? '/devices'
|
||||
|
||||
session.set('tok', { userID: 'u-1', username: 'admin', tenantID: 't-1', permissions: perms })
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<Routes>
|
||||
<Route element={<AppShell />}>
|
||||
<Route path="*" element={<div className="p-4 text-slate-500">сторінка</div>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue