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",
"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:
- web/**
- apps/web/**
- packages/ui/**
- packages/shared/**
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`) |
| Паттерны | `src/lib/ui/patterns/` | FormField, AppDataTable, ConfirmDialog, EmptyState |
| App chrome | `src/lib/ui/app/` | Layout, PageHeader, `notify` |
| Legacy | `src/lib/components/ui/` | Re-export; **не добавлять новые файлы** |
| Примитивы | `packages/ui/src/components/` | shadcn-svelte (только CLI) |
| Паттерны | `apps/web/src/lib/components/patterns/` | AppDataTable, FormField, ConfirmDialog |
| Shared UI | `apps/web/src/lib/components/` | PageShell, QueryState, SectionCards |
| 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
npm run check
npm run lint
pnpm dlx shadcn-svelte@latest add <component> -y -o
```
Если `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.
*Проверка:* review.
- **WEB-01** Новый UI — сначала shadcn-svelte docs; не HTML+CSS с нуля
- **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/`.
*Проверка:* review.
---
## 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
---
```powershell
pnpm --filter @evobgp/web check
pnpm --filter @evobgp/web lint
```
## Enforcement
**Обязательный финальный шаг агента при правках `web/**`:** `npm run check` **и** `npm run lint` (см. **WEB-19**). Только `check` недостаточно.
```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`.
PR checklist: `check` + `lint` exit 0; примитивы в `@evobgp/ui`, не дубли в routes.
+11 -11
View File
@@ -98,9 +98,9 @@ jobs:
openapi=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
;;
deploy/bird/*)
@@ -126,7 +126,7 @@ jobs:
docs/*)
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
;;
*)
@@ -169,16 +169,16 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: web/package-lock.json
- name: npm ci, check, lint
node-version: "22"
- uses: pnpm/action-setup@v4
with:
version: 10.33.2
- name: pnpm install, check, lint
run: |
set -euxo pipefail
cd web
npm ci
npm run check
npm run lint
pnpm install --frozen-lockfile
pnpm --filter @evobgp/web check
pnpm --filter @evobgp/web lint
# ---------------------------------------------------------------------------
go:
+2 -2
View File
@@ -13,7 +13,7 @@ repos:
hooks:
- id: prettier-web
name: prettier (web)
entry: bash -c 'cd web && npx prettier --check .'
entry: bash -c 'pnpm --filter @evobgp/web lint'
language: system
files: ^web/
files: ^apps/web/
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)** — оглавление и роли читателя.
2. **[docs/architecture.md](docs/architecture.md)** — компоненты `cmd/`, карта `internal/`, потоки данных (одного этого файла обычно достаточно для ориентации).
3. Задача-специфично: [docs/api.md](docs/api.md), [docs/access.md](docs/access.md), [web/README.md](web/README.md) — только если меняете API, доступ или фронт.
Источник правды по HTTP-контракту: **[docs/openapi.yaml](docs/openapi.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
```
EvoBGP/
├── cmd/, internal/ # Go API /v1
├── apps/web/ # SvelteKit SPA (@evobgp/web)
├── packages/ui/ # @evobgp/ui — shadcn-svelte primitives
├── packages/shared/ # @evobgp/shared — types, Zod contracts
└── pnpm-workspace.yaml
```
Если `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
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",
"tailwind": {
"css": "src/routes/layout.css",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/ui/core",
"ui": "@evobgp/ui/components",
"hooks": "$lib/hooks",
"lib": "$lib"
},
+4 -1
View File
@@ -1,5 +1,5 @@
{
"name": "web",
"name": "@evobgp/web",
"private": true,
"version": "0.0.1",
"type": "module",
@@ -33,6 +33,9 @@
"vite": "^7.3.1"
},
"dependencies": {
"@evobgp/shared": "workspace:*",
"@evobgp/ui": "workspace:*",
"@tanstack/svelte-query": "^5.90.2",
"@tanstack/table-core": "^8.21.3",
"bits-ui": "^2.17.2",
"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">
import { apiMutate } from '$lib/api/client.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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
import {
Dialog,
DialogContent,
@@ -16,12 +16,17 @@
DialogFooter,
DialogHeader,
DialogTitle
} from '$lib/ui/core/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/dialog/index.js';
import {
Select,
SelectContent,
SelectItem,
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 Plus from '@lucide/svelte/icons/plus';
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">
import { apiMutate } from '$lib/api/client.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 {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from '$lib/ui/core/dialog/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/dialog/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 Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil';
@@ -1,25 +1,25 @@
<script lang="ts">
import { apiMutate } from '$lib/api/client.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 {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from '$lib/ui/core/dialog/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/dialog/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 Plus from '@lucide/svelte/icons/plus';
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
} from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/card/index.js';
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 ModuleAsEntryDialog from '$lib/components/modules/ModuleAsEntryDialog.svelte';
import Plus from '@lucide/svelte/icons/plus';
@@ -8,9 +8,9 @@
NONE_OPTION,
nullableSelectValue
} from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Label } from '$lib/ui/core/label/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import {
Dialog,
DialogContent,
@@ -18,8 +18,13 @@
DialogTitle,
DialogFooter,
DialogDescription
} from '$lib/ui/core/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
} from '@evobgp/ui/components/dialog/index.js';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
type Props = {
@@ -14,17 +14,22 @@
NONE_OPTION,
nullableSelectValue
} from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Label } from '$lib/ui/core/label/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from '$lib/ui/core/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
} from '@evobgp/ui/components/dialog/index.js';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
type Props = {
@@ -6,18 +6,18 @@
communityLabel,
normalizeCdnSourceKind
} from '$lib/components/modules/module-helpers.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/card/index.js';
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 ModuleCdnSourceDialog from '$lib/components/modules/ModuleCdnSourceDialog.svelte';
import Plus from '@lucide/svelte/icons/plus';
@@ -2,9 +2,9 @@
import { apiMutate } from '$lib/api/client.js';
import type { ModuleCreate } from '$lib/api/types.js';
import { moduleTypeRu } from '$lib/ui-labels.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Label } from '$lib/ui/core/label/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import {
Dialog,
DialogContent,
@@ -12,9 +12,14 @@
DialogTitle,
DialogFooter,
DialogDescription
} from '$lib/ui/core/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
import { Switch } from '$lib/ui/core/switch/index.js';
} from '@evobgp/ui/components/dialog/index.js';
import {
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';
type Props = {
@@ -2,8 +2,8 @@
import { resolve } from '$app/paths';
import type { ModuleRow } from '$lib/api/types.js';
import { moduleEnabledRu, moduleEnabledBadgeVariant, moduleTypeRu } from '$lib/ui-labels.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Pencil from '@lucide/svelte/icons/pencil';
@@ -6,17 +6,17 @@
sanitizeFilenamePart,
supportsCsvIO
} from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/card/index.js';
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 ModuleDomainEntryDialog from '$lib/components/modules/ModuleDomainEntryDialog.svelte';
import Plus from '@lucide/svelte/icons/plus';
@@ -8,17 +8,22 @@
NONE_OPTION,
nullableSelectValue
} from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Label } from '$lib/ui/core/label/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from '$lib/ui/core/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
} from '@evobgp/ui/components/dialog/index.js';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
type Props = {
@@ -16,19 +16,24 @@
NONE_OPTION,
nullableSelectValue
} from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Label } from '$lib/ui/core/label/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from '$lib/ui/core/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
import { Switch } from '$lib/ui/core/switch/index.js';
} from '@evobgp/ui/components/dialog/index.js';
import {
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';
const dohPolicyOptions: { value: DohResolverPolicy; label: string; hint: string }[] = [
@@ -2,17 +2,22 @@
import { apiMutate } from '$lib/api/client.js';
import type { BgpCommunity, IpRangeEntry, IpRangeEntryCreate } from '$lib/api/types.js';
import { communityLabel, communityOptionLabel } from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Label } from '$lib/ui/core/label/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from '$lib/ui/core/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
} from '@evobgp/ui/components/dialog/index.js';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
type Props = {
@@ -6,17 +6,17 @@
sanitizeFilenamePart,
supportsCsvIO
} from '$lib/components/modules/module-helpers.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Checkbox } from '@evobgp/ui/components/checkbox/index.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/card/index.js';
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 ModuleIpRangeEntryDialog from '$lib/components/modules/ModuleIpRangeEntryDialog.svelte';
import Plus from '@lucide/svelte/icons/plus';
@@ -7,8 +7,8 @@
dohProfileLabel,
moduleDohProfileIds
} from '$lib/components/modules/module-helpers.js';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/ui/core/card/index.js';
import CardSkeleton from '$lib/ui/patterns/feedback/card-skeleton.svelte';
import { Card, CardContent, CardHeader, CardTitle } from '@evobgp/ui/components/card/index.js';
import CardSkeleton from '$lib/components/patterns/feedback/card-skeleton.svelte';
import { cn } from '$lib/utils.js';
import ArrowDownUp from '@lucide/svelte/icons/arrow-down-up';
import Timer from '@lucide/svelte/icons/timer';
@@ -1,14 +1,14 @@
<script lang="ts">
import { resolve } from '$app/paths';
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 {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
type Props = {
mod: ModuleRow;
@@ -35,31 +35,31 @@
type ScheduleEditor,
type ScheduleMode
} 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 {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
} from '@evobgp/ui/components/card/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from '$lib/ui/core/dialog/index.js';
import { Switch } from '$lib/ui/core/switch/index.js';
import { Label } from '$lib/ui/core/label/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import type { DataTableColumn } from '$lib/ui/patterns/data-table/types.js';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/dialog/index.js';
import { Switch } from '@evobgp/ui/components/switch/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import { Checkbox } from '@evobgp/ui/components/checkbox/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 type { DataTableColumn } from '$lib/components/patterns/data-table/types.js';
import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil';
@@ -17,17 +17,17 @@
type PostgresMaintLog,
type CorrelationResponse
} 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 {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
} from '@evobgp/ui/components/card/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import {
Table,
TableBody,
@@ -35,9 +35,9 @@
TableHead,
TableHeader,
TableRow
} from '$lib/ui/core/table/index.js';
import { Switch } from '$lib/ui/core/switch/index.js';
import { Label } from '$lib/ui/core/label/index.js';
} from '@evobgp/ui/components/table/index.js';
import { Switch } from '@evobgp/ui/components/switch/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import Database from '@lucide/svelte/icons/database';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
@@ -14,33 +14,33 @@
} from '$lib/runtime-logs/runtime-logs-api.js';
import { formatBytes } from '$lib/monitoring/postgres.js';
import { formatDateTime } from '$lib/modules/display.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '$lib/ui/core/dialog/index.js';
} from '@evobgp/ui/components/dialog/index.js';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '$lib/ui/core/dropdown-menu/index.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/ui/core/tabs/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import type { DataTableColumn } from '$lib/ui/patterns/data-table/types.js';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/dropdown-menu/index.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs/index.js';
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import type { DataTableColumn } from '$lib/components/patterns/data-table/types.js';
import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import ScrollPreBlock from '$lib/components/app/scroll-pre-block.svelte';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import {
@@ -1,6 +1,6 @@
<script lang="ts">
import { Label } from '$lib/ui/core/label/index.js';
import { Switch } from '$lib/ui/core/switch/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import { Switch } from '@evobgp/ui/components/switch/index.js';
import { readNetworkAutoRefresh, writeNetworkAutoRefresh } from '$lib/network/network-metrics.js';
type Props = {
@@ -3,14 +3,14 @@
import { resolve } from '$app/paths';
import { loadSettings, partitionSettings } from '$lib/settings/settings-api.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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
import { notifyApiError } from '$lib/ui/app/toast.js';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
@@ -8,9 +8,9 @@
networkOverallStatusHint,
networkOverallStatusLabel
} from '$lib/network/network-metrics.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import KpiMetricsGrid from '$lib/ui/patterns/kpi/kpi-metrics-grid.svelte';
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import KpiMetricsGrid from '$lib/components/patterns/kpi/kpi-metrics-grid.svelte';
import NetworkSpeakerStatusCard from '$lib/components/network/NetworkSpeakerStatusCard.svelte';
import CheckCircle from '@lucide/svelte/icons/check-circle';
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
@@ -1,16 +1,16 @@
<script lang="ts">
import { apiMutate } from '$lib/api/client.js';
import type { PeerRow, BgpPeerCreate, SpeakerRow, PeerSessionOnSpeaker } from '$lib/api/types.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Label } from '$lib/ui/core/label/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
import {
Dialog,
DialogContent,
@@ -18,13 +18,18 @@
DialogTitle,
DialogFooter,
DialogDescription
} from '$lib/ui/core/dialog/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
import { Switch } from '$lib/ui/core/switch/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/dialog/index.js';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} 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 Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil';
@@ -9,17 +9,17 @@
speakerLiveAgentError,
speakerLiveBgpError
} from '$lib/network/network-metrics.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Separator } from '$lib/ui/core/separator/index.js';
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Separator } from '@evobgp/ui/components/separator/index.js';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle
} from '$lib/ui/core/sheet/index.js';
} from '@evobgp/ui/components/sheet/index.js';
import {
Table,
TableBody,
@@ -27,7 +27,7 @@
TableHead,
TableHeader,
TableRow
} from '$lib/ui/core/table/index.js';
} from '@evobgp/ui/components/table/index.js';
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
type Props = {
@@ -6,14 +6,14 @@
speakerHasDrift,
speakerLabel
} 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 {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
import { cn } from '$lib/utils.js';
import Server from '@lucide/svelte/icons/server';
@@ -6,15 +6,15 @@
speakerDisplayStatus,
speakerHasDrift
} from '$lib/network/network-metrics.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
import {
Dialog,
DialogContent,
@@ -22,12 +22,12 @@
DialogTitle,
DialogFooter,
DialogDescription
} from '$lib/ui/core/dialog/index.js';
import { Checkbox } from '$lib/ui/core/checkbox/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import AppInput from '$lib/ui/patterns/form/app-input.svelte';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/dialog/index.js';
import { Checkbox } from '@evobgp/ui/components/checkbox/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 Plus from '@lucide/svelte/icons/plus';
import Pencil from '@lucide/svelte/icons/pencil';
@@ -1,14 +1,19 @@
<script lang="ts">
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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
} from '@evobgp/ui/components/card/index.js';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger
} from '@evobgp/ui/components/select/index.js';
import { formatDateTime } from '$lib/modules/display.js';
import { sortRevisionDiffItems } from '$lib/sort-prefixes.js';
@@ -1,7 +1,7 @@
<script lang="ts">
import { Button } from '$lib/ui/core/button/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Label } from '$lib/ui/core/label/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import { Label } from '@evobgp/ui/components/label/index.js';
import Filter from '@lucide/svelte/icons/filter';
import Search from '@lucide/svelte/icons/search';
import X from '@lucide/svelte/icons/x';
@@ -2,16 +2,16 @@
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
import type { JobRow } from '$lib/api/types.js';
import type { JobDetailedReport, JobLogEntry } from './types.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte';
} from '@evobgp/ui/components/card/index.js';
import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
import { formatDateTime } from '$lib/modules/display.js';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import Eye from '@lucide/svelte/icons/eye';
@@ -1,8 +1,8 @@
<script lang="ts">
import type { BirdStatus } from '$lib/api/types.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Card } from '$lib/ui/core/card/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import { Card } from '@evobgp/ui/components/card/index.js';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import Play from '@lucide/svelte/icons/play';
import RotateCcw from '@lucide/svelte/icons/rotate-ccw';
@@ -1,14 +1,14 @@
<script lang="ts">
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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
} from '@evobgp/ui/components/card/index.js';
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import { formatDateTime } from '$lib/modules/display.js';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import Undo from '@lucide/svelte/icons/undo';
@@ -6,9 +6,9 @@
getCoreRowModel,
getPaginationRowModel
} from '@tanstack/table-core';
import { createSvelteTable, FlexRender } from '$lib/ui/core/data-table/index.js';
import * as Table from '$lib/ui/core/table/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { createSvelteTable, FlexRender } from '@evobgp/ui/components/data-table/index.js';
import * as Table from '@evobgp/ui/components/table/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
type Props = {
rows: RowData[];
@@ -8,15 +8,15 @@
networkOverallStatusHint,
networkOverallStatusLabel
} from '$lib/network/network-metrics.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
} from '@evobgp/ui/components/card/index.js';
import CheckCircle from '@lucide/svelte/icons/check-circle';
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
import XCircle from '@lucide/svelte/icons/x-circle';
@@ -4,16 +4,16 @@
import { formatDateTime } from '$lib/modules/display.js';
import { jobKindTitle } from '$lib/operations/job-kind-label.js';
import { jobStatusRu, jobStatusBadgeVariant } from '$lib/ui-labels.js';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
} from '@evobgp/ui/components/card/index.js';
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import ExternalLink from '@lucide/svelte/icons/external-link';
@@ -2,15 +2,15 @@
import { resolve } from '$app/paths';
import type { RevisionRow } from '$lib/api/types.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 {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
} from '@evobgp/ui/components/card/index.js';
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
import ExternalLink from '@lucide/svelte/icons/external-link';
@@ -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,
AlertDialogHeader,
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';
const state = $derived(confirmState.current);
@@ -1,14 +1,14 @@
<script lang="ts" generics="T extends Record<string, unknown>">
import type { Snippet } from 'svelte';
import { cn } from '$lib/utils.js';
import * as Table from '$lib/ui/core/table/index.js';
import { Skeleton } from '$lib/ui/core/skeleton/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte';
import * as Table from '@evobgp/ui/components/table/index.js';
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
import ArrowUpDown from '@lucide/svelte/icons/arrow-up-down';
import ArrowUp from '@lucide/svelte/icons/arrow-up';
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';
type Props = {
@@ -19,6 +19,8 @@
error?: string | null;
emptyTitle?: string;
emptyDescription?: string;
/** Client-side page size; 0 = all rows */
pageSize?: number;
toolbar?: Snippet;
cell: Snippet<[{ row: T; column: DataTableColumn<T> }]>;
class?: string;
@@ -32,6 +34,7 @@
error = null,
emptyTitle = 'Нет записей',
emptyDescription,
pageSize = 0,
toolbar,
cell,
class: className
@@ -39,6 +42,7 @@
let sortColumnId = $state<string | null>(null);
let sortDir = $state<'asc' | 'desc'>('asc');
let pageIndex = $state(0);
const sortedRows = $derived.by(() => {
if (!sortColumnId) return rows;
@@ -57,6 +61,21 @@
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>) {
if (!col.sortable) return;
if (sortColumnId === col.id) {
@@ -131,7 +150,7 @@
</Table.Cell>
</Table.Row>
{:else}
{#each sortedRows as row (rowKey(row))}
{#each paginatedRows as row (rowKey(row))}
<Table.Row>
{#each columns as col (col.id)}
<Table.Cell class={col.class}>
@@ -144,4 +163,27 @@
</Table.Body>
</Table.Root>
</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>
@@ -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">
import { Card, CardContent, CardHeader } from '$lib/ui/core/card/index.js';
import { Skeleton } from '$lib/ui/core/skeleton/index.js';
import { Card, CardContent, CardHeader } from '@evobgp/ui/components/card/index.js';
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
</script>
<Card>
@@ -1,6 +1,6 @@
<script lang="ts">
import { Skeleton } from '$lib/ui/core/skeleton/index.js';
import * as Table from '$lib/ui/core/table/index.js';
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
import * as Table from '@evobgp/ui/components/table/index.js';
type Props = {
columns?: number;
@@ -1,5 +1,5 @@
<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 type { ComponentProps } from 'svelte';
@@ -1,5 +1,5 @@
<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 type { ComponentProps } from 'svelte';
@@ -1,7 +1,7 @@
<script lang="ts">
import type { Snippet } from 'svelte';
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 = {
label: string;
@@ -1,15 +1,15 @@
<script lang="ts">
import type { Component } from 'svelte';
import { Badge } from '$lib/ui/core/badge/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import { Badge } from '@evobgp/ui/components/badge/index.js';
import { Button } from '@evobgp/ui/components/button/index.js';
import {
Card,
CardContent,
CardHeader,
CardTitle,
CardDescription
} from '$lib/ui/core/card/index.js';
import CardSkeleton from '$lib/ui/patterns/feedback/card-skeleton.svelte';
} from '@evobgp/ui/components/card/index.js';
import CardSkeleton from '$lib/components/patterns/feedback/card-skeleton.svelte';
import { cn } from '$lib/utils.js';
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,
type AdditionalSettingEntry
} 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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/card/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Save from '@lucide/svelte/icons/save';
import Plus from '@lucide/svelte/icons/plus';
@@ -14,17 +14,17 @@
patchSettings
} from '$lib/settings/settings-api.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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
} from '@evobgp/ui/components/card/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import FormField from '$lib/components/patterns/form/form-field.svelte';
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
import Save from '@lucide/svelte/icons/save';
import Info from '@lucide/svelte/icons/info';
@@ -21,17 +21,17 @@
} from '$lib/settings/settings-api.js';
import { REVISION_SETTING_KEYS } from '$lib/settings/settings-known-keys.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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/card/index.js';
import { Input } from '@evobgp/ui/components/input/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 Save from '@lucide/svelte/icons/save';
import Trash2 from '@lucide/svelte/icons/trash-2';
@@ -22,19 +22,24 @@
} from '$lib/runtime-logs/runtime-logs-auto-api.js';
import { isRuntimeLogsUnavailable } from '$lib/runtime-logs/runtime-logs-api.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 {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '$lib/ui/core/card/index.js';
import { Input } from '$lib/ui/core/input/index.js';
import { Switch } from '$lib/ui/core/switch/index.js';
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/ui/core/select/index.js';
import FormField from '$lib/ui/patterns/form/form-field.svelte';
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
} from '@evobgp/ui/components/card/index.js';
import { Input } from '@evobgp/ui/components/input/index.js';
import { Switch } from '@evobgp/ui/components/switch/index.js';
import {
Select,
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 Save from '@lucide/svelte/icons/save';
import Play from '@lucide/svelte/icons/play';
@@ -2,9 +2,10 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
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 { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/ui/core/tabs/index.js';
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs/index.js';
import TenantBirdSettingsCard from '$lib/components/tenant-settings/TenantBirdSettingsCard.svelte';
import TenantRevisionSettingsCard from '$lib/components/tenant-settings/TenantRevisionSettingsCard.svelte';
import TenantAdditionalSettingsCard from '$lib/components/tenant-settings/TenantAdditionalSettingsCard.svelte';
@@ -44,7 +45,7 @@
});
</script>
<div class="flex flex-col gap-6">
<PageShell>
<PageHeader
title="Параметры tenant"
description="Параметры control plane для текущего tenant (API /v1/settings). Токен и тема интерфейса — в разделе «Настройки»."
@@ -62,7 +63,7 @@
</Alert>
<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">
<TabsTrigger value="bird">BIRD</TabsTrigger>
<TabsTrigger value="revision">Ревизии</TabsTrigger>
@@ -87,4 +88,4 @@
<TenantAdditionalSettingsCard />
</TabsContent>
</Tabs>
</div>
</PageShell>

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