fix(web): выровнять sheets и header chrome с CFDM/EvoBGP
Убрать растягивание полей в sheet (auto-rows-min + Field), footer в ряд; SystemMonitor pill; дашборд без DataGrid. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMemo, useState } from 'react'
|
||||
import type { ColumnDef } from '@tanstack/react-table'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
ServerIcon,
|
||||
BanIcon,
|
||||
@@ -23,32 +22,46 @@ import {
|
||||
FrameTitle,
|
||||
} from '@/components/reui/frame'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemGroup,
|
||||
ItemTitle,
|
||||
} from '@evofw/ui/components/item'
|
||||
import {
|
||||
dashboardQueryOptions,
|
||||
agentsQueryOptions,
|
||||
recentStatsQueryOptions,
|
||||
} from '@/queries'
|
||||
import type { Agent } from '@evofw/shared'
|
||||
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'
|
||||
|
||||
export const Route = createFileRoute('/_auth/')({
|
||||
component: DashboardPage,
|
||||
})
|
||||
|
||||
const EMPTY_AGENTS: Agent[] = []
|
||||
|
||||
function DashboardPage() {
|
||||
const dash = useQuery(dashboardQueryOptions())
|
||||
const agents = useQuery(agentsQueryOptions())
|
||||
const stats = useQuery(recentStatsQueryOptions())
|
||||
|
||||
const d = dash.data
|
||||
const items = agents.data?.items ?? []
|
||||
const pending = items.filter((a) => a.status === 'pending')
|
||||
const applyErrors = items.filter((a) => a.last_apply_error)
|
||||
const items = agents.data?.items ?? EMPTY_AGENTS
|
||||
|
||||
const pending = useMemo(
|
||||
() => items.filter((a) => a.status === 'pending'),
|
||||
[items],
|
||||
)
|
||||
const applyErrors = useMemo(
|
||||
() => items.filter((a) => a.last_apply_error),
|
||||
[items],
|
||||
)
|
||||
const previewAgents = useMemo(() => items.slice(0, 8), [items])
|
||||
const samples = useMemo(
|
||||
() => (stats.data?.items ?? []).slice(0, 10),
|
||||
[stats.data?.items],
|
||||
)
|
||||
|
||||
const kpiCards: KpiStatCard[] = useMemo(
|
||||
() => [
|
||||
@@ -92,93 +105,40 @@ function DashboardPage() {
|
||||
[d],
|
||||
)
|
||||
|
||||
const quickActions: QuickActionItem[] = [
|
||||
{
|
||||
id: 'agents',
|
||||
title: 'Агенты',
|
||||
description: 'Enroll и approve',
|
||||
to: '/agents',
|
||||
icon: <ServerIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
},
|
||||
{
|
||||
id: 'lists',
|
||||
title: 'Списки',
|
||||
description: 'Blocklists / sources',
|
||||
to: '/lists',
|
||||
icon: <ListIcon aria-hidden />,
|
||||
iconClassName: 'text-primary',
|
||||
},
|
||||
{
|
||||
id: 'rules',
|
||||
title: 'Правила',
|
||||
description: 'Allow / deny policy',
|
||||
to: '/rules',
|
||||
icon: <BanIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
},
|
||||
]
|
||||
|
||||
const agentColumns: ColumnDef<Agent>[] = useMemo(
|
||||
const quickActions: QuickActionItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Имя',
|
||||
cell: ({ row }) => (
|
||||
<Link
|
||||
to="/agents/$id"
|
||||
params={{ id: row.original.id }}
|
||||
className="font-medium underline-offset-4 hover:underline"
|
||||
>
|
||||
{row.original.name}
|
||||
</Link>
|
||||
),
|
||||
id: 'agents',
|
||||
title: 'Агенты',
|
||||
description: 'Enroll и approve',
|
||||
to: '/agents',
|
||||
icon: <ServerIcon aria-hidden />,
|
||||
iconClassName: 'text-info',
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Статус',
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
variant={
|
||||
row.original.status === 'approved'
|
||||
? 'success-light'
|
||||
: row.original.status === 'pending'
|
||||
? 'warning-light'
|
||||
: 'secondary'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
),
|
||||
id: 'lists',
|
||||
title: 'Списки',
|
||||
description: 'Blocklists / sources',
|
||||
to: '/lists',
|
||||
icon: <ListIcon aria-hidden />,
|
||||
iconClassName: 'text-primary',
|
||||
},
|
||||
{ accessorKey: 'policy_mode', header: 'Режим' },
|
||||
{
|
||||
accessorKey: 'last_apply_packets_dropped',
|
||||
header: 'Dropped',
|
||||
cell: ({ row }) => (
|
||||
<span className="tabular-nums">
|
||||
{row.original.last_apply_packets_dropped ?? 0}
|
||||
</span>
|
||||
),
|
||||
id: 'rules',
|
||||
title: 'Правила',
|
||||
description: 'Allow / deny policy',
|
||||
to: '/rules',
|
||||
icon: <BanIcon aria-hidden />,
|
||||
iconClassName: 'text-warning',
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
const agentsTable = useReactTable({
|
||||
data: items.slice(0, 8),
|
||||
columns: agentColumns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getRowId: (r) => r.id,
|
||||
})
|
||||
|
||||
const samples = (stats.data?.items ?? []).slice(0, 10)
|
||||
|
||||
return (
|
||||
<PageShell>
|
||||
<PageHeader
|
||||
title="Дашборд"
|
||||
title="Панель управления"
|
||||
description="Обзор агентов и пакетной статистики"
|
||||
/>
|
||||
<OpsDashboard
|
||||
@@ -191,10 +151,45 @@ function DashboardPage() {
|
||||
<FrameHeader>
|
||||
<FrameTitle>Агенты</FrameTitle>
|
||||
</FrameHeader>
|
||||
<FramePanel className="p-0">
|
||||
<DataGrid table={agentsTable} recordCount={items.length}>
|
||||
<DataGridTable />
|
||||
</DataGrid>
|
||||
<FramePanel>
|
||||
{previewAgents.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">Нет агентов</p>
|
||||
) : (
|
||||
<ItemGroup className="gap-2">
|
||||
{previewAgents.map((a) => (
|
||||
<Item key={a.id} variant="outline" size="sm">
|
||||
<ItemContent className="flex flex-row items-center justify-between gap-2">
|
||||
<ItemTitle className="truncate font-medium">
|
||||
<Link
|
||||
to="/agents/$id"
|
||||
params={{ id: a.id }}
|
||||
className="hover:underline"
|
||||
>
|
||||
{a.name}
|
||||
</Link>
|
||||
</ItemTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant={
|
||||
a.status === 'approved'
|
||||
? 'success-light'
|
||||
: a.status === 'pending'
|
||||
? 'warning-light'
|
||||
: 'secondary'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{a.status}
|
||||
</Badge>
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
↓{a.last_apply_packets_dropped ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
))}
|
||||
</ItemGroup>
|
||||
)}
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
<Frame dense spacing="sm">
|
||||
@@ -252,7 +247,10 @@ function DashboardPage() {
|
||||
{applyErrors.map((a) => (
|
||||
<div key={`err-${a.id}`} className="text-sm">
|
||||
<span className="text-destructive font-medium">{a.name}</span>
|
||||
<span className="text-muted-foreground"> — {a.last_apply_error}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{' '}
|
||||
— {a.last_apply_error}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,8 @@ import { PageHeader, PageShell, ResourcePage } from '@/components/reui-kit'
|
||||
import { listsQueryOptions } from '@/queries'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import { Button } from '@evofw/ui/components/button'
|
||||
import { Field, FieldLabel } from '@evofw/ui/components/field'
|
||||
import { Input } from '@evofw/ui/components/input'
|
||||
import { Label } from '@evofw/ui/components/label'
|
||||
import { Textarea } from '@evofw/ui/components/textarea'
|
||||
import {
|
||||
Select,
|
||||
@@ -205,18 +205,22 @@ function ListsPage() {
|
||||
/>
|
||||
|
||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
<SheetContent className="flex flex-col gap-4 sm:max-w-md">
|
||||
<SheetHeader>
|
||||
<SheetContent className="flex flex-col gap-0 overflow-hidden sm:max-w-md">
|
||||
<SheetHeader className="shrink-0">
|
||||
<SheetTitle>Новый список</SheetTitle>
|
||||
<SheetDescription>Источник префиксов для правил</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-1 flex-col gap-3 overflow-y-auto px-1">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Имя</Label>
|
||||
<Input value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Тип</Label>
|
||||
<div className="grid flex-1 auto-rows-min gap-4 overflow-y-auto px-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="list-name">Имя</FieldLabel>
|
||||
<Input
|
||||
id="list-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Тип</FieldLabel>
|
||||
<Select
|
||||
value={type}
|
||||
onValueChange={(v) => setType(v as typeof type)}
|
||||
@@ -233,9 +237,9 @@ function ListsPage() {
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="list-extra">
|
||||
{type === 'static'
|
||||
? 'CIDR (через пробел/запятую)'
|
||||
: type === 'json_url'
|
||||
@@ -243,15 +247,16 @@ function ListsPage() {
|
||||
: type === 'domains'
|
||||
? 'Домены'
|
||||
: 'Community ID'}
|
||||
</Label>
|
||||
</FieldLabel>
|
||||
<Textarea
|
||||
id="list-extra"
|
||||
value={extra}
|
||||
onChange={(e) => setExtra(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<SheetFooter className="mt-0 shrink-0 flex-row flex-wrap gap-2 border-t">
|
||||
<Button variant="outline" onClick={() => setSheetOpen(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
} from '@/queries'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import { Button } from '@evofw/ui/components/button'
|
||||
import { Field, FieldLabel } from '@evofw/ui/components/field'
|
||||
import { Input } from '@evofw/ui/components/input'
|
||||
import { Label } from '@evofw/ui/components/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -183,21 +183,22 @@ function RulesPage() {
|
||||
/>
|
||||
|
||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
<SheetContent className="flex flex-col gap-4 sm:max-w-md">
|
||||
<SheetHeader>
|
||||
<SheetContent className="flex flex-col gap-0 overflow-hidden sm:max-w-md">
|
||||
<SheetHeader className="shrink-0">
|
||||
<SheetTitle>Новое правило</SheetTitle>
|
||||
<SheetDescription>Priority + action + list scope</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="grid flex-1 gap-3 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Priority</Label>
|
||||
<div className="grid flex-1 auto-rows-min gap-4 overflow-y-auto px-4 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rule-priority">Priority</FieldLabel>
|
||||
<Input
|
||||
id="rule-priority"
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label>Action</Label>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Action</FieldLabel>
|
||||
<Select
|
||||
value={action}
|
||||
onValueChange={(v) => setAction(v as 'allow' | 'deny')}
|
||||
@@ -210,14 +211,14 @@ function RulesPage() {
|
||||
<SelectItem value="allow">allow</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:col-span-2">
|
||||
<Label>Список</Label>
|
||||
</Field>
|
||||
<Field className="sm:col-span-2">
|
||||
<FieldLabel>Список</FieldLabel>
|
||||
<Select
|
||||
value={listId || null}
|
||||
onValueChange={(v) => setListId(v ?? '')}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="IP list" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -228,16 +229,16 @@ function RulesPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:col-span-2">
|
||||
<Label>Scope</Label>
|
||||
</Field>
|
||||
<Field className="sm:col-span-2">
|
||||
<FieldLabel>Scope</FieldLabel>
|
||||
<Select
|
||||
value={agentId}
|
||||
onValueChange={(v) => {
|
||||
if (v) setAgentId(v)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -249,9 +250,9 @@ function RulesPage() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<SheetFooter className="mt-0 shrink-0 flex-row flex-wrap gap-2 border-t">
|
||||
<Button variant="outline" onClick={() => setSheetOpen(false)}>
|
||||
Отмена
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user