diff --git a/apps/web/src/components/data-grid-cell.tsx b/apps/web/src/components/data-grid-cell.tsx
new file mode 100644
index 0000000..5a3d23f
--- /dev/null
+++ b/apps/web/src/components/data-grid-cell.tsx
@@ -0,0 +1,46 @@
+import type { ReactNode } from 'react'
+
+import { cn } from '@evofw/ui/lib/utils'
+
+const ACCENT_CLASS = {
+ primary: 'font-medium text-primary',
+ default: 'font-medium text-foreground',
+ mono: 'font-mono text-sm text-primary',
+} as const
+
+export function DataGridPrimaryCell({
+ title,
+ subtitle,
+ accent = 'default',
+ className,
+}: {
+ title: ReactNode
+ subtitle?: ReactNode
+ accent?: keyof typeof ACCENT_CLASS
+ className?: string
+}) {
+ return (
+
+ {title}
+ {subtitle ? (
+ {subtitle}
+ ) : null}
+
+ )
+}
+
+export function DataGridMutedCell({
+ children,
+ className,
+}: {
+ children: ReactNode
+ className?: string
+}) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/apps/web/src/components/status-badge.tsx b/apps/web/src/components/status-badge.tsx
index 2d3c6d3..fe09ded 100644
--- a/apps/web/src/components/status-badge.tsx
+++ b/apps/web/src/components/status-badge.tsx
@@ -11,13 +11,24 @@ const STATUS_VARIANT: Record = {
synced: 'success-light',
ok: 'success-light',
up: 'success-light',
+ enabled: 'success-light',
+ approved: 'success-light',
+ allow: 'success-light',
pending_push: 'secondary',
+ pending: 'warning-light',
warning: 'warning-light',
degraded: 'warning-light',
conflict: 'destructive-light',
error: 'destructive-light',
expired: 'destructive-light',
down: 'destructive-light',
+ deny: 'destructive-light',
+ revoked: 'secondary',
+ disabled: 'secondary',
+ static: 'secondary',
+ domains: 'info-light',
+ json_url: 'info-light',
+ evobgp_community: 'info-light',
unknown: 'outline',
}
@@ -37,6 +48,13 @@ const STATUS_LABELS: Record = {
active: 'Активен',
synced: 'Синхронизировано',
pending_push: 'Ожидает отправки',
+ pending: 'Pending',
+ approved: 'Approved',
+ revoked: 'Revoked',
+ enabled: 'Включён',
+ disabled: 'Выключен',
+ allow: 'allow',
+ deny: 'deny',
conflict: 'Конфликт',
error: 'Ошибка',
ok: 'OK',
@@ -45,6 +63,10 @@ const STATUS_LABELS: Record = {
degraded: 'Slow',
down: 'Down',
expired: 'Истёк',
+ static: 'static',
+ domains: 'domains',
+ json_url: 'json_url',
+ evobgp_community: 'evobgp_community',
unknown: 'Неизвестно',
}
@@ -60,8 +82,16 @@ export function StatusBadge({
const variant = STATUS_VARIANT[status] ?? 'outline'
const dotColor = DOT_COLOR[variant] ?? 'bg-muted-foreground'
return (
-
-
+
+
{label ?? STATUS_LABELS[status] ?? status}
)
diff --git a/apps/web/src/queries/index.ts b/apps/web/src/queries/index.ts
index 6ba2fd2..29408b7 100644
--- a/apps/web/src/queries/index.ts
+++ b/apps/web/src/queries/index.ts
@@ -32,6 +32,17 @@ export const listsQueryOptions = () =>
queryFn: () => apiFetch<{ items: IpList[] }>('/api/v1/lists'),
})
+export const listQueryOptions = (id: string) =>
+ queryOptions({
+ queryKey: ['lists', id],
+ queryFn: () =>
+ apiFetch<
+ IpList & {
+ entries: string[]
+ }
+ >(`/api/v1/lists/${id}`),
+ })
+
export const policySetsQueryOptions = () =>
queryOptions({
queryKey: ['policy-sets'],
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts
index a1f71e4..18cde6e 100644
--- a/apps/web/src/routeTree.gen.ts
+++ b/apps/web/src/routeTree.gen.ts
@@ -11,14 +11,15 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as AuthRouteImport } from './routes/_auth'
import { Route as AuthIndexRouteImport } from './routes/_auth/index'
-import { Route as AuthAgentsRouteImport } from './routes/_auth/agents'
-import { Route as AuthListsRouteImport } from './routes/_auth/lists'
-import { Route as AuthRulesRouteImport } from './routes/_auth/rules'
import { Route as AuthSettingsRouteImport } from './routes/_auth/settings'
import { Route as AuthStatsRouteImport } from './routes/_auth/stats'
import { Route as AuthCallbackRouteImport } from './routes/auth.callback'
-import { Route as AuthAgentsIdRouteImport } from './routes/_auth/agents.$id'
-import { Route as AuthRulesSetIdRouteImport } from './routes/_auth/rules.$setId'
+import { Route as AuthAgentsIndexRouteImport } from './routes/_auth/agents/index'
+import { Route as AuthAgentsIdRouteImport } from './routes/_auth/agents/$id'
+import { Route as AuthListsIndexRouteImport } from './routes/_auth/lists/index'
+import { Route as AuthListsIdRouteImport } from './routes/_auth/lists/$id'
+import { Route as AuthRulesIndexRouteImport } from './routes/_auth/rules/index'
+import { Route as AuthRulesSetIdRouteImport } from './routes/_auth/rules/$setId'
const AuthRoute = AuthRouteImport.update({
id: '/_auth',
@@ -29,21 +30,6 @@ const AuthIndexRoute = AuthIndexRouteImport.update({
path: '/',
getParentRoute: () => AuthRoute,
} as any)
-const AuthAgentsRoute = AuthAgentsRouteImport.update({
- id: '/agents',
- path: '/agents',
- getParentRoute: () => AuthRoute,
-} as any)
-const AuthListsRoute = AuthListsRouteImport.update({
- id: '/lists',
- path: '/lists',
- getParentRoute: () => AuthRoute,
-} as any)
-const AuthRulesRoute = AuthRulesRouteImport.update({
- id: '/rules',
- path: '/rules',
- getParentRoute: () => AuthRoute,
-} as any)
const AuthSettingsRoute = AuthSettingsRouteImport.update({
id: '/settings',
path: '/settings',
@@ -59,87 +45,113 @@ const AuthCallbackRoute = AuthCallbackRouteImport.update({
path: '/auth/callback',
getParentRoute: () => rootRouteImport,
} as any)
+const AuthAgentsIndexRoute = AuthAgentsIndexRouteImport.update({
+ id: '/agents/',
+ path: '/agents/',
+ getParentRoute: () => AuthRoute,
+} as any)
const AuthAgentsIdRoute = AuthAgentsIdRouteImport.update({
- id: '/$id',
- path: '/$id',
- getParentRoute: () => AuthAgentsRoute,
+ id: '/agents/$id',
+ path: '/agents/$id',
+ getParentRoute: () => AuthRoute,
+} as any)
+const AuthListsIndexRoute = AuthListsIndexRouteImport.update({
+ id: '/lists/',
+ path: '/lists/',
+ getParentRoute: () => AuthRoute,
+} as any)
+const AuthListsIdRoute = AuthListsIdRouteImport.update({
+ id: '/lists/$id',
+ path: '/lists/$id',
+ getParentRoute: () => AuthRoute,
+} as any)
+const AuthRulesIndexRoute = AuthRulesIndexRouteImport.update({
+ id: '/rules/',
+ path: '/rules/',
+ getParentRoute: () => AuthRoute,
} as any)
const AuthRulesSetIdRoute = AuthRulesSetIdRouteImport.update({
- id: '/$setId',
- path: '/$setId',
- getParentRoute: () => AuthRulesRoute,
+ id: '/rules/$setId',
+ path: '/rules/$setId',
+ getParentRoute: () => AuthRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof AuthIndexRoute
- '/agents': typeof AuthAgentsRouteWithChildren
- '/lists': typeof AuthListsRoute
- '/rules': typeof AuthRulesRouteWithChildren
'/settings': typeof AuthSettingsRoute
'/stats': typeof AuthStatsRoute
'/auth/callback': typeof AuthCallbackRoute
'/agents/$id': typeof AuthAgentsIdRoute
+ '/lists/$id': typeof AuthListsIdRoute
'/rules/$setId': typeof AuthRulesSetIdRoute
+ '/agents/': typeof AuthAgentsIndexRoute
+ '/lists/': typeof AuthListsIndexRoute
+ '/rules/': typeof AuthRulesIndexRoute
}
export interface FileRoutesByTo {
- '/agents': typeof AuthAgentsRouteWithChildren
- '/lists': typeof AuthListsRoute
- '/rules': typeof AuthRulesRouteWithChildren
'/settings': typeof AuthSettingsRoute
'/stats': typeof AuthStatsRoute
'/auth/callback': typeof AuthCallbackRoute
'/': typeof AuthIndexRoute
'/agents/$id': typeof AuthAgentsIdRoute
+ '/lists/$id': typeof AuthListsIdRoute
'/rules/$setId': typeof AuthRulesSetIdRoute
+ '/agents': typeof AuthAgentsIndexRoute
+ '/lists': typeof AuthListsIndexRoute
+ '/rules': typeof AuthRulesIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/_auth': typeof AuthRouteWithChildren
- '/_auth/agents': typeof AuthAgentsRouteWithChildren
- '/_auth/lists': typeof AuthListsRoute
- '/_auth/rules': typeof AuthRulesRouteWithChildren
'/_auth/settings': typeof AuthSettingsRoute
'/_auth/stats': typeof AuthStatsRoute
'/auth/callback': typeof AuthCallbackRoute
'/_auth/': typeof AuthIndexRoute
'/_auth/agents/$id': typeof AuthAgentsIdRoute
+ '/_auth/lists/$id': typeof AuthListsIdRoute
'/_auth/rules/$setId': typeof AuthRulesSetIdRoute
+ '/_auth/agents/': typeof AuthAgentsIndexRoute
+ '/_auth/lists/': typeof AuthListsIndexRoute
+ '/_auth/rules/': typeof AuthRulesIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
- | '/agents'
- | '/lists'
- | '/rules'
| '/settings'
| '/stats'
| '/auth/callback'
| '/agents/$id'
+ | '/lists/$id'
| '/rules/$setId'
+ | '/agents/'
+ | '/lists/'
+ | '/rules/'
fileRoutesByTo: FileRoutesByTo
to:
- | '/agents'
- | '/lists'
- | '/rules'
| '/settings'
| '/stats'
| '/auth/callback'
| '/'
| '/agents/$id'
+ | '/lists/$id'
| '/rules/$setId'
+ | '/agents'
+ | '/lists'
+ | '/rules'
id:
| '__root__'
| '/_auth'
- | '/_auth/agents'
- | '/_auth/lists'
- | '/_auth/rules'
| '/_auth/settings'
| '/_auth/stats'
| '/auth/callback'
| '/_auth/'
| '/_auth/agents/$id'
+ | '/_auth/lists/$id'
| '/_auth/rules/$setId'
+ | '/_auth/agents/'
+ | '/_auth/lists/'
+ | '/_auth/rules/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -163,27 +175,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthIndexRouteImport
parentRoute: typeof AuthRoute
}
- '/_auth/agents': {
- id: '/_auth/agents'
- path: '/agents'
- fullPath: '/agents'
- preLoaderRoute: typeof AuthAgentsRouteImport
- parentRoute: typeof AuthRoute
- }
- '/_auth/lists': {
- id: '/_auth/lists'
- path: '/lists'
- fullPath: '/lists'
- preLoaderRoute: typeof AuthListsRouteImport
- parentRoute: typeof AuthRoute
- }
- '/_auth/rules': {
- id: '/_auth/rules'
- path: '/rules'
- fullPath: '/rules'
- preLoaderRoute: typeof AuthRulesRouteImport
- parentRoute: typeof AuthRoute
- }
'/_auth/settings': {
id: '/_auth/settings'
path: '/settings'
@@ -205,63 +196,73 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthCallbackRouteImport
parentRoute: typeof rootRouteImport
}
+ '/_auth/agents/': {
+ id: '/_auth/agents/'
+ path: '/agents'
+ fullPath: '/agents/'
+ preLoaderRoute: typeof AuthAgentsIndexRouteImport
+ parentRoute: typeof AuthRoute
+ }
'/_auth/agents/$id': {
id: '/_auth/agents/$id'
- path: '/$id'
+ path: '/agents/$id'
fullPath: '/agents/$id'
preLoaderRoute: typeof AuthAgentsIdRouteImport
- parentRoute: typeof AuthAgentsRoute
+ parentRoute: typeof AuthRoute
+ }
+ '/_auth/lists/': {
+ id: '/_auth/lists/'
+ path: '/lists'
+ fullPath: '/lists/'
+ preLoaderRoute: typeof AuthListsIndexRouteImport
+ parentRoute: typeof AuthRoute
+ }
+ '/_auth/lists/$id': {
+ id: '/_auth/lists/$id'
+ path: '/lists/$id'
+ fullPath: '/lists/$id'
+ preLoaderRoute: typeof AuthListsIdRouteImport
+ parentRoute: typeof AuthRoute
+ }
+ '/_auth/rules/': {
+ id: '/_auth/rules/'
+ path: '/rules'
+ fullPath: '/rules/'
+ preLoaderRoute: typeof AuthRulesIndexRouteImport
+ parentRoute: typeof AuthRoute
}
'/_auth/rules/$setId': {
id: '/_auth/rules/$setId'
- path: '/$setId'
+ path: '/rules/$setId'
fullPath: '/rules/$setId'
preLoaderRoute: typeof AuthRulesSetIdRouteImport
- parentRoute: typeof AuthRulesRoute
+ parentRoute: typeof AuthRoute
}
}
}
-interface AuthAgentsRouteChildren {
- AuthAgentsIdRoute: typeof AuthAgentsIdRoute
-}
-
-const AuthAgentsRouteChildren: AuthAgentsRouteChildren = {
- AuthAgentsIdRoute: AuthAgentsIdRoute,
-}
-
-const AuthAgentsRouteWithChildren = AuthAgentsRoute._addFileChildren(
- AuthAgentsRouteChildren,
-)
-
-interface AuthRulesRouteChildren {
- AuthRulesSetIdRoute: typeof AuthRulesSetIdRoute
-}
-
-const AuthRulesRouteChildren: AuthRulesRouteChildren = {
- AuthRulesSetIdRoute: AuthRulesSetIdRoute,
-}
-
-const AuthRulesRouteWithChildren = AuthRulesRoute._addFileChildren(
- AuthRulesRouteChildren,
-)
-
interface AuthRouteChildren {
- AuthAgentsRoute: typeof AuthAgentsRouteWithChildren
- AuthListsRoute: typeof AuthListsRoute
- AuthRulesRoute: typeof AuthRulesRouteWithChildren
AuthSettingsRoute: typeof AuthSettingsRoute
AuthStatsRoute: typeof AuthStatsRoute
AuthIndexRoute: typeof AuthIndexRoute
+ AuthAgentsIdRoute: typeof AuthAgentsIdRoute
+ AuthListsIdRoute: typeof AuthListsIdRoute
+ AuthRulesSetIdRoute: typeof AuthRulesSetIdRoute
+ AuthAgentsIndexRoute: typeof AuthAgentsIndexRoute
+ AuthListsIndexRoute: typeof AuthListsIndexRoute
+ AuthRulesIndexRoute: typeof AuthRulesIndexRoute
}
const AuthRouteChildren: AuthRouteChildren = {
- AuthAgentsRoute: AuthAgentsRouteWithChildren,
- AuthListsRoute: AuthListsRoute,
- AuthRulesRoute: AuthRulesRouteWithChildren,
AuthSettingsRoute: AuthSettingsRoute,
AuthStatsRoute: AuthStatsRoute,
AuthIndexRoute: AuthIndexRoute,
+ AuthAgentsIdRoute: AuthAgentsIdRoute,
+ AuthListsIdRoute: AuthListsIdRoute,
+ AuthRulesSetIdRoute: AuthRulesSetIdRoute,
+ AuthAgentsIndexRoute: AuthAgentsIndexRoute,
+ AuthListsIndexRoute: AuthListsIndexRoute,
+ AuthRulesIndexRoute: AuthRulesIndexRoute,
}
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
diff --git a/apps/web/src/routes/_auth/agents.$id.tsx b/apps/web/src/routes/_auth/agents/$id.tsx
similarity index 74%
rename from apps/web/src/routes/_auth/agents.$id.tsx
rename to apps/web/src/routes/_auth/agents/$id.tsx
index 3b63098..cdea734 100644
--- a/apps/web/src/routes/_auth/agents.$id.tsx
+++ b/apps/web/src/routes/_auth/agents/$id.tsx
@@ -1,14 +1,15 @@
import { createFileRoute, Link } from '@tanstack/react-router'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
-import { useEffect, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
+import type { ColumnDef } from '@tanstack/react-table'
import {
BanIcon,
CheckCircle2Icon,
CpuIcon,
ClockIcon,
} from 'lucide-react'
-import { PageHeader, PageShell, DetailPanel } from '@/components/reui-kit'
+import { PageShell, DetailPanel } from '@/components/reui-kit'
import {
Frame,
FrameDescription,
@@ -16,7 +17,12 @@ import {
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
-import { Badge } from '@/components/reui/badge'
+import { StatusBadge } from '@/components/status-badge'
+import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
+import { DataGridPrimaryCell } from '@/components/data-grid-cell'
+import { DataGrid } from '@/components/reui/data-grid/data-grid'
+import { DataGridTable } from '@/components/reui/data-grid/data-grid-table'
+import { getCoreRowModel, useReactTable } from '@tanstack/react-table'
import {
agentQueryOptions,
agentsQueryOptions,
@@ -36,6 +42,7 @@ import {
SelectValue,
} from '@evofw/ui/components/select'
import { Skeleton } from '@evofw/ui/components/skeleton'
+import type { PolicySet } from '@evofw/shared'
export const Route = createFileRoute('/_auth/agents/$id')({
component: AgentDetailPage,
@@ -115,11 +122,81 @@ function AgentDetailPage() {
onError: (e: Error) => toast.error(e.message),
})
+ const setColumns: ColumnDef[] = useMemo(
+ () => [
+ {
+ id: 'select',
+ enableSorting: false,
+ header: () => Выбор,
+ cell: ({ row }) => (
+ {
+ setSelectedSets(
+ v
+ ? [...assignedIds, row.original.id]
+ : assignedIds.filter((x) => x !== row.original.id),
+ )
+ }}
+ aria-label={`Назначить ${row.original.name}`}
+ />
+ ),
+ },
+ {
+ accessorKey: 'name',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+ e.stopPropagation()}
+ >
+
+
+ ),
+ },
+ {
+ accessorKey: 'enabled',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ ),
+ },
+ {
+ accessorKey: 'rules_count',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+ {row.original.rules_count ?? 0}
+ ),
+ },
+ ],
+ [assignedIds],
+ )
+
+ const setsTable = useReactTable({
+ data: setsQ.data?.items ?? [],
+ columns: setColumns,
+ getCoreRowModel: getCoreRowModel(),
+ getRowId: (r) => r.id,
+ })
+
const a = agentQ.data
if (agentQ.isLoading || !a) {
return (
-
)
@@ -133,23 +210,14 @@ function AgentDetailPage() {
description={`${a.platform} · gen ${a.policy_generation}`}
actions={
-
+ }
>
- {a.status}
-
-
-
-
+ К списку
+
}
/>
@@ -213,57 +281,31 @@ function AgentDetailPage() {
-
+
Наборы правил
Можно назначить несколько — мержатся при sync
-
-
- {(setsQ.data?.items ?? []).map((s) => {
- const checked = assignedIds.includes(s.id)
- return (
-
- )
- })}
-
+
+
+
+
+
+
-
+
@@ -301,6 +343,7 @@ function AgentDetailPage() {