'use client'; import {useEffect, useMemo, useState} from 'react'; import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core'; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable'; import {CSS} from '@dnd-kit/utilities'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import {ArrowDown, ArrowUp, GripVertical} from 'lucide-react'; import {toast} from 'sonner'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import {Checkbox} from '@/components/ui/checkbox'; import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, } from '@/components/ui/field'; import {EmptyStateWithBorder} from '@/components/layout/empty'; import {ErrorInline} from '@/components/layout/error'; import {LoadingStateWithBorder} from '@/components/layout/loading'; import type {ProxyRouteItem} from '@/lib/services/openflare'; import {WafService} from '@/lib/services/openflare'; import {getErrorMessage} from '../../components/helpers'; import {proxyRouteFormIds} from '../helpers'; import {SectionShell} from './section-shell'; interface WafSectionProps { route: ProxyRouteItem; onSavingChange?: (saving: boolean) => void; } interface SortableRuleProps { id: number; name: string; index: number; total: number; onMove: (from: number, to: number) => void; } export function reorderRuleIDs(ids: number[], activeID: number, overID: number) { const from = ids.indexOf(activeID); const to = ids.indexOf(overID); return from < 0 || to < 0 || from === to ? ids : arrayMove(ids, from, to); } function SortableRule({id, name, index, total, onMove}: SortableRuleProps) { const {attributes, listeners, setNodeRef, transform, transition} = useSortable({id}); return (
{name}
); } export function WafSection({ route, onSavingChange }: WafSectionProps) { const queryClient = useQueryClient(); const [selectedIDs, setSelectedIDs] = useState([]); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, {coordinateGetter: sortableKeyboardCoordinates}), ); const wafQuery = useQuery({ queryKey: ['openflare', 'waf', 'site-rule-groups', route.id], queryFn: () => WafService.listSiteRuleGroups(route.id), }); const wafMutation = useMutation({ mutationFn: (ids: number[]) => WafService.updateSiteRuleGroups(route.id, ids), onMutate: () => { onSavingChange?.(true); }, onSettled: () => { onSavingChange?.(false); }, onSuccess: async (result) => { setSelectedIDs(result.applied_ids); toast.success('WAF 规则组已更新'); await Promise.all([ queryClient.invalidateQueries({ queryKey: ['openflare', 'waf', 'site-rule-groups', route.id], }), queryClient.invalidateQueries({ queryKey: ['openflare', 'waf', 'rule-groups'] }), queryClient.invalidateQueries({ queryKey: ['openflare', 'config-versions', 'diff'] }), ]); }, onError: (error) => { toast.error('保存失败', { description: getErrorMessage(error) }); }, }); useEffect(() => { if (wafQuery.data) { setSelectedIDs(wafQuery.data.applied_ids); } }, [wafQuery.data]); const selectedSet = useMemo(() => new Set(selectedIDs), [selectedIDs]); const ruleMap = useMemo( () => new Map((wafQuery.data?.rule_groups ?? []).map((rule) => [rule.id, rule])), [wafQuery.data?.rule_groups], ); const moveRule = (from: number, to: number) => { if (to < 0 || to >= selectedIDs.length) return; setSelectedIDs((current) => arrayMove(current, from, to)); }; const handleDragEnd = ({active, over}: DragEndEvent) => { if (!over || active.id === over.id) return; setSelectedIDs((current) => reorderRuleIDs(current, Number(active.id), Number(over.id)), ); }; return ( {wafQuery.isLoading ? ( ) : wafQuery.isError ? ( void wafQuery.refetch()} /> ) : (
{ event.preventDefault(); wafMutation.mutate(selectedIDs); }} > {wafQuery.data?.global_rule_group ? (

Global Rule Group

{wafQuery.data.global_rule_group.name}

始终生效
) : null}
选择自定义规则 选中的规则会按下方执行顺序依次运行。 {(wafQuery.data?.rule_groups ?? []).map((group) => { const checkboxID = `waf-rule-${group.id}`; return ( { setSelectedIDs((current) => checked ? [...current, group.id] : current.filter((id) => id !== group.id), ); }} /> {group.name} {group.enabled ? '启用中' : '已停用'} ·{' '} {group.graph.nodes.length} 个节点 ); })}
{selectedIDs.length > 0 ? (

执行顺序

自定义规则按此顺序执行,可拖动或使用上下移动按钮调整。

{selectedIDs.map((id, index) => { const rule = ruleMap.get(id); return rule ? ( ) : null; })}
) : null} {(wafQuery.data?.rule_groups ?? []).length === 0 ? ( ) : null} )}
); }