416 lines
20 KiB
Markdown
416 lines
20 KiB
Markdown
# Панель экосистемы
|
||
|
||
Статическая навигационная панель для быстрого доступа к сервисам инфраструктуры. Поддерживает гибкую иерархию данных, поиск, дерево навигации, несколько режимов отображения и настройку внешнего вида.
|
||
|
||
Стек: HTML, CSS, vanilla JavaScript. Без сборщиков и фреймворков.
|
||
|
||
## Оглавление
|
||
|
||
- [Возможности](#возможности)
|
||
- [Быстрый старт](#быстрый-старт)
|
||
- [Структура проекта](#структура-проекта)
|
||
- [Конфигурация](#конфигурация)
|
||
- [`settings.json`](#settingsjson)
|
||
- [Пресет организации (`defaults` + `features`)](#пресет-организации-defaults--features)
|
||
- [Форматы иерархии](#форматы-иерархии)
|
||
- [Схема `panel-data`](#схема-panel-data)
|
||
- [Поля данных](#поля-данных)
|
||
- [Примеры данных](#примеры-данных)
|
||
- [Готовые демо-страницы (`examples/`)](#готовые-демо-страницы-examples)
|
||
- [Типы сервисов (`serviceMeta`)](#типы-сервисов-servicemeta)
|
||
- [Рекомендуемые типы сервисов](#рекомендуемые-типы-сервисов)
|
||
- [Настройки интерфейса (⚙)](#настройки-интерфейса-)
|
||
- [Поведение интерфейса](#поведение-интерфейса)
|
||
- [localStorage](#localstorage)
|
||
- [Разработка](#разработка)
|
||
- [Ограничения](#ограничения)
|
||
- [Лицензия](#лицензия)
|
||
|
||
## Возможности
|
||
|
||
- **Древовидная навигация** слева в стиле файлового менеджера
|
||
- **Каталог карточек** справа с collapsible-разделами
|
||
- **Поиск** по названию, тегам, типу сервиса и родительским разделам
|
||
- **4 режима отображения**: плитки, список, компактные плитки, компактный список
|
||
- **Темы**: светлая / тёмная, 4 пресета акцента + свой градиент
|
||
- **Настройки** через popup (иконка ⚙)
|
||
- **Изменяемая ширина** бокового меню (drag)
|
||
- **Сохранение состояния** в `localStorage` (настройки UI, раскрытие веток)
|
||
- **Конфигурация через JSON** — локальный объект или загрузка по URL
|
||
- **Гибкая иерархия**: полная вложенность, раздел без подраздела, плоский список
|
||
|
||
## Быстрый старт
|
||
|
||
Панель использует `fetch`, поэтому её нужно открывать через HTTP-сервер, а не напрямую как `file://`.
|
||
|
||
```bash
|
||
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:**
|
||
|
||
```json
|
||
{
|
||
"dataUrl": "./panel-data.json"
|
||
}
|
||
```
|
||
|
||
Можно указать внешний адрес или любой из example-файлов:
|
||
|
||
```json
|
||
{
|
||
"dataUrl": "./panel-data-flat.example.json"
|
||
}
|
||
```
|
||
|
||
**Вариант 2 — inline-объект в том же файле:**
|
||
|
||
```json
|
||
{
|
||
"dataUrl": "",
|
||
"data": {
|
||
"title": "Заголовок панели",
|
||
"sections": []
|
||
}
|
||
}
|
||
```
|
||
|
||
> Если `dataUrl` не пустой, он имеет **приоритет** над полем `data`.
|
||
|
||
При ошибке загрузки или невалидном JSON на странице показывается экран с описанием проблемы.
|
||
|
||
#### Пресет организации (`defaults` + `features`)
|
||
|
||
В `settings.json` можно задать дефолтный вид панели для организации и ограничить, что пользователь может менять в ⚙.
|
||
|
||
```json
|
||
{
|
||
"dataUrl": "./panel-data.json",
|
||
"defaults": {
|
||
"navLayout": "sidebar",
|
||
"theme": "dark",
|
||
"view": "cards",
|
||
"accent": "violet",
|
||
"showNumbers": false,
|
||
"showTreeTags": true,
|
||
"treeTwoColumn": false,
|
||
"autoCollapseSearch": true,
|
||
"sidebarWidth": 390,
|
||
"searchPlaceholder": "Grafana, S3, журнал, сервер..."
|
||
},
|
||
"features": {
|
||
"allowNavLayoutChange": true,
|
||
"allowSearch": true,
|
||
"allowViewChange": true,
|
||
"allowShowNumbers": true,
|
||
"allowTreeOptions": true,
|
||
"allowSidebarResize": true,
|
||
"allowThemeChange": true,
|
||
"allowAccentChange": true
|
||
}
|
||
}
|
||
```
|
||
|
||
| `defaults` | Варианты | Описание |
|
||
|------------|----------|----------|
|
||
| `navLayout` | `"sidebar"`, `"top"` | `"sidebar"` — дерево слева; `"top"` — шапка с названием и поиском, без дерева |
|
||
| `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` | Теги рядом с элементами в дереве (взаимоисключимо с `treeTwoColumn`) |
|
||
| `treeTwoColumn` | `true`, `false` | Элементы папок дерева в 2 колонки (взаимоисключимо с `showTreeTags`) |
|
||
| `autoCollapseSearch` | `true`, `false` | При поиске сворачивать ветки дерева без совпадений |
|
||
| `sidebarWidth` | `260`–`560` (`760` при `treeTwoColumn: true`) | Ширина боковой панели в px; игнорируется при `navLayout: "top"` |
|
||
| `searchPlaceholder` | произвольная строка | Текст-подсказка в поле поиска |
|
||
|
||
| `features` | Варианты | Описание |
|
||
|------------|----------|----------|
|
||
| `allowNavLayoutChange` | `true`, `false` | Переключатель «Сбоку / Сверху» |
|
||
| `allowSearch` | `true`, `false` | Поле поиска |
|
||
| `allowViewChange` | `true`, `false` | Режимы каталога |
|
||
| `allowShowNumbers` | `true`, `false` | Нумерация |
|
||
| `allowTreeOptions` | `true`, `false` | Теги, 2 колонки, авто-раскрытие дерева |
|
||
| `allowSidebarResize` | `true`, `false` | Drag-resize боковой панели |
|
||
| `allowThemeChange` | `true`, `false` | Светлая / тёмная тема |
|
||
| `allowAccentChange` | `true`, `false` | Пресеты и свой градиент |
|
||
|
||
Пользовательские изменения сохраняются в `localStorage`, но заблокированные `features` всегда возвращаются к `defaults`.
|
||
|
||
Пример пресета «только шапка»: [`settings-top-bar.example.json`](settings-top-bar.example.json) — `navLayout: "top"`, дерево и resize отключены.
|
||
|
||
### Форматы иерархии
|
||
|
||
Панель поддерживает три базовых формата. Старый **section → group → item** работает без изменений.
|
||
|
||
| Формат | Структура | Дерево | Каталог |
|
||
|--------|-----------|--------|---------|
|
||
| **Полная** | `sections[].groups[].items[]` | раздел → подраздел → файлы | collapsible разделы и подразделы |
|
||
| **Раздел → item** | `sections[].items[]` | раздел → файлы | карточки внутри раздела |
|
||
| **Плоский список** | корневой `items[]` | плоский список файлов | только сетка карточек |
|
||
| **Смешанный** | `sections[].items[]` + `groups[]` | прямые файлы, затем подразделы | прямые карточки, затем подразделы |
|
||
|
||
Минимум данных: корень должен содержать `sections` **или** `items`. Каждый раздел — `groups` **или** `items`. Каждый элемент — `title` и `type`.
|
||
|
||
### Схема `panel-data`
|
||
|
||
Абстрактная структура файла данных (без привязки к конкретным сервисам):
|
||
|
||
```json
|
||
{
|
||
"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` | Отображаемый номер |
|
||
|
||
- `type` — ключ из встроенного `defaultServiceMeta` или из `serviceMeta` в JSON
|
||
- `id` элемента нужен, если нет `number` — иначе ключ генерируется автоматически
|
||
|
||
### Примеры данных
|
||
|
||
Готовые демо-страницы — в папке [`examples/`](examples/README.md). Корневые `panel-data-*.example.json` — те же структуры в виде JSON-сниппетов.
|
||
|
||
### Готовые демо-страницы (`examples/`)
|
||
|
||
Каждый пример — отдельная страница с `index.html`, `settings.json` и `panel-data.json`. Общие ресурсы в `examples/shared/`.
|
||
|
||
```bash
|
||
cd examples
|
||
npx serve .
|
||
```
|
||
|
||
| Путь | Формат |
|
||
|------|--------|
|
||
| `/` | оглавление примеров |
|
||
| `/full/` | section → group → item |
|
||
| `/section-items/` | section → item |
|
||
| `/flat/` | плоский `items[]` |
|
||
| `/mixed/` | items + groups в разделе |
|
||
|
||
Подробнее: [examples/README.md](examples/README.md).
|
||
|
||
### Типы сервисов (`serviceMeta`)
|
||
|
||
Базовые типы заданы в `script.js` (`defaultServiceMeta`):
|
||
|
||
`ceph`, `cloudbeaver`, `config`, `gitea`, `grafana`, `ipmi`, `kubernetes`, `ldap`, `messages`, `opensearch`, `postgresql`, `prometheus`, `rabbitmq`, `redmine`, `rules`, `xpath`, `xmpp`
|
||
|
||
Переопределение или добавление типов в JSON:
|
||
|
||
```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` |
|
||
|
||
Чтобы подключить тип:
|
||
|
||
1. Добавь запись в `serviceMeta` вашего JSON **или** в `defaultServiceMeta` в `script.js`
|
||
2. При необходимости положи отдельную SVG в `assets/`
|
||
3. Укажи `"type": "nginx"` (и т.д.) в элементах
|
||
|
||
## Настройки интерфейса (⚙)
|
||
|
||
Popup сгруппирован по разделам. Доступные блоки зависят от `features` в `settings.json`. Изменения пользователя сохраняются в `localStorage` (`ecosystem-panel-preferences`).
|
||
|
||
| Раздел | Настройки |
|
||
|--------|-----------|
|
||
| **Макет** | Панель навигации: **Сбоку** (дерево + поиск) или **Сверху** (только название и поиск) |
|
||
| **Каталог** | Вид карточек, нумерация |
|
||
| **Дерево** | Теги, 2 колонки, авто-раскрытие поиска (только при `navLayout: sidebar`) |
|
||
| **Оформление** | Тема, акцент, свой градиент |
|
||
|
||
### Режим «Сверху»
|
||
|
||
Компактная шапка над каталогом: заголовок панели, поиск и ⚙. Дерево навигации скрыто — переход только через карточки каталога. Подходит для простых порталов и когда дерево избыточно.
|
||
|
||
### Свой градиент
|
||
|
||
Кнопка **«Свой»** открывает настройки:
|
||
|
||
- угол (0–360°)
|
||
- цвет «от» и «до»
|
||
|
||
Значения сохраняются в `localStorage`.
|
||
|
||
## Поведение интерфейса
|
||
|
||
### Поиск
|
||
|
||
Фильтрует каталог и подсвечивает совпадения в дереве и карточках. Ищет по:
|
||
|
||
- названию элемента
|
||
- номеру
|
||
- тегам
|
||
- типу сервиса
|
||
- названиям родительских разделов
|
||
|
||
### Дерево навигации
|
||
|
||
- Клик по **папке** — раскрытие/сворачивание
|
||
- Клик по **названию** — прокрутка к элементу в каталоге
|
||
- Состояние раскрытия сохраняется отдельно для дерева и контента (`ecosystem-panel-collapse-state`)
|
||
|
||
### Адаптив
|
||
|
||
- **Touch-устройства** (`pointer: coarse` / `hover: none`): stacked-layout, меню сверху
|
||
- **Мышь** (`pointer: fine` + `hover: hover`): sidebar + resizer + каталог в колонках
|
||
- **2 колонки в дереве на touch**: в portrait или при ширине от 520px
|
||
|
||
### Ширина layout
|
||
|
||
- Обычный режим: max **1560px**, sidebar до **560px**
|
||
- Режим «2 колонки в дереве»: max **1920px**, sidebar до **760px**
|
||
|
||
## localStorage
|
||
|
||
| Ключ | Содержимое |
|
||
|------|------------|
|
||
| `ecosystem-panel-preferences` | Тема, акцент, вид, ширина sidebar, toggle-настройки |
|
||
| `ecosystem-panel-collapse-state` | Раскрытые/свернутые ветки дерева и каталога |
|
||
|
||
## Разработка
|
||
|
||
### Добавить элемент
|
||
|
||
1. Открой `panel-data.json` (или свой JSON из `dataUrl`)
|
||
2. Найди нужный раздел / подраздел **или** добавь в `items` / корневой `items`
|
||
3. Добавь объект с полями `title`, `type`, при необходимости `tags`, `url`, `id`, `number`
|
||
4. Обнови страницу
|
||
|
||
### Добавить иконку и тип
|
||
|
||
1. Положи SVG в `assets/` (или переиспользуй существующую)
|
||
2. Зарегистрируй тип в `serviceMeta` JSON или в `defaultServiceMeta` в `script.js`
|
||
|
||
### Внешний источник данных
|
||
|
||
```json
|
||
{
|
||
"dataUrl": "https://your-server/panel-data.json"
|
||
}
|
||
```
|
||
|
||
Убедись, что сервер отдаёт `Content-Type: application/json` и разрешает CORS при другом домене.
|
||
|
||
## Ограничения
|
||
|
||
- Требуется HTTP-сервер (`fetch` не работает с `file://`)
|
||
- Внешний `dataUrl` должен быть доступен по CORS
|
||
- Настройки UI хранятся локально в браузере и не синхронизируются между устройствами
|
||
|
||
## Лицензия
|
||
|
||
Приватный проект. Используй и изменяй по своему усмотрению.
|