mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
前端优化
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import type {ProxyRoutePoWConfig, WAFRuleGroup, WAFRuleGroupPayload,} from '@/lib/services/openflare';
|
||||
import type {
|
||||
ProxyRoutePoWConfig,
|
||||
WAFIPGroup,
|
||||
WAFIPGroupPayload,
|
||||
WAFRuleGroup,
|
||||
WAFRuleGroupPayload,
|
||||
} from '@/lib/services/openflare';
|
||||
|
||||
export type WAFTab = 'basic' | 'lists' | 'pow' | 'block';
|
||||
export type RuleListType = 'whitelist' | 'blacklist';
|
||||
@@ -266,3 +272,63 @@ export const automaticPresetRules = [
|
||||
expr: 'ip_host_count > 50 && ip_host_ratio > 0.5',
|
||||
},
|
||||
];
|
||||
|
||||
export type IPGroupViewEntry = {
|
||||
ip: string;
|
||||
capturedAt?: string;
|
||||
banRemaining?: string;
|
||||
};
|
||||
|
||||
export function buildIPGroupPayloadFromGroup(
|
||||
group: WAFIPGroup,
|
||||
ipList: string[],
|
||||
): WAFIPGroupPayload {
|
||||
return {
|
||||
name: group.name,
|
||||
type: group.type,
|
||||
enabled: group.enabled,
|
||||
ip_list: ipList,
|
||||
auto_config: group.auto_config ?? {},
|
||||
subscription_url: group.subscription_url ?? '',
|
||||
subscription_format: group.subscription_format ?? 'text',
|
||||
subscription_mapping_rule: group.subscription_mapping_rule ?? '',
|
||||
sync_interval_minutes: group.sync_interval_minutes || 1440,
|
||||
remark: group.remark ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
export function formatIPGroupBanRemaining(
|
||||
capturedAt: string,
|
||||
ttlSeconds: number,
|
||||
now = new Date(),
|
||||
): string {
|
||||
if (ttlSeconds <= 0) {
|
||||
return '永久';
|
||||
}
|
||||
const capturedDate = new Date(capturedAt);
|
||||
if (Number.isNaN(capturedDate.getTime())) {
|
||||
return '—';
|
||||
}
|
||||
const expireDate = new Date(capturedDate.getTime() + ttlSeconds * 1000);
|
||||
if (expireDate.getTime() <= now.getTime()) {
|
||||
return '已过期';
|
||||
}
|
||||
const diffMins = Math.round((expireDate.getTime() - now.getTime()) / (60 * 1000));
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins} 分钟后`;
|
||||
}
|
||||
return `${Math.round(diffMins / 60)} 小时后`;
|
||||
}
|
||||
|
||||
export function getIPGroupViewEntries(group: WAFIPGroup): IPGroupViewEntry[] {
|
||||
if (group.type === 'automatic' && group.ext_ips && group.ext_ips.length > 0) {
|
||||
const ttl =
|
||||
typeof group.auto_config?.ttl === 'number' ? group.auto_config.ttl : -1;
|
||||
return group.ext_ips.map((item) => ({
|
||||
ip: item.ip,
|
||||
capturedAt: item.captured_at,
|
||||
banRemaining: formatIPGroupBanRemaining(item.captured_at, ttl),
|
||||
}));
|
||||
}
|
||||
return (group.ip_list ?? []).map((ip) => ({ ip }));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
'use client';
|
||||
|
||||
import {Loader2, Trash2} from 'lucide-react';
|
||||
import {useMemo, useState} from 'react';
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {EmptyStateWithBorder} from '@/components/layout/empty';
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
||||
import type {WAFIPGroup} from '@/lib/services/openflare';
|
||||
import {formatDateTime} from '@/lib/utils';
|
||||
|
||||
import {getIPGroupViewEntries, ipGroupTypeLabels, type IPGroupViewEntry} from './helpers';
|
||||
|
||||
interface IPGroupViewDialogProps {
|
||||
open: boolean;
|
||||
group: WAFIPGroup | null;
|
||||
loading: boolean;
|
||||
removingIp: string | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onRemoveIp: (ip: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function IPGroupViewDialog({
|
||||
open,
|
||||
group,
|
||||
loading,
|
||||
removingIp,
|
||||
onOpenChange,
|
||||
onRemoveIp,
|
||||
}: IPGroupViewDialogProps) {
|
||||
const [deleteTarget, setDeleteTarget] = useState<IPGroupViewEntry | null>(null);
|
||||
|
||||
const entries = useMemo(
|
||||
() => (group ? getIPGroupViewEntries(group) : []),
|
||||
[group],
|
||||
);
|
||||
|
||||
const showAutomaticMeta = group?.type === 'automatic';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
onOpenChange(nextOpen);
|
||||
if (!nextOpen) {
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{group ? `查看 ${group.name}` : '查看 IP 组'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{group
|
||||
? `${ipGroupTypeLabels[group.type]} · 共 ${entries.length} 条 IP`
|
||||
: '查看当前 IP 组中的 IP 列表,并可移除不需要的条目。'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
加载 IP 列表...
|
||||
</div>
|
||||
) : !group ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">未选择 IP 组。</p>
|
||||
) : entries.length === 0 ? (
|
||||
<EmptyStateWithBorder
|
||||
description={
|
||||
group.type === 'automatic'
|
||||
? '该自动规则暂未抓取任何 IP,可点击「立即执行」手动触发抓取。'
|
||||
: '当前 IP 组暂无 IP 条目。'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{group.type === 'subscription' ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
订阅类型在下次同步时可能重新拉取已删除的 IP。
|
||||
</p>
|
||||
) : null}
|
||||
<div className="rounded-lg border border-dashed">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>IP 地址</TableHead>
|
||||
{showAutomaticMeta ? (
|
||||
<>
|
||||
<TableHead>抓取时间</TableHead>
|
||||
<TableHead>封禁剩余</TableHead>
|
||||
</>
|
||||
) : null}
|
||||
<TableHead className="w-[80px] text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{entries.map((entry) => (
|
||||
<TableRow key={entry.ip}>
|
||||
<TableCell className="font-mono text-sm">{entry.ip}</TableCell>
|
||||
{showAutomaticMeta ? (
|
||||
<>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{entry.capturedAt ? formatDateTime(entry.capturedAt) : '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{entry.banRemaining ?? '—'}</TableCell>
|
||||
</>
|
||||
) : null}
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 text-destructive hover:text-destructive"
|
||||
disabled={removingIp === entry.ip}
|
||||
onClick={() => setDeleteTarget(entry)}
|
||||
>
|
||||
{removingIp === entry.ip ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<Trash2 className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">删除 {entry.ip}</span>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog
|
||||
open={Boolean(deleteTarget)}
|
||||
onOpenChange={(nextOpen) => !nextOpen && setDeleteTarget(null)}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除 IP</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
确认从 IP 组「{group?.name}」中移除 {deleteTarget?.ip} 吗?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={Boolean(removingIp)}>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive text-white hover:bg-destructive/90"
|
||||
disabled={Boolean(removingIp)}
|
||||
onClick={async () => {
|
||||
if (!deleteTarget) return;
|
||||
await onRemoveIp(deleteTarget.ip);
|
||||
setDeleteTarget(null);
|
||||
}}
|
||||
>
|
||||
{removingIp ? '删除中...' : '确认删除'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import {Download, MoreHorizontal, Pencil, Play, Trash2} from 'lucide-react';
|
||||
import {Download, Eye, MoreHorizontal, Pencil, Play, Trash2} from 'lucide-react';
|
||||
|
||||
import {Badge} from '@/components/ui/badge';
|
||||
import {Button} from '@/components/ui/button';
|
||||
@@ -20,6 +20,7 @@ import {ipGroupTypeLabels} from './helpers';
|
||||
interface IPGroupsTableProps {
|
||||
groups: WAFIPGroup[];
|
||||
syncingId: number | null;
|
||||
onView: (group: WAFIPGroup) => void;
|
||||
onEdit: (group: WAFIPGroup) => void;
|
||||
onDelete: (group: WAFIPGroup) => void;
|
||||
onSync: (group: WAFIPGroup) => void;
|
||||
@@ -29,6 +30,7 @@ interface IPGroupsTableProps {
|
||||
export function IPGroupsTable({
|
||||
groups,
|
||||
syncingId,
|
||||
onView,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onSync,
|
||||
@@ -78,6 +80,10 @@ export function IPGroupsTable({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onView(group)}>
|
||||
<Eye className="size-4 mr-2" />
|
||||
查看
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onEdit(group)}>
|
||||
<Pencil className="size-4 mr-2" />
|
||||
编辑
|
||||
|
||||
@@ -200,21 +200,6 @@ export function RuleGroupDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border border-dashed p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>启用规则组</FormLabel>
|
||||
<FormDescription>关闭后保留配置,但不会参与匹配。</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remark"
|
||||
@@ -229,6 +214,20 @@ export function RuleGroupDialog({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center justify-between rounded-lg border border-dashed p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>启用规则组</FormLabel>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="lists" className="space-y-4 mt-4">
|
||||
|
||||
@@ -23,9 +23,10 @@ import {LoadingStateWithBorder} from '@/components/layout/loading';
|
||||
import type {WAFIPGroup, WAFIPGroupAutoTestResult, WAFIPGroupPayload,} from '@/lib/services/openflare';
|
||||
import {WafService} from '@/lib/services/openflare';
|
||||
|
||||
import {getErrorMessage, parseAutomaticConfig} from '../components/helpers';
|
||||
import {buildIPGroupPayloadFromGroup, getErrorMessage, parseAutomaticConfig} from '../components/helpers';
|
||||
import {IPGroupDialog} from '../components/ip-group-dialog';
|
||||
import {IPGroupTestDialog} from '../components/ip-group-test-dialog';
|
||||
import {IPGroupViewDialog} from '../components/ip-group-view-dialog';
|
||||
import {IPGroupsTable} from '../components/ip-groups-table';
|
||||
|
||||
const ipGroupsQueryKey = ['openflare', 'waf', 'ip-groups'];
|
||||
@@ -38,6 +39,9 @@ export default function WafIPGroupsPage() {
|
||||
const [testOpen, setTestOpen] = useState(false);
|
||||
const [testResult, setTestResult] = useState<WAFIPGroupAutoTestResult | null>(null);
|
||||
const [syncingId, setSyncingId] = useState<number | null>(null);
|
||||
const [viewOpen, setViewOpen] = useState(false);
|
||||
const [viewingGroup, setViewingGroup] = useState<WAFIPGroup | null>(null);
|
||||
const [removingIp, setRemovingIp] = useState<string | null>(null);
|
||||
|
||||
const groupsQuery = useQuery({
|
||||
queryKey: ipGroupsQueryKey,
|
||||
@@ -105,6 +109,33 @@ export default function WafIPGroupsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const viewGroupQuery = useQuery({
|
||||
queryKey: ['openflare', 'waf', 'ip-groups', viewingGroup?.id],
|
||||
queryFn: () => WafService.getIPGroup(viewingGroup!.id),
|
||||
enabled: viewOpen && viewingGroup !== null,
|
||||
});
|
||||
|
||||
const removeIpMutation = useMutation({
|
||||
mutationFn: async ({ group, ip }: { group: WAFIPGroup; ip: string }) => {
|
||||
const nextIpList = group.ip_list.filter((item) => item !== ip);
|
||||
return WafService.updateIPGroup(group.id, buildIPGroupPayloadFromGroup(group, nextIpList));
|
||||
},
|
||||
onMutate: ({ ip }) => {
|
||||
setRemovingIp(ip);
|
||||
},
|
||||
onSuccess: async (updatedGroup) => {
|
||||
toast.success('IP 已移除');
|
||||
setViewingGroup(updatedGroup);
|
||||
await invalidate();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(getErrorMessage(error));
|
||||
},
|
||||
onSettled: () => {
|
||||
setRemovingIp(null);
|
||||
},
|
||||
});
|
||||
|
||||
const testMutation = useMutation({
|
||||
mutationFn: (group: WAFIPGroup) =>
|
||||
WafService.testIPGroup({
|
||||
@@ -146,7 +177,19 @@ export default function WafIPGroupsPage() {
|
||||
testMutation.mutate(group);
|
||||
};
|
||||
|
||||
const handleView = (group: WAFIPGroup) => {
|
||||
setViewingGroup(group);
|
||||
setViewOpen(true);
|
||||
};
|
||||
|
||||
const handleRemoveIp = async (ip: string) => {
|
||||
const group = viewGroupQuery.data ?? viewingGroup;
|
||||
if (!group) return;
|
||||
await removeIpMutation.mutateAsync({ group, ip });
|
||||
};
|
||||
|
||||
const groups = groupsQuery.data ?? [];
|
||||
const viewGroup = viewGroupQuery.data ?? viewingGroup;
|
||||
|
||||
return (
|
||||
<div className="py-6 px-1 space-y-6">
|
||||
@@ -205,6 +248,7 @@ export default function WafIPGroupsPage() {
|
||||
<IPGroupsTable
|
||||
groups={groups}
|
||||
syncingId={syncingId}
|
||||
onView={handleView}
|
||||
onEdit={handleEdit}
|
||||
onDelete={setDeleteTarget}
|
||||
onSync={(group) => syncMutation.mutate(group.id)}
|
||||
@@ -234,6 +278,20 @@ export default function WafIPGroupsPage() {
|
||||
onOpenChange={setTestOpen}
|
||||
/>
|
||||
|
||||
<IPGroupViewDialog
|
||||
open={viewOpen}
|
||||
group={viewGroup}
|
||||
loading={viewGroupQuery.isFetching && !viewGroupQuery.data}
|
||||
removingIp={removingIp}
|
||||
onOpenChange={(open) => {
|
||||
setViewOpen(open);
|
||||
if (!open) {
|
||||
setViewingGroup(null);
|
||||
}
|
||||
}}
|
||||
onRemoveIp={handleRemoveIp}
|
||||
/>
|
||||
|
||||
<AlertDialog
|
||||
open={Boolean(deleteTarget)}
|
||||
onOpenChange={(open) => !open && setDeleteTarget(null)}
|
||||
|
||||
@@ -50,10 +50,10 @@ export const openflareWebsiteNavGroup: OpenFlareNavGroup = {
|
||||
title: '网站管理',
|
||||
icon: Globe,
|
||||
items: [
|
||||
{title: '网站', url: '/websites', childUrls: ['/websites/detail']},
|
||||
{title: '证书', url: '/websites/certificates'},
|
||||
{title: 'DNS', url: '/websites/dns-accounts'},
|
||||
{title: '源站', url: '/origins', childUrls: ['/origins/detail']},
|
||||
{title: '域名列表', url: '/websites', childUrls: ['/websites/detail']},
|
||||
{title: 'TLS证书', url: '/websites/certificates'},
|
||||
{title: 'DNS账号', url: '/websites/dns-accounts'},
|
||||
{title: '源站地址', url: '/origins', childUrls: ['/origins/detail']},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -695,6 +695,11 @@ func buildIPGroup(group *model.OpenFlareWAFIPGroup, input IPGroupInput) (*model.
|
||||
group.Type = groupType
|
||||
group.Enabled = input.Enabled
|
||||
group.IPList = string(ipListJSON)
|
||||
if groupType == wafIPGroupTypeAutomatic {
|
||||
if err := pruneIPGroupExtIPs(group, normalizedIPs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
group.AutoConfig = autoConfig
|
||||
group.SubscriptionURL = subscriptionURL
|
||||
group.SubscriptionFormat = subscriptionFormat
|
||||
@@ -785,6 +790,34 @@ func loadIPGroupReferenceCounts(ctx context.Context) (map[uint]int, error) {
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func pruneIPGroupExtIPs(group *model.OpenFlareWAFIPGroup, ipList []string) error {
|
||||
if group == nil {
|
||||
return nil
|
||||
}
|
||||
allowed := make(map[string]struct{}, len(ipList))
|
||||
for _, ip := range ipList {
|
||||
allowed[ip] = struct{}{}
|
||||
}
|
||||
var extIPs []ipGroupExtIP
|
||||
if group.ExtIPs != "" && group.ExtIPs != "[]" {
|
||||
if err := json.Unmarshal([]byte(group.ExtIPs), &extIPs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pruned := make([]ipGroupExtIP, 0, len(extIPs))
|
||||
for _, extIP := range extIPs {
|
||||
if _, ok := allowed[extIP.IP]; ok {
|
||||
pruned = append(pruned, extIP)
|
||||
}
|
||||
}
|
||||
extIPsJSON, err := json.Marshal(pruned)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
group.ExtIPs = string(extIPsJSON)
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeIPList(items []string) ([]string, error) {
|
||||
normalized := make([]string, 0, len(items))
|
||||
for _, raw := range items {
|
||||
|
||||
@@ -65,3 +65,46 @@ func TestCreateRuleGroup(t *testing.T) {
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestPruneIPGroupExtIPs(t *testing.T) {
|
||||
group := &model.OpenFlareWAFIPGroup{
|
||||
ExtIPs: `[{"ip":"203.0.113.10","captured_at":"2026-06-18T10:00:00Z"},{"ip":"203.0.113.11","captured_at":"2026-06-18T11:00:00Z"}]`,
|
||||
}
|
||||
err := pruneIPGroupExtIPs(group, []string{"203.0.113.10"})
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `[{"ip":"203.0.113.10","captured_at":"2026-06-18T10:00:00Z"}]`, group.ExtIPs)
|
||||
}
|
||||
|
||||
func TestUpdateIPGroupPrunesAutomaticExtIPs(t *testing.T) {
|
||||
cleanup := setupWAFTestDB(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
created, err := CreateIPGroup(ctx, IPGroupInput{
|
||||
Name: "auto group",
|
||||
Type: wafIPGroupTypeAutomatic,
|
||||
Enabled: true,
|
||||
AutoConfig: []byte(`{"lookback_minutes":60,"ttl":-1,"rules":[{"name":"scan","expr":"request_count > 1"}]}`),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
group, err := model.GetOpenFlareWAFIPGroupByID(ctx, created.ID)
|
||||
require.NoError(t, err)
|
||||
group.IPList = `["203.0.113.10","203.0.113.11"]`
|
||||
group.ExtIPs = `[{"ip":"203.0.113.10","captured_at":"2026-06-18T10:00:00Z"},{"ip":"203.0.113.11","captured_at":"2026-06-18T11:00:00Z"}]`
|
||||
require.NoError(t, model.UpdateOpenFlareWAFIPGroup(ctx, group))
|
||||
|
||||
updated, err := UpdateIPGroup(ctx, created.ID, IPGroupInput{
|
||||
Name: created.Name,
|
||||
Type: created.Type,
|
||||
Enabled: created.Enabled,
|
||||
IPList: []string{"203.0.113.10"},
|
||||
AutoConfig: created.AutoConfig,
|
||||
Remark: created.Remark,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, updated.IPList, 1)
|
||||
assert.Equal(t, "203.0.113.10", updated.IPList[0])
|
||||
require.Len(t, updated.ExtIPs, 1)
|
||||
assert.Equal(t, "203.0.113.10", updated.ExtIPs[0].IP)
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ sidebar: false
|
||||
|
||||
- 移除 Wavelet 顶栏 OpenFlare 服务端版本入口按钮;版本升级能力保留在 Admin 设置 OpenFlare 运维 Tab。
|
||||
- 将 Wavelet 默认上游仓库调整为 `Rain-kl/OpenFlare`,站点名称、邮件模板、前端默认标题与页脚品牌统一初始化为 OpenFlare;新增 goose 迁移回填既有环境的旧 Wavelet 默认值。
|
||||
- Wavelet WAF IP 组列表新增「查看」操作,支持在查看弹窗中浏览当前 IP 并移除单条 IP;自动类型同步裁剪 `ext_ips` 以与 `ip_list` 保持一致。
|
||||
|
||||
## [v2.3.4] - 2026-06-17
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Globe2, Network, Plus, Save, ShieldCheck, Trash2 } from 'lucide-react';
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||
import type {ReactNode} from 'react';
|
||||
import {useEffect, useMemo, useState} from 'react';
|
||||
import {useRouter} from 'next/navigation';
|
||||
import {Globe2, Network, Plus, Save, ShieldCheck, Trash2} from 'lucide-react';
|
||||
|
||||
import { EmptyState } from '@/components/feedback/empty-state';
|
||||
import { ErrorState } from '@/components/feedback/error-state';
|
||||
import { InlineMessage } from '@/components/feedback/inline-message';
|
||||
import { LoadingState } from '@/components/feedback/loading-state';
|
||||
import { PageHeader } from '@/components/layout/page-header';
|
||||
import { AppCard } from '@/components/ui/app-card';
|
||||
import { getProxyRoutes } from '@/features/proxy-routes/api/proxy-routes';
|
||||
import {EmptyState} from '@/components/feedback/empty-state';
|
||||
import {ErrorState} from '@/components/feedback/error-state';
|
||||
import {InlineMessage} from '@/components/feedback/inline-message';
|
||||
import {LoadingState} from '@/components/feedback/loading-state';
|
||||
import {PageHeader} from '@/components/layout/page-header';
|
||||
import {AppCard} from '@/components/ui/app-card';
|
||||
import {getProxyRoutes} from '@/features/proxy-routes/api/proxy-routes';
|
||||
import {
|
||||
DangerButton,
|
||||
PrimaryButton,
|
||||
@@ -30,18 +30,14 @@ import {
|
||||
replaceWAFRuleGroupSites,
|
||||
updateWAFRuleGroup,
|
||||
} from '@/features/waf/api/waf';
|
||||
import type {
|
||||
WAFIPGroup,
|
||||
WAFRuleGroup,
|
||||
WAFRuleGroupPayload,
|
||||
} from '@/features/waf/types';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import type {WAFIPGroup, WAFRuleGroup, WAFRuleGroupPayload,} from '@/features/waf/types';
|
||||
import {cn} from '@/lib/utils/cn';
|
||||
|
||||
import { RuleEntryModal } from './rule-entry-modal';
|
||||
import { SiteApplyDrawer } from './site-apply-drawer';
|
||||
import { PowTabPanel } from './pow-tab-panel';
|
||||
import { RuleListSection } from './rule-list-section';
|
||||
import { TabButton } from './tab-button';
|
||||
import {RuleEntryModal} from './rule-entry-modal';
|
||||
import {SiteApplyDrawer} from './site-apply-drawer';
|
||||
import {PowTabPanel} from './pow-tab-panel';
|
||||
import {RuleListSection} from './rule-list-section';
|
||||
import {TabButton} from './tab-button';
|
||||
import {
|
||||
buildCountryOptions,
|
||||
buildDraft,
|
||||
@@ -56,12 +52,7 @@ import {
|
||||
textToList,
|
||||
updateDraftList,
|
||||
} from './helpers';
|
||||
import type {
|
||||
FeedbackState,
|
||||
ListFieldKey,
|
||||
RuleModalState,
|
||||
WAFTab,
|
||||
} from './types';
|
||||
import type {FeedbackState, ListFieldKey, RuleModalState, WAFTab,} from './types';
|
||||
|
||||
export function WAFPage() {
|
||||
const router = useRouter();
|
||||
@@ -444,7 +435,6 @@ export function WAFPage() {
|
||||
</ResourceField>
|
||||
<ToggleField
|
||||
label="启用规则组"
|
||||
description="关闭后保留配置,但不会参与匹配。"
|
||||
checked={draft.enabled}
|
||||
onChange={(checked) =>
|
||||
setDraft((current) => ({
|
||||
|
||||
Reference in New Issue
Block a user