| assets | ||
| examples | ||
| index.html | ||
| panel-data-flat.example.json | ||
| panel-data-full.example.json | ||
| panel-data-mixed.example.json | ||
| panel-data-section-items.example.json | ||
| panel-data.json | ||
| README.md | ||
| script.js | ||
| settings-top-bar.example.json | ||
| settings.json | ||
| styles.css | ||
Панель экосистемы
Статическая навигационная панель для быстрого доступа к сервисам инфраструктуры. Поддерживает гибкую иерархию данных, поиск, дерево навигации, несколько режимов отображения и настройку внешнего вида.
Стек: HTML, CSS, vanilla JavaScript. Без сборщиков и фреймворков.
Оглавление
- Возможности
- Быстрый старт
- Структура проекта
- Конфигурация
- Настройки интерфейса (⚙)
- Поведение интерфейса
- localStorage
- Разработка
- Ограничения
- Лицензия
Возможности
- Древовидная навигация слева в стиле файлового менеджера
- Каталог карточек справа с collapsible-разделами
- Поиск по названию, тегам, типу сервиса и родительским разделам
- 4 режима отображения: плитки, список, компактные плитки, компактный список
- Темы: светлая / тёмная, 4 пресета акцента + свой градиент
- Настройки через popup (иконка ⚙)
- Изменяемая ширина бокового меню и высота верхней шапки (drag, можно скрыть ручки)
- Габариты — лимиты ширины контента, меню и шапки (⚙, для администратора)
- Колонки каталога — число элементов в строке для каждого из 4 режимов отображения
- Сохранение состояния в
localStorage(настройки UI, раскрытие веток) - Конфигурация через JSON — локальный объект или загрузка по URL
- Гибкая иерархия: полная вложенность, раздел без подраздела, плоский список
Быстрый старт
Панель использует fetch, поэтому её нужно открывать через HTTP-сервер, а не напрямую как file://.
cd ecosystem
npx serve .
Открой в браузере адрес, который покажет сервер (обычно http://localhost:3000).
Структура проекта
ecosystem/
├── index.html # Разметка страницы
├── styles.css # Стили, темы, адаптив
├── script.js # Логика, рендер, примеры структур
├── settings.json # Данные + пресет UI организации
├── settings-top-bar.example.json # Пример пресета «шапка сверху»
├── panel-data.json # Данные навигации (рабочий файл)
├── examples/ # Готовые демо-страницы (см. examples/README.md)
│ ├── index.html # Оглавление примеров
│ ├── shared/ # styles, script, assets для демо
│ ├── full/ # section → group → item
│ ├── section-items/ # section → item
│ ├── flat/ # плоский items[]
│ └── mixed/ # items + groups
├── panel-data-full.example.json # JSON-сниппет (без страницы)
├── assets/ # SVG-иконки сервисов
└── README.md
Конфигурация
settings.json
Указывает, откуда загружать данные панели.
Вариант 1 — загрузка из файла или URL:
{
"dataUrl": "./panel-data.json"
}
Можно указать внешний адрес или любой из example-файлов:
{
"dataUrl": "./panel-data-flat.example.json"
}
Вариант 2 — inline-объект в том же файле:
{
"dataUrl": "",
"data": {
"title": "Заголовок панели",
"sections": []
}
}
Если
dataUrlне пустой, он имеет приоритет над полемdata.
При ошибке загрузки или невалидном JSON на странице показывается экран с описанием проблемы.
Пресет организации (defaults + features)
В settings.json можно задать дефолтный вид панели для организации и ограничить, что пользователь может менять в ⚙.
{
"dataUrl": "./panel-data.json",
"defaults": {
"navLayout": "sidebar",
"showTree": true,
"showResizeHandles": true,
"theme": "dark",
"view": "cards",
"accent": "violet",
"showNumbers": false,
"showTreeTags": true,
"showTreeType": false,
"treeMetaLayout": "inline",
"treeColumns": 2,
"autoCollapseSearch": true,
"sidebarWidth": 390,
"topBarHeight": 140,
"shellMaxWidth": 2800,
"sidebarMinWidth": 260,
"sidebarMaxWidth": 800,
"sidebarMaxWidthWide": 1400,
"treeSidebarBaseWidth": 420,
"treeSidebarColumnWidth": 120,
"topBarMinHeight": 80,
"topBarMaxHeight": 560,
"catalogCardsColumns": 3,
"catalogListColumns": 1,
"catalogCompactCardsColumns": 5,
"catalogCompactListColumns": 3,
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
},
"limits": {
"shellMaxWidth": { "min": 1200, "max": 3840 },
"sidebarMinWidth": { "min": 200, "max": 480 },
"sidebarMaxWidth": { "min": 400, "max": 1200 },
"sidebarMaxWidthWide": { "min": 560, "max": 2000 },
"treeSidebarBaseWidth": { "min": 280, "max": 720 },
"treeSidebarColumnWidth": { "min": 60, "max": 200 },
"topBarMinHeight": { "min": 64, "max": 240 },
"topBarMaxHeight": { "min": 160, "max": 800 },
"catalogCardsColumns": { "min": 1, "max": 6 },
"catalogListColumns": { "min": 1, "max": 4 },
"catalogCompactCardsColumns": { "min": 2, "max": 8 },
"catalogCompactListColumns": { "min": 1, "max": 6 }
},
"features": {
"allowNavLayoutChange": true,
"allowShowTreeChange": true,
"allowSearch": true,
"allowViewChange": true,
"allowCatalogColumnsChange": true,
"allowShowNumbers": true,
"allowTreeOptions": true,
"allowSidebarResize": true,
"allowLayoutLimitsChange": true,
"allowThemeChange": true,
"allowAccentChange": true
}
}
defaults |
Варианты | Описание |
|---|---|---|
navLayout |
"sidebar", "top" |
"sidebar" — панель слева; "top" — шапка с названием и поиском |
showTree |
true, false |
Показывать дерево навигации (в любом режиме макета) |
showResizeHandles |
true, false |
Показывать ручки drag-resize меню / шапки |
theme |
"dark", "light" |
Стартовая цветовая тема |
view |
"cards", "list", "compact-cards", "compact-list" |
Режим отображения каталога |
accent |
"violet", "cyan", "lime", "amber", "custom" |
Пресет акцента; при "custom" используются поля градиента ниже |
customAccentFrom |
#rrggbb |
Цвет «от» для accent: "custom" (по умолчанию #8b5cf6) |
customAccentTo |
#rrggbb |
Цвет «до» для accent: "custom" (по умолчанию #22d3ee) |
customAccentAngle |
0–360 |
Угол градиента в градусах (по умолчанию 135) |
showNumbers |
true, false |
Показывать номера разделов, подразделов и элементов |
showTreeTags |
true, false |
Теги элементов в дереве (оформление как на карточках) |
showTreeType |
true, false |
Подпись типа сервиса в дереве |
treeMetaLayout |
"inline", "stacked" |
Расположение тегов и типа: сбоку от названия или снизу |
treeColumns |
1–6 (по умолчанию 2) |
Число колонок элементов внутри папок дерева |
autoCollapseSearch |
true, false |
При поиске сворачивать ветки дерева без совпадений |
sidebarWidth |
sidebarMinWidth–sidebarMaxWidth (или sidebarMaxWidthWide при treeColumns > 1) |
Текущая ширина бокового меню (drag-resize); при navLayout: "top" не применяется |
topBarHeight |
topBarMinHeight–topBarMaxHeight (до ~75% высоты окна) |
Текущая высота верхней шапки (drag-resize); только при navLayout: "top" |
shellMaxWidth |
см. limits.shellMaxWidth |
Максимальная ширина контейнера .shell (вся страница) |
sidebarMinWidth |
см. limits.sidebarMinWidth |
Минимальная ширина бокового меню при drag-resize |
sidebarMaxWidth |
см. limits.sidebarMaxWidth |
Максимальная ширина меню при treeColumns: 1 |
sidebarMaxWidthWide |
см. limits.sidebarMaxWidthWide |
Потолок ширины меню при нескольких колонках дерева |
treeSidebarBaseWidth |
см. limits.treeSidebarBaseWidth |
База формулы ширины меню: base + columns × step |
treeSidebarColumnWidth |
см. limits.treeSidebarColumnWidth |
Прирост ширины меню за каждую колонку дерева |
topBarMinHeight |
см. limits.topBarMinHeight |
Минимальная высота шапки в режиме «Сверху» |
topBarMaxHeight |
см. limits.topBarMaxHeight |
Максимальная высота шапки (также ограничена ~75% окна) |
catalogCardsColumns |
см. limits.catalogCardsColumns |
Колонок в строке в режиме «Плитки» |
catalogListColumns |
см. limits.catalogListColumns |
Колонок в строке в режиме «Список» |
catalogCompactCardsColumns |
см. limits.catalogCompactCardsColumns |
Колонок в строке в режиме «Компакт плитки» |
catalogCompactListColumns |
см. limits.catalogCompactListColumns |
Колонок в строке в режиме «Компакт список» |
searchPlaceholder |
произвольная строка | Текст-подсказка в поле поиска |
limits |
Формат | Описание |
|---|---|---|
shellMaxWidth |
{ "min", "max" } |
Диапазон слайдера «Ширина контента» в ⚙ → Габариты |
sidebarMinWidth |
{ "min", "max" } |
Диапазон «Меню: мин. ширина» |
sidebarMaxWidth |
{ "min", "max" } |
Диапазон «Меню: макс. (1 кол.)» |
sidebarMaxWidthWide |
{ "min", "max" } |
Диапазон «Меню: макс. (неск. кол.)» |
treeSidebarBaseWidth |
{ "min", "max" } |
Диапазон «Дерево: база ширины» |
treeSidebarColumnWidth |
{ "min", "max" } |
Диапазон «Дерево: ширина колонки» |
topBarMinHeight |
{ "min", "max" } |
Диапазон «Шапка: мин. высота» |
topBarMaxHeight |
{ "min", "max" } |
Диапазон «Шапка: макс. высота» |
catalogCardsColumns |
{ "min", "max" } |
Диапазон колонок каталога «Плитки» |
catalogListColumns |
{ "min", "max" } |
Диапазон колонок каталога «Список» |
catalogCompactCardsColumns |
{ "min", "max" } |
Диапазон колонок «Компакт плитки» |
catalogCompactListColumns |
{ "min", "max" } |
Диапазон колонок «Компакт список» |
При treeColumns > 1 фактический потолок ширины меню: min(sidebarMaxWidthWide, treeSidebarBaseWidth + treeColumns × treeSidebarColumnWidth).
shellMaxWidth автоматически не опускается ниже sidebarMaxWidthWide + 320, чтобы оставалось место для каталога.
features |
Варианты | Описание |
|---|---|---|
allowNavLayoutChange |
true, false |
Переключатель «Сбоку / Сверху» |
allowShowTreeChange |
true, false |
Toggle «Показывать дерево» в ⚙ → Макет |
allowSearch |
true, false |
Поле поиска |
allowViewChange |
true, false |
Режимы каталога (плитки / список / компакт) |
allowCatalogColumnsChange |
true, false |
Слайдеры «колонок в строке» для каждого вида каталога |
allowShowNumbers |
true, false |
Нумерация |
allowTreeOptions |
true, false |
Колонки дерева (1–6), теги, тип, расположение мета, авто-раскрытие |
allowSidebarResize |
true, false |
Drag-resize и toggle «Ручки изменения размера» |
allowLayoutLimitsChange |
true, false |
Раздел ⚙ «Габариты»: лимиты ширины контента и навигации |
allowThemeChange |
true, false |
Светлая / тёмная тема |
allowAccentChange |
true, false |
Пресеты и свой градиент |
Пользовательские изменения сохраняются в localStorage, но заблокированные features всегда возвращаются к defaults.
Пример пресета «шапка сверху»: settings-top-bar.example.json — navLayout: "top", дерево по умолчанию скрыто (showTree: false), но его можно включить.
Форматы иерархии
Панель поддерживает три базовых формата. Старый section → group → item работает без изменений.
| Формат | Структура | Дерево | Каталог |
|---|---|---|---|
| Полная | sections[].groups[].items[] |
раздел → подраздел → файлы | collapsible разделы и подразделы |
| Раздел → item | sections[].items[] |
раздел → файлы | карточки внутри раздела |
| Плоский список | корневой items[] |
плоский список файлов | только сетка карточек |
| Смешанный | sections[].items[] + groups[] |
прямые файлы, затем подразделы | прямые карточки, затем подразделы |
Минимум данных: корень должен содержать sections или items. Каждый раздел — groups или items. Каждый элемент — title и type.
Схема panel-data
Абстрактная структура файла данных (без привязки к конкретным сервисам):
{
"title": "<заголовок панели>",
"sections": [
{
"id": "<slug-раздела>",
"number": "<опционально>",
"title": "<название раздела>",
"description": "<опционально>",
"items": [
{
"id": "<стабильный-ключ>",
"number": "<опционально>",
"title": "<название>",
"type": "<ключ из serviceMeta>",
"tags": ["<тег>"],
"url": "<ссылка>"
}
],
"groups": [
{
"id": "<slug-подраздела>",
"number": "<опционально>",
"title": "<название подраздела>",
"description": "<опционально>",
"items": ["<элементы — та же форма>"]
}
]
}
],
"items": ["<альтернатива sections: плоский список>"],
"serviceMeta": {
"<type-key>": {
"label": "<подпись типа>",
"icon": "assets/<иконка>.svg"
}
}
}
Используй либо sections, либо корневой items. Внутри раздела — либо groups, либо items, либо оба.
Поля данных
| Уровень | Поля | Описание |
|---|---|---|
| Корень | title |
Заголовок панели (sidebar + <title>) |
sections |
Массив разделов (если нет корневого items) |
|
items |
Плоский список без разделов | |
serviceMeta |
Дополнительные или переопределённые типы сервисов | |
| section | id, title |
Обязательные идентификатор и название |
number, description |
Опционально | |
groups, items |
Хотя бы одно из двух | |
| group | id, title, items |
Подраздел с элементами |
number, description |
Опционально | |
| item | title, type |
Обязательные название и тип |
url |
Ссылка; "#" — заглушка |
|
tags |
Массив строк для поиска | |
id |
Стабильный ключ (DOM, фокус из дерева) | |
number |
Отображаемый номер | |
icon |
Опционально: свой путь к SVG (иначе — иконка из type) |
type— ключ из встроенногоdefaultServiceMetaили изserviceMetaв JSON; определяет иконку и подпись на карточкеidэлемента нужен, если нетnumber— иначе ключ генерируется автоматически
Примеры данных
Готовые демо-страницы — в папке examples/. Корневые panel-data-*.example.json — те же структуры в виде JSON-сниппетов.
Готовые демо-страницы (examples/)
Каждый пример — отдельная страница с index.html, settings.json и panel-data.json. Общие ресурсы в examples/shared/.
cd examples
npx serve .
| Путь | Формат |
|---|---|
/ |
оглавление примеров |
/full/ |
section → group → item |
/section-items/ |
section → item |
/flat/ |
плоский items[] |
/mixed/ |
items + groups в разделе |
Подробнее: examples/README.md.
Типы сервисов (serviceMeta)
Базовые типы заданы в script.js (defaultServiceMeta):
analytics, api, calendar, ceph, chat, cicd, cloudbeaver, config, drive, forum, gitea, grafana, ipmi, kubernetes, ldap, messages, opensearch, postgresql, prometheus, rabbitmq, redmine, rules, social, vault, video, vpn, wiki, xpath, xmpp
Каждый тип имеет собственную SVG в assets/ (например, postgresql.svg, cloudbeaver.svg). Иконка показывается на карточке каталога и в дереве навигации.
Переопределение или добавление типов в JSON:
{
"serviceMeta": {
"my-type": {
"label": "Подпись",
"icon": "assets/config.svg"
}
}
}
Рекомендуемые типы сервисов
Популярные type, которые можно добавить в serviceMeta (иконки — переиспользовать из assets/):
| Категория | Типы (type) |
Комментарий |
|---|---|---|
| Прокси / балансировка | nginx, traefik |
reverse proxy, ingress |
| Контейнеры | docker, portainer, argocd |
runtime, UI, GitOps |
| CI/CD | jenkins, gitlab, harbor |
сборка, репозиторий, registry |
| Базы данных | mysql, mongodb, redis |
рядом с уже имеющимися postgresql, opensearch |
| Очереди / стриминг | kafka |
аналог rabbitmq |
| Мониторинг / логи | loki, zabbix, sentry |
логи, метрики, ошибки |
| Безопасность / IAM | vault, keycloak, consul |
секреты, SSO, service mesh |
| Поиск | elasticsearch, sonarqube |
полнотекст, качество кода |
| Хранилище | minio |
S3-совместимое |
| IaC | terraform |
инфраструктура как код |
| Трекеры | jira |
задачи, рядом с redmine |
Чтобы подключить тип:
- Добавь запись в
serviceMetaвашего JSON или вdefaultServiceMetaвscript.js - При необходимости положи отдельную SVG в
assets/ - Укажи
"type": "nginx"(и т.д.) в элементах
Настройки интерфейса (⚙)
Popup сгруппирован по разделам. Доступные блоки зависят от features в settings.json. Изменения пользователя сохраняются в localStorage (ecosystem-panel-preferences).
| Раздел | Настройки |
|---|---|
| Макет | Сбоку / Сверху; toggle «Показывать дерево»; toggle «Ручки изменения размера» |
| Габариты | Лимиты ширины контента, меню и шапки (сворачиваемый блок; при allowLayoutLimitsChange) |
| Каталог | Вид карточек, колонок в строке для каждого вида, нумерация |
| Дерево | Колонок 1–6, теги, тип, расположение мета, авто-раскрытие (при showTree: true) |
| Оформление | Тема, акцент, свой градиент |
Режим «Сверху»
Компактная шапка: заголовок, поиск и ⚙. Дерево по умолчанию скрыто, но его можно включить (showTree: true) — оно появится под шапкой на всю ширину.
Скрытие дерева
Дерево управляется отдельно от макета (showTree). Переключение — в ⚙ → Макет.
Свой градиент
Кнопка «Свой» открывает настройки:
- угол (0–360°)
- цвет «от» и «до»
Значения сохраняются в localStorage.
Поведение интерфейса
Поиск
Фильтрует каталог и подсвечивает совпадения в дереве и карточках. Ищет по:
- названию элемента
- номеру
- тегам
- типу сервиса
- названиям родительских разделов
Дерево навигации
- Клик по папке — раскрытие/сворачивание
- Клик по названию — прокрутка к элементу в каталоге
- Состояние раскрытия сохраняется отдельно для дерева и контента (
ecosystem-panel-collapse-state)
Адаптив
- Touch-устройства (
pointer: coarse/hover: none): stacked-layout, меню сверху, ручки resize скрыты - Мышь (
pointer: fine+hover: hover): sidebar + resizer + каталог в колонках - На узких экранах число колонок каталога дополнительно ограничивается в CSS (чтобы сетка не ломалась)
Ширина и габариты layout
Лимиты задаются в settings.json → defaults и limits, настраиваются пользователем в ⚙ → Габариты (если allowLayoutLimitsChange: true):
shellMaxWidth— максимальная ширина всей панели (по умолчанию до 2800px)sidebarMaxWidth/sidebarMaxWidthWide— потолок ширины меню (1 колонка дерева / несколько)topBarMinHeight/topBarMaxHeight— диапазон высоты шапки в режиме «Сверху»- Текущие
sidebarWidthиtopBarHeight— drag-resize; сохраняются вlocalStorage
localStorage
| Ключ | Содержимое |
|---|---|
ecosystem-panel-preferences |
Тема, акцент, вид, ширина sidebar, toggle-настройки |
ecosystem-panel-collapse-state |
Раскрытые/свернутые ветки дерева и каталога |
Разработка
Добавить элемент
- Открой
panel-data.json(или свой JSON изdataUrl) - Найди нужный раздел / подраздел или добавь в
items/ корневойitems - Добавь объект с полями
title,type, при необходимостиtags,url,id,number - Обнови страницу
Добавить иконку и тип
- Положи SVG в
assets/(или переиспользуй существующую) - Зарегистрируй тип в
serviceMetaJSON или вdefaultServiceMetaвscript.js
Внешний источник данных
{
"dataUrl": "https://your-server/panel-data.json"
}
Убедись, что сервер отдаёт Content-Type: application/json и разрешает CORS при другом домене.
Ограничения
- Требуется HTTP-сервер (
fetchне работает сfile://) - Внешний
dataUrlдолжен быть доступен по CORS - Настройки UI хранятся локально в браузере и не синхронизируются между устройствами
Лицензия
Приватный проект. Используй и изменяй по своему усмотрению.