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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user