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 组。",