'use client'; import {Suspense, useCallback, useEffect, useMemo, useState} from 'react'; import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; import axios from 'axios'; import {ArrowLeft, GitBranch, Save} from 'lucide-react'; import {useRouter, useSearchParams} from 'next/navigation'; import {toast} from 'sonner'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import {Label} from '@/components/ui/label'; import {Skeleton} from '@/components/ui/skeleton'; import {Switch} from '@/components/ui/switch'; import {services, type WAFRule, type WAFRuleGraph, type WAFRuleNode} from '@/lib/services'; import {getErrorMessage} from '../../components/helpers'; import {validateGraph} from './components/graph-validation'; import {NodeProperties} from './components/node-properties'; import {RuleFlowCanvas} from './components/rule-flow-canvas'; import {UnsavedChanges} from './components/unsaved-changes'; import {findGraphErrorTarget, type GraphErrorTarget} from './components/editor-behavior'; export default function WAFRuleEditorPage() { return }>; } function EditorContent() { const router = useRouter(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const id = Number(searchParams.get('id')); const [graph, setGraph] = useState(); const [revision, setRevision] = useState(0); const [selectedId, setSelectedId] = useState(); const [selectedEdgeId, setSelectedEdgeId] = useState(); const [focusTarget, setFocusTarget] = useState(); const [dirty, setDirty] = useState(false); const [conflict, setConflict] = useState(false); const ruleQueryKey = ['waf-rule', id] as const; const ruleQuery = useQuery({queryKey: ruleQueryKey, queryFn: () => services.openflareWaf.getRule(id), enabled: Number.isFinite(id) && id > 0}); const ipGroupsQuery = useQuery({queryKey: ['waf-ip-groups'], queryFn: () => services.openflareWaf.listIPGroups()}); useEffect(() => { if (ruleQuery.data && !dirty) { setGraph(ruleQuery.data.graph); setRevision(ruleQuery.data.revision); } }, [dirty, ruleQuery.data]); const issues = useMemo(() => graph ? validateGraph(graph) : [], [graph]); const selected = graph?.nodes.find((node) => node.id === selectedId); const saveMutation = useMutation({ mutationFn: () => services.openflareWaf.saveRuleGraph(id, {revision, graph: graph!}), onSuccess: (rule) => { setGraph(rule.graph); setRevision(rule.revision); setDirty(false); setConflict(false); queryClient.setQueryData(['waf-rule', id], rule); toast.success('规则图已保存'); }, onError: (error) => { if (axios.isAxiosError(error) && error.response?.status === 409) { setConflict(true); toast.error('规则已在其他页面更新,请重新加载'); return; } const payload = axios.isAxiosError(error) ? error.response?.data : error; const target = graph ? findGraphErrorTarget(payload, graph.nodes.map((node) => node.id), graph.edges.map((edge) => edge.id)) : undefined; if (target?.kind === 'node') { setSelectedEdgeId(undefined); setSelectedId(target.id); } if (target?.kind === 'edge') { setSelectedId(undefined); setSelectedEdgeId(target.id); } setFocusTarget(target ? {...target} : undefined); toast.error('保存失败,请检查标记的节点和连线'); }, }); const metaMutation = useMutation({ mutationFn: (enabled: boolean) => services.openflareWaf.updateRuleMeta(id, { name: ruleQuery.data!.name, enabled, }), onMutate: async (enabled) => { await queryClient.cancelQueries({queryKey: ruleQueryKey}); const previous = queryClient.getQueryData(ruleQueryKey); queryClient.setQueryData(ruleQueryKey, (current) => current ? {...current, enabled} : current, ); return {previous}; }, onError: (error, _enabled, context) => { if (context?.previous) { queryClient.setQueryData(ruleQueryKey, context.previous); } toast.error(getErrorMessage(error)); }, onSuccess: (rule) => { queryClient.setQueryData(ruleQueryKey, rule); void queryClient.invalidateQueries({queryKey: ruleQueryKey, refetchType: 'none'}); void queryClient.invalidateQueries({queryKey: ['openflare', 'waf', 'rule-groups']}); void queryClient.invalidateQueries({queryKey: ['openflare', 'config-versions', 'diff']}); toast.success(rule.enabled ? '规则已启用' : '规则已停用'); }, }); const changeGraph = useCallback((next: WAFRuleGraph, persistent = true) => { setGraph(next); if (persistent) { setDirty(true); setConflict(false); } }, []); const changeNode = useCallback((next: WAFRuleNode) => { if (!graph) return; changeGraph({...graph, nodes: graph.nodes.map((node) => node.id === next.id ? next : node)}); }, [changeGraph, graph]); const leave = () => { if (!dirty || window.confirm('存在未保存的更改,确定离开吗?')) router.push('/waf'); }; if (!Number.isFinite(id) || id <= 0) return

缺少有效的规则 ID。

; if (ruleQuery.isError) return

规则加载失败,请重试。

; if (ruleQuery.isLoading || !graph || !ruleQuery.data) return ; return (

{ruleQuery.data.name}

{ruleQuery.data.enabled ? '已启用' : '已停用'}{issues.length === 0 ? '图校验通过' : `${issues.length} 个问题`}{dirty && 未保存}
0 || saveMutation.isPending || metaMutation.isPending} onCheckedChange={(enabled) => metaMutation.mutate(enabled)} />
{conflict && }
); } function EditorSkeleton() { return
; }