feat(web): Lists/Rules UX parity с Агентами и правило kpi-hybrid
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m50s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

Hybrid row icons, row click, icon actions; DetailPanel header; ToggleGroup режима фильтра (settings-3).

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-07-21 02:36:09 +07:00
co-authored by Cursor
parent 90d50c2556
commit 41e4ca1410
12 changed files with 432 additions and 171 deletions
@@ -0,0 +1,51 @@
import { Globe, List, Network } from 'lucide-react'
import { cn } from '@evofw/ui/lib/utils'
import { Item, ItemMedia } from '@evofw/ui/components/item'
import { isManualListType } from '@evofw/shared'
type ListTypeIconProps = {
type: string
className?: string
}
/**
* KPI-style type tile for list rows.
* Preview DNA: https://reui.io/preview/base/stats-12
*/
export function ListTypeIcon({ type, className }: ListTypeIconProps) {
const kind = isManualListType(type)
? 'static'
: type === 'evobgp_community'
? 'evobgp_community'
: 'json_url'
const Icon =
kind === 'evobgp_community' ? Network : kind === 'json_url' ? Globe : List
const color =
kind === 'evobgp_community'
? 'text-info'
: kind === 'json_url'
? 'text-warning'
: 'text-muted-foreground'
return (
<Item
className={cn(
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
color,
className,
)}
aria-label={
kind === 'evobgp_community'
? 'EvoBGP'
: kind === 'json_url'
? 'JSON URL'
: 'Ручной'
}
>
<ItemMedia variant="icon" className="size-auto">
<Icon aria-hidden />
</ItemMedia>
</Item>
)
}
@@ -1,6 +1,5 @@
import { Link } from '@tanstack/react-router'
import type { ColumnDef } from '@tanstack/react-table'
import { ListIcon, RefreshCwIcon, Trash2 } from 'lucide-react'
import { RefreshCwIcon, Trash2 } from 'lucide-react'
import type { FilterFieldConfig } from '@/components/reui/filters'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import {
@@ -8,6 +7,7 @@ import {
DataGridPrimaryCell,
} from '@/components/data-grid-cell'
import { StatusBadge } from '@/components/status-badge'
import { ListTypeIcon } from '@/components/lists/list-type-icon'
import { Button } from '@evofw/ui/components/button'
import { isManualListType, type IpList } from '@evofw/shared'
@@ -58,15 +58,8 @@ export function createListColumns(opts: {
<DataGridColumnHeader column={column} title="Список" />
),
cell: ({ row }) => (
<Link
to="/lists/$id"
params={{ id: row.original.id }}
className="flex min-w-0 items-center gap-2"
>
<ListIcon
className="text-muted-foreground size-4 shrink-0"
aria-hidden
/>
<div className="flex min-w-0 items-center gap-3">
<ListTypeIcon type={row.original.type} />
<DataGridPrimaryCell
accent="primary"
title={row.original.name}
@@ -78,7 +71,7 @@ export function createListColumns(opts: {
: undefined
}
/>
</Link>
</div>
),
meta: { headerTitle: 'Список' },
},
@@ -137,15 +130,6 @@ export function createListColumns(opts: {
}
/>
</Button>
<Button
size="sm"
variant="outline"
render={
<Link to="/lists/$id" params={{ id: row.original.id }} />
}
>
Открыть
</Button>
<Button
size="icon-sm"
variant="ghost"
@@ -0,0 +1,69 @@
import { cn } from '@evofw/ui/lib/utils'
import {
ToggleGroup,
ToggleGroupItem,
} from '@evofw/ui/components/toggle-group'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
type PolicyMode = 'blacklist' | 'whitelist'
type PolicyModeToggleProps = {
value: PolicyMode
onChange: (mode: PolicyMode) => void
disabled?: boolean
className?: string
}
/**
* Filter mode — settings-3 ToggleGroup pattern.
* Preview: https://reui.io/preview/base/settings-3
* Docs: https://ui.shadcn.com/docs/components/base/toggle-group
*/
export function PolicyModeToggle({
value,
onChange,
disabled,
className,
}: PolicyModeToggleProps) {
return (
<Frame dense spacing="sm" className={cn(className)}>
<FrameHeader>
<FrameTitle>Режим фильтра</FrameTitle>
<FrameDescription>
Чёрный список: блокировать deny. Белый список: пропускать только
allow, остальное (forward) DROP.
</FrameDescription>
</FrameHeader>
<FramePanel>
<ToggleGroup
multiple={false}
value={[value]}
onValueChange={(next) => {
const mode = next[0]
if (mode === 'blacklist' || mode === 'whitelist') {
onChange(mode)
}
}}
variant="outline"
size="sm"
disabled={disabled}
aria-label="Режим фильтра"
className="flex flex-wrap justify-start gap-1"
>
<ToggleGroupItem value="blacklist" aria-label="Чёрный список">
Чёрный список
</ToggleGroupItem>
<ToggleGroupItem value="whitelist" aria-label="Белый список">
Белый список
</ToggleGroupItem>
</ToggleGroup>
</FramePanel>
</Frame>
)
}
@@ -0,0 +1,30 @@
import { Shield } from 'lucide-react'
import { cn } from '@evofw/ui/lib/utils'
import { Item, ItemMedia } from '@evofw/ui/components/item'
type PolicySetIconProps = {
mode?: 'blacklist' | 'whitelist' | string | null
className?: string
}
/**
* KPI-style tile for policy set rows.
* Preview DNA: https://reui.io/preview/base/stats-12
*/
export function PolicySetIcon({ mode, className }: PolicySetIconProps) {
const isWhitelist = mode === 'whitelist'
return (
<Item
className={cn(
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
isWhitelist ? 'text-warning' : 'text-muted-foreground',
className,
)}
aria-label={isWhitelist ? 'Whitelist' : 'Blacklist'}
>
<ItemMedia variant="icon" className="size-auto">
<Shield aria-hidden />
</ItemMedia>
</Item>
)
}