diff --git a/frontend/app/(main)/waf/components/ip-group-view-dialog.tsx b/frontend/app/(main)/waf/components/ip-group-view-dialog.tsx
index 3f1c9ac9..633b28d2 100644
--- a/frontend/app/(main)/waf/components/ip-group-view-dialog.tsx
+++ b/frontend/app/(main)/waf/components/ip-group-view-dialog.tsx
@@ -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(
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,10 +107,16 @@ export function IPGroupViewDialog({
{group
- ? t('viewDialog.summary', {
- type: t(`types.${group.type}`),
- count: entries.length,
- })
+ ? 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,
+ })
: t('viewDialog.fallbackDesc')}
@@ -122,64 +145,95 @@ export function IPGroupViewDialog({
{t('viewDialog.subscriptionHint')}
) : null}
-
-
-
-
- {t('viewDialog.ipAddress')}
- {showAutomaticMeta ? (
- <>
- {t('viewDialog.capturedAt')}
- {t('viewDialog.banRemaining')}
- >
- ) : null}
-
- {t('viewDialog.actions')}
-
-
-
-
- {entries.map((entry) => (
-
-
- {entry.ip}
-
+
+
+ setSearchKeyword(e.target.value)}
+ placeholder={t('viewDialog.searchPlaceholder')}
+ className='pl-8 pr-8'
+ />
+ {searchKeyword ? (
+
+ ) : null}
+
+ {filteredEntries.length === 0 ? (
+
+ ) : (
+
+
+
+
+ {t('viewDialog.ipAddress')}
{showAutomaticMeta ? (
<>
-
- {entry.capturedAt
- ? formatDateTime(entry.capturedAt)
- : '—'}
-
-
- {entry.banRemaining ?? '—'}
-
+ {t('viewDialog.capturedAt')}
+
+ {t('viewDialog.banRemaining')}
+
>
) : null}
-
-
-
+
+ {t('viewDialog.actions')}
+
- ))}
-
-
-
+
+
+ {filteredEntries.map((entry) => (
+
+
+ {entry.ip}
+
+ {showAutomaticMeta ? (
+ <>
+
+ {entry.capturedAt
+ ? formatDateTime(entry.capturedAt)
+ : '—'}
+
+
+ {entry.banRemaining ?? '—'}
+
+ >
+ ) : null}
+
+
+
+
+ ))}
+
+
+
+ )}
)}
diff --git a/frontend/messages/fragments/security.en.json b/frontend/messages/fragments/security.en.json
index a064b59a..bebdd246 100644
--- a/frontend/messages/fragments/security.en.json
+++ b/frontend/messages/fragments/security.en.json
@@ -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.",
diff --git a/frontend/messages/fragments/security.zh-CN.json b/frontend/messages/fragments/security.zh-CN.json
index f2baa1e0..b5dad040 100644
--- a/frontend/messages/fragments/security.zh-CN.json
+++ b/frontend/messages/fragments/security.zh-CN.json
@@ -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 组。",