'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()} /> ) : (
{ event.preventDefault(); wafMutation.mutate(selectedIDs); }} > {wafQuery.data?.global_rule_group ? (

Global Rule Group

{wafQuery.data.global_rule_group.name}

{t('alwaysOn')}
) : null}
{t('selectCustomRules')} {t('selectCustomRulesDesc')} {(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 ? t('ruleEnabled') : t('ruleDisabled')} ยท{' '} {t('nodeCount', { count: group.graph.nodes.length })} ); })}
{selectedIDs.length > 0 ? (

{t('executionOrder')}

{t('executionOrderDesc')}

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