feat(web): enhance quick action grid and resource page with search functionality
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m50s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- 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:
Denozordec
2026-07-21 04:03:32 +07:00
co-authored by Cursor
parent 0653313a57
commit 33d301b191
11 changed files with 1082 additions and 431 deletions
@@ -21,6 +21,7 @@ import {
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { EmptyState } from '@/components/empty-state'
import { Button } from '@evofw/ui/components/button'
import { Switch } from '@evofw/ui/components/switch'
import { Item, ItemMedia } from '@evofw/ui/components/item'
@@ -31,6 +32,7 @@ import { apiFetch } from '@/lib/api'
* Ordered firewall rules — ReUI Sortable + settings-8 DNA.
* Preview: https://reui.io/preview/base/components/c-sortable-5
* · https://reui.io/preview/base/settings-8
* · https://reui.io/preview/base/empty-state-12
* Docs: https://reui.io/docs/components/base/sortable
*/
@@ -41,11 +43,24 @@ function ruleTarget(r: PolicyRule): string {
return '—'
}
function ruleSubtitle(r: PolicyRule): string | null {
const parts: string[] = []
if (r.list_id && (r.cidr || r.hostname)) {
parts.push(`list:${r.list_id.slice(0, 8)}`)
}
if (r.comment) parts.push(r.comment)
if (typeof r.resolved_count === 'number' && r.resolved_count > 0) {
parts.push(`${r.resolved_count} prefixes`)
}
return parts.length > 0 ? parts.join(' · ') : null
}
type PolicyRulesSortableProps = {
setId: string
rules: PolicyRule[]
policyMode: 'blacklist' | 'whitelist'
onDelete: (id: string) => void
onAdd?: () => void
}
export function PolicyRulesSortable({
@@ -53,6 +68,7 @@ export function PolicyRulesSortable({
rules: rulesProp,
policyMode,
onDelete,
onAdd,
}: PolicyRulesSortableProps) {
const qc = useQueryClient()
const [items, setItems] = useState(rulesProp)
@@ -71,17 +87,8 @@ export function PolicyRulesSortable({
void qc.invalidateQueries({ queryKey: ['policy-sets', setId] })
void qc.invalidateQueries({ queryKey: ['policy-sets'] })
},
onError: (e: Error, _vars, context) => {
onError: (e: Error) => {
toast.error(e.message)
if (context && typeof context === 'object' && 'prev' in context) {
setItems((context as { prev: PolicyRule[] }).prev)
}
},
onMutate: async (ordered_ids) => {
const prev = items
const byId = new Map(items.map((r) => [r.id, r]))
setItems(ordered_ids.map((id) => byId.get(id)!).filter(Boolean))
return { prev }
},
})
@@ -118,20 +125,43 @@ export function PolicyRulesSortable({
</FramePanel>
</Frame>
{items.length === 0 ? (
<Frame dense spacing="sm">
<FramePanel className="text-muted-foreground py-8 text-center text-sm">
Нет правил добавьте CIDR, список или hostname
</FramePanel>
</Frame>
) : (
<Frame dense spacing="sm" stacked>
<FrameHeader className="px-4 py-3">
<FrameTitle>Правила</FrameTitle>
<Frame dense spacing="sm" stacked>
<FrameHeader className="flex-row items-start justify-between gap-3 px-4 py-3">
<div className="flex min-w-0 flex-col gap-px">
<div className="flex flex-wrap items-center gap-2">
<FrameTitle>Правила</FrameTitle>
<Badge variant="secondary" size="sm">
{items.length}
</Badge>
</div>
<FrameDescription>
Перетащите для порядка · Switch вкл/выкл
</FrameDescription>
</FrameHeader>
</div>
{onAdd ? (
<Button size="sm" variant="outline" onClick={onAdd}>
Правило
</Button>
) : null}
</FrameHeader>
{items.length === 0 ? (
<FramePanel className="p-0">
<EmptyState
title="Нет правил"
description="Добавьте CIDR, список или hostname"
action={
onAdd ? (
<Button size="sm" onClick={onAdd}>
Правило
</Button>
) : undefined
}
centered={false}
className="py-10"
/>
</FramePanel>
) : (
<FramePanel className="p-0">
<Sortable
value={items}
@@ -148,6 +178,7 @@ export function PolicyRulesSortable({
{items.map((r) => {
const enabled = r.enabled !== false
const isDeny = r.action === 'deny'
const subtitle = ruleSubtitle(r)
return (
<SortableItem
key={r.id}
@@ -178,7 +209,7 @@ export function PolicyRulesSortable({
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex flex-wrap items-center gap-2">
<span className="truncate font-medium font-mono text-sm">
<span className="truncate font-mono text-sm font-medium">
{ruleTarget(r)}
</span>
<Badge
@@ -195,9 +226,9 @@ export function PolicyRulesSortable({
</Badge>
) : null}
</div>
{r.comment ? (
{subtitle ? (
<span className="text-muted-foreground truncate text-xs">
{r.comment}
{subtitle}
</span>
) : null}
</div>
@@ -224,8 +255,8 @@ export function PolicyRulesSortable({
})}
</Sortable>
</FramePanel>
</Frame>
)}
)}
</Frame>
</div>
)
}