Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fef952403 | ||
|
|
07ee6acddb | ||
|
|
1c7152a285 | ||
|
|
a37c931ee7 |
No files matched your search
@@ -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`
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
description: UI-паттерны apps/web — shared components, spacing, матрица (Svelte, без ReUI)
|
||||||
|
globs: apps/web/**/*
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# Frontend UI Patterns (Svelte)
|
||||||
|
|
||||||
|
Эталон UX: vps-tracker (`PageShell`, `QueryState`, `SectionCards`, `DataTableCard`). ReUI **не используется** — Data Table = shadcn-svelte + `AppDataTable` / `DataTableCard`.
|
||||||
|
|
||||||
|
## Иерархия
|
||||||
|
|
||||||
|
```
|
||||||
|
@evobgp/ui/components/* ← shadcn-svelte CLI (packages/ui)
|
||||||
|
apps/web/src/lib/components/ ← shared + domain + layout
|
||||||
|
page-shell.svelte
|
||||||
|
query-state.svelte
|
||||||
|
section-cards.svelte
|
||||||
|
data-table-card.svelte
|
||||||
|
status-badge.svelte
|
||||||
|
form-sheet.svelte
|
||||||
|
crud-list-page.svelte
|
||||||
|
list-filters-bar.svelte
|
||||||
|
patterns/ ← AppDataTable, FormField, ConfirmDialog, EmptyState
|
||||||
|
layout/app-shell.svelte
|
||||||
|
domain-*/ ← modules, network, operations…
|
||||||
|
```
|
||||||
|
|
||||||
|
## Матрица
|
||||||
|
|
||||||
|
| Элемент | Компонент | Primitive |
|
||||||
|
|---------|-----------|-----------|
|
||||||
|
| Page wrapper | `PageShell` | — |
|
||||||
|
| Page title | `PageHeader` (`ui/app/page-header`) — **без icon** | — |
|
||||||
|
| Stat metrics | `SectionCards` | `Card` |
|
||||||
|
| Data list | `DataTableCard` + `AppDataTable` | `Table` |
|
||||||
|
| Filters | `ListFiltersBar` | `Select`, `Badge` |
|
||||||
|
| Empty | `EmptyState` | — |
|
||||||
|
| Loading / Error | `QueryState` | `Skeleton`, `Alert` |
|
||||||
|
| Status | `StatusBadge` | `Badge` |
|
||||||
|
| Create/Edit form | `FormSheet` / Dialog | `Sheet`, `Field` |
|
||||||
|
| Delete confirm | `ConfirmDialog` | `AlertDialog` |
|
||||||
|
| Nav | `AppShell` + `navGroups` | `Sidebar` |
|
||||||
|
| Breadcrumbs | `AppShell` header (parent route) | `Breadcrumb` |
|
||||||
|
| Tabs (dashboard, ops) | `LINE_TAB_TRIGGER_CLASS` | `Tabs` |
|
||||||
|
|
||||||
|
## Запрещено
|
||||||
|
|
||||||
|
- `icon` / `iconClass` в `PageHeader`
|
||||||
|
- `KpiMetricsGrid` — только `SectionCards`
|
||||||
|
- `mx-auto max-w-*` на `PageShell` list-страниц
|
||||||
|
- Голый `AppDataTable` без `DataTableCard` на list-экранах
|
||||||
|
|
||||||
|
## Spacing
|
||||||
|
|
||||||
|
- `flex` + `gap-*`, не `space-y-*`
|
||||||
|
- Page: `gap-4 md:gap-6`, padding `p-4 md:p-6`
|
||||||
|
- Loading страницы → `Skeleton`, не page-level `Spinner`
|
||||||
|
- Max 1 primary CTA на экран
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- shadcn-svelte: https://shadcn-svelte.com/docs
|
||||||
|
- llms.txt: https://shadcn-svelte.com/llms.txt
|
||||||
|
- Data layer: `@tanstack/svelte-query` в `apps/web/src/lib/queries/`
|
||||||
|
|
||||||
|
## Overlay
|
||||||
|
|
||||||
|
| Сценарий | Компонент |
|
||||||
|
|----------|-----------|
|
||||||
|
| Create/edit | `Sheet` / `FormSheet` |
|
||||||
|
| Destructive | `ConfirmDialog` |
|
||||||
|
| Preview | `Dialog` |
|
||||||
+33
-104
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,70 +1,53 @@
|
|||||||
# Руководство для ИИ-агентов (экономия контекста)
|
# EvoBGP — руководство для ИИ-агентов
|
||||||
|
|
||||||
Краткие ориентиры по репозиторию **EvoBGP**, чтобы не тратить токены на полное сканирование дерева и повторное чтение одних и тех же файлов.
|
Краткая карта репозитория. Полная архитектура: [docs/architecture.md](docs/architecture.md). HTTP: [docs/openapi.yaml](docs/openapi.yaml).
|
||||||
|
|
||||||
## С чего начать (минимум чтения)
|
## Monorepo (фронтенд)
|
||||||
|
|
||||||
0. **Инженерные правила** — при изменении кода следовать [.cursor/rules/engineering.mdc](.cursor/rules/engineering.mdc); для `web/` — [.cursor/rules/web-shadcn.mdc](.cursor/rules/web-shadcn.mdc); для `birdfmt` / `pipeline` / BIRD — [.cursor/rules/networking-bird.mdc](.cursor/rules/networking-bird.mdc). **Context7 (документация библиотек)** — закреплённые ID стека: [.cursor/rules/context7-stack.mdc](.cursor/rules/context7-stack.mdc); скилл [.cursor/skills/context7-evobgp/SKILL.md](.cursor/skills/context7-evobgp/SKILL.md).
|
```
|
||||||
1. **[docs/README.md](docs/README.md)** — оглавление и роли читателя.
|
EvoBGP/
|
||||||
2. **[docs/architecture.md](docs/architecture.md)** — компоненты `cmd/`, карта `internal/`, потоки данных (одного этого файла обычно достаточно для ориентации).
|
├── cmd/, internal/ # Go API /v1
|
||||||
3. Задача-специфично: [docs/api.md](docs/api.md), [docs/access.md](docs/access.md), [web/README.md](web/README.md) — только если меняете API, доступ или фронт.
|
├── apps/web/ # SvelteKit SPA (@evobgp/web)
|
||||||
|
├── packages/ui/ # @evobgp/ui — shadcn-svelte primitives
|
||||||
Источник правды по HTTP-контракту: **[docs/openapi.yaml](docs/openapi.yaml)**. Не дублируйте длинные фрагменты спецификации в ответах — ссылайтесь на путь и тег/операцию.
|
├── packages/shared/ # @evobgp/shared — types, Zod contracts
|
||||||
|
└── pnpm-workspace.yaml
|
||||||
## Карта кода (куда смотреть)
|
|
||||||
|
|
||||||
| Область | Где искать |
|
|
||||||
|---------|------------|
|
|
||||||
| REST, auth, CORS | `internal/httpapi/` |
|
|
||||||
| Бизнес-слой и абстракция хранилища | `internal/store/` |
|
|
||||||
| PostgreSQL | `internal/repository/`, `internal/db/`, `migrations/` |
|
|
||||||
| Фоновые задачи | `internal/jobs/` |
|
|
||||||
| Цепочка refresh модуля (ingest+render, BIRD preview) | `internal/pipeline/` |
|
|
||||||
| Конфиг BIRD, `birdc` | `internal/birdfmt/`, `internal/birddeploy/` |
|
|
||||||
| Бандлы и подписи | `internal/bundle/`, `internal/signing/` |
|
|
||||||
| Точки входа процессов | `cmd/*/` |
|
|
||||||
| Веб (SvelteKit) | `web/` |
|
|
||||||
| Compose, деплой | `deploy/compose/` |
|
|
||||||
|
|
||||||
Точки входа бинарников и их роли — в таблице в начале [docs/architecture.md](docs/architecture.md).
|
|
||||||
|
|
||||||
## Как не раздувать контекст
|
|
||||||
|
|
||||||
- **Сначала узкий поиск:** `grep`/поиск по символу или короткий семантический запрос по одной папке (`internal/httpapi/`, `internal/pipeline/`, …), а не чтение всех `.go` подряд.
|
|
||||||
- **Читайте файлы целиком только при необходимости:** большие файлы — с `offset`/`limit` или по найденным строкам.
|
|
||||||
- **Не подтягивайте в контекст:** `web/node_modules/`, сгенерированные артефакты сборки, бинарники, полный `openapi.html`, если достаточно `openapi.yaml`.
|
|
||||||
- **Повторное использование:** если [docs/architecture.md](docs/architecture.md) уже описывает поток — не пересказывайте его длинно; укажите документ и конкретный подпункт задачи.
|
|
||||||
- **Длинные планы:** `.cursor/plans/*.plan.md` — для истории решений; для навигации пользователю достаточно `docs/`; не читайте план целиком без причины.
|
|
||||||
|
|
||||||
## Коммиты (Conventional Commits)
|
|
||||||
|
|
||||||
Если пользователь просит **коммит**, **commit message**, **закоммить**, **git commit**, **`/commit-message`** или это следует из плана — **сразу**:
|
|
||||||
|
|
||||||
1. Shell: `powershell -NoProfile -File scripts/commit/staged-context.ps1` (первый вызов, до текста коммита).
|
|
||||||
2. Скилл [.cursor/skills/commit-message/SKILL.md](.cursor/skills/commit-message/SKILL.md) и правило [.cursor/rules/conventional-commits.mdc](.cursor/rules/conventional-commits.mdc).
|
|
||||||
|
|
||||||
Без вывода скрипта (exit 0) **не** придумывать сообщение коммита. Заголовок — EN, тело — RU; несвязанные области — auto-split (скилл).
|
|
||||||
|
|
||||||
**Кнопка ✨ Generate commit message в Source Control** skill/rule **не** использует. Для сообщений по правилам EvoBGP — Agent Chat → **`/commit-message`** (см. [.cursor/commands/commit-message.md](.cursor/commands/commit-message.md)).
|
|
||||||
|
|
||||||
## Команды и среда
|
|
||||||
|
|
||||||
- Консоль пользователя: **PowerShell**; пути в стиле `deploy\compose`.
|
|
||||||
- Быстрый старт и переменные: [docs/quickstart.md](docs/quickstart.md), [README.md](README.md).
|
|
||||||
- **Go:** после правок — `gofmt -w`, `go vet ./...`, `scripts/lint-go.ps1` (как CI golangci-lint).
|
|
||||||
|
|
||||||
## Язык документации проекта
|
|
||||||
|
|
||||||
Пользовательская документация в `docs/` — преимущественно на русском. Комментарии и имена в коде — в существующем стиле репозитория.
|
|
||||||
|
|
||||||
## Svelte / фронтенд
|
|
||||||
|
|
||||||
При правках `web/**/*.svelte` или Svelte-модулей следуйте [.cursor/rules/web-shadcn.mdc](.cursor/rules/web-shadcn.mdc) (**WEB-19**): перед завершением задачи **обязательно**:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cd web
|
|
||||||
npm run check
|
|
||||||
npm run lint
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Если `lint` падает — `npx prettier --write .` и повторить обе команды. CI job `web` не пропускает без этого.
|
| Слой | Импорт |
|
||||||
|
|------|--------|
|
||||||
|
| Примитивы | `@evobgp/ui/components/*` |
|
||||||
|
| CSS | `@evobgp/ui/styles/globals.css` |
|
||||||
|
| Типы/контракты | `@evobgp/shared/types/*`, `@evobgp/shared/contracts/*` |
|
||||||
|
| Паттерны UI | `$lib/components/*`, `$lib/components/patterns/*` |
|
||||||
|
| Queries | `$lib/queries/*` (`@tanstack/svelte-query`) |
|
||||||
|
|
||||||
|
## С чего начать
|
||||||
|
|
||||||
|
1. **Правила:** [engineering.mdc](.cursor/rules/engineering.mdc), [web-shadcn.mdc](.cursor/rules/web-shadcn.mdc), [frontend-monorepo.mdc](.cursor/rules/frontend-monorepo.mdc)
|
||||||
|
2. **Фронт:** [apps/web/README.md](apps/web/README.md)
|
||||||
|
3. **Go:** `internal/httpapi/`, `internal/store/`, `internal/pipeline/`
|
||||||
|
|
||||||
|
## Команды
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
pnpm install
|
||||||
|
pnpm --filter @evobgp/web dev
|
||||||
|
pnpm --filter @evobgp/web build
|
||||||
|
pnpm --filter @evobgp/web check
|
||||||
|
pnpm --filter @evobgp/web lint
|
||||||
|
```
|
||||||
|
|
||||||
|
После правок `apps/web/**` — **обязательно** `check` и `lint` (WEB-19).
|
||||||
|
|
||||||
|
## UX-эталон (vps-tracker, без ReUI)
|
||||||
|
|
||||||
|
- **Shell:** `layout/app-shell.svelte` — sidebar-07, `collapsible="icon"`, 5 nav-групп (`nav.ts`), `AppBrand`, `ThemeMenu` в top header, breadcrumb с parent
|
||||||
|
- **Страница:** `PageShell` → `PageHeader` (без icon) → `QueryState` / контент
|
||||||
|
- **Метрики:** `SectionCards` (не KpiMetricsGrid)
|
||||||
|
- **Таблицы:** `DataTableCard` + `AppDataTable` (не голый `Card` + `Table`)
|
||||||
|
- **Tabs:** line-variant через `LINE_TAB_TRIGGER_CLASS` (`$lib/ui/app/tabs.ts`)
|
||||||
|
- **Тема:** `packages/ui/src/styles/globals.css` синхронизирован с vps-tracker (semantic tokens)
|
||||||
|
|
||||||
|
## Коммиты
|
||||||
|
|
||||||
|
При запросе коммита: [conventional-commits.mdc](.cursor/rules/conventional-commits.mdc), скилл `.cursor/skills/commit-message/SKILL.md`.
|
||||||
@@ -1,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 для локального и эталонного развёртывания.
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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"
|
||||||
},
|
},
|
||||||
File renamed without changes.
@@ -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",
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1 @@
|
|||||||
|
export * from '@evobgp/shared/types/api.js';
|
||||||
File renamed without changes.
File renamed without changes.
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
+13
-8
@@ -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';
|
||||||
File renamed without changes.
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts" generics="T">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
|
||||||
|
import PageShell from '$lib/components/page-shell.svelte';
|
||||||
|
import QueryState from '$lib/components/query-state.svelte';
|
||||||
|
import TableSkeleton from '$lib/components/table-skeleton.svelte';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
actions?: Snippet;
|
||||||
|
data: T | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
isError: boolean;
|
||||||
|
error?: unknown;
|
||||||
|
onRetry?: () => void;
|
||||||
|
empty?: boolean;
|
||||||
|
emptyTitle?: string;
|
||||||
|
emptyDescription?: string;
|
||||||
|
emptyAction?: Snippet;
|
||||||
|
skeleton?: Snippet;
|
||||||
|
sheet?: Snippet;
|
||||||
|
children: Snippet<[T]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actions,
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
onRetry,
|
||||||
|
empty = false,
|
||||||
|
emptyTitle,
|
||||||
|
emptyDescription,
|
||||||
|
emptyAction,
|
||||||
|
skeleton,
|
||||||
|
sheet,
|
||||||
|
children
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PageShell>
|
||||||
|
<PageHeader {title} {description}>
|
||||||
|
{#snippet actions()}
|
||||||
|
{#if actions}
|
||||||
|
{@render actions()}
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</PageHeader>
|
||||||
|
<QueryState
|
||||||
|
{data}
|
||||||
|
{isLoading}
|
||||||
|
{isError}
|
||||||
|
{error}
|
||||||
|
{onRetry}
|
||||||
|
{empty}
|
||||||
|
{emptyTitle}
|
||||||
|
{emptyDescription}
|
||||||
|
{emptyAction}
|
||||||
|
skeleton={skeleton ?? tableSkeleton}
|
||||||
|
{children}
|
||||||
|
/>
|
||||||
|
{#if sheet}
|
||||||
|
{@render sheet()}
|
||||||
|
{/if}
|
||||||
|
</PageShell>
|
||||||
|
|
||||||
|
{#snippet tableSkeleton()}
|
||||||
|
<TableSkeleton />
|
||||||
|
{/snippet}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription
|
||||||
|
} from '@evobgp/ui/components/card/index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
toolbar?: Snippet;
|
||||||
|
class?: string;
|
||||||
|
children: Snippet;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { title, description, toolbar, class: className, children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card class={cn('gap-0 py-0', className)}>
|
||||||
|
{#if title || description || toolbar}
|
||||||
|
<CardHeader
|
||||||
|
class="flex flex-row flex-wrap items-start justify-between gap-2 border-b px-4 py-3"
|
||||||
|
>
|
||||||
|
<div class="flex min-w-0 flex-col gap-0.5">
|
||||||
|
{#if title}
|
||||||
|
<CardTitle class="text-base">{title}</CardTitle>
|
||||||
|
{/if}
|
||||||
|
{#if description}
|
||||||
|
<CardDescription>{description}</CardDescription>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if toolbar}
|
||||||
|
<div class="flex shrink-0 items-center gap-2">{@render toolbar()}</div>
|
||||||
|
{/if}
|
||||||
|
</CardHeader>
|
||||||
|
{/if}
|
||||||
|
<CardContent class="p-0">{@render children()}</CardContent>
|
||||||
|
</Card>
|
||||||
+7
-7
@@ -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';
|
||||||
+7
-7
@@ -1,25 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { apiMutate } from '$lib/api/client.js';
|
import { apiMutate } from '$lib/api/client.js';
|
||||||
import type { DohProfile, DohProfileCreate } from '$lib/api/types.js';
|
import type { DohProfile, DohProfileCreate } from '$lib/api/types.js';
|
||||||
import { Button } from '$lib/ui/core/button/index.js';
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardDescription
|
CardDescription
|
||||||
} from '$lib/ui/core/card/index.js';
|
} from '@evobgp/ui/components/card/index.js';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogFooter
|
DialogFooter
|
||||||
} from '$lib/ui/core/dialog/index.js';
|
} from '@evobgp/ui/components/dialog/index.js';
|
||||||
import FormField from '$lib/ui/patterns/form/form-field.svelte';
|
import FormField from '$lib/components/patterns/form/form-field.svelte';
|
||||||
import AppInput from '$lib/ui/patterns/form/app-input.svelte';
|
import AppInput from '$lib/components/patterns/form/app-input.svelte';
|
||||||
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
|
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
|
||||||
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
|
import { confirm } from '$lib/components/patterns/confirm/confirm-state.svelte.js';
|
||||||
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
|
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
|
||||||
import Plus from '@lucide/svelte/icons/plus';
|
import Plus from '@lucide/svelte/icons/plus';
|
||||||
import Pencil from '@lucide/svelte/icons/pencil';
|
import Pencil from '@lucide/svelte/icons/pencil';
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import * as Sheet from '@evobgp/ui/components/sheet/index.js';
|
||||||
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSubmit?: () => void;
|
||||||
|
submitLabel?: string;
|
||||||
|
submitting?: boolean;
|
||||||
|
children: Snippet;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
open,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
onOpenChange,
|
||||||
|
onSubmit,
|
||||||
|
submitLabel = 'Сохранить',
|
||||||
|
submitting = false,
|
||||||
|
children
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sheet.Root {open} {onOpenChange}>
|
||||||
|
<Sheet.Content class="flex w-full flex-col gap-0 sm:max-w-lg">
|
||||||
|
<Sheet.Header>
|
||||||
|
<Sheet.Title>{title}</Sheet.Title>
|
||||||
|
{#if description}
|
||||||
|
<Sheet.Description>{description}</Sheet.Description>
|
||||||
|
{/if}
|
||||||
|
</Sheet.Header>
|
||||||
|
<div class="flex flex-1 flex-col gap-4 overflow-y-auto px-4 py-4">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
{#if onSubmit}
|
||||||
|
<Sheet.Footer>
|
||||||
|
<Button onclick={onSubmit} disabled={submitting}>{submitLabel}</Button>
|
||||||
|
</Sheet.Footer>
|
||||||
|
{/if}
|
||||||
|
</Sheet.Content>
|
||||||
|
</Sheet.Root>
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import * as Sidebar from '@evobgp/ui/components/sidebar/index.js';
|
||||||
|
import * as Breadcrumb from '@evobgp/ui/components/breadcrumb/index.js';
|
||||||
|
import { Separator } from '@evobgp/ui/components/separator/index.js';
|
||||||
|
import type { ThemePreference } from '$lib/theme.js';
|
||||||
|
import { allNavItems, navGroups, parentRoute, routeLabels } from '$lib/ui/app/layout/nav.js';
|
||||||
|
import AppBrand from '$lib/ui/app/layout/app-brand.svelte';
|
||||||
|
import ThemeMenu from '$lib/ui/app/layout/theme-menu.svelte';
|
||||||
|
import AppVersion from '$lib/ui/app/layout/app-version.svelte';
|
||||||
|
import AppMobileNav from '$lib/ui/app/layout/app-mobile-nav.svelte';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: Snippet;
|
||||||
|
theme?: ThemePreference;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { children, theme = $bindable<ThemePreference>('system') }: Props = $props();
|
||||||
|
|
||||||
|
let mobileNavOpen = $state(false);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const navHref = (href: string) => resolve(href as any);
|
||||||
|
|
||||||
|
function isActive(href: string) {
|
||||||
|
const pathname = page.url.pathname;
|
||||||
|
if (href === '/') return pathname === '/';
|
||||||
|
return pathname === href || pathname.startsWith(href + '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeItem = $derived(allNavItems.find((i) => isActive(i.href)) ?? allNavItems[0]);
|
||||||
|
|
||||||
|
const parentHref = $derived(parentRoute[activeItem.href]);
|
||||||
|
const parentLabel = $derived(parentHref ? routeLabels[parentHref] : null);
|
||||||
|
const currentLabel = $derived(routeLabels[activeItem.href] ?? 'EvoBGP');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Sidebar.Provider>
|
||||||
|
<Sidebar.Root collapsible="icon">
|
||||||
|
<Sidebar.Header>
|
||||||
|
<AppBrand />
|
||||||
|
</Sidebar.Header>
|
||||||
|
<Sidebar.Content>
|
||||||
|
{#each navGroups as group (group.label)}
|
||||||
|
<Sidebar.Group>
|
||||||
|
<Sidebar.GroupLabel>{group.label}</Sidebar.GroupLabel>
|
||||||
|
<Sidebar.GroupContent>
|
||||||
|
<Sidebar.Menu>
|
||||||
|
{#each group.items as item (item.href)}
|
||||||
|
{@const Icon = item.icon}
|
||||||
|
<Sidebar.MenuItem>
|
||||||
|
<Sidebar.MenuButton isActive={isActive(item.href)} tooltipContent={item.label}>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<a href={navHref(item.href)} {...props}>
|
||||||
|
<Icon class="size-4" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</a>
|
||||||
|
{/snippet}
|
||||||
|
</Sidebar.MenuButton>
|
||||||
|
</Sidebar.MenuItem>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Menu>
|
||||||
|
</Sidebar.GroupContent>
|
||||||
|
</Sidebar.Group>
|
||||||
|
{/each}
|
||||||
|
</Sidebar.Content>
|
||||||
|
<Sidebar.Footer>
|
||||||
|
<AppVersion />
|
||||||
|
</Sidebar.Footer>
|
||||||
|
<Sidebar.Rail />
|
||||||
|
</Sidebar.Root>
|
||||||
|
<Sidebar.Inset>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 flex h-16 shrink-0 items-center gap-2 border-b bg-background/95 px-4 backdrop-blur supports-[backdrop-filter]:bg-background/80"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 md:hidden">
|
||||||
|
<AppMobileNav bind:open={mobileNavOpen} bind:theme />
|
||||||
|
<span class="font-semibold tracking-tight">EvoBGP</span>
|
||||||
|
</div>
|
||||||
|
<Sidebar.Trigger class="-ms-1 hidden md:flex" />
|
||||||
|
<Separator orientation="vertical" class="mr-2 hidden h-4 md:block" />
|
||||||
|
<Breadcrumb.Root class="hidden min-w-0 md:flex">
|
||||||
|
<Breadcrumb.List>
|
||||||
|
{#if parentLabel && parentHref}
|
||||||
|
<Breadcrumb.Item class="hidden md:block">
|
||||||
|
<Breadcrumb.Link href={navHref(parentHref)}>{parentLabel}</Breadcrumb.Link>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Separator class="hidden md:block" />
|
||||||
|
{/if}
|
||||||
|
<Breadcrumb.Item>
|
||||||
|
<Breadcrumb.Page>{currentLabel}</Breadcrumb.Page>
|
||||||
|
</Breadcrumb.Item>
|
||||||
|
</Breadcrumb.List>
|
||||||
|
</Breadcrumb.Root>
|
||||||
|
<div class="ml-auto flex items-center gap-2">
|
||||||
|
<ThemeMenu bind:theme class="hidden md:flex" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="flex flex-1 flex-col gap-4 p-4 md:gap-6 md:p-6">
|
||||||
|
{@render children()}
|
||||||
|
</main>
|
||||||
|
</Sidebar.Inset>
|
||||||
|
</Sidebar.Provider>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { Badge } from '@evobgp/ui/components/badge/index.js';
|
||||||
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
|
import X from '@lucide/svelte/icons/x';
|
||||||
|
|
||||||
|
export type FilterChip = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
chips?: FilterChip[];
|
||||||
|
onRemoveChip?: (id: string) => void;
|
||||||
|
onClear?: () => void;
|
||||||
|
children?: Snippet;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { chips = [], onRemoveChip, onClear, children }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
{#if children}
|
||||||
|
{@render children()}
|
||||||
|
{/if}
|
||||||
|
{#each chips as chip (chip.id)}
|
||||||
|
<Badge variant="secondary" class="gap-1 pr-1">
|
||||||
|
{chip.label}
|
||||||
|
{#if onRemoveChip}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
class="size-5"
|
||||||
|
onclick={() => onRemoveChip(chip.id)}
|
||||||
|
aria-label="Убрать фильтр {chip.label}"
|
||||||
|
>
|
||||||
|
<X class="size-3" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</Badge>
|
||||||
|
{/each}
|
||||||
|
{#if chips.length > 0 && onClear}
|
||||||
|
<Button variant="ghost" size="sm" onclick={onClear}>Сбросить</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
+5
-5
@@ -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';
|
||||||
+10
-5
@@ -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 = {
|
||||||
+10
-5
@@ -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
-6
@@ -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';
|
||||||
+11
-6
@@ -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 = {
|
||||||
+3
-9
@@ -2,8 +2,8 @@
|
|||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import type { ModuleRow } from '$lib/api/types.js';
|
import type { ModuleRow } from '$lib/api/types.js';
|
||||||
import { moduleEnabledRu, moduleEnabledBadgeVariant, moduleTypeRu } from '$lib/ui-labels.js';
|
import { moduleEnabledRu, moduleEnabledBadgeVariant, moduleTypeRu } from '$lib/ui-labels.js';
|
||||||
import { Badge } from '$lib/ui/core/badge/index.js';
|
import { Badge } from '@evobgp/ui/components/badge/index.js';
|
||||||
import { Button } from '$lib/ui/core/button/index.js';
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
|
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
|
||||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||||
import Pencil from '@lucide/svelte/icons/pencil';
|
import Pencil from '@lucide/svelte/icons/pencil';
|
||||||
@@ -26,13 +26,7 @@
|
|||||||
<Button variant="ghost" size="icon-sm" class="mt-1 shrink-0" href={resolve('/modules')}>
|
<Button variant="ghost" size="icon-sm" class="mt-1 shrink-0" href={resolve('/modules')}>
|
||||||
<ArrowLeft class="size-4" />
|
<ArrowLeft class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<PageHeader
|
<PageHeader class="min-w-0 flex-1" title={mod.name} description={mod.id}>
|
||||||
class="min-w-0 flex-1"
|
|
||||||
title={mod.name}
|
|
||||||
description={mod.id}
|
|
||||||
icon={Blocks}
|
|
||||||
iconClass="bg-chart-2/15 text-chart-2"
|
|
||||||
>
|
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
<Badge variant="outline">{moduleTypeRu(mod.type)}</Badge>
|
<Badge variant="outline">{moduleTypeRu(mod.type)}</Badge>
|
||||||
+5
-5
@@ -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';
|
||||||
+10
-5
@@ -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 = {
|
||||||
+12
-7
@@ -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 }[] = [
|
||||||
+10
-5
@@ -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 = {
|
||||||
+5
-5
@@ -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';
|
||||||
+2
-2
@@ -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';
|
||||||
+2
-2
@@ -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;
|
||||||
File renamed without changes.
+13
-13
@@ -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';
|
||||||
+8
-8
@@ -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';
|
||||||
|
|
||||||
+10
-10
@@ -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 {
|
||||||
+2
-2
@@ -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 = {
|
||||||
+2
-2
@@ -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';
|
||||||
+25
-89
@@ -8,9 +8,10 @@
|
|||||||
networkOverallStatusHint,
|
networkOverallStatusHint,
|
||||||
networkOverallStatusLabel
|
networkOverallStatusLabel
|
||||||
} from '$lib/network/network-metrics.js';
|
} from '$lib/network/network-metrics.js';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
|
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
|
||||||
import { Button } from '$lib/ui/core/button/index.js';
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
import KpiMetricsGrid from '$lib/ui/patterns/kpi/kpi-metrics-grid.svelte';
|
import SectionCards, { type SectionCardItem } from '$lib/components/section-cards.svelte';
|
||||||
|
import SectionCardsSkeleton from '$lib/components/section-cards-skeleton.svelte';
|
||||||
import NetworkSpeakerStatusCard from '$lib/components/network/NetworkSpeakerStatusCard.svelte';
|
import NetworkSpeakerStatusCard from '$lib/components/network/NetworkSpeakerStatusCard.svelte';
|
||||||
import CheckCircle from '@lucide/svelte/icons/check-circle';
|
import CheckCircle from '@lucide/svelte/icons/check-circle';
|
||||||
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
|
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
|
||||||
@@ -41,45 +42,6 @@
|
|||||||
onSpeakerSelect
|
onSpeakerSelect
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const statAccents = [
|
|
||||||
{
|
|
||||||
border: 'border-l-chart-3',
|
|
||||||
bg: 'bg-chart-3/5',
|
|
||||||
iconBg: 'bg-chart-3/15',
|
|
||||||
iconText: 'text-chart-3'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
border: 'border-l-chart-2',
|
|
||||||
bg: 'bg-chart-2/5',
|
|
||||||
iconBg: 'bg-chart-2/15',
|
|
||||||
iconText: 'text-chart-2'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
border: 'border-l-chart-4',
|
|
||||||
bg: 'bg-chart-4/5',
|
|
||||||
iconBg: 'bg-chart-4/15',
|
|
||||||
iconText: 'text-chart-4'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
border: 'border-l-warning',
|
|
||||||
bg: 'bg-warning/5',
|
|
||||||
iconBg: 'bg-warning/15',
|
|
||||||
iconText: 'text-warning'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
border: 'border-l-destructive',
|
|
||||||
bg: 'bg-destructive/5',
|
|
||||||
iconBg: 'bg-destructive/15',
|
|
||||||
iconText: 'text-destructive'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
border: 'border-l-info',
|
|
||||||
bg: 'bg-info/10',
|
|
||||||
iconBg: 'bg-info/15',
|
|
||||||
iconText: 'text-info'
|
|
||||||
}
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const metrics = $derived(aggregateNetworkMetrics(peers, speakers, bird));
|
const metrics = $derived(aggregateNetworkMetrics(peers, speakers, bird));
|
||||||
const overallStatus = $derived(deriveNetworkOverallStatus(metrics));
|
const overallStatus = $derived(deriveNetworkOverallStatus(metrics));
|
||||||
const overallHint = $derived(networkOverallStatusHint(overallStatus, metrics));
|
const overallHint = $derived(networkOverallStatusHint(overallStatus, metrics));
|
||||||
@@ -91,76 +53,51 @@
|
|||||||
return `${bird.bgp_established}/${bird.bgp_sessions_total}`;
|
return `${bird.bgp_established}/${bird.bgp_sessions_total}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const kpiCards = $derived.by(() => [
|
const sectionCards = $derived.by((): SectionCardItem[] => [
|
||||||
{
|
{
|
||||||
id: 'peers',
|
|
||||||
label: 'BGP-пиры',
|
label: 'BGP-пиры',
|
||||||
value: initialLoading ? '—' : String(metrics.peersTotal),
|
value: initialLoading ? '—' : String(metrics.peersTotal),
|
||||||
description: initialLoading
|
hint: initialLoading
|
||||||
? ''
|
? ''
|
||||||
: `${metrics.peersEstablished} Established из ${metrics.peersEnabled} вкл.`,
|
: `${metrics.peersEstablished} Established / ${metrics.peersEnabled} вкл.`,
|
||||||
icon: Share2,
|
icon: Share2,
|
||||||
accent: statAccents[0],
|
variant: metrics.peersMismatch > 0 ? 'warning' : 'default'
|
||||||
badge: metrics.peersMismatch > 0 ? `mismatch ${metrics.peersMismatch}` : 'peers',
|
|
||||||
badgeClass:
|
|
||||||
metrics.peersMismatch > 0 ? 'border-warning/30 bg-warning/15 text-warning' : undefined
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'established',
|
|
||||||
label: 'Активные сессии',
|
label: 'Активные сессии',
|
||||||
value: initialLoading ? '—' : String(metrics.peersEstablished),
|
value: initialLoading ? '—' : String(metrics.peersEstablished),
|
||||||
description: 'Established среди включённых пиров',
|
hint: 'Established',
|
||||||
icon: CheckCircle2,
|
icon: CheckCircle2
|
||||||
accent: statAccents[1],
|
|
||||||
badge: metrics.peersEstablished > 0 ? 'Established' : 'нет сессий',
|
|
||||||
badgeClass:
|
|
||||||
metrics.peersEstablished > 0 ? 'border-success/30 bg-success/15 text-success' : undefined
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'speakers',
|
|
||||||
label: 'Спикеры online',
|
label: 'Спикеры online',
|
||||||
value: initialLoading ? '—' : `${metrics.speakersOnline}/${metrics.speakersTotal}`,
|
value: initialLoading ? '—' : `${metrics.speakersOnline}/${metrics.speakersTotal}`,
|
||||||
description: 'agent + BGP poll',
|
hint: 'agent + BGP poll',
|
||||||
icon: Server,
|
icon: Server,
|
||||||
accent: statAccents[2],
|
variant: metrics.speakersOnline < metrics.speakersTotal ? 'warning' : 'default'
|
||||||
badge: metrics.speakersOnline === metrics.speakersTotal ? 'все online' : 'есть offline',
|
|
||||||
badgeClass:
|
|
||||||
metrics.speakersOnline === metrics.speakersTotal
|
|
||||||
? 'border-success/30 bg-success/15 text-success'
|
|
||||||
: 'border-warning/30 bg-warning/15 text-warning'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'drift',
|
|
||||||
label: 'Drift',
|
label: 'Drift',
|
||||||
value: initialLoading ? '—' : String(metrics.speakersDrift),
|
value: initialLoading ? '—' : String(metrics.speakersDrift),
|
||||||
description: 'applied ≠ published',
|
hint: 'applied ≠ published',
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
accent: statAccents[3],
|
variant: metrics.speakersDrift > 0 ? 'warning' : 'default'
|
||||||
badge: metrics.speakersDrift > 0 ? 'требует apply' : 'синхронно',
|
|
||||||
badgeVariant: metrics.speakersDrift > 0 ? ('secondary' as const) : ('outline' as const)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'poll-errors',
|
|
||||||
label: 'Ошибки опроса',
|
label: 'Ошибки опроса',
|
||||||
value: initialLoading ? '—' : String(metrics.pollErrors),
|
value: initialLoading ? '—' : String(metrics.pollErrors),
|
||||||
description: 'agent или BGP poll',
|
hint: 'agent / BGP poll',
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
accent: statAccents[4],
|
variant: metrics.pollErrors > 0 ? 'destructive' : 'default'
|
||||||
badge: metrics.pollErrors > 0 ? 'ошибки' : 'ok',
|
|
||||||
badgeClass:
|
|
||||||
metrics.pollErrors === 0 ? 'border-success/30 bg-success/15 text-success' : undefined
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cp-bird',
|
|
||||||
label: 'BGP на CP',
|
label: 'BGP на CP',
|
||||||
value: initialLoading ? '—' : birdText,
|
value: initialLoading ? '—' : birdText,
|
||||||
description: bird?.birdc_configured
|
hint: bird?.birdc_configured ? 'Established / total' : (bird?.message ?? 'birdc N/A'),
|
||||||
? 'Established / total на API-хосте'
|
|
||||||
: (bird?.message ?? 'birdc не настроен'),
|
|
||||||
icon: Bird,
|
icon: Bird,
|
||||||
accent: statAccents[5],
|
onClick: () => {
|
||||||
badge: !bird?.birdc_configured ? 'N/A' : bird?.healthy ? 'В норме' : 'Деградация',
|
window.location.href = resolve('/monitoring');
|
||||||
href: '/monitoring' as const
|
}
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
@@ -206,12 +143,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<KpiMetricsGrid
|
{#if initialLoading || loading}
|
||||||
cards={kpiCards}
|
<SectionCardsSkeleton count={6} class="sm:grid-cols-2 xl:grid-cols-3" />
|
||||||
loading={initialLoading || loading}
|
{:else}
|
||||||
skeletonCount={6}
|
<SectionCards items={sectionCards} class="sm:grid-cols-2 xl:grid-cols-3" />
|
||||||
class="sm:grid-cols-2 xl:grid-cols-3"
|
{/if}
|
||||||
/>
|
|
||||||
|
|
||||||
<section class="flex min-w-0 flex-col gap-4">
|
<section class="flex min-w-0 flex-col gap-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
+16
-11
@@ -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';
|
||||||
+6
-6
@@ -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 = {
|
||||||
+2
-2
@@ -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';
|
||||||
|
|
||||||
+9
-9
@@ -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';
|
||||||
+8
-3
@@ -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';
|
||||||
|
|
||||||
+3
-3
@@ -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';
|
||||||
+4
-4
@@ -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';
|
||||||
+3
-3
@@ -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';
|
||||||
+3
-3
@@ -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';
|
||||||
File renamed without changes.
+3
-3
@@ -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[];
|
||||||
File renamed without changes.
+3
-3
@@ -8,15 +8,15 @@
|
|||||||
networkOverallStatusHint,
|
networkOverallStatusHint,
|
||||||
networkOverallStatusLabel
|
networkOverallStatusLabel
|
||||||
} from '$lib/network/network-metrics.js';
|
} from '$lib/network/network-metrics.js';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
|
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
|
||||||
import { Button } from '$lib/ui/core/button/index.js';
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardDescription
|
CardDescription
|
||||||
} from '$lib/ui/core/card/index.js';
|
} from '@evobgp/ui/components/card/index.js';
|
||||||
import CheckCircle from '@lucide/svelte/icons/check-circle';
|
import CheckCircle from '@lucide/svelte/icons/check-circle';
|
||||||
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
|
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
|
||||||
import XCircle from '@lucide/svelte/icons/x-circle';
|
import XCircle from '@lucide/svelte/icons/x-circle';
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import type { JobRow } from '$lib/api/types.js';
|
||||||
|
import { formatDateTime } from '$lib/modules/display.js';
|
||||||
|
import { jobKindTitle } from '$lib/operations/job-kind-label.js';
|
||||||
|
import { jobStatusRu, jobStatusBadgeVariant } from '$lib/ui-labels.js';
|
||||||
|
import { Badge } from '@evobgp/ui/components/badge/index.js';
|
||||||
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
|
import DataTableCard from '$lib/components/data-table-card.svelte';
|
||||||
|
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
|
||||||
|
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||||
|
import ExternalLink from '@lucide/svelte/icons/external-link';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: JobRow[];
|
||||||
|
moduleNameById: ReadonlyMap<string, string>;
|
||||||
|
loading?: boolean;
|
||||||
|
initialLoading?: boolean;
|
||||||
|
error?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
items,
|
||||||
|
moduleNameById,
|
||||||
|
loading = false,
|
||||||
|
initialLoading = false,
|
||||||
|
error = null
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ id: 'kind', label: 'Вид', sortable: true, sortValue: (j: JobRow) => j.kind },
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
label: 'Статус',
|
||||||
|
sortable: true,
|
||||||
|
sortValue: (j: JobRow) => j.status
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'created',
|
||||||
|
label: 'Создана',
|
||||||
|
sortable: true,
|
||||||
|
sortValue: (j: JobRow) => j.created_at ?? ''
|
||||||
|
},
|
||||||
|
{ id: 'actions', label: '', class: 'w-10' }
|
||||||
|
] as const;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DataTableCard title="Последние задачи" description="Фоновые задачи ingest, refresh и apply">
|
||||||
|
{#snippet toolbar()}
|
||||||
|
<Button variant="outline" size="sm" href={resolve('/operations?tab=jobs')}>
|
||||||
|
Все
|
||||||
|
<ArrowRight class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
<AppDataTable
|
||||||
|
columns={[...columns]}
|
||||||
|
rows={items}
|
||||||
|
rowKey={(j) => j.job_id}
|
||||||
|
loading={initialLoading || loading}
|
||||||
|
{error}
|
||||||
|
emptyTitle="Нет задач"
|
||||||
|
emptyDescription="Задачи появятся после refresh или деплоя."
|
||||||
|
>
|
||||||
|
{#snippet cell({ row: j, column })}
|
||||||
|
{#if column.id === 'kind'}
|
||||||
|
<span class="font-medium">{jobKindTitle(j, moduleNameById)}</span>
|
||||||
|
{:else if column.id === 'status'}
|
||||||
|
<Badge variant={jobStatusBadgeVariant(j.status)}>{jobStatusRu(j.status)}</Badge>
|
||||||
|
{:else if column.id === 'created'}
|
||||||
|
<span class="text-xs whitespace-nowrap text-muted-foreground">
|
||||||
|
{formatDateTime(j.created_at)}
|
||||||
|
</span>
|
||||||
|
{:else if column.id === 'actions'}
|
||||||
|
<Button variant="ghost" size="icon-sm" href={resolve('/operations?tab=jobs')}>
|
||||||
|
<ExternalLink class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</AppDataTable>
|
||||||
|
</DataTableCard>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
|
import type { RevisionRow } from '$lib/api/types.js';
|
||||||
|
import { formatDateTime } from '$lib/modules/display.js';
|
||||||
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
|
import DataTableCard from '$lib/components/data-table-card.svelte';
|
||||||
|
import AppDataTable from '$lib/components/patterns/data-table/app-data-table.svelte';
|
||||||
|
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||||
|
import ExternalLink from '@lucide/svelte/icons/external-link';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: RevisionRow[];
|
||||||
|
loading?: boolean;
|
||||||
|
initialLoading?: boolean;
|
||||||
|
error?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { items, loading = false, initialLoading = false, error = null }: Props = $props();
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
label: 'ID',
|
||||||
|
sortable: true,
|
||||||
|
sortValue: (rev: RevisionRow) => rev.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'created',
|
||||||
|
label: 'Создана',
|
||||||
|
sortable: true,
|
||||||
|
sortValue: (rev: RevisionRow) => rev.created_at ?? ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prefixes',
|
||||||
|
label: 'Префиксов',
|
||||||
|
sortable: true,
|
||||||
|
sortValue: (rev: RevisionRow) => rev.materialized_prefix_count ?? 0
|
||||||
|
},
|
||||||
|
{ id: 'actions', label: '', class: 'w-10' }
|
||||||
|
] as const;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<DataTableCard
|
||||||
|
title="Последние ревизии"
|
||||||
|
description="Снимки конфигурации BIRD после обновления модулей"
|
||||||
|
>
|
||||||
|
{#snippet toolbar()}
|
||||||
|
<Button variant="outline" size="sm" href={resolve('/operations')}>
|
||||||
|
Все
|
||||||
|
<ArrowRight class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
<AppDataTable
|
||||||
|
columns={[...columns]}
|
||||||
|
rows={items}
|
||||||
|
rowKey={(rev) => rev.id}
|
||||||
|
loading={initialLoading || loading}
|
||||||
|
{error}
|
||||||
|
emptyTitle="Нет ревизий"
|
||||||
|
emptyDescription="Ревизии появятся после обновления модулей."
|
||||||
|
>
|
||||||
|
{#snippet cell({ row: rev, column })}
|
||||||
|
{#if column.id === 'id'}
|
||||||
|
<span class="font-mono text-xs">{rev.id.slice(0, 8)}…</span>
|
||||||
|
{:else if column.id === 'created'}
|
||||||
|
<span class="text-sm whitespace-nowrap text-muted-foreground">
|
||||||
|
{formatDateTime(rev.created_at)}
|
||||||
|
</span>
|
||||||
|
{:else if column.id === 'prefixes'}
|
||||||
|
<span class="tabular-nums">{rev.materialized_prefix_count}</span>
|
||||||
|
{:else if column.id === 'actions'}
|
||||||
|
<Button variant="ghost" size="icon-sm" href={resolve('/operations')}>
|
||||||
|
<ExternalLink class="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</AppDataTable>
|
||||||
|
</DataTableCard>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: Snippet;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { children, class: className }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn('flex flex-col gap-4 md:gap-6', className)}>
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
+2
-2
@@ -8,7 +8,7 @@
|
|||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle
|
AlertDialogTitle
|
||||||
} from '$lib/ui/core/alert-dialog/index.js';
|
} from '@evobgp/ui/components/alert-dialog/index.js';
|
||||||
import { closeConfirm, confirmState } from './confirm-state.svelte.js';
|
import { closeConfirm, confirmState } from './confirm-state.svelte.js';
|
||||||
|
|
||||||
const state = $derived(confirmState.current);
|
const state = $derived(confirmState.current);
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
class={state.destructive
|
class={state.destructive
|
||||||
? 'text-destructive-foreground bg-destructive hover:bg-destructive/90'
|
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||||
: ''}
|
: ''}
|
||||||
disabled={state.loading}
|
disabled={state.loading}
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
File renamed without changes.
+48
-6
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts" generics="T extends Record<string, unknown>">
|
<script lang="ts" generics="T extends Record<string, unknown>">
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { cn } from '$lib/utils.js';
|
import { cn } from '$lib/utils.js';
|
||||||
import * as Table from '$lib/ui/core/table/index.js';
|
import * as Table from '@evobgp/ui/components/table/index.js';
|
||||||
import { Skeleton } from '$lib/ui/core/skeleton/index.js';
|
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
|
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
|
||||||
import EmptyState from '$lib/ui/patterns/empty-state/empty-state.svelte';
|
import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
|
||||||
import ArrowUpDown from '@lucide/svelte/icons/arrow-up-down';
|
import ArrowUpDown from '@lucide/svelte/icons/arrow-up-down';
|
||||||
import ArrowUp from '@lucide/svelte/icons/arrow-up';
|
import ArrowUp from '@lucide/svelte/icons/arrow-up';
|
||||||
import ArrowDown from '@lucide/svelte/icons/arrow-down';
|
import ArrowDown from '@lucide/svelte/icons/arrow-down';
|
||||||
import { Button } from '$lib/ui/core/button/index.js';
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
import type { DataTableColumn } from './types.js';
|
import type { DataTableColumn } from './types.js';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
error?: string | null;
|
error?: string | null;
|
||||||
emptyTitle?: string;
|
emptyTitle?: string;
|
||||||
emptyDescription?: string;
|
emptyDescription?: string;
|
||||||
|
/** Client-side page size; 0 = all rows */
|
||||||
|
pageSize?: number;
|
||||||
toolbar?: Snippet;
|
toolbar?: Snippet;
|
||||||
cell: Snippet<[{ row: T; column: DataTableColumn<T> }]>;
|
cell: Snippet<[{ row: T; column: DataTableColumn<T> }]>;
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -32,6 +34,7 @@
|
|||||||
error = null,
|
error = null,
|
||||||
emptyTitle = 'Нет записей',
|
emptyTitle = 'Нет записей',
|
||||||
emptyDescription,
|
emptyDescription,
|
||||||
|
pageSize = 0,
|
||||||
toolbar,
|
toolbar,
|
||||||
cell,
|
cell,
|
||||||
class: className
|
class: className
|
||||||
@@ -39,6 +42,7 @@
|
|||||||
|
|
||||||
let sortColumnId = $state<string | null>(null);
|
let sortColumnId = $state<string | null>(null);
|
||||||
let sortDir = $state<'asc' | 'desc'>('asc');
|
let sortDir = $state<'asc' | 'desc'>('asc');
|
||||||
|
let pageIndex = $state(0);
|
||||||
|
|
||||||
const sortedRows = $derived.by(() => {
|
const sortedRows = $derived.by(() => {
|
||||||
if (!sortColumnId) return rows;
|
if (!sortColumnId) return rows;
|
||||||
@@ -57,6 +61,21 @@
|
|||||||
return copy;
|
return copy;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const paginatedRows = $derived.by(() => {
|
||||||
|
if (!pageSize || pageSize <= 0) return sortedRows;
|
||||||
|
const start = pageIndex * pageSize;
|
||||||
|
return sortedRows.slice(start, start + pageSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageCount = $derived(
|
||||||
|
pageSize > 0 ? Math.max(1, Math.ceil(sortedRows.length / pageSize)) : 1
|
||||||
|
);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
rows;
|
||||||
|
if (pageIndex !== 0) pageIndex = 0;
|
||||||
|
});
|
||||||
|
|
||||||
function toggleSort(col: DataTableColumn<T>) {
|
function toggleSort(col: DataTableColumn<T>) {
|
||||||
if (!col.sortable) return;
|
if (!col.sortable) return;
|
||||||
if (sortColumnId === col.id) {
|
if (sortColumnId === col.id) {
|
||||||
@@ -131,7 +150,7 @@
|
|||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
{:else}
|
{:else}
|
||||||
{#each sortedRows as row (rowKey(row))}
|
{#each paginatedRows as row (rowKey(row))}
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
{#each columns as col (col.id)}
|
{#each columns as col (col.id)}
|
||||||
<Table.Cell class={col.class}>
|
<Table.Cell class={col.class}>
|
||||||
@@ -144,4 +163,27 @@
|
|||||||
</Table.Body>
|
</Table.Body>
|
||||||
</Table.Root>
|
</Table.Root>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if pageSize > 0 && sortedRows.length > pageSize}
|
||||||
|
<div class="flex items-center justify-between gap-2 text-sm text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
{pageIndex * pageSize + 1}–{Math.min((pageIndex + 1) * pageSize, sortedRows.length)} из
|
||||||
|
{sortedRows.length}
|
||||||
|
</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={pageIndex === 0}
|
||||||
|
onclick={() => (pageIndex -= 1)}>Назад</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={pageIndex >= pageCount - 1}
|
||||||
|
onclick={() => (pageIndex += 1)}>Вперёд</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
File renamed without changes.
File renamed without changes.
+2
-2
@@ -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>
|
||||||
+2
-2
@@ -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
-1
@@ -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
-1
@@ -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
-1
@@ -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;
|
||||||
File renamed without changes.
@@ -0,0 +1,69 @@
|
|||||||
|
<script lang="ts" generics="T">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import AlertCircle from '@lucide/svelte/icons/alert-circle';
|
||||||
|
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||||
|
import { Button } from '@evobgp/ui/components/button/index.js';
|
||||||
|
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
|
||||||
|
import EmptyState from '$lib/components/patterns/empty-state/empty-state.svelte';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: T | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
isError: boolean;
|
||||||
|
error?: unknown;
|
||||||
|
empty?: boolean;
|
||||||
|
emptyTitle?: string;
|
||||||
|
emptyDescription?: string;
|
||||||
|
emptyAction?: Snippet;
|
||||||
|
onRetry?: () => void;
|
||||||
|
skeleton?: Snippet;
|
||||||
|
children: Snippet<[T]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
empty = false,
|
||||||
|
emptyTitle = 'Нет данных',
|
||||||
|
emptyDescription,
|
||||||
|
emptyAction,
|
||||||
|
onRetry,
|
||||||
|
skeleton,
|
||||||
|
children
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const errorMessage = $derived(
|
||||||
|
error instanceof Error ? error.message : 'Не удалось загрузить данные'
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isLoading}
|
||||||
|
{#if skeleton}
|
||||||
|
{@render skeleton()}
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<Skeleton class="h-8 w-48" />
|
||||||
|
<Skeleton class="h-32 w-full" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else if isError}
|
||||||
|
<EmptyState
|
||||||
|
title="Ошибка загрузки"
|
||||||
|
description={errorMessage}
|
||||||
|
icon={AlertCircle}
|
||||||
|
action={onRetry ? retryAction : undefined}
|
||||||
|
/>
|
||||||
|
{:else if empty || data == null}
|
||||||
|
<EmptyState title={emptyTitle} description={emptyDescription} action={emptyAction} />
|
||||||
|
{:else}
|
||||||
|
{@render children(data)}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#snippet retryAction()}
|
||||||
|
<Button variant="outline" size="sm" onclick={onRetry}>
|
||||||
|
<RefreshCw class="size-4" />
|
||||||
|
Повторить
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Card, CardContent } from '@evobgp/ui/components/card/index.js';
|
||||||
|
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
count?: number;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { count = 4, class: className }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn('grid gap-3 sm:grid-cols-2 lg:grid-cols-3', className)}>
|
||||||
|
{#each Array.from({ length: count }) as _, i (i)}
|
||||||
|
<Card>
|
||||||
|
<CardContent class="flex items-start gap-2.5 px-3 py-2.5">
|
||||||
|
<Skeleton class="size-7 shrink-0 rounded-md" />
|
||||||
|
<div class="flex flex-1 flex-col gap-2">
|
||||||
|
<Skeleton class="h-3 w-20" />
|
||||||
|
<Skeleton class="h-6 w-16" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Component, Snippet } from 'svelte';
|
||||||
|
import { Card, CardContent } from '@evobgp/ui/components/card/index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
|
||||||
|
export type SectionCardItem = {
|
||||||
|
label: string | Snippet;
|
||||||
|
value: string | number | Snippet;
|
||||||
|
hint?: string | Snippet;
|
||||||
|
icon?: Component;
|
||||||
|
badge?: Snippet;
|
||||||
|
variant?: 'default' | 'warning' | 'destructive';
|
||||||
|
active?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
items: SectionCardItem[];
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { items, class: className }: Props = $props();
|
||||||
|
|
||||||
|
const variantClass: Record<NonNullable<SectionCardItem['variant']>, string> = {
|
||||||
|
default: '',
|
||||||
|
warning: 'border-warning/50',
|
||||||
|
destructive: 'border-destructive/50'
|
||||||
|
};
|
||||||
|
|
||||||
|
const valueVariantClass: Record<NonNullable<SectionCardItem['variant']>, string> = {
|
||||||
|
default: '',
|
||||||
|
warning: 'text-warning-foreground',
|
||||||
|
destructive: 'text-destructive'
|
||||||
|
};
|
||||||
|
|
||||||
|
function sectionGridClass(count: number): string {
|
||||||
|
if (count <= 1) return 'grid-cols-1';
|
||||||
|
if (count === 2) return 'sm:grid-cols-2';
|
||||||
|
if (count === 3) return 'sm:grid-cols-2 lg:grid-cols-3';
|
||||||
|
if (count === 4) return 'sm:grid-cols-2 lg:grid-cols-4';
|
||||||
|
if (count === 5) return 'sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5';
|
||||||
|
if (count === 6) return 'sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6';
|
||||||
|
return 'sm:grid-cols-2 lg:grid-cols-3';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={cn('grid gap-3', sectionGridClass(items.length), className)}>
|
||||||
|
{#each items as item, idx (typeof item.label === 'string' ? item.label : idx)}
|
||||||
|
{@const clickable = Boolean(item.onClick)}
|
||||||
|
<Card
|
||||||
|
class={cn(
|
||||||
|
'gap-0',
|
||||||
|
variantClass[item.variant ?? 'default'],
|
||||||
|
item.active && 'border-primary ring-1 ring-primary/30',
|
||||||
|
clickable && 'cursor-pointer transition-colors hover:bg-muted/40'
|
||||||
|
)}
|
||||||
|
onclick={item.onClick}
|
||||||
|
role={clickable ? 'button' : undefined}
|
||||||
|
tabindex={clickable ? 0 : undefined}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (clickable && (e.key === 'Enter' || e.key === ' ')) {
|
||||||
|
e.preventDefault();
|
||||||
|
item.onClick?.();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent class="flex items-start gap-2.5 px-3 py-2.5">
|
||||||
|
{#if item.icon}
|
||||||
|
<span
|
||||||
|
class="flex size-7 shrink-0 items-center justify-center rounded-md bg-muted/60 text-muted-foreground"
|
||||||
|
>
|
||||||
|
<item.icon class="size-4" />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
{#if typeof item.label === 'string'}
|
||||||
|
<span class="truncate text-xs text-muted-foreground">{item.label}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="truncate text-xs text-muted-foreground">{@render item.label()}</span>
|
||||||
|
{/if}
|
||||||
|
{#if item.badge}
|
||||||
|
<span class="shrink-0">{@render item.badge()}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex min-w-0 items-baseline gap-1.5">
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
'flex items-center gap-1 text-lg font-semibold tabular-nums',
|
||||||
|
valueVariantClass[item.variant ?? 'default']
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{#if typeof item.value === 'string' || typeof item.value === 'number'}
|
||||||
|
{item.value}
|
||||||
|
{:else}
|
||||||
|
{@render item.value()}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{#if item.hint}
|
||||||
|
{#if typeof item.hint === 'string'}
|
||||||
|
<span class="truncate text-xs text-muted-foreground">· {item.hint}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="truncate text-xs text-muted-foreground">· {@render item.hint()}</span>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Badge } from '@evobgp/ui/components/badge/index.js';
|
||||||
|
import { cn } from '$lib/utils.js';
|
||||||
|
import type { BadgeVariant } from '@evobgp/ui/components/badge/badge.svelte';
|
||||||
|
|
||||||
|
type SemanticStatus = 'success' | 'warning' | 'info';
|
||||||
|
|
||||||
|
const STATUS_VARIANT: Record<string, BadgeVariant | SemanticStatus> = {
|
||||||
|
active: 'success',
|
||||||
|
ok: 'success',
|
||||||
|
enabled: 'success',
|
||||||
|
paid: 'success',
|
||||||
|
paused: 'secondary',
|
||||||
|
disabled: 'secondary',
|
||||||
|
archived: 'outline',
|
||||||
|
error: 'destructive',
|
||||||
|
failed: 'destructive',
|
||||||
|
running: 'info',
|
||||||
|
warning: 'warning',
|
||||||
|
stale: 'warning',
|
||||||
|
overdue: 'warning'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEMANTIC_CLASS: Record<SemanticStatus, string> = {
|
||||||
|
success: 'border-success/30 bg-success/10 text-success-foreground',
|
||||||
|
warning: 'border-warning/30 bg-warning/10 text-warning-foreground',
|
||||||
|
info: 'border-info/30 bg-info/10 text-info-foreground'
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
status: string;
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { status, label }: Props = $props();
|
||||||
|
|
||||||
|
const mapped = $derived(STATUS_VARIANT[status] ?? 'outline');
|
||||||
|
const isSemantic = $derived(mapped === 'success' || mapped === 'warning' || mapped === 'info');
|
||||||
|
const variant = $derived(isSemantic ? 'outline' : (mapped as BadgeVariant));
|
||||||
|
const text = $derived(label ?? status);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Badge {variant} class={cn(isSemantic ? SEMANTIC_CLASS[mapped as SemanticStatus] : undefined)}>
|
||||||
|
{text}
|
||||||
|
</Badge>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Skeleton } from '@evobgp/ui/components/skeleton/index.js';
|
||||||
|
import * as Table from '@evobgp/ui/components/table/index.js';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
columns?: number;
|
||||||
|
rows?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { columns = 4, rows = 5 }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rounded-md border">
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Body>
|
||||||
|
{#each Array(rows) as _, ri (ri)}
|
||||||
|
<Table.Row>
|
||||||
|
{#each Array(columns) as _, ci (ci)}
|
||||||
|
<Table.Cell>
|
||||||
|
<Skeleton class="h-5 w-full max-w-[10rem]" />
|
||||||
|
</Table.Cell>
|
||||||
|
{/each}
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
+5
-5
@@ -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';
|
||||||
+5
-5
@@ -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';
|
||||||
+5
-5
@@ -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';
|
||||||
+12
-7
@@ -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';
|
||||||
+12
-13
@@ -2,14 +2,15 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
|
import PageShell from '$lib/components/page-shell.svelte';
|
||||||
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
|
import PageHeader from '$lib/ui/app/page-header/page-header.svelte';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '$lib/ui/core/alert/index.js';
|
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert/index.js';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '$lib/ui/core/tabs/index.js';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs/index.js';
|
||||||
import TenantBirdSettingsCard from '$lib/components/tenant-settings/TenantBirdSettingsCard.svelte';
|
import TenantBirdSettingsCard from '$lib/components/tenant-settings/TenantBirdSettingsCard.svelte';
|
||||||
import TenantRevisionSettingsCard from '$lib/components/tenant-settings/TenantRevisionSettingsCard.svelte';
|
import TenantRevisionSettingsCard from '$lib/components/tenant-settings/TenantRevisionSettingsCard.svelte';
|
||||||
import TenantAdditionalSettingsCard from '$lib/components/tenant-settings/TenantAdditionalSettingsCard.svelte';
|
import TenantAdditionalSettingsCard from '$lib/components/tenant-settings/TenantAdditionalSettingsCard.svelte';
|
||||||
import TenantRuntimeLogsSettingsCard from '$lib/components/tenant-settings/TenantRuntimeLogsSettingsCard.svelte';
|
import TenantRuntimeLogsSettingsCard from '$lib/components/tenant-settings/TenantRuntimeLogsSettingsCard.svelte';
|
||||||
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
|
import { LINE_TAB_TRIGGER_CLASS } from '$lib/ui/app/tabs.js';
|
||||||
import Info from '@lucide/svelte/icons/info';
|
import Info from '@lucide/svelte/icons/info';
|
||||||
|
|
||||||
type TenantSettingsTab = 'bird' | 'revision' | 'runtime-logs' | 'additional';
|
type TenantSettingsTab = 'bird' | 'revision' | 'runtime-logs' | 'additional';
|
||||||
@@ -44,12 +45,10 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-6">
|
<PageShell>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Параметры tenant"
|
title="Параметры tenant"
|
||||||
description="Параметры control plane для текущего tenant (API /v1/settings). Токен и тема интерфейса — в разделе «Настройки»."
|
description="Параметры control plane для текущего tenant (API /v1/settings). Токен и тема интерфейса — в разделе «Настройки»."
|
||||||
icon={SlidersHorizontal}
|
|
||||||
iconClass="bg-chart-5/15 text-chart-5"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert class="border-info/30 bg-info/5">
|
<Alert class="border-info/30 bg-info/5">
|
||||||
@@ -62,12 +61,12 @@
|
|||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Tabs bind:value={activeTab}>
|
<Tabs bind:value={activeTab}>
|
||||||
<div class="overflow-x-auto pb-1 [scrollbar-gutter:stable]">
|
<div class="border-b">
|
||||||
<TabsList class="inline-flex min-w-max">
|
<TabsList class="h-auto w-full justify-start rounded-none bg-transparent p-0">
|
||||||
<TabsTrigger value="bird">BIRD</TabsTrigger>
|
<TabsTrigger value="bird" class={LINE_TAB_TRIGGER_CLASS}>BIRD</TabsTrigger>
|
||||||
<TabsTrigger value="revision">Ревизии</TabsTrigger>
|
<TabsTrigger value="revision" class={LINE_TAB_TRIGGER_CLASS}>Ревизии</TabsTrigger>
|
||||||
<TabsTrigger value="runtime-logs">Файловые логи</TabsTrigger>
|
<TabsTrigger value="runtime-logs" class={LINE_TAB_TRIGGER_CLASS}>Файловые логи</TabsTrigger>
|
||||||
<TabsTrigger value="additional">Дополнительно</TabsTrigger>
|
<TabsTrigger value="additional" class={LINE_TAB_TRIGGER_CLASS}>Дополнительно</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -87,4 +86,4 @@
|
|||||||
<TenantAdditionalSettingsCard />
|
<TenantAdditionalSettingsCard />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</PageShell>
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 386 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user