feat(web): enhance quick action grid and resource page with search functionality
- Updated QuickActionItem interface to support optional `onSelect` handler and `badgeLabel`. - Refactored QuickActionGrid to conditionally render links or buttons based on the presence of a `to` property. - Introduced search functionality in ResourcePage, allowing users to filter items based on a search query. - Added search input to the ResourcePage toolbar, improving user experience for data management. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -7,8 +7,10 @@ import {
|
||||
CircleAlertIcon,
|
||||
Copy,
|
||||
Inbox,
|
||||
ListIcon,
|
||||
Pencil,
|
||||
Plus,
|
||||
ShieldIcon,
|
||||
Trash2,
|
||||
UserPlus,
|
||||
WifiOff,
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
KpiStatGrid,
|
||||
PageHeader,
|
||||
PageShell,
|
||||
QuickActionGrid,
|
||||
ResourcePage,
|
||||
} from '@/components/reui-kit'
|
||||
import {
|
||||
@@ -63,6 +66,16 @@ import {
|
||||
} from '@evofw/ui/components/tooltip'
|
||||
import type { Agent } from '@evofw/shared'
|
||||
|
||||
const packetFmt = new Intl.NumberFormat('ru-RU', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
})
|
||||
|
||||
function formatPackets(n: number | undefined, hasApply: boolean): string {
|
||||
if (!hasApply || n === undefined) return '—'
|
||||
return packetFmt.format(n)
|
||||
}
|
||||
|
||||
/**
|
||||
* Agents ops console — Solutions Agents DNA.
|
||||
* Preview: https://reui.io/preview/base/solution-agents-1 · stats-12 · data-grid-filtering-2
|
||||
@@ -80,6 +93,7 @@ function AgentsPage() {
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [filters, setFilters] = useState<Filter[]>([])
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [activeTab, setActiveTab] = useState('all')
|
||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||
|
||||
@@ -184,11 +198,59 @@ function AgentsPage() {
|
||||
return undefined
|
||||
}, [])
|
||||
|
||||
const getSearchText = useCallback(
|
||||
(item: Agent) =>
|
||||
[item.name, item.hostname ?? '', item.last_seen_ip ?? '']
|
||||
.filter(Boolean)
|
||||
.join(' '),
|
||||
[],
|
||||
)
|
||||
|
||||
const tabFilter = useCallback((item: Agent, tabId: string) => {
|
||||
if (tabId === 'all') return true
|
||||
return item.status === tabId
|
||||
}, [])
|
||||
|
||||
const quickActions = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'add',
|
||||
title: 'Добавить агента',
|
||||
description: 'Invite + install one-liner',
|
||||
icon: <Plus aria-hidden />,
|
||||
iconClassName: 'text-primary [&_svg]:text-current',
|
||||
badgeLabel: 'Открыть',
|
||||
onSelect: () => setCreateOpen(true),
|
||||
},
|
||||
{
|
||||
id: 'pending',
|
||||
title: 'Pending',
|
||||
description: `${counts.pending} ждут approve`,
|
||||
icon: <Inbox aria-hidden />,
|
||||
iconClassName: 'text-warning [&_svg]:text-current',
|
||||
badgeLabel: 'Показать',
|
||||
onSelect: () => setActiveTab('pending'),
|
||||
},
|
||||
{
|
||||
id: 'rules',
|
||||
title: 'Наборы правил',
|
||||
description: 'Политика firewall',
|
||||
to: '/rules',
|
||||
icon: <ShieldIcon aria-hidden />,
|
||||
iconClassName: 'text-info [&_svg]:text-current',
|
||||
},
|
||||
{
|
||||
id: 'lists',
|
||||
title: 'Списки',
|
||||
description: 'IP / CIDR / community',
|
||||
to: '/lists',
|
||||
icon: <ListIcon aria-hidden />,
|
||||
iconClassName: 'text-muted-foreground [&_svg]:text-current',
|
||||
},
|
||||
],
|
||||
[counts.pending],
|
||||
)
|
||||
|
||||
const handleCopyCurl = useCallback(
|
||||
(curl: string, e?: MouseEvent) => {
|
||||
e?.stopPropagation()
|
||||
@@ -252,6 +314,66 @@ function AgentsPage() {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'dropped',
|
||||
accessorFn: (row) => row.last_apply_packets_dropped ?? -1,
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Dropped" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const a = row.original
|
||||
const hasApply = Boolean(a.last_apply_at || a.last_apply_status)
|
||||
const text = formatPackets(a.last_apply_packets_dropped, hasApply)
|
||||
if (text === '—') {
|
||||
return <DataGridMutedCell>—</DataGridMutedCell>
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span className="text-warning cursor-default tabular-nums text-sm font-medium" />
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Dropped с последнего apply
|
||||
{a.last_apply_at ? ` · ${a.last_apply_at}` : ''}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'accepted',
|
||||
accessorFn: (row) => row.last_apply_packets_accepted ?? -1,
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Accepted" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const a = row.original
|
||||
const hasApply = Boolean(a.last_apply_at || a.last_apply_status)
|
||||
const text = formatPackets(a.last_apply_packets_accepted, hasApply)
|
||||
if (text === '—') {
|
||||
return <DataGridMutedCell>—</DataGridMutedCell>
|
||||
}
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span className="text-success cursor-default tabular-nums text-sm font-medium" />
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Accepted с последнего apply
|
||||
{a.last_apply_at ? ` · ${a.last_apply_at}` : ''}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'install',
|
||||
enableSorting: false,
|
||||
@@ -369,6 +491,8 @@ function AgentsPage() {
|
||||
|
||||
<KpiStatGrid cards={kpiCards} isLoading={agentsQ.isLoading} />
|
||||
|
||||
<QuickActionGrid actions={quickActions} />
|
||||
|
||||
{counts.pending > 0 ? (
|
||||
<Frame dense spacing="sm">
|
||||
<FrameHeader className="flex-row items-start justify-between gap-3">
|
||||
@@ -425,6 +549,10 @@ function AgentsPage() {
|
||||
onFiltersChange={setFilters}
|
||||
onClearFilters={() => setFilters([])}
|
||||
getFilterFieldValue={getFilterFieldValue}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
searchPlaceholder="Поиск агентов…"
|
||||
getSearchText={getSearchText}
|
||||
onRowClick={(row) =>
|
||||
void navigate({ to: '/agents/$id', params: { id: row.id } })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user