Compare commits

...
2 Commits
Author SHA1 Message Date
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
379 changed files with 8587 additions and 1101 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
```
+65
View File
@@ -0,0 +1,65 @@
---
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`) | — |
| 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` | `Sidebar` |
| Breadcrumbs | `AppShell` header | `Breadcrumb` |
## 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
+43 -65
View File
@@ -1,70 +1,48 @@
# Руководство для ИИ-агентов (экономия контекста) # 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: `PageShell`, `SectionCards`, `QueryState`, `DataTableCard`, sidebar-07 layout. ReUI не используется — Data Table на shadcn-svelte.
## Коммиты
При запросе коммита: [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,53 @@
<script lang="ts">
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';
type Props = {
title: string;
description?: string;
actions?: Snippet;
data: unknown;
isLoading: boolean;
isError: boolean;
error?: unknown;
onRetry?: () => void;
skeleton?: Snippet;
content: Snippet;
};
let {
title,
description,
actions,
data,
isLoading,
isError,
error,
onRetry,
skeleton,
content
}: Props = $props();
</script>
<PageShell>
<PageHeader {title} {description}>
{#snippet actions()}
{#if actions}
{@render actions()}
{/if}
{/snippet}
</PageHeader>
{#if isLoading}
{#if skeleton}
{@render skeleton()}
{/if}
{:else if isError}
<QueryState {data} isLoading={false} isError={true} {error} {onRetry} children={emptyChild} />
{:else}
{@render content()}
{/if}
</PageShell>
{#snippet emptyChild()}{/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,116 @@
<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 { mainNav, bottomNav } from '$lib/ui/app/layout/nav.js';
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);
const routeLabels: Record<string, string> = {
'/': 'Обзор',
...Object.fromEntries(mainNav.map((i) => [i.href, i.label])),
...Object.fromEntries(bottomNav.map((i) => [i.href, i.label]))
};
const breadcrumbLabel = $derived(routeLabels[page.url.pathname] ?? 'EvoBGP');
// 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 + '/');
}
</script>
<Sidebar.Provider>
<Sidebar.Root>
<Sidebar.Header class="border-b border-sidebar-border">
<div class="flex items-center gap-2 px-2 py-1">
<div class="flex min-w-0 flex-1 flex-col group-data-[collapsible=icon]:hidden">
<a href={navHref('/')} class="truncate font-semibold tracking-tight">EvoBGP</a>
<p class="truncate text-xs text-muted-foreground">Панель управления</p>
</div>
<ThemeMenu bind:theme />
</div>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Операции</Sidebar.GroupLabel>
<Sidebar.GroupContent>
<Sidebar.Menu>
{#each mainNav as item (item.href)}
{@const Icon = item.icon}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={isActive(item.href)}>
{#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>
</Sidebar.Content>
<Sidebar.Footer class="border-t border-sidebar-border">
<Sidebar.Menu>
{#each bottomNav as item (item.href)}
{@const Icon = item.icon}
<Sidebar.MenuItem>
<Sidebar.MenuButton isActive={isActive(item.href)}>
{#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>
<AppVersion />
</Sidebar.Footer>
<Sidebar.Rail />
</Sidebar.Root>
<Sidebar.Inset>
<header
class="sticky top-0 z-10 flex h-14 shrink-0 items-center gap-2 border-b bg-background/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-background/60"
>
<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="mx-2 hidden h-4 md:block" />
<Breadcrumb.Root class="hidden min-w-0 md:flex">
<Breadcrumb.List>
<Breadcrumb.Item>
<Breadcrumb.Page>{breadcrumbLabel}</Breadcrumb.Page>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
</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';
@@ -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,9 @@
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 KpiMetricsGrid from '$lib/components/patterns/kpi/kpi-metrics-grid.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';
@@ -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';
@@ -4,16 +4,16 @@
import { formatDateTime } from '$lib/modules/display.js'; import { formatDateTime } from '$lib/modules/display.js';
import { jobKindTitle } from '$lib/operations/job-kind-label.js'; import { jobKindTitle } from '$lib/operations/job-kind-label.js';
import { jobStatusRu, jobStatusBadgeVariant } from '$lib/ui-labels.js'; import { jobStatusRu, jobStatusBadgeVariant } 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 { 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 ArrowRight from '@lucide/svelte/icons/arrow-right'; import ArrowRight from '@lucide/svelte/icons/arrow-right';
import ExternalLink from '@lucide/svelte/icons/external-link'; import ExternalLink from '@lucide/svelte/icons/external-link';
@@ -2,15 +2,15 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import type { RevisionRow } from '$lib/api/types.js'; import type { RevisionRow } from '$lib/api/types.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 { 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 ArrowRight from '@lucide/svelte/icons/arrow-right'; import ArrowRight from '@lucide/svelte/icons/arrow-right';
import ExternalLink from '@lucide/svelte/icons/external-link'; import ExternalLink from '@lucide/svelte/icons/external-link';
@@ -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);
@@ -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;
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>
@@ -0,0 +1,31 @@
<script lang="ts">
import type { Component, Snippet } from 'svelte';
import { cn } from '$lib/utils.js';
type Props = {
title?: string;
description?: string;
icon?: Component;
action?: Snippet;
class?: string;
};
let { title = 'Нет данных', description, icon: Icon, action, class: className }: Props = $props();
</script>
<div
class={cn('flex flex-col items-center justify-center gap-2 px-4 py-12 text-center', className)}
>
{#if Icon}
<div class="mb-1 text-muted-foreground/60" aria-hidden="true">
<Icon class="size-10" />
</div>
{/if}
<p class="text-sm font-medium">{title}</p>
{#if description}
<p class="max-w-sm text-sm text-muted-foreground">{description}</p>
{/if}
{#if action}
<div class="mt-2">{@render action()}</div>
{/if}
</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;
@@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import type { Component } from 'svelte'; import type { Component } from 'svelte';
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 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 ArrowRight from '@lucide/svelte/icons/arrow-right'; import ArrowRight from '@lucide/svelte/icons/arrow-right';
@@ -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/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',
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,30 @@
<script lang="ts">
import { Badge } from '@evobgp/ui/components/badge/index.js';
type BadgeVariant = 'default' | 'secondary' | 'destructive' | 'outline';
const STATUS_VARIANT: Record<string, BadgeVariant> = {
active: 'default',
ok: 'default',
enabled: 'default',
paused: 'secondary',
disabled: 'secondary',
error: 'destructive',
failed: 'destructive',
running: 'outline',
warning: 'outline',
stale: 'outline'
};
type Props = {
status: string;
label?: string;
};
let { status, label }: Props = $props();
const variant = $derived(STATUS_VARIANT[status] ?? 'outline');
const text = $derived(label ?? status);
</script>
<Badge {variant}>{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,9 +2,10 @@
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';
@@ -44,7 +45,7 @@
}); });
</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). Токен и тема интерфейса — в разделе «Настройки»."
@@ -62,7 +63,7 @@
</Alert> </Alert>
<Tabs bind:value={activeTab}> <Tabs bind:value={activeTab}>
<div class="overflow-x-auto pb-1 [scrollbar-gutter:stable]"> <div class="[scrollbar-gutter:stable] overflow-x-auto pb-1">
<TabsList class="inline-flex min-w-max"> <TabsList class="inline-flex min-w-max">
<TabsTrigger value="bird">BIRD</TabsTrigger> <TabsTrigger value="bird">BIRD</TabsTrigger>
<TabsTrigger value="revision">Ревизии</TabsTrigger> <TabsTrigger value="revision">Ревизии</TabsTrigger>
@@ -87,4 +88,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