feat(waf): add search and filter in ip group view dialog

This commit is contained in:
ryan
2026-09-26 15:15:26 +08:00
parent b1626d068f
commit 4d52a5b097
3 changed files with 121 additions and 59 deletions
@@ -1,8 +1,8 @@
'use client';
import { Loader2, Trash2 } from 'lucide-react';
import { Loader2, Search, Trash2, X } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
AlertDialog,
@@ -24,6 +24,7 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { EmptyStateWithBorder } from '@/components/layout/empty';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
@@ -59,6 +60,14 @@ export function IPGroupViewDialog({
const [deleteTarget, setDeleteTarget] = useState<IPGroupViewEntry | null>(
null,
);
const [searchKeyword, setSearchKeyword] = useState('');
useEffect(() => {
if (!open) {
setSearchKeyword('');
setDeleteTarget(null);
}
}, [open]);
const entries = useMemo(
() =>
@@ -68,6 +77,13 @@ export function IPGroupViewDialog({
[group, t],
);
const trimmedKeyword = searchKeyword.trim().toLowerCase();
const filteredEntries = useMemo(() => {
if (!trimmedKeyword) return entries;
return entries.filter((e) => e.ip.toLowerCase().includes(trimmedKeyword));
}, [entries, trimmedKeyword]);
const showAutomaticMeta = group?.type === 'automatic';
return (
@@ -78,6 +94,7 @@ export function IPGroupViewDialog({
onOpenChange(nextOpen);
if (!nextOpen) {
setDeleteTarget(null);
setSearchKeyword('');
}
}}
>
@@ -90,7 +107,13 @@ export function IPGroupViewDialog({
</DialogTitle>
<DialogDescription>
{group
? t('viewDialog.summary', {
? trimmedKeyword
? t('viewDialog.summaryFiltered', {
type: t(`types.${group.type}`),
count: entries.length,
matched: filteredEntries.length,
})
: t('viewDialog.summary', {
type: t(`types.${group.type}`),
count: entries.length,
})
@@ -122,6 +145,34 @@ export function IPGroupViewDialog({
{t('viewDialog.subscriptionHint')}
</p>
) : null}
<div className='relative'>
<Search className='pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground' />
<Input
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
placeholder={t('viewDialog.searchPlaceholder')}
className='pl-8 pr-8'
/>
{searchKeyword ? (
<Button
type='button'
variant='ghost'
size='icon'
className='absolute right-1 top-1/2 size-7 -translate-y-1/2 text-muted-foreground hover:text-foreground'
onClick={() => setSearchKeyword('')}
>
<X className='size-3.5' />
<span className='sr-only'>
{t('viewDialog.clearSearch')}
</span>
</Button>
) : null}
</div>
{filteredEntries.length === 0 ? (
<EmptyStateWithBorder
description={t('viewDialog.noSearchResult')}
/>
) : (
<div className='rounded-lg border border-dashed'>
<Table>
<TableHeader>
@@ -130,7 +181,9 @@ export function IPGroupViewDialog({
{showAutomaticMeta ? (
<>
<TableHead>{t('viewDialog.capturedAt')}</TableHead>
<TableHead>{t('viewDialog.banRemaining')}</TableHead>
<TableHead>
{t('viewDialog.banRemaining')}
</TableHead>
</>
) : null}
<TableHead className='w-[80px] text-right'>
@@ -139,7 +192,7 @@ export function IPGroupViewDialog({
</TableRow>
</TableHeader>
<TableBody>
{entries.map((entry) => (
{filteredEntries.map((entry) => (
<TableRow key={entry.ip}>
<TableCell className='font-mono text-sm'>
{entry.ip}
@@ -180,6 +233,7 @@ export function IPGroupViewDialog({
</TableBody>
</Table>
</div>
)}
</div>
)}
@@ -257,6 +257,10 @@
"titleNamed": "View {name}",
"title": "View IP group",
"summary": "{type} · {count} IPs",
"summaryFiltered": "{type} · {count} IPs ({matched} matched)",
"searchPlaceholder": "Search IP addresses...",
"clearSearch": "Clear search",
"noSearchResult": "No matching IP addresses found.",
"fallbackDesc": "View IPs in this group and remove entries you no longer need.",
"loading": "Loading IP list...",
"noGroup": "No IP group selected.",
@@ -257,6 +257,10 @@
"titleNamed": "查看 {name}",
"title": "查看 IP 组",
"summary": "{type} · 共 {count} 条 IP",
"summaryFiltered": "{type} · 共 {count} 条 IP(匹配 {matched} 条)",
"searchPlaceholder": "搜索 IP 地址...",
"clearSearch": "清空搜索",
"noSearchResult": "未找到匹配的 IP 地址。",
"fallbackDesc": "查看当前 IP 组中的 IP 列表,并可移除不需要的条目。",
"loading": "加载 IP 列表...",
"noGroup": "未选择 IP 组。",