'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()}
/>
) : (
)}
);
}