'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 { useTranslations } from 'next-intl';
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 t = useTranslations('proxyRoutes');
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
return (
{name}
);
}
export function WafSection({ route, onSavingChange }: WafSectionProps) {
const t = useTranslations('proxyRoutes');
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(t('wafUpdated'));
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(t('saveFailed'), {
description: getErrorMessage(error, t('requestFailed')),
});
},
});
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()}
/>
) : (
)}
);
}