Compare commits

...
4 Commits
Author SHA1 Message Date
Denozordec 2fef952403 feat(ui): refactor components and enhance UX
CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 47s
CI / go (push) Successful in 1m1s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 4m36s
Updated various UI components to improve user experience and maintainability. Key changes include:
- Refactored `PageHeader` to remove icon handling, simplifying its structure.
- Replaced `KpiMetricsGrid` with `SectionCards` for better layout consistency.
- Enhanced `AppShell` and navigation components to utilize a grouped navigation structure, improving accessibility and organization.
- Introduced new properties in `crud-list-page` for handling empty states and loading indicators.
- Updated styles and classes for better visual consistency across components.

These changes streamline the UI and enhance the overall user experience across the application.
2026-07-01 00:42:50 +07:00
Denozordec 07ee6acddb fix(data-table): prevent unnecessary page reset on data change
Updated the data table component to only reset the page index to 0 when it is not already at 0, improving user experience during data updates. Additionally, refactored the module selection logic to prune invalid selected module IDs more efficiently, enhancing the overall functionality of the module management interface.
2026-07-01 00:27:39 +07:00
Denozordec 1c7152a285 feat(docker): update BIRD build process and documentation
CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Has been skipped
CI / go (push) Successful in 1m8s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 5m10s
Refined the BIRD build process in the Docker setup by consolidating the source download and extraction steps. Enhanced the `bird-from-source.sh` script to include a fallback mechanism for downloading the BIRD tarball from GitHub if the primary source fails. Updated the README documentation to clarify the build process and provide links to the BIRD source. This improves reliability and user understanding of the setup.
2026-07-01 00:11:16 +07:00
Denozordec a37c931ee7 feat(monorepo): restructure web components and update configurations
CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 27s
CI / web (push) Successful in 38s
CI / go (push) Successful in 2m36s
CI / bird2 (push) Successful in 15s
CI / release (push) Failing after 3m7s
Refactored the project structure to support a monorepo setup, moving the web application to `apps/web/` and updating related configurations. Adjusted pre-commit hooks to use `pnpm` for linting and formatting. Updated CI workflows to reflect the new directory structure and dependencies. Removed legacy files and configurations from the previous `web/` directory, streamlining the project for better maintainability and clarity.
2026-06-30 23:54:28 +07:00
386 changed files with 9578 additions and 2307 deletions
+34
View File
@@ -0,0 +1,34 @@
# shadcn-svelte в EvoBGP monorepo
Использовать при добавлении примитивов, блоков, правке темы в `apps/web` / `packages/ui`.
## Структура
- Примитивы: `packages/ui/src/components/``@evobgp/ui/components/*`
- Паттерны приложения: `apps/web/src/lib/components/patterns/`
- CLI всегда из `apps/web/`
## Workflow
1. Проверить, есть ли компонент в `packages/ui/src/components/`
2. `cd apps/web && pnpm dlx shadcn-svelte@latest add <name> -y -o`
3. Если файлы в `apps/web/@evobgp/ui/` — перенести в `packages/ui/src/components/`
4. Импорты shadcn в app: `@evobgp/ui/components/<name>/index.js`
5. `pnpm --filter @evobgp/web check && pnpm --filter @evobgp/web lint`
## Тема
- `packages/ui/src/styles/globals.css` с `@source`
- Импорт в `apps/web/src/routes/layout.css`: `@import '@evobgp/ui/styles/globals.css';`
## Не использовать
- React shadcn/ui примеры без адаптации под Svelte 5
- ReUI (`@reui/*`) — только React
- Legacy `$lib/components/ui/` re-exports
## Документация
- https://shadcn-svelte.com/docs
- https://shadcn-svelte.com/llms.txt
- Monorepo: `.cursor/rules/frontend-monorepo.mdc`
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"pid": 44608, "pid": 46304,
"version": "0.9.9", "version": "0.9.9",
"socketPath": "\\\\.\\pipe\\codegraph-97b92efdcc5351da", "socketPath": "\\\\.\\pipe\\codegraph-97b92efdcc5351da",
"startedAt": 1781240018712 "startedAt": 1782824332519
} }
+75
View File
@@ -0,0 +1,75 @@
---
description: shadcn-svelte Monorepo — apps/web + packages/ui + packages/shared, CLI workflow
globs: apps/web/**/*,packages/ui/**/*,packages/shared/**/*
alwaysApply: false
---
# Frontend Monorepo (shadcn-svelte)
Структура по образцу vps-tracker, стек — **SvelteKit 2 + Svelte 5 + shadcn-svelte** (не React/ReUI).
## Layout
```
apps/web/ # SvelteKit (routes, queries, domain components)
packages/ui/ # @evobgp/ui — shadcn-svelte primitives (только CLI output)
packages/shared/ # @evobgp/shared — Zod contracts, API types
```
Go API в корне репозитория (`internal/`, `cmd/`) без изменений.
## Два components.json
| Файл | Назначение |
|------|------------|
| `apps/web/components.json` | App aliases; `ui` → `@evobgp/ui/components` |
| `packages/ui/components.json` | UI package aliases |
**Синхронизировать:** `style`, `iconLibrary`, `baseColor` в обоих файлах.
## CLI — только из apps/web
```powershell
cd apps/web
pnpm dlx shadcn-svelte@latest add button -y -o
pnpm dlx shadcn-svelte@latest add sidebar -y -o
pnpm dlx shadcn-svelte@latest add breadcrumb -y -o
```
Перед обновлением существующих компонентов: `pnpm dlx shadcn-svelte@latest add button --dry-run`
## Куда CLI кладёт файлы
| Команда | Куда |
|---------|------|
| `add button` | `packages/ui/src/components/button/` |
| `add sidebar` | `packages/ui/src/components/sidebar/` |
Если CLI кладёт в `apps/web/@evobgp/ui/` — перенести в `packages/ui/src/components/`.
## Импорты
```svelte
import { Button } from '@evobgp/ui/components/button/index.js';
import { cn } from '@evobgp/ui/lib/utils';
import '@evobgp/ui/styles/globals.css'; // только в routes/layout.css
import type { ModuleRow } from '@evobgp/shared/types/api.js';
import { moduleCreateSchema } from '@evobgp/shared/contracts/modules.js';
```
| Запрещено | Разрешено |
|-----------|-----------|
| `$lib/ui/core/*` | `@evobgp/ui/components/*` |
| `apps/web/src/lib/components/ui/` (legacy re-export) | `packages/ui/src/components/` |
| Ручное редактирование темы вне CLI | shadcn-svelte theming docs |
## globals.css
`packages/ui/src/styles/globals.css` с `@source` на `packages/ui` и `apps/web/src`.
## Проверка после правок
```powershell
pnpm --filter @evobgp/web check
pnpm --filter @evobgp/web lint
```
+73
View File
@@ -0,0 +1,73 @@
---
description: UI-паттерны apps/web — shared components, spacing, матрица (Svelte, без ReUI)
globs: apps/web/**/*
alwaysApply: false
---
# Frontend UI Patterns (Svelte)
Эталон UX: vps-tracker (`PageShell`, `QueryState`, `SectionCards`, `DataTableCard`). ReUI **не используется** — Data Table = shadcn-svelte + `AppDataTable` / `DataTableCard`.
## Иерархия
```
@evobgp/ui/components/* ← shadcn-svelte CLI (packages/ui)
apps/web/src/lib/components/ ← shared + domain + layout
page-shell.svelte
query-state.svelte
section-cards.svelte
data-table-card.svelte
status-badge.svelte
form-sheet.svelte
crud-list-page.svelte
list-filters-bar.svelte
patterns/ ← AppDataTable, FormField, ConfirmDialog, EmptyState
layout/app-shell.svelte
domain-*/ ← modules, network, operations…
```
## Матрица
| Элемент | Компонент | Primitive |
|---------|-----------|-----------|
| Page wrapper | `PageShell` | — |
| Page title | `PageHeader` (`ui/app/page-header`) — **без icon** | — |
| Stat metrics | `SectionCards` | `Card` |
| Data list | `DataTableCard` + `AppDataTable` | `Table` |
| Filters | `ListFiltersBar` | `Select`, `Badge` |
| Empty | `EmptyState` | — |
| Loading / Error | `QueryState` | `Skeleton`, `Alert` |
| Status | `StatusBadge` | `Badge` |
| Create/Edit form | `FormSheet` / Dialog | `Sheet`, `Field` |
| Delete confirm | `ConfirmDialog` | `AlertDialog` |
| Nav | `AppShell` + `navGroups` | `Sidebar` |
| Breadcrumbs | `AppShell` header (parent route) | `Breadcrumb` |
| Tabs (dashboard, ops) | `LINE_TAB_TRIGGER_CLASS` | `Tabs` |
## Запрещено
- `icon` / `iconClass` в `PageHeader`
- `KpiMetricsGrid` — только `SectionCards`
- `mx-auto max-w-*` на `PageShell` list-страниц
- Голый `AppDataTable` без `DataTableCard` на list-экранах
## Spacing
- `flex` + `gap-*`, не `space-y-*`
- Page: `gap-4 md:gap-6`, padding `p-4 md:p-6`
- Loading страницы → `Skeleton`, не page-level `Spinner`
- Max 1 primary CTA на экран
## Docs
- shadcn-svelte: https://shadcn-svelte.com/docs
- llms.txt: https://shadcn-svelte.com/llms.txt
- Data layer: `@tanstack/svelte-query` в `apps/web/src/lib/queries/`
## Overlay
| Сценарий | Компонент |
|----------|-----------|
| Create/edit | `Sheet` / `FormSheet` |
| Destructive | `ConfirmDialog` |
| Preview | `Dialog` |
+33 -104
View File
@@ -1,126 +1,55 @@
--- ---
description: EvoBGP WebUI — shadcn-svelte, Svelte 5, слои ui/core|patterns|app description: EvoBGP WebUI — shadcn-svelte monorepo, Svelte 5, @evobgp/ui
globs: globs:
- web/** - apps/web/**
- packages/ui/**
- packages/shared/**
alwaysApply: false alwaysApply: false
--- ---
# Web UI — shadcn-svelte # Web UI — shadcn-svelte (monorepo)
**Источник правды:** https://shadcn-svelte.com/docs (не React shadcn/ui, не Legacy Docs). **Источник правды:** https://shadcn-svelte.com/docs
Общие правила Go/API: `.cursor/rules/engineering.mdc`. Локальная карта: `web/README.md`. Monorepo: [`frontend-monorepo.mdc`](frontend-monorepo.mdc), паттерны: [`frontend-ui-patterns.mdc`](frontend-ui-patterns.mdc). Skill: [`.agents/skills/shadcn-svelte/SKILL.md`](.agents/skills/shadcn-svelte/SKILL.md).
## Слои UI ## Слои
| Слой | Путь | Назначение | | Слой | Путь | Назначение |
|------|------|------------| |------|------|------------|
| Примитивы | `src/lib/ui/core/` | shadcn-svelte (только CLI `add`) | | Примитивы | `packages/ui/src/components/` | shadcn-svelte (только CLI) |
| Паттерны | `src/lib/ui/patterns/` | FormField, AppDataTable, ConfirmDialog, EmptyState | | Паттерны | `apps/web/src/lib/components/patterns/` | AppDataTable, FormField, ConfirmDialog |
| App chrome | `src/lib/ui/app/` | Layout, PageHeader, `notify` | | Shared UI | `apps/web/src/lib/components/` | PageShell, QueryState, SectionCards |
| Legacy | `src/lib/components/ui/` | Re-export; **не добавлять новые файлы** | | App chrome | `apps/web/src/lib/ui/app/` | PageHeader, toast, nav |
| Контракты | `packages/shared/` | types + Zod |
Тема: `src/routes/layout.css`, `src/lib/ui/app/tokens.md`. CLI из `web/`: `npx shadcn-svelte@latest add <component> -y -o`. Тема: `packages/ui/src/styles/globals.css` → импорт в `apps/web/src/routes/layout.css`.
--- CLI из `apps/web/`:
## Правила
**WEB-01** | MUST | Перед новым UI — проверить https://shadcn-svelte.com/docs/components; использовать компонент, не HTML+CSS с нуля.
*Rationale:* Open Code + единый дизайн.
*Проверка:* review; нет голых `<button class=…>`.
**WEB-02** | MUST | Отсутствующий примитив — `npx shadcn-svelte@latest add <component> -y -o` → `src/lib/ui/core/`.
*Rationale:* Distribution через CLI и `components.json`.
*Проверка:* файлы только в `ui/core`.
**WEB-03** | NEVER | Альтернативные UI-kit'ы (Material, Vuetify, DaisyUI-only без shadcn-примитива).
*Проверка:* `package.json` review.
**WEB-04** | MUST | Комозиция по docs: все sub-компоненты (`DialogHeader`, `TableRow`, `Field`, …).
*Проверка:* сверка со страницей компонента в docs.
**WEB-05** | MUST | Формы — Formsnap + `sveltekit-superforms`; UI в `ui/patterns/form`, не ad-hoc валидация на странице.
*Проверка:* https://shadcn-svelte.com/docs/components/form
**WEB-06** | MUST | Таблицы — Data Table + `@tanstack/table-core`; на страницах — `AppDataTable` из patterns.
*Проверка:* https://shadcn-svelte.com/docs/components/data-table
**WEB-07** | MUST | Toast — Sonner через `notify` из `ui/app/toast.js`.
*Проверка:* https://shadcn-svelte.com/docs/components/sonner
**WEB-08** | MUST | Иконки — `@lucide/svelte` (`components.json` → `iconLibrary: lucide`).
*Проверка:* imports.
**WEB-09** | MUST | Цвета — CSS-переменные `layout.css` и токены `tokens.md`; не hex/rgb на страницах.
*Проверка:* grep `#[0-9a-f]{3,6}` в `routes/`.
**WEB-10** | SHOULD | Кастомизация — правка `ui/core` (Open Code), не `!important` поверх API.
*Проверка:* review.
**WEB-11** | MUST | `routes/**` — композиция `ui/core` + `ui/patterns` + `ui/app`; не копировать целые примитивы shadcn в route.
*Проверка:* review.
**WEB-12** | NEVER | Примеры React shadcn/ui или Svelte 4 Legacy без адаптации под https://shadcn-svelte.com/docs/migration/svelte-5
*Проверка:* `npm run check`.
**WEB-13** | MUST | Реактивность — Svelte 5 runes (`$state`, `$derived`, `$effect`); не `export let` для локального state страниц.
*Проверка:* `npm run check`; Svelte MCP.
**WEB-14** | SHOULD | Нетривиальный UI — прочитать страницу компонента (props, a11y).
*Проверка:* PR description.
**WEB-15** | MUST | Сомнения — https://shadcn-svelte.com/llms.txt , Svelte MCP, `npm run check`.
*Проверка:* локально.
**WEB-19** | MUST | **После любого изменения `web/**`** — перед завершением задачи агент **обязан** выполнить в `web/`:
```powershell ```powershell
npm run check pnpm dlx shadcn-svelte@latest add <component> -y -o
npm run lint
``` ```
Если `npm run lint` падает (Prettier) — **сначала** `npx prettier --write <изменённые файлы>` или `npx prettier --write .`, затем снова `npm run check` и `npm run lint`. Не сдавать PR/ответ, пока обе команды не exit 0.
*Rationale:* CI job `web` = `check` + `prettier --check`; `svelte-check` не ловит форматирование.
*Проверка:* CI job `web`; pre-commit hook `prettier-web`.
**WEB-16** | MUST | Подтверждение удаления — `ConfirmDialog` из patterns, не `window.confirm`. ## Правила (кратко)
*Проверка:* review.
**WEB-17** | MUST | Пустые списки — `EmptyState` из patterns. - **WEB-01** Новый UI — сначала shadcn-svelte docs; не HTML+CSS с нуля
*Проверка:* review. - **WEB-02** Примитивы только через CLI → `packages/ui`
- **WEB-05** Формы — Formsnap + superforms; `patterns/form/`
- **WEB-06** Таблицы — `AppDataTable` / `DataTableCard`
- **WEB-07** Toast — `notify` из `ui/app/toast.ts`
- **WEB-08** Иконки — `@lucide/svelte`
- **WEB-09** Цвета — CSS variables; не hex в routes
- **WEB-13** Svelte 5 runes
- **WEB-16** Удаление — `ConfirmDialog`
- **WEB-17** Пустые списки — `EmptyState`
- **WEB-19** После правок `apps/web/**`:
**WEB-18** | SHOULD | Повторяемая комбинация core (≥2 раза) — вынести в `ui/patterns/`. ```powershell
*Проверка:* review. pnpm --filter @evobgp/web check
pnpm --filter @evobgp/web lint
--- ```
## Documentation Sync (Web)
**DOC-SYNC-06** | MUST | UI — первично https://shadcn-svelte.com/docs; при конфликте с блогами/Stack Overflow побеждает официальная страница компонента.
**DOC-SYNC-07** | MUST | Перед `add` — сверить Installation/Theming с `web/components.json` и `src/routes/layout.css`.
Tailwind v4: https://shadcn-svelte.com/docs/migration/tailwind-v4
---
## Enforcement ## Enforcement
**Обязательный финальный шаг агента при правках `web/**`:** `npm run check` **и** `npm run lint` (см. **WEB-19**). Только `check` недостаточно. PR checklist: `check` + `lint` exit 0; примитивы в `@evobgp/ui`, не дубли в routes.
```powershell
cd web
npm run check
npm run lint
# при warn/fail lint:
npx prettier --write .
npm run check
npm run lint
```
**PR checklist `web/**`:**
- [ ] `npm run check` — exit 0
- [ ] `npm run lint` (prettier --check) — exit 0
- [ ] `ui/core` / `ui/patterns`, не дубли примитивов
- [ ] Новые примитивы через shadcn CLI
- [ ] Ссылка на docs компонента (если новый паттерн)
**CI:** job `web` — `npm run check` + `npm run lint`.
+11 -11
View File
@@ -98,9 +98,9 @@ jobs:
openapi=true openapi=true
go=true go=true
;; ;;
web/README.md|web/components.json) apps/web/README.md|apps/web/components.json)
;; ;;
web/*) apps/web/*|packages/ui/*|packages/shared/*|pnpm-workspace.yaml|pnpm-lock.yaml)
web=true web=true
;; ;;
deploy/bird/*) deploy/bird/*)
@@ -126,7 +126,7 @@ jobs:
docs/*) docs/*)
go=true go=true
;; ;;
package.json|package-lock.json|.releaserc.json) package.json|package-lock.json|pnpm-lock.yaml|pnpm-workspace.yaml|.releaserc.json)
full_pipeline=true full_pipeline=true
;; ;;
*) *)
@@ -169,16 +169,16 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
cache: npm - uses: pnpm/action-setup@v4
cache-dependency-path: web/package-lock.json with:
- name: npm ci, check, lint version: 10.33.2
- name: pnpm install, check, lint
run: | run: |
set -euxo pipefail set -euxo pipefail
cd web pnpm install --frozen-lockfile
npm ci pnpm --filter @evobgp/web check
npm run check pnpm --filter @evobgp/web lint
npm run lint
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
go: go:
+2 -2
View File
@@ -13,7 +13,7 @@ repos:
hooks: hooks:
- id: prettier-web - id: prettier-web
name: prettier (web) name: prettier (web)
entry: bash -c 'cd web && npx prettier --check .' entry: bash -c 'pnpm --filter @evobgp/web lint'
language: system language: system
files: ^web/ files: ^apps/web/
pass_filenames: false pass_filenames: false
+48 -65
View File
@@ -1,70 +1,53 @@
# Руководство для ИИ-агентов (экономия контекста) # EvoBGP — руководство для ИИ-агентов
Краткие ориентиры по репозиторию **EvoBGP**, чтобы не тратить токены на полное сканирование дерева и повторное чтение одних и тех же файлов. Краткая карта репозитория. Полная архитектура: [docs/architecture.md](docs/architecture.md). HTTP: [docs/openapi.yaml](docs/openapi.yaml).
## С чего начать (минимум чтения) ## Monorepo (фронтенд)
0. **Инженерные правила** — при изменении кода следовать [.cursor/rules/engineering.mdc](.cursor/rules/engineering.mdc); для `web/` — [.cursor/rules/web-shadcn.mdc](.cursor/rules/web-shadcn.mdc); для `birdfmt` / `pipeline` / BIRD — [.cursor/rules/networking-bird.mdc](.cursor/rules/networking-bird.mdc). **Context7 (документация библиотек)** — закреплённые ID стека: [.cursor/rules/context7-stack.mdc](.cursor/rules/context7-stack.mdc); скилл [.cursor/skills/context7-evobgp/SKILL.md](.cursor/skills/context7-evobgp/SKILL.md). ```
1. **[docs/README.md](docs/README.md)** — оглавление и роли читателя. EvoBGP/
2. **[docs/architecture.md](docs/architecture.md)** — компоненты `cmd/`, карта `internal/`, потоки данных (одного этого файла обычно достаточно для ориентации). ├── cmd/, internal/ # Go API /v1
3. Задача-специфично: [docs/api.md](docs/api.md), [docs/access.md](docs/access.md), [web/README.md](web/README.md) — только если меняете API, доступ или фронт. ├── apps/web/ # SvelteKit SPA (@evobgp/web)
├── packages/ui/ # @evobgp/ui — shadcn-svelte primitives
Источник правды по HTTP-контракту: **[docs/openapi.yaml](docs/openapi.yaml)**. Не дублируйте длинные фрагменты спецификации в ответах — ссылайтесь на путь и тег/операцию. ├── packages/shared/ # @evobgp/shared — types, Zod contracts
└── pnpm-workspace.yaml
## Карта кода (куда смотреть)
| Область | Где искать |
|---------|------------|
| REST, auth, CORS | `internal/httpapi/` |
| Бизнес-слой и абстракция хранилища | `internal/store/` |
| PostgreSQL | `internal/repository/`, `internal/db/`, `migrations/` |
| Фоновые задачи | `internal/jobs/` |
| Цепочка refresh модуля (ingest+render, BIRD preview) | `internal/pipeline/` |
| Конфиг BIRD, `birdc` | `internal/birdfmt/`, `internal/birddeploy/` |
| Бандлы и подписи | `internal/bundle/`, `internal/signing/` |
| Точки входа процессов | `cmd/*/` |
| Веб (SvelteKit) | `web/` |
| Compose, деплой | `deploy/compose/` |
Точки входа бинарников и их роли — в таблице в начале [docs/architecture.md](docs/architecture.md).
## Как не раздувать контекст
- **Сначала узкий поиск:** `grep`/поиск по символу или короткий семантический запрос по одной папке (`internal/httpapi/`, `internal/pipeline/`, …), а не чтение всех `.go` подряд.
- **Читайте файлы целиком только при необходимости:** большие файлы — с `offset`/`limit` или по найденным строкам.
- **Не подтягивайте в контекст:** `web/node_modules/`, сгенерированные артефакты сборки, бинарники, полный `openapi.html`, если достаточно `openapi.yaml`.
- **Повторное использование:** если [docs/architecture.md](docs/architecture.md) уже описывает поток — не пересказывайте его длинно; укажите документ и конкретный подпункт задачи.
- **Длинные планы:** `.cursor/plans/*.plan.md` — для истории решений; для навигации пользователю достаточно `docs/`; не читайте план целиком без причины.
## Коммиты (Conventional Commits)
Если пользователь просит **коммит**, **commit message**, **закоммить**, **git commit**, **`/commit-message`** или это следует из плана — **сразу**:
1. Shell: `powershell -NoProfile -File scripts/commit/staged-context.ps1` (первый вызов, до текста коммита).
2. Скилл [.cursor/skills/commit-message/SKILL.md](.cursor/skills/commit-message/SKILL.md) и правило [.cursor/rules/conventional-commits.mdc](.cursor/rules/conventional-commits.mdc).
Без вывода скрипта (exit 0) **не** придумывать сообщение коммита. Заголовок — EN, тело — RU; несвязанные области — auto-split (скилл).
**Кнопка ✨ Generate commit message в Source Control** skill/rule **не** использует. Для сообщений по правилам EvoBGP — Agent Chat → **`/commit-message`** (см. [.cursor/commands/commit-message.md](.cursor/commands/commit-message.md)).
## Команды и среда
- Консоль пользователя: **PowerShell**; пути в стиле `deploy\compose`.
- Быстрый старт и переменные: [docs/quickstart.md](docs/quickstart.md), [README.md](README.md).
- **Go:** после правок — `gofmt -w`, `go vet ./...`, `scripts/lint-go.ps1` (как CI golangci-lint).
## Язык документации проекта
Пользовательская документация в `docs/` — преимущественно на русском. Комментарии и имена в коде — в существующем стиле репозитория.
## Svelte / фронтенд
При правках `web/**/*.svelte` или Svelte-модулей следуйте [.cursor/rules/web-shadcn.mdc](.cursor/rules/web-shadcn.mdc) (**WEB-19**): перед завершением задачи **обязательно**:
```powershell
cd web
npm run check
npm run lint
``` ```
Если `lint` падает — `npx prettier --write .` и повторить обе команды. CI job `web` не пропускает без этого. | Слой | Импорт |
|------|--------|
| Примитивы | `@evobgp/ui/components/*` |
| CSS | `@evobgp/ui/styles/globals.css` |
| Типы/контракты | `@evobgp/shared/types/*`, `@evobgp/shared/contracts/*` |
| Паттерны UI | `$lib/components/*`, `$lib/components/patterns/*` |
| Queries | `$lib/queries/*` (`@tanstack/svelte-query`) |
## С чего начать
1. **Правила:** [engineering.mdc](.cursor/rules/engineering.mdc), [web-shadcn.mdc](.cursor/rules/web-shadcn.mdc), [frontend-monorepo.mdc](.cursor/rules/frontend-monorepo.mdc)
2. **Фронт:** [apps/web/README.md](apps/web/README.md)
3. **Go:** `internal/httpapi/`, `internal/store/`, `internal/pipeline/`
## Команды
```powershell
pnpm install
pnpm --filter @evobgp/web dev
pnpm --filter @evobgp/web build
pnpm --filter @evobgp/web check
pnpm --filter @evobgp/web lint
```
После правок `apps/web/**`**обязательно** `check` и `lint` (WEB-19).
## UX-эталон (vps-tracker, без ReUI)
- **Shell:** `layout/app-shell.svelte` — sidebar-07, `collapsible="icon"`, 5 nav-групп (`nav.ts`), `AppBrand`, `ThemeMenu` в top header, breadcrumb с parent
- **Страница:** `PageShell``PageHeader` (без icon) → `QueryState` / контент
- **Метрики:** `SectionCards` (не KpiMetricsGrid)
- **Таблицы:** `DataTableCard` + `AppDataTable` (не голый `Card` + `Table`)
- **Tabs:** line-variant через `LINE_TAB_TRIGGER_CLASS` (`$lib/ui/app/tabs.ts`)
- **Тема:** `packages/ui/src/styles/globals.css` синхронизирован с vps-tracker (semantic tokens)
## Коммиты
При запросе коммита: [conventional-commits.mdc](.cursor/rules/conventional-commits.mdc), скилл `.cursor/skills/commit-message/SKILL.md`.
+1 -1
View File
@@ -1,6 +1,6 @@
# EvoBGP # EvoBGP
Control plane для управления префиксами, модулями ingest, ревизиями конфигурации BIRD и выкладкой на BGP-спикеры. Репозиторий включает HTTP API на Go, веб-интерфейс (`web/`), CLI для реплик (`evobgp-node`), агент и Docker Compose для локального и эталонного развёртывания. Control plane для управления префиксами, модулями ingest, ревизиями конфигурации BIRD и выкладкой на BGP-спикеры. Репозиторий включает HTTP API на Go, веб-интерфейс (`apps/web/`), CLI для реплик (`evobgp-node`), агент и Docker Compose для локального и эталонного развёртывания.
## Документация ## Документация
View File
+40
View File
@@ -0,0 +1,40 @@
# EvoBGP WebUI (`@evobgp/web`)
SvelteKit-панель управления EvoBGP. Monorepo: `apps/web` + `packages/ui` + `packages/shared`.
Правила: [.cursor/rules/web-shadcn.mdc](../../.cursor/rules/web-shadcn.mdc), [frontend-monorepo.mdc](../../.cursor/rules/frontend-monorepo.mdc).
## Структура
| Путь | Назначение |
| ------------------------------ | ------------------------------------------- |
| `packages/ui/src/components/` | shadcn-svelte примитивы (`@evobgp/ui`) |
| `src/lib/components/` | PageShell, QueryState, SectionCards, domain |
| `src/lib/components/patterns/` | AppDataTable, FormField, ConfirmDialog |
| `src/lib/ui/app/` | PageHeader, nav, toast |
| `src/lib/queries/` | TanStack Query factories |
| `packages/shared/` | API types + Zod contracts |
## Разработка
Из корня репозитория:
```powershell
pnpm install
pnpm --filter @evobgp/web dev
pnpm --filter @evobgp/web check
pnpm --filter @evobgp/web lint
pnpm --filter @evobgp/web build
```
Добавление shadcn-svelte (из `apps/web/`):
```powershell
pnpm dlx shadcn-svelte@latest add <component> -y -o
```
Тема: `packages/ui/src/styles/globals.css` → импорт в `src/routes/layout.css`.
## Проверка перед PR
`pnpm --filter @evobgp/web check` и `lint` — exit 0 (WEB-19).
@@ -1,13 +1,13 @@
{ {
"$schema": "https://www.shadcn-svelte.com/schema.json", "$schema": "https://www.shadcn-svelte.com/schema.json",
"tailwind": { "tailwind": {
"css": "src/routes/layout.css", "css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral" "baseColor": "neutral"
}, },
"aliases": { "aliases": {
"components": "$lib/components", "components": "$lib/components",
"utils": "$lib/utils", "utils": "$lib/utils",
"ui": "$lib/ui/core", "ui": "@evobgp/ui/components",
"hooks": "$lib/hooks", "hooks": "$lib/hooks",
"lib": "$lib" "lib": "$lib"
}, },
+4 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "web", "name": "@evobgp/web",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.1",
"type": "module", "type": "module",
@@ -33,6 +33,9 @@
"vite": "^7.3.1" "vite": "^7.3.1"
}, },
"dependencies": { "dependencies": {
"@evobgp/shared": "workspace:*",
"@evobgp/ui": "workspace:*",
"@tanstack/svelte-query": "^5.90.2",
"@tanstack/table-core": "^8.21.3", "@tanstack/table-core": "^8.21.3",
"bits-ui": "^2.17.2", "bits-ui": "^2.17.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
View File
+1
View File
@@ -0,0 +1 @@
export * from '@evobgp/shared/types/api.js';

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import { apiMutate } from '$lib/api/client.js'; import { apiMutate } from '$lib/api/client.js';
import type { ApiKey, ApiKeyCreate, ApiKeyCreated, ApiKeyRole } from '$lib/api/types.js'; import type { ApiKey, ApiKeyCreate, ApiKeyCreated, ApiKeyRole } from '$lib/api/types.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -16,12 +16,17 @@
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle DialogTitle
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
import FormField from '$lib/ui/patterns/form/form-field.svelte'; Select,
import AppInput from '$lib/ui/patterns/form/app-input.svelte'; SelectContent,
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; SelectItem,
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import FormField from '$lib/components/patterns/form/form-field.svelte';
import AppInput from '$lib/components/patterns/form/app-input.svelte';
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
import Trash2 from '@lucide/svelte/icons/trash-2'; import Trash2 from '@lucide/svelte/icons/trash-2';
@@ -0,0 +1,73 @@
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
import PageShell from '$lib/components/page-shell.svelte';
import QueryState from '$lib/components/query-state.svelte';
import TableSkeleton from '$lib/components/table-skeleton.svelte';
type Props = {
title: string;
description?: string;
actions?: Snippet;
data: T | undefined;
isLoading: boolean;
isError: boolean;
error?: unknown;
onRetry?: () => void;
empty?: boolean;
emptyTitle?: string;
emptyDescription?: string;
emptyAction?: Snippet;
skeleton?: Snippet;
sheet?: Snippet;
children: Snippet<[T]>;
};
let {
title,
description,
actions,
data,
isLoading,
isError,
error,
onRetry,
empty = false,
emptyTitle,
emptyDescription,
emptyAction,
skeleton,
sheet,
children
}: Props = $props();
</script>
<PageShell>
<PageHeader {title} {description}>
{#snippet actions()}
{#if actions}
{@render actions()}
{/if}
{/snippet}
</PageHeader>
<QueryState
{data}
{isLoading}
{isError}
{error}
{onRetry}
{empty}
{emptyTitle}
{emptyDescription}
{emptyAction}
skeleton={skeleton ?? tableSkeleton}
{children}
/>
{#if sheet}
{@render sheet()}
{/if}
</PageShell>
{#snippet tableSkeleton()}
<TableSkeleton />
{/snippet}
@@ -0,0 +1,42 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '@evobgp/ui/components/card/index.js';
import { cn } from '$lib/utils.js';
type Props = {
title?: string;
description?: string;
toolbar?: Snippet;
class?: string;
children: Snippet;
};
let { title, description, toolbar, class: className, children }: Props = $props();
</script>
<Card class={cn('gap-0 py-0', className)}>
{#if title || description || toolbar}
<CardHeader
class="flex flex-row flex-wrap items-start justify-between gap-2 border-b px-4 py-3"
>
<div class="flex min-w-0 flex-col gap-0.5">
{#if title}
<CardTitle class="text-base">{title}</CardTitle>
{/if}
{#if description}
<CardDescription>{description}</CardDescription>
{/if}
</div>
{#if toolbar}
<div class="flex shrink-0 items-center gap-2">{@render toolbar()}</div>
{/if}
</CardHeader>
{/if}
<CardContent class="p-0">{@render children()}</CardContent>
</Card>
@@ -1,25 +1,25 @@
<script lang="ts"> <script lang="ts">
import { apiMutate } from '$lib/api/client.js'; import { apiMutate } from '$lib/api/client.js';
import type { BgpCommunity, BgpCommunityCreate } from '$lib/api/types.js'; import type { BgpCommunity, BgpCommunityCreate } from '$lib/api/types.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter DialogFooter
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte'; import FormField from '$lib/components/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte'; import AppInput from '$lib/components/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil'; import Pencil from '@lucide/svelte/icons/pencil';
@@ -1,25 +1,25 @@
<script lang="ts"> <script lang="ts">
import { apiMutate } from '$lib/api/client.js'; import { apiMutate } from '$lib/api/client.js';
import type { DohProfile, DohProfileCreate } from '$lib/api/types.js'; import type { DohProfile, DohProfileCreate } from '$lib/api/types.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter DialogFooter
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte'; import FormField from '$lib/components/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte'; import AppInput from '$lib/components/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil'; import Pencil from '@lucide/svelte/icons/pencil';
@@ -0,0 +1,46 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import * as Sheet from '@evobgp/ui/components/sheet/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
type Props = {
open: boolean;
title: string;
description?: string;
onOpenChange: (open: boolean) => void;
onSubmit?: () => void;
submitLabel?: string;
submitting?: boolean;
children: Snippet;
};
let {
open,
title,
description,
onOpenChange,
onSubmit,
submitLabel = 'Сохранить',
submitting = false,
children
}: Props = $props();
</script>
<Sheet.Root {open} {onOpenChange}>
<Sheet.Content class="flex w-full flex-col gap-0 sm:max-w-lg">
<Sheet.Header>
<Sheet.Title>{title}</Sheet.Title>
{#if description}
<Sheet.Description>{description}</Sheet.Description>
{/if}
</Sheet.Header>
<div class="flex flex-1 flex-col gap-4 overflow-y-auto px-4 py-4">
{@render children()}
</div>
{#if onSubmit}
<Sheet.Footer>
<Button onclick={onSubmit} disabled={submitting}>{submitLabel}</Button>
</Sheet.Footer>
{/if}
</Sheet.Content>
</Sheet.Root>
@@ -0,0 +1,105 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { page } from '$app/state';
import { resolve } from '$app/paths';
import * as Sidebar from '@evobgp/ui/components/sidebar/index.js';
import * as Breadcrumb from '@evobgp/ui/components/breadcrumb/index.js';
import { Separator } from '@evobgp/ui/components/separator/index.js';
import type { ThemePreference } from '$lib/theme.js';
import { allNavItems, navGroups, parentRoute, routeLabels } from '$lib/ui/app/layout/nav.js';
import AppBrand from '$lib/ui/app/layout/app-brand.svelte';
import ThemeMenu from '$lib/ui/app/layout/theme-menu.svelte';
import AppVersion from '$lib/ui/app/layout/app-version.svelte';
import AppMobileNav from '$lib/ui/app/layout/app-mobile-nav.svelte';
type Props = {
children: Snippet;
theme?: ThemePreference;
};
let { children, theme = $bindable<ThemePreference>('system') }: Props = $props();
let mobileNavOpen = $state(false);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const navHref = (href: string) => resolve(href as any);
function isActive(href: string) {
const pathname = page.url.pathname;
if (href === '/') return pathname === '/';
return pathname === href || pathname.startsWith(href + '/');
}
const activeItem = $derived(allNavItems.find((i) => isActive(i.href)) ?? allNavItems[0]);
const parentHref = $derived(parentRoute[activeItem.href]);
const parentLabel = $derived(parentHref ? routeLabels[parentHref] : null);
const currentLabel = $derived(routeLabels[activeItem.href] ?? 'EvoBGP');
</script>
<Sidebar.Provider>
<Sidebar.Root collapsible="icon">
<Sidebar.Header>
<AppBrand />
</Sidebar.Header>
<Sidebar.Content>
{#each navGroups as group (group.label)}
<Sidebar.Group>
<Sidebar.GroupLabel>{group.label}</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each group.items as item (item.href)}
{@const Icon = item.icon}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={isActive(item.href)} tooltipContent={item.label}>
{#snippet child({ props })}
<a href={navHref(item.href)} {...props}>
<Icon class="size-4" />
<span>{item.label}</span>
</a>
{/snippet}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.GroupContent>
</Sidebar.Group>
{/each}
</Sidebar.Content>
<Sidebar.Footer>
<AppVersion />
</Sidebar.Footer>
<Sidebar.Rail />
</Sidebar.Root>
<Sidebar.Inset>
<header
class="sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-background/80"
>
<div class="flex items-center gap-2 md:hidden">
<AppMobileNav bind:open={mobileNavOpen} bind:theme />
<span class="font-semibold tracking-tight">EvoBGP</span>
</div>
<Sidebar.Trigger class="-ms-1 hidden md:flex" />
<Separator orientation="vertical" class="mr-2 hidden h-4 md:block" />
<Breadcrumb.Root class="hidden min-w-0 md:flex">
<Breadcrumb.List>
{#if parentLabel && parentHref}
<Breadcrumb.Item class="hidden md:block">
<Breadcrumb.Link href={navHref(parentHref)}>{parentLabel}</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator class="hidden md:block" />
{/if}
<Breadcrumb.Item>
<Breadcrumb.Page>{currentLabel}</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
<div class="ml-auto flex items-center gap-2">
<ThemeMenu bind:theme class="hidden md:flex" />
</div>
</header>
<main class="flex flex-1 flex-col gap-4 p-4 md:gap-6 md:p-6">
{@render children()}
</main>
</Sidebar.Inset>
</Sidebar.Provider>
@@ -0,0 +1,45 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import X from '@lucide/svelte/icons/x';
export type FilterChip = {
id: string;
label: string;
};
type Props = {
chips?: FilterChip[];
onRemoveChip?: (id: string) => void;
onClear?: () => void;
children?: Snippet;
};
let { chips = [], onRemoveChip, onClear, children }: Props = $props();
</script>
<div class="flex flex-wrap items-center gap-2">
{#if children}
{@render children()}
{/if}
{#each chips as chip (chip.id)}
<Badge variant="secondary" class="gap-1 pr-1">
{chip.label}
{#if onRemoveChip}
<Button
variant="ghost"
size="icon-sm"
class="size-5"
onclick={() => onRemoveChip(chip.id)}
aria-label="Убрать фильтр {chip.label}"
>
<X class="size-3" />
</Button>
{/if}
</Badge>
{/each}
{#if chips.length > 0 && onClear}
<Button variant="ghost" size="sm" onclick={onClear}>Сбросить</Button>
{/if}
</div>
@@ -8,17 +8,17 @@
supportsCsvIO supportsCsvIO
} from '$lib/components/modules/module-helpers.js'; } from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js'; import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import ModuleAsEntryDialog from '$lib/components/modules/ModuleAsEntryDialog.svelte'; import ModuleAsEntryDialog from '$lib/components/modules/ModuleAsEntryDialog.svelte';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
@@ -8,9 +8,9 @@
NONE_OPTION, NONE_OPTION,
nullableSelectValue nullableSelectValue
} from '$lib/components/modules/module-helpers.js'; } from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -18,8 +18,13 @@
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
DialogDescription DialogDescription
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
type Props = { type Props = {
@@ -14,17 +14,22 @@
NONE_OPTION, NONE_OPTION,
nullableSelectValue nullableSelectValue
} from '$lib/components/modules/module-helpers.js'; } from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter DialogFooter
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
type Props = { type Props = {
@@ -6,18 +6,18 @@
communityLabel, communityLabel,
normalizeCdnSourceKind normalizeCdnSourceKind
} from '$lib/components/modules/module-helpers.js'; } from '$lib/components/modules/module-helpers.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js'; import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import ModuleCdnSourceDialog from '$lib/components/modules/ModuleCdnSourceDialog.svelte'; import ModuleCdnSourceDialog from '$lib/components/modules/ModuleCdnSourceDialog.svelte';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
@@ -2,9 +2,9 @@
import { apiMutate } from '$lib/api/client.js'; import { apiMutate } from '$lib/api/client.js';
import type { ModuleCreate } from '$lib/api/types.js'; import type { ModuleCreate } from '$lib/api/types.js';
import { moduleTypeRu } from '$lib/ui-labels.js'; import { moduleTypeRu } from '$lib/ui-labels.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -12,9 +12,14 @@
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
DialogDescription DialogDescription
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
import { Switch } from '$lib/ui/core/switch/index.js'; Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { Switch } from '@evobgp/ui/components/switch/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
type Props = { type Props = {
@@ -2,8 +2,8 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import type { ModuleRow } from '$lib/api/types.js'; import type { ModuleRow } from '$lib/api/types.js';
import { moduleEnabledRu, moduleEnabledBadgeVariant, moduleTypeRu } from '$lib/ui-labels.js'; import { moduleEnabledRu, moduleEnabledBadgeVariant, moduleTypeRu } from '$lib/ui-labels.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import PageHeader from '$lib/ui/app/page-header/page-header.svelte'; import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
import ArrowLeft from '@lucide/svelte/icons/arrow-left'; import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Pencil from '@lucide/svelte/icons/pencil'; import Pencil from '@lucide/svelte/icons/pencil';
@@ -26,13 +26,7 @@
<Button variant="ghost" size="icon-sm" class="mt-1 shrink-0" href={resolve('/modules')}> <Button variant="ghost" size="icon-sm" class="mt-1 shrink-0" href={resolve('/modules')}>
<ArrowLeft class="size-4" /> <ArrowLeft class="size-4" />
</Button> </Button>
<PageHeader <PageHeader class="min-w-0 flex-1" title={mod.name} description={mod.id}>
class="min-w-0 flex-1"
title={mod.name}
description={mod.id}
icon={Blocks}
iconClass="bg-chart-2/15 text-chart-2"
>
{#snippet actions()} {#snippet actions()}
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<Badge variant="outline">{moduleTypeRu(mod.type)}</Badge> <Badge variant="outline">{moduleTypeRu(mod.type)}</Badge>
@@ -6,17 +6,17 @@
sanitizeFilenamePart, sanitizeFilenamePart,
supportsCsvIO supportsCsvIO
} from '$lib/components/modules/module-helpers.js'; } from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js'; import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import ModuleDomainEntryDialog from '$lib/components/modules/ModuleDomainEntryDialog.svelte'; import ModuleDomainEntryDialog from '$lib/components/modules/ModuleDomainEntryDialog.svelte';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
@@ -8,17 +8,22 @@
NONE_OPTION, NONE_OPTION,
nullableSelectValue nullableSelectValue
} from '$lib/components/modules/module-helpers.js'; } from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter DialogFooter
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
type Props = { type Props = {
@@ -16,19 +16,24 @@
NONE_OPTION, NONE_OPTION,
nullableSelectValue nullableSelectValue
} from '$lib/components/modules/module-helpers.js'; } from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js'; import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter DialogFooter
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
import { Switch } from '$lib/ui/core/switch/index.js'; Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { Switch } from '@evobgp/ui/components/switch/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
const dohPolicyOptions: { value: DohResolverPolicy; label: string; hint: string }[] = [ const dohPolicyOptions: { value: DohResolverPolicy; label: string; hint: string }[] = [
@@ -2,17 +2,22 @@
import { apiMutate } from '$lib/api/client.js'; import { apiMutate } from '$lib/api/client.js';
import type { BgpCommunity, IpRangeEntry, IpRangeEntryCreate } from '$lib/api/types.js'; import type { BgpCommunity, IpRangeEntry, IpRangeEntryCreate } from '$lib/api/types.js';
import { communityLabel, communityOptionLabel } from '$lib/components/modules/module-helpers.js'; import { communityLabel, communityOptionLabel } from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter DialogFooter
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
type Props = { type Props = {
@@ -6,17 +6,17 @@
sanitizeFilenamePart, sanitizeFilenamePart,
supportsCsvIO supportsCsvIO
} from '$lib/components/modules/module-helpers.js'; } from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js'; import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import ModuleIpRangeEntryDialog from '$lib/components/modules/ModuleIpRangeEntryDialog.svelte'; import ModuleIpRangeEntryDialog from '$lib/components/modules/ModuleIpRangeEntryDialog.svelte';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
@@ -7,8 +7,8 @@
dohProfileLabel, dohProfileLabel,
moduleDohProfileIds moduleDohProfileIds
} from '$lib/components/modules/module-helpers.js'; } from '$lib/components/modules/module-helpers.js';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/ui/core/card/index.js'; import { Card, CardContent, CardHeader, CardTitle } from '@evobgp/ui/components/card/index.js';
import CardSkeleton from '$lib/ui/patterns/feedback/card-skeleton.svelte'; import CardSkeleton from '$lib/components/patterns/feedback/card-skeleton.svelte';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import ArrowDownUp from '@lucide/svelte/icons/arrow-down-up'; import ArrowDownUp from '@lucide/svelte/icons/arrow-down-up';
import Timer from '@lucide/svelte/icons/timer'; import Timer from '@lucide/svelte/icons/timer';
@@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import type { AsEntry, CdnSource, DomainEntry, IpRangeEntry, ModuleRow } from '$lib/api/types.js'; import type { AsEntry, CdnSource, DomainEntry, IpRangeEntry, ModuleRow } from '$lib/api/types.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
type Props = { type Props = {
mod: ModuleRow; mod: ModuleRow;
@@ -35,31 +35,31 @@
type ScheduleEditor, type ScheduleEditor,
type ScheduleMode type ScheduleMode
} from '$lib/maintenance/policy-schedule.js'; } from '$lib/maintenance/policy-schedule.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js'; import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter DialogFooter
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Switch } from '$lib/ui/core/switch/index.js'; import { Switch } from '@evobgp/ui/components/switch/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js'; import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte'; import FormField from '$lib/components/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte'; import AppInput from '$lib/components/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import type { DataTableColumn } from '$lib/ui/patterns/data-table/types.js'; import type { DataTableColumn } from '$lib/components/patterns/data-table/types.js';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil'; import Pencil from '@lucide/svelte/icons/pencil';
@@ -17,17 +17,17 @@
type PostgresMaintLog, type PostgresMaintLog,
type CorrelationResponse type CorrelationResponse
} from '$lib/monitoring/postgres.js'; } from '$lib/monitoring/postgres.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/ui/core/tabs/index.js'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js'; import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { import {
Table, Table,
TableBody, TableBody,
@@ -35,9 +35,9 @@
TableHead, TableHead,
TableHeader, TableHeader,
TableRow TableRow
} from '$lib/ui/core/table/index.js'; } from '@evobgp/ui/components/table/index.js';
import { Switch } from '$lib/ui/core/switch/index.js'; import { Switch } from '@evobgp/ui/components/switch/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import Database from '@lucide/svelte/icons/database'; import Database from '@lucide/svelte/icons/database';
import RefreshCw from '@lucide/svelte/icons/refresh-cw'; import RefreshCw from '@lucide/svelte/icons/refresh-cw';
@@ -14,33 +14,33 @@
} from '$lib/runtime-logs/runtime-logs-api.js'; } from '$lib/runtime-logs/runtime-logs-api.js';
import { formatBytes } from '$lib/monitoring/postgres.js'; import { formatBytes } from '$lib/monitoring/postgres.js';
import { formatDateTime } from '$lib/modules/display.js'; import { formatDateTime } from '$lib/modules/display.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle DialogTitle
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from '$lib/ui/core/dropdown-menu/index.js'; } from '@evobgp/ui/components/dropdown-menu/index.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/ui/core/tabs/index.js'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import type { DataTableColumn } from '$lib/ui/patterns/data-table/types.js'; import type { DataTableColumn } from '$lib/components/patterns/data-table/types.js';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte'; import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import ScrollPreBlock from '$lib/components/app/scroll-pre-block.svelte'; import ScrollPreBlock from '$lib/components/app/scroll-pre-block.svelte';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import { import {
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import { Switch } from '$lib/ui/core/switch/index.js'; import { Switch } from '@evobgp/ui/components/switch/index.js';
import { readNetworkAutoRefresh, writeNetworkAutoRefresh } from '$lib/network/network-metrics.js'; import { readNetworkAutoRefresh, writeNetworkAutoRefresh } from '$lib/network/network-metrics.js';
type Props = { type Props = {
@@ -3,14 +3,14 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import { loadSettings, partitionSettings } from '$lib/settings/settings-api.js'; import { loadSettings, partitionSettings } from '$lib/settings/settings-api.js';
import { BIRD_SETTING_KEYS } from '$lib/settings/settings-known-keys.js'; import { BIRD_SETTING_KEYS } from '$lib/settings/settings-known-keys.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { notifyApiError } from '$lib/ui/app/toast.js'; import { notifyApiError } from '$lib/ui/app/toast.js';
import ArrowRight from '@lucide/svelte/icons/arrow-right'; import ArrowRight from '@lucide/svelte/icons/arrow-right';
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal'; import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
@@ -8,9 +8,10 @@
networkOverallStatusHint, networkOverallStatusHint,
networkOverallStatusLabel networkOverallStatusLabel
} from '$lib/network/network-metrics.js'; } from '$lib/network/network-metrics.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js'; import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import KpiMetricsGrid from '$lib/ui/patterns/kpi/kpi-metrics-grid.svelte'; import SectionCards, { type SectionCardItem } from '$lib/components/section-cards.svelte';
import SectionCardsSkeleton from '$lib/components/section-cards-skeleton.svelte';
import NetworkSpeakerStatusCard from '$lib/components/network/NetworkSpeakerStatusCard.svelte'; import NetworkSpeakerStatusCard from '$lib/components/network/NetworkSpeakerStatusCard.svelte';
import CheckCircle from '@lucide/svelte/icons/check-circle'; import CheckCircle from '@lucide/svelte/icons/check-circle';
import AlertTriangle from '@lucide/svelte/icons/alert-triangle'; import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
@@ -41,45 +42,6 @@
onSpeakerSelect onSpeakerSelect
}: Props = $props(); }: Props = $props();
const statAccents = [
{
border: 'border-l-chart-3',
bg: 'bg-chart-3/5',
iconBg: 'bg-chart-3/15',
iconText: 'text-chart-3'
},
{
border: 'border-l-chart-2',
bg: 'bg-chart-2/5',
iconBg: 'bg-chart-2/15',
iconText: 'text-chart-2'
},
{
border: 'border-l-chart-4',
bg: 'bg-chart-4/5',
iconBg: 'bg-chart-4/15',
iconText: 'text-chart-4'
},
{
border: 'border-l-warning',
bg: 'bg-warning/5',
iconBg: 'bg-warning/15',
iconText: 'text-warning'
},
{
border: 'border-l-destructive',
bg: 'bg-destructive/5',
iconBg: 'bg-destructive/15',
iconText: 'text-destructive'
},
{
border: 'border-l-info',
bg: 'bg-info/10',
iconBg: 'bg-info/15',
iconText: 'text-info'
}
] as const;
const metrics = $derived(aggregateNetworkMetrics(peers, speakers, bird)); const metrics = $derived(aggregateNetworkMetrics(peers, speakers, bird));
const overallStatus = $derived(deriveNetworkOverallStatus(metrics)); const overallStatus = $derived(deriveNetworkOverallStatus(metrics));
const overallHint = $derived(networkOverallStatusHint(overallStatus, metrics)); const overallHint = $derived(networkOverallStatusHint(overallStatus, metrics));
@@ -91,76 +53,51 @@
return `${bird.bgp_established}/${bird.bgp_sessions_total}`; return `${bird.bgp_established}/${bird.bgp_sessions_total}`;
}); });
const kpiCards = $derived.by(() => [ const sectionCards = $derived.by((): SectionCardItem[] => [
{ {
id: 'peers',
label: 'BGP-пиры', label: 'BGP-пиры',
value: initialLoading ? '—' : String(metrics.peersTotal), value: initialLoading ? '—' : String(metrics.peersTotal),
description: initialLoading hint: initialLoading
? '' ? ''
: `${metrics.peersEstablished} Established из ${metrics.peersEnabled} вкл.`, : `${metrics.peersEstablished} Established / ${metrics.peersEnabled} вкл.`,
icon: Share2, icon: Share2,
accent: statAccents[0], variant: metrics.peersMismatch > 0 ? 'warning' : 'default'
badge: metrics.peersMismatch > 0 ? `mismatch ${metrics.peersMismatch}` : 'peers',
badgeClass:
metrics.peersMismatch > 0 ? 'border-warning/30 bg-warning/15 text-warning' : undefined
}, },
{ {
id: 'established',
label: 'Активные сессии', label: 'Активные сессии',
value: initialLoading ? '—' : String(metrics.peersEstablished), value: initialLoading ? '—' : String(metrics.peersEstablished),
description: 'Established среди включённых пиров', hint: 'Established',
icon: CheckCircle2, icon: CheckCircle2
accent: statAccents[1],
badge: metrics.peersEstablished > 0 ? 'Established' : 'нет сессий',
badgeClass:
metrics.peersEstablished > 0 ? 'border-success/30 bg-success/15 text-success' : undefined
}, },
{ {
id: 'speakers',
label: 'Спикеры online', label: 'Спикеры online',
value: initialLoading ? '—' : `${metrics.speakersOnline}/${metrics.speakersTotal}`, value: initialLoading ? '—' : `${metrics.speakersOnline}/${metrics.speakersTotal}`,
description: 'agent + BGP poll', hint: 'agent + BGP poll',
icon: Server, icon: Server,
accent: statAccents[2], variant: metrics.speakersOnline < metrics.speakersTotal ? 'warning' : 'default'
badge: metrics.speakersOnline === metrics.speakersTotal ? 'все online' : 'есть offline',
badgeClass:
metrics.speakersOnline === metrics.speakersTotal
? 'border-success/30 bg-success/15 text-success'
: 'border-warning/30 bg-warning/15 text-warning'
}, },
{ {
id: 'drift',
label: 'Drift', label: 'Drift',
value: initialLoading ? '—' : String(metrics.speakersDrift), value: initialLoading ? '—' : String(metrics.speakersDrift),
description: 'applied ≠ published', hint: 'applied ≠ published',
icon: GitBranch, icon: GitBranch,
accent: statAccents[3], variant: metrics.speakersDrift > 0 ? 'warning' : 'default'
badge: metrics.speakersDrift > 0 ? 'требует apply' : 'синхронно',
badgeVariant: metrics.speakersDrift > 0 ? ('secondary' as const) : ('outline' as const)
}, },
{ {
id: 'poll-errors',
label: 'Ошибки опроса', label: 'Ошибки опроса',
value: initialLoading ? '—' : String(metrics.pollErrors), value: initialLoading ? '—' : String(metrics.pollErrors),
description: 'agent или BGP poll', hint: 'agent / BGP poll',
icon: Activity, icon: Activity,
accent: statAccents[4], variant: metrics.pollErrors > 0 ? 'destructive' : 'default'
badge: metrics.pollErrors > 0 ? 'ошибки' : 'ok',
badgeClass:
metrics.pollErrors === 0 ? 'border-success/30 bg-success/15 text-success' : undefined
}, },
{ {
id: 'cp-bird',
label: 'BGP на CP', label: 'BGP на CP',
value: initialLoading ? '—' : birdText, value: initialLoading ? '—' : birdText,
description: bird?.birdc_configured hint: bird?.birdc_configured ? 'Established / total' : (bird?.message ?? 'birdc N/A'),
? 'Established / total на API-хосте'
: (bird?.message ?? 'birdc не настроен'),
icon: Bird, icon: Bird,
accent: statAccents[5], onClick: () => {
badge: !bird?.birdc_configured ? 'N/A' : bird?.healthy ? 'В норме' : 'Деградация', window.location.href = resolve('/monitoring');
href: '/monitoring' as const }
} }
]); ]);
</script> </script>
@@ -206,12 +143,11 @@
{/if} {/if}
{/if} {/if}
<KpiMetricsGrid {#if initialLoading || loading}
cards={kpiCards} <SectionCardsSkeleton count={6} class="sm:grid-cols-2 xl:grid-cols-3" />
loading={initialLoading || loading} {:else}
skeletonCount={6} <SectionCards items={sectionCards} class="sm:grid-cols-2 xl:grid-cols-3" />
class="sm:grid-cols-2 xl:grid-cols-3" {/if}
/>
<section class="flex min-w-0 flex-col gap-4"> <section class="flex min-w-0 flex-col gap-4">
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import { apiMutate } from '$lib/api/client.js'; import { apiMutate } from '$lib/api/client.js';
import type { PeerRow, BgpPeerCreate, SpeakerRow, PeerSessionOnSpeaker } from '$lib/api/types.js'; import type { PeerRow, BgpPeerCreate, SpeakerRow, PeerSessionOnSpeaker } from '$lib/api/types.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -18,13 +18,18 @@
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
DialogDescription DialogDescription
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
import { Switch } from '$lib/ui/core/switch/index.js'; Select,
import FormField from '$lib/ui/patterns/form/form-field.svelte'; SelectContent,
import AppInput from '$lib/ui/patterns/form/app-input.svelte'; SelectItem,
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; SelectTrigger
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; } from '@evobgp/ui/components/select/index.js';
import { Switch } from '@evobgp/ui/components/switch/index.js';
import FormField from '$lib/components/patterns/form/form-field.svelte';
import AppInput from '$lib/components/patterns/form/app-input.svelte';
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil'; import Pencil from '@lucide/svelte/icons/pencil';
@@ -9,17 +9,17 @@
speakerLiveAgentError, speakerLiveAgentError,
speakerLiveBgpError speakerLiveBgpError
} from '$lib/network/network-metrics.js'; } from '$lib/network/network-metrics.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js'; import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Separator } from '$lib/ui/core/separator/index.js'; import { Separator } from '@evobgp/ui/components/separator/index.js';
import { import {
Sheet, Sheet,
SheetContent, SheetContent,
SheetDescription, SheetDescription,
SheetHeader, SheetHeader,
SheetTitle SheetTitle
} from '$lib/ui/core/sheet/index.js'; } from '@evobgp/ui/components/sheet/index.js';
import { import {
Table, Table,
TableBody, TableBody,
@@ -27,7 +27,7 @@
TableHead, TableHead,
TableHeader, TableHeader,
TableRow TableRow
} from '$lib/ui/core/table/index.js'; } from '@evobgp/ui/components/table/index.js';
import AlertTriangle from '@lucide/svelte/icons/alert-triangle'; import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
type Props = { type Props = {
@@ -6,14 +6,14 @@
speakerHasDrift, speakerHasDrift,
speakerLabel speakerLabel
} from '$lib/network/network-metrics.js'; } from '$lib/network/network-metrics.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import Server from '@lucide/svelte/icons/server'; import Server from '@lucide/svelte/icons/server';
@@ -6,15 +6,15 @@
speakerDisplayStatus, speakerDisplayStatus,
speakerHasDrift speakerHasDrift
} from '$lib/network/network-metrics.js'; } from '$lib/network/network-metrics.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -22,12 +22,12 @@
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
DialogDescription DialogDescription
} from '$lib/ui/core/dialog/index.js'; } from '@evobgp/ui/components/dialog/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js'; import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte'; import FormField from '$lib/components/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte'; import AppInput from '$lib/components/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil'; import Pencil from '@lucide/svelte/icons/pencil';
@@ -1,14 +1,19 @@
<script lang="ts"> <script lang="ts">
import type { RevisionDiff, RevisionPrefix, RevisionRow } from '$lib/api/types.js'; import type { RevisionDiff, RevisionPrefix, RevisionRow } from '$lib/api/types.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { formatDateTime } from '$lib/modules/display.js'; import { formatDateTime } from '$lib/modules/display.js';
import { sortRevisionDiffItems } from '$lib/sort-prefixes.js'; import { sortRevisionDiffItems } from '$lib/sort-prefixes.js';
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
import Filter from '@lucide/svelte/icons/filter'; import Filter from '@lucide/svelte/icons/filter';
import Search from '@lucide/svelte/icons/search'; import Search from '@lucide/svelte/icons/search';
import X from '@lucide/svelte/icons/x'; import X from '@lucide/svelte/icons/x';
@@ -2,16 +2,16 @@
import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { JobRow } from '$lib/api/types.js'; import type { JobRow } from '$lib/api/types.js';
import type { JobDetailedReport, JobLogEntry } from './types.js'; import type { JobDetailedReport, JobLogEntry } from './types.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte'; import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
import { formatDateTime } from '$lib/modules/display.js'; import { formatDateTime } from '$lib/modules/display.js';
import RefreshCw from '@lucide/svelte/icons/refresh-cw'; import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import Eye from '@lucide/svelte/icons/eye'; import Eye from '@lucide/svelte/icons/eye';
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { BirdStatus } from '$lib/api/types.js'; import type { BirdStatus } from '$lib/api/types.js';
import { Badge } from '$lib/ui/core/badge/index.js'; import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { Card } from '$lib/ui/core/card/index.js'; import { Card } from '@evobgp/ui/components/card/index.js';
import RefreshCw from '@lucide/svelte/icons/refresh-cw'; import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import Play from '@lucide/svelte/icons/play'; import Play from '@lucide/svelte/icons/play';
import RotateCcw from '@lucide/svelte/icons/rotate-ccw'; import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
@@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import type { RevisionRow } from '$lib/api/types.js'; import type { RevisionRow } from '$lib/api/types.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte'; import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { formatDateTime } from '$lib/modules/display.js'; import { formatDateTime } from '$lib/modules/display.js';
import RefreshCw from '@lucide/svelte/icons/refresh-cw'; import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import Undo from '@lucide/svelte/icons/undo'; import Undo from '@lucide/svelte/icons/undo';
@@ -6,9 +6,9 @@
getCoreRowModel, getCoreRowModel,
getPaginationRowModel getPaginationRowModel
} from '@tanstack/table-core'; } from '@tanstack/table-core';
import { createSvelteTable, FlexRender } from '$lib/ui/core/data-table/index.js'; import { createSvelteTable, FlexRender } from '@evobgp/ui/components/data-table/index.js';
import * as Table from '$lib/ui/core/table/index.js'; import * as Table from '@evobgp/ui/components/table/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
type Props = { type Props = {
rows: RowData[]; rows: RowData[];
@@ -8,15 +8,15 @@
networkOverallStatusHint, networkOverallStatusHint,
networkOverallStatusLabel networkOverallStatusLabel
} from '$lib/network/network-metrics.js'; } from '$lib/network/network-metrics.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js'; import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
CardDescription CardDescription
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import CheckCircle from '@lucide/svelte/icons/check-circle'; import CheckCircle from '@lucide/svelte/icons/check-circle';
import AlertTriangle from '@lucide/svelte/icons/alert-triangle'; import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
import XCircle from '@lucide/svelte/icons/x-circle'; import XCircle from '@lucide/svelte/icons/x-circle';
@@ -0,0 +1,80 @@
<script lang="ts">
import { resolve } from '$app/paths';
import type { JobRow } from '$lib/api/types.js';
import { formatDateTime } from '$lib/modules/display.js';
import { jobKindTitle } from '$lib/operations/job-kind-label.js';
import { jobStatusRu, jobStatusBadgeVariant } from '$lib/ui-labels.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import DataTableCard from '$lib/components/data-table-card.svelte';
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import ExternalLink from '@lucide/svelte/icons/external-link';
type Props = {
items: JobRow[];
moduleNameById: ReadonlyMap<string, string>;
loading?: boolean;
initialLoading?: boolean;
error?: string | null;
};
let {
items,
moduleNameById,
loading = false,
initialLoading = false,
error = null
}: Props = $props();
const columns = [
{ id: 'kind', label: 'Вид', sortable: true, sortValue: (j: JobRow) => j.kind },
{
id: 'status',
label: 'Статус',
sortable: true,
sortValue: (j: JobRow) => j.status
},
{
id: 'created',
label: 'Создана',
sortable: true,
sortValue: (j: JobRow) => j.created_at ?? ''
},
{ id: 'actions', label: '', class: 'w-10' }
] as const;
</script>
<DataTableCard title="Последние задачи" description="Фоновые задачи ingest, refresh и apply">
{#snippet toolbar()}
<Button variant="outline" size="sm" href={resolve('/operations?tab=jobs')}>
Все
<ArrowRight class="size-3.5" />
</Button>
{/snippet}
<AppDataTable
columns={[...columns]}
rows={items}
rowKey={(j) => j.job_id}
loading={initialLoading || loading}
{error}
emptyTitle="Нет задач"
emptyDescription="Задачи появятся после refresh или деплоя."
>
{#snippet cell({ row: j, column })}
{#if column.id === 'kind'}
<span class="font-medium">{jobKindTitle(j, moduleNameById)}</span>
{:else if column.id === 'status'}
<Badge variant={jobStatusBadgeVariant(j.status)}>{jobStatusRu(j.status)}</Badge>
{:else if column.id === 'created'}
<span class="text-xs whitespace-nowrap text-muted-foreground">
{formatDateTime(j.created_at)}
</span>
{:else if column.id === 'actions'}
<Button variant="ghost" size="icon-sm" href={resolve('/operations?tab=jobs')}>
<ExternalLink class="size-3.5" />
</Button>
{/if}
{/snippet}
</AppDataTable>
</DataTableCard>
@@ -0,0 +1,78 @@
<script lang="ts">
import { resolve } from '$app/paths';
import type { RevisionRow } from '$lib/api/types.js';
import { formatDateTime } from '$lib/modules/display.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import DataTableCard from '$lib/components/data-table-card.svelte';
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import ExternalLink from '@lucide/svelte/icons/external-link';
type Props = {
items: RevisionRow[];
loading?: boolean;
initialLoading?: boolean;
error?: string | null;
};
let { items, loading = false, initialLoading = false, error = null }: Props = $props();
const columns = [
{
id: 'id',
label: 'ID',
sortable: true,
sortValue: (rev: RevisionRow) => rev.id
},
{
id: 'created',
label: 'Создана',
sortable: true,
sortValue: (rev: RevisionRow) => rev.created_at ?? ''
},
{
id: 'prefixes',
label: 'Префиксов',
sortable: true,
sortValue: (rev: RevisionRow) => rev.materialized_prefix_count ?? 0
},
{ id: 'actions', label: '', class: 'w-10' }
] as const;
</script>
<DataTableCard
title="Последние ревизии"
description="Снимки конфигурации BIRD после обновления модулей"
>
{#snippet toolbar()}
<Button variant="outline" size="sm" href={resolve('/operations')}>
Все
<ArrowRight class="size-3.5" />
</Button>
{/snippet}
<AppDataTable
columns={[...columns]}
rows={items}
rowKey={(rev) => rev.id}
loading={initialLoading || loading}
{error}
emptyTitle="Нет ревизий"
emptyDescription="Ревизии появятся после обновления модулей."
>
{#snippet cell({ row: rev, column })}
{#if column.id === 'id'}
<span class="font-mono text-xs">{rev.id.slice(0, 8)}</span>
{:else if column.id === 'created'}
<span class="text-sm whitespace-nowrap text-muted-foreground">
{formatDateTime(rev.created_at)}
</span>
{:else if column.id === 'prefixes'}
<span class="tabular-nums">{rev.materialized_prefix_count}</span>
{:else if column.id === 'actions'}
<Button variant="ghost" size="icon-sm" href={resolve('/operations')}>
<ExternalLink class="size-3.5" />
</Button>
{/if}
{/snippet}
</AppDataTable>
</DataTableCard>
@@ -0,0 +1,15 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { cn } from '$lib/utils.js';
type Props = {
children: Snippet;
class?: string;
};
let { children, class: className }: Props = $props();
</script>
<div class={cn('flex flex-col gap-4 md:gap-6', className)}>
{@render children()}
</div>
@@ -8,7 +8,7 @@
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle AlertDialogTitle
} from '$lib/ui/core/alert-dialog/index.js'; } from '@evobgp/ui/components/alert-dialog/index.js';
import { closeConfirm, confirmState } from './confirm-state.svelte.js'; import { closeConfirm, confirmState } from './confirm-state.svelte.js';
const state = $derived(confirmState.current); const state = $derived(confirmState.current);
@@ -39,7 +39,7 @@
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
class={state.destructive class={state.destructive
? 'text-destructive-foreground bg-destructive hover:bg-destructive/90' ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: ''} : ''}
disabled={state.loading} disabled={state.loading}
onclick={(e) => { onclick={(e) => {
@@ -1,14 +1,14 @@
<script lang="ts" generics="T extends Record<string, unknown>"> <script lang="ts" generics="T extends Record<string, unknown>">
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import * as Table from '$lib/ui/core/table/index.js'; import * as Table from '@evobgp/ui/components/table/index.js';
import { Skeleton } from '$lib/ui/core/skeleton/index.js'; import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js'; import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte'; import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
import ArrowUpDown from '@lucide/svelte/icons/arrow-up-down'; import ArrowUpDown from '@lucide/svelte/icons/arrow-up-down';
import ArrowUp from '@lucide/svelte/icons/arrow-up'; import ArrowUp from '@lucide/svelte/icons/arrow-up';
import ArrowDown from '@lucide/svelte/icons/arrow-down'; import ArrowDown from '@lucide/svelte/icons/arrow-down';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import type { DataTableColumn } from './types.js'; import type { DataTableColumn } from './types.js';
type Props = { type Props = {
@@ -19,6 +19,8 @@
error?: string | null; error?: string | null;
emptyTitle?: string; emptyTitle?: string;
emptyDescription?: string; emptyDescription?: string;
/** Client-side page size; 0 = all rows */
pageSize?: number;
toolbar?: Snippet; toolbar?: Snippet;
cell: Snippet<[{ row: T; column: DataTableColumn<T> }]>; cell: Snippet<[{ row: T; column: DataTableColumn<T> }]>;
class?: string; class?: string;
@@ -32,6 +34,7 @@
error = null, error = null,
emptyTitle = 'Нет записей', emptyTitle = 'Нет записей',
emptyDescription, emptyDescription,
pageSize = 0,
toolbar, toolbar,
cell, cell,
class: className class: className
@@ -39,6 +42,7 @@
let sortColumnId = $state<string | null>(null); let sortColumnId = $state<string | null>(null);
let sortDir = $state<'asc' | 'desc'>('asc'); let sortDir = $state<'asc' | 'desc'>('asc');
let pageIndex = $state(0);
const sortedRows = $derived.by(() => { const sortedRows = $derived.by(() => {
if (!sortColumnId) return rows; if (!sortColumnId) return rows;
@@ -57,6 +61,21 @@
return copy; return copy;
}); });
const paginatedRows = $derived.by(() => {
if (!pageSize || pageSize <= 0) return sortedRows;
const start = pageIndex * pageSize;
return sortedRows.slice(start, start + pageSize);
});
const pageCount = $derived(
pageSize > 0 ? Math.max(1, Math.ceil(sortedRows.length / pageSize)) : 1
);
$effect(() => {
rows;
if (pageIndex !== 0) pageIndex = 0;
});
function toggleSort(col: DataTableColumn<T>) { function toggleSort(col: DataTableColumn<T>) {
if (!col.sortable) return; if (!col.sortable) return;
if (sortColumnId === col.id) { if (sortColumnId === col.id) {
@@ -131,7 +150,7 @@
</Table.Cell> </Table.Cell>
</Table.Row> </Table.Row>
{:else} {:else}
{#each sortedRows as row (rowKey(row))} {#each paginatedRows as row (rowKey(row))}
<Table.Row> <Table.Row>
{#each columns as col (col.id)} {#each columns as col (col.id)}
<Table.Cell class={col.class}> <Table.Cell class={col.class}>
@@ -144,4 +163,27 @@
</Table.Body> </Table.Body>
</Table.Root> </Table.Root>
</div> </div>
{#if pageSize > 0 && sortedRows.length > pageSize}
<div class="flex items-center justify-between gap-2 text-sm text-muted-foreground">
<span>
{pageIndex * pageSize + 1}{Math.min((pageIndex + 1) * pageSize, sortedRows.length)} из
{sortedRows.length}
</span>
<div class="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={pageIndex === 0}
onclick={() => (pageIndex -= 1)}>Назад</Button
>
<Button
variant="outline"
size="sm"
disabled={pageIndex >= pageCount - 1}
onclick={() => (pageIndex += 1)}>Вперёд</Button
>
</div>
</div>
{/if}
</div> </div>
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Card, CardContent, CardHeader } from '$lib/ui/core/card/index.js'; import { Card, CardContent, CardHeader } from '@evobgp/ui/components/card/index.js';
import { Skeleton } from '$lib/ui/core/skeleton/index.js'; import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
</script> </script>
<Card> <Card>
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Skeleton } from '$lib/ui/core/skeleton/index.js'; import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
import * as Table from '$lib/ui/core/table/index.js'; import * as Table from '@evobgp/ui/components/table/index.js';
type Props = { type Props = {
columns?: number; columns?: number;
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import type { ComponentProps } from 'svelte'; import type { ComponentProps } from 'svelte';
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Textarea } from '$lib/ui/core/textarea/index.js'; import { Textarea } from '@evobgp/ui/components/textarea/index.js';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import type { ComponentProps } from 'svelte'; import type { ComponentProps } from 'svelte';
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { Label } from '$lib/ui/core/label/index.js'; import { Label } from '@evobgp/ui/components/label/index.js';
type Props = { type Props = {
label: string; label: string;
@@ -0,0 +1,69 @@
<script lang="ts" generics="T">
import type { Snippet } from 'svelte';
import AlertCircle from '@lucide/svelte/icons/alert-circle';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
type Props = {
data: T | undefined;
isLoading: boolean;
isError: boolean;
error?: unknown;
empty?: boolean;
emptyTitle?: string;
emptyDescription?: string;
emptyAction?: Snippet;
onRetry?: () => void;
skeleton?: Snippet;
children: Snippet<[T]>;
};
let {
data,
isLoading,
isError,
error,
empty = false,
emptyTitle = 'Нет данных',
emptyDescription,
emptyAction,
onRetry,
skeleton,
children
}: Props = $props();
const errorMessage = $derived(
error instanceof Error ? error.message : 'Не удалось загрузить данные'
);
</script>
{#if isLoading}
{#if skeleton}
{@render skeleton()}
{:else}
<div class="flex flex-col gap-3">
<Skeleton class="h-8 w-48" />
<Skeleton class="h-32 w-full" />
</div>
{/if}
{:else if isError}
<EmptyState
title="Ошибка загрузки"
description={errorMessage}
icon={AlertCircle}
action={onRetry ? retryAction : undefined}
/>
{:else if empty || data == null}
<EmptyState title={emptyTitle} description={emptyDescription} action={emptyAction} />
{:else}
{@render children(data)}
{/if}
{#snippet retryAction()}
<Button variant="outline" size="sm" onclick={onRetry}>
<RefreshCw class="size-4" />
Повторить
</Button>
{/snippet}
@@ -0,0 +1,26 @@
<script lang="ts">
import { Card, CardContent } from '@evobgp/ui/components/card/index.js';
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
import { cn } from '$lib/utils.js';
type Props = {
count?: number;
class?: string;
};
let { count = 4, class: className }: Props = $props();
</script>
<div class={cn('grid gap-3 sm:grid-cols-2 lg:grid-cols-3', className)}>
{#each Array.from({ length: count }) as _, i (i)}
<Card>
<CardContent class="flex items-start gap-2.5 px-3 py-2.5">
<Skeleton class="size-7 shrink-0 rounded-md" />
<div class="flex flex-1 flex-col gap-2">
<Skeleton class="h-3 w-20" />
<Skeleton class="h-6 w-16" />
</div>
</CardContent>
</Card>
{/each}
</div>
@@ -0,0 +1,111 @@
<script lang="ts">
import type { Component, Snippet } from 'svelte';
import { Card, CardContent } from '@evobgp/ui/components/card/index.js';
import { cn } from '$lib/utils.js';
export type SectionCardItem = {
label: string | Snippet;
value: string | number | Snippet;
hint?: string | Snippet;
icon?: Component;
badge?: Snippet;
variant?: 'default' | 'warning' | 'destructive';
active?: boolean;
onClick?: () => void;
};
type Props = {
items: SectionCardItem[];
class?: string;
};
let { items, class: className }: Props = $props();
const variantClass: Record<NonNullable<SectionCardItem['variant']>, string> = {
default: '',
warning: 'border-warning/50',
destructive: 'border-destructive/50'
};
const valueVariantClass: Record<NonNullable<SectionCardItem['variant']>, string> = {
default: '',
warning: 'text-warning-foreground',
destructive: 'text-destructive'
};
function sectionGridClass(count: number): string {
if (count <= 1) return 'grid-cols-1';
if (count === 2) return 'sm:grid-cols-2';
if (count === 3) return 'sm:grid-cols-2 lg:grid-cols-3';
if (count === 4) return 'sm:grid-cols-2 lg:grid-cols-4';
if (count === 5) return 'sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5';
if (count === 6) return 'sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6';
return 'sm:grid-cols-2 lg:grid-cols-3';
}
</script>
<div class={cn('grid gap-3', sectionGridClass(items.length), className)}>
{#each items as item, idx (typeof item.label === 'string' ? item.label : idx)}
{@const clickable = Boolean(item.onClick)}
<Card
class={cn(
'gap-0',
variantClass[item.variant ?? 'default'],
item.active && 'border-primary ring-1 ring-primary/30',
clickable && 'cursor-pointer transition-colors hover:bg-muted/40'
)}
onclick={item.onClick}
role={clickable ? 'button' : undefined}
tabindex={clickable ? 0 : undefined}
onkeydown={(e) => {
if (clickable && (e.key === 'Enter' || e.key === ' ')) {
e.preventDefault();
item.onClick?.();
}
}}
>
<CardContent class="flex items-start gap-2.5 px-3 py-2.5">
{#if item.icon}
<span
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-muted/60 text-muted-foreground"
>
<item.icon class="size-4" />
</span>
{/if}
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
<div class="flex items-center justify-between gap-2">
{#if typeof item.label === 'string'}
<span class="truncate text-xs text-muted-foreground">{item.label}</span>
{:else}
<span class="truncate text-xs text-muted-foreground">{@render item.label()}</span>
{/if}
{#if item.badge}
<span class="shrink-0">{@render item.badge()}</span>
{/if}
</div>
<div class="flex min-w-0 items-baseline gap-1.5">
<span
class={cn(
'flex items-center gap-1 text-lg font-semibold tabular-nums',
valueVariantClass[item.variant ?? 'default']
)}
>
{#if typeof item.value === 'string' || typeof item.value === 'number'}
{item.value}
{:else}
{@render item.value()}
{/if}
</span>
{#if item.hint}
{#if typeof item.hint === 'string'}
<span class="truncate text-xs text-muted-foreground">· {item.hint}</span>
{:else}
<span class="truncate text-xs text-muted-foreground">· {@render item.hint()}</span>
{/if}
{/if}
</div>
</div>
</CardContent>
</Card>
{/each}
</div>
@@ -0,0 +1,45 @@
<script lang="ts">
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { cn } from '$lib/utils.js';
import type { BadgeVariant } from '@evobgp/ui/components/badge/badge.svelte';
type SemanticStatus = 'success' | 'warning' | 'info';
const STATUS_VARIANT: Record<string, BadgeVariant | SemanticStatus> = {
active: 'success',
ok: 'success',
enabled: 'success',
paid: 'success',
paused: 'secondary',
disabled: 'secondary',
archived: 'outline',
error: 'destructive',
failed: 'destructive',
running: 'info',
warning: 'warning',
stale: 'warning',
overdue: 'warning'
};
const SEMANTIC_CLASS: Record<SemanticStatus, string> = {
success: 'border-success/30 bg-success/10 text-success-foreground',
warning: 'border-warning/30 bg-warning/10 text-warning-foreground',
info: 'border-info/30 bg-info/10 text-info-foreground'
};
type Props = {
status: string;
label?: string;
};
let { status, label }: Props = $props();
const mapped = $derived(STATUS_VARIANT[status] ?? 'outline');
const isSemantic = $derived(mapped === 'success' || mapped === 'warning' || mapped === 'info');
const variant = $derived(isSemantic ? 'outline' : (mapped as BadgeVariant));
const text = $derived(label ?? status);
</script>
<Badge {variant} class={cn(isSemantic ? SEMANTIC_CLASS[mapped as SemanticStatus] : undefined)}>
{text}
</Badge>
@@ -0,0 +1,27 @@
<script lang="ts">
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
import * as Table from '@evobgp/ui/components/table/index.js';
type Props = {
columns?: number;
rows?: number;
};
let { columns = 4, rows = 5 }: Props = $props();
</script>
<div class="rounded-md border">
<Table.Root>
<Table.Body>
{#each Array(rows) as _, ri (ri)}
<Table.Row>
{#each Array(columns) as _, ci (ci)}
<Table.Cell>
<Skeleton class="h-5 w-full max-w-[10rem]" />
</Table.Cell>
{/each}
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
@@ -6,17 +6,17 @@
patchSettings, patchSettings,
type AdditionalSettingEntry type AdditionalSettingEntry
} from '$lib/settings/settings-api.js'; } from '$lib/settings/settings-api.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte'; import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Save from '@lucide/svelte/icons/save'; import Save from '@lucide/svelte/icons/save';
import Plus from '@lucide/svelte/icons/plus'; import Plus from '@lucide/svelte/icons/plus';
@@ -14,17 +14,17 @@
patchSettings patchSettings
} from '$lib/settings/settings-api.js'; } from '$lib/settings/settings-api.js';
import { BIRD_SETTING_KEYS } from '$lib/settings/settings-known-keys.js'; import { BIRD_SETTING_KEYS } from '$lib/settings/settings-known-keys.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js'; import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte'; import FormField from '$lib/components/patterns/form/form-field.svelte';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Save from '@lucide/svelte/icons/save'; import Save from '@lucide/svelte/icons/save';
import Info from '@lucide/svelte/icons/info'; import Info from '@lucide/svelte/icons/info';
@@ -21,17 +21,17 @@
} from '$lib/settings/settings-api.js'; } from '$lib/settings/settings-api.js';
import { REVISION_SETTING_KEYS } from '$lib/settings/settings-known-keys.js'; import { REVISION_SETTING_KEYS } from '$lib/settings/settings-known-keys.js';
import { formatBytes } from '$lib/monitoring/postgres.js'; import { formatBytes } from '$lib/monitoring/postgres.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte'; import FormField from '$lib/components/patterns/form/form-field.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Save from '@lucide/svelte/icons/save'; import Save from '@lucide/svelte/icons/save';
import Trash2 from '@lucide/svelte/icons/trash-2'; import Trash2 from '@lucide/svelte/icons/trash-2';
@@ -22,19 +22,24 @@
} from '$lib/runtime-logs/runtime-logs-auto-api.js'; } from '$lib/runtime-logs/runtime-logs-auto-api.js';
import { isRuntimeLogsUnavailable } from '$lib/runtime-logs/runtime-logs-api.js'; import { isRuntimeLogsUnavailable } from '$lib/runtime-logs/runtime-logs-api.js';
import { formatBytes } from '$lib/monitoring/postgres.js'; import { formatBytes } from '$lib/monitoring/postgres.js';
import { Button } from '$lib/ui/core/button/index.js'; import { Button } from '@evobgp/ui/components/button/index.js';
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle CardTitle
} from '$lib/ui/core/card/index.js'; } from '@evobgp/ui/components/card/index.js';
import { Input } from '$lib/ui/core/input/index.js'; import { Input } from '@evobgp/ui/components/input/index.js';
import { Switch } from '$lib/ui/core/switch/index.js'; import { Switch } from '@evobgp/ui/components/switch/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js'; import {
import FormField from '$lib/ui/patterns/form/form-field.svelte'; Select,
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js'; SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import FormField from '$lib/components/patterns/form/form-field.svelte';
import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js'; import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Save from '@lucide/svelte/icons/save'; import Save from '@lucide/svelte/icons/save';
import Play from '@lucide/svelte/icons/play'; import Play from '@lucide/svelte/icons/play';
@@ -2,14 +2,15 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import PageShell from '$lib/components/page-shell.svelte';
import PageHeader from '$lib/ui/app/page-header/page-header.svelte'; import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js'; import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/ui/core/tabs/index.js'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs/index.js';
import TenantBirdSettingsCard from '$lib/components/tenant-settings/TenantBirdSettingsCard.svelte'; import TenantBirdSettingsCard from '$lib/components/tenant-settings/TenantBirdSettingsCard.svelte';
import TenantRevisionSettingsCard from '$lib/components/tenant-settings/TenantRevisionSettingsCard.svelte'; import TenantRevisionSettingsCard from '$lib/components/tenant-settings/TenantRevisionSettingsCard.svelte';
import TenantAdditionalSettingsCard from '$lib/components/tenant-settings/TenantAdditionalSettingsCard.svelte'; import TenantAdditionalSettingsCard from '$lib/components/tenant-settings/TenantAdditionalSettingsCard.svelte';
import TenantRuntimeLogsSettingsCard from '$lib/components/tenant-settings/TenantRuntimeLogsSettingsCard.svelte'; import TenantRuntimeLogsSettingsCard from '$lib/components/tenant-settings/TenantRuntimeLogsSettingsCard.svelte';
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal'; import { LINE_TAB_TRIGGER_CLASS } from '$lib/ui/app/tabs.js';
import Info from '@lucide/svelte/icons/info'; import Info from '@lucide/svelte/icons/info';
type TenantSettingsTab = 'bird' | 'revision' | 'runtime-logs' | 'additional'; type TenantSettingsTab = 'bird' | 'revision' | 'runtime-logs' | 'additional';
@@ -44,12 +45,10 @@
}); });
</script> </script>
<div class="flex flex-col gap-6"> <PageShell>
<PageHeader <PageHeader
title="Параметры tenant" title="Параметры tenant"
description="Параметры control plane для текущего tenant (API /v1/settings). Токен и тема интерфейса — в разделе «Настройки»." description="Параметры control plane для текущего tenant (API /v1/settings). Токен и тема интерфейса — в разделе «Настройки»."
icon={SlidersHorizontal}
iconClass="bg-chart-5/15 text-chart-5"
/> />
<Alert class="border-info/30 bg-info/5"> <Alert class="border-info/30 bg-info/5">
@@ -62,12 +61,12 @@
</Alert> </Alert>
<Tabs bind:value={activeTab}> <Tabs bind:value={activeTab}>
<div class="overflow-x-auto pb-1 [scrollbar-gutter:stable]"> <div class="border-b">
<TabsList class="inline-flex min-w-max"> <TabsList class="h-auto w-full justify-start rounded-none bg-transparent p-0">
<TabsTrigger value="bird">BIRD</TabsTrigger> <TabsTrigger value="bird" class={LINE_TAB_TRIGGER_CLASS}>BIRD</TabsTrigger>
<TabsTrigger value="revision">Ревизии</TabsTrigger> <TabsTrigger value="revision" class={LINE_TAB_TRIGGER_CLASS}>Ревизии</TabsTrigger>
<TabsTrigger value="runtime-logs">Файловые логи</TabsTrigger> <TabsTrigger value="runtime-logs" class={LINE_TAB_TRIGGER_CLASS}>Файловые логи</TabsTrigger>
<TabsTrigger value="additional">Дополнительно</TabsTrigger> <TabsTrigger value="additional" class={LINE_TAB_TRIGGER_CLASS}>Дополнительно</TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@@ -87,4 +86,4 @@
<TenantAdditionalSettingsCard /> <TenantAdditionalSettingsCard />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </PageShell>

Some files were not shown because too many files have changed in this diff Show More