Files
EvoBGP/apps/web/src/queries/overview.ts
T
DenozordecandCursor c144b49acf
CI / changes (push) Successful in 17s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 26s
CI / web (push) Successful in 46s
CI / go (push) Successful in 1m1s
CI / bird2 (push) Successful in 17s
CI / release (push) Failing after 2m22s
feat!(web): migrate UI from SvelteKit to React + shadcn/ui + ReUI
Web UI полностью переведён с SvelteKit на новый стек: React 19,
TanStack Router/Query/Table/Virtual, shadcn/ui (base-nova) и ReUI
enterprise-компоненты (data-grid, filters, autocomplete). Новый код
разложен по слоям: packages/ui (shadcn-примитивы), apps/web
(роуты, shared-обёртки, ReUI-адаптации).

BREAKING CHANGE: меняется структура и инструментинг фронтенда.

- apps/web/ — новый Vite + React-проект (@evobgp/web), file-based
  роуты TanStack Router; экраны dashboard, modules, monitoring,
  network, operations, schedule, settings, tenant-settings, access,
  directories.
- packages/ui/ — shadcn/ui-примитивы (@evobgp/ui) с общими стилями
  globals.css и cn-утилитой; CLI shadcn запускается из apps/web.
- apps/web/src/components/reui/ — enterprise-паттерны ReUI.
- pnpm workspace (pnpm-workspace.yaml, pnpm-lock.yaml, tsconfig.base.json)
  заменяет npm-проект в web/.
- web/ переименован в web-legacy-svelte/ (архив-референс для миграции);
  импорты оттуда запрещены правилом WEB-22.
- CI (.gitea/workflows/ci.yaml): job web переведён на Node 22 + pnpm 10
  (typecheck/lint/build через pnpm --filter @evobgp/web); пути триггеров
  обновлены под apps/web|packages/ui.
- deploy/docker/evobgp-web/Dockerfile: сборка из корня репозитория,
  pnpm install --frozen-lockfile, выход dist из apps/web/dist.
- .cursor/rules/web-shadcn.mdc, context7-stack.mdc, engineering.mdc,
  AGENTS.md — обновлены под React-стек (WEB-01..WEB-22, DOC-SYNC-06/07).

Проверки WEB-19 локально: typecheck, lint, build — exit 0.

Co-authored-by: Cursor <[email protected]>
2026-07-02 17:59:59 +07:00

116 lines
3.2 KiB
TypeScript

import { queryOptions } from '@tanstack/react-query'
import { apiJSON } from '@/lib/api-client'
import type {
JobRow,
JobsResponse,
ModuleRow,
ModulesResponse,
PeerRow,
PeersResponse,
RevisionRow,
RevisionsResponse,
SpeakerRow,
SpeakersResponse,
} from '@/types/api'
export const overviewKeys = {
all: ['overview'] as const,
modules: () => [...overviewKeys.all, 'modules'] as const,
peers: () => [...overviewKeys.all, 'peers'] as const,
speakers: () => [...overviewKeys.all, 'speakers'] as const,
revisions: () => [...overviewKeys.all, 'revisions'] as const,
jobs: () => [...overviewKeys.all, 'jobs'] as const,
health: () => [...overviewKeys.all, 'health'] as const,
}
export function overviewModulesQueryOptions() {
return queryOptions<ModulesResponse>({
queryKey: overviewKeys.modules(),
queryFn: () => apiJSON<ModulesResponse>('/v1/modules?limit=200'),
staleTime: 60_000,
})
}
export function overviewPeersQueryOptions() {
return queryOptions<PeersResponse>({
queryKey: overviewKeys.peers(),
queryFn: () => apiJSON<PeersResponse>('/v1/peers?limit=200&live=1'),
staleTime: 30_000,
})
}
export function overviewSpeakersQueryOptions() {
return queryOptions<SpeakersResponse>({
queryKey: overviewKeys.speakers(),
queryFn: () => apiJSON<SpeakersResponse>('/v1/speakers?limit=200&live=1'),
staleTime: 30_000,
})
}
export function overviewRevisionsQueryOptions() {
return queryOptions<RevisionsResponse>({
queryKey: overviewKeys.revisions(),
queryFn: () => apiJSON<RevisionsResponse>('/v1/revisions?limit=10'),
staleTime: 60_000,
})
}
export function overviewJobsQueryOptions() {
return queryOptions<JobsResponse>({
queryKey: overviewKeys.jobs(),
queryFn: () => apiJSON<JobsResponse>('/v1/jobs?limit=10'),
staleTime: 15_000,
})
}
export function overviewHealthQueryOptions() {
return queryOptions<boolean>({
queryKey: overviewKeys.health(),
queryFn: async () => {
const res = await fetch('/v1/health')
return res.ok
},
staleTime: 30_000,
})
}
// Selectors / helpers
export type NetworkMetrics = {
peersTotal: number
peersEnabled: number
peersEstablished: number
peersMismatch: number
speakersTotal: number
speakersOnline: number
}
export function aggregateNetworkMetrics(
peers: PeerRow[],
speakers: SpeakerRow[],
): NetworkMetrics {
const peersEnabled = peers.filter((p) => p.enabled !== false).length
const peersEstablished = peers.filter((p) => p.session_state === 'Established').length
const peersMismatch = peers.filter((p) => p.session_mismatch).length
const speakersOnline = speakers.filter((s) => s.live?.agent_ok).length
return {
peersTotal: peers.length,
peersEnabled,
peersEstablished,
peersMismatch,
speakersTotal: speakers.length,
speakersOnline,
}
}
export function runningJobCount(jobs: JobRow[]): number {
return jobs.filter((j) => j.status === 'running' || j.status === 'queued').length
}
export function moduleNameById(modules: ModuleRow[]): Map<string, string> {
return new Map(modules.map((m) => [m.id, m.name]))
}
export function recentRevisions(revisions: RevisionRow[], n = 10): RevisionRow[] {
return revisions.slice(0, n)
}