This commit is contained in:
ryan
2026-07-13 15:08:40 +08:00
parent 08e8eea932
commit 1eff7878a1
441 changed files with 26491 additions and 18405 deletions
@@ -1,6 +1,6 @@
'use client';
import {useEffect, useMemo, useState} from 'react';
import { useEffect, useMemo, useState } from 'react';
import {
closestCenter,
DndContext,
@@ -17,14 +17,14 @@ import {
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 { 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 { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Field,
FieldDescription,
@@ -33,15 +33,15 @@ import {
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 { 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';
import { getErrorMessage } from '../../components/helpers';
import { proxyRouteFormIds } from '../helpers';
import { SectionShell } from './section-shell';
interface WafSectionProps {
route: ProxyRouteItem;
@@ -56,50 +56,57 @@ interface SortableRuleProps {
onMove: (from: number, to: number) => void;
}
export function reorderRuleIDs(ids: number[], activeID: number, overID: number) {
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});
function SortableRule({ id, name, index, total, onMove }: SortableRuleProps) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
return (
<div
ref={setNodeRef}
style={{transform: CSS.Transform.toString(transform), transition}}
className="flex items-center gap-2 rounded-lg border p-3"
style={{ transform: CSS.Transform.toString(transform), transition }}
className='flex items-center gap-2 rounded-lg border p-3'
>
<Button
type="button"
variant="ghost"
size="icon-sm"
type='button'
variant='ghost'
size='icon-sm'
aria-label={`拖动${name}`}
{...attributes}
{...listeners}
>
<GripVertical data-icon="inline-start" />
<GripVertical data-icon='inline-start' />
</Button>
<span className="min-w-0 flex-1 truncate text-sm font-medium">{name}</span>
<span className='min-w-0 flex-1 truncate text-sm font-medium'>
{name}
</span>
<Button
type="button"
variant="ghost"
size="icon-sm"
type='button'
variant='ghost'
size='icon-sm'
aria-label={`上移${name}`}
disabled={index === 0}
onClick={() => onMove(index, index - 1)}
>
<ArrowUp data-icon="inline-start" />
<ArrowUp data-icon='inline-start' />
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
type='button'
variant='ghost'
size='icon-sm'
aria-label={`下移${name}`}
disabled={index === total - 1}
onClick={() => onMove(index, index + 1)}
>
<ArrowDown data-icon="inline-start" />
<ArrowDown data-icon='inline-start' />
</Button>
</div>
);
@@ -110,7 +117,9 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
const [selectedIDs, setSelectedIDs] = useState<number[]>([]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {coordinateGetter: sortableKeyboardCoordinates}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
const wafQuery = useQuery({
@@ -119,7 +128,8 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
});
const wafMutation = useMutation({
mutationFn: (ids: number[]) => WafService.updateSiteRuleGroups(route.id, ids),
mutationFn: (ids: number[]) =>
WafService.updateSiteRuleGroups(route.id, ids),
onMutate: () => {
onSavingChange?.(true);
},
@@ -133,8 +143,12 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
queryClient.invalidateQueries({
queryKey: ['openflare', 'waf', 'site-rule-groups', route.id],
}),
queryClient.invalidateQueries({ queryKey: ['openflare', 'waf', 'rule-groups'] }),
queryClient.invalidateQueries({ queryKey: ['openflare', 'config-versions', 'diff'] }),
queryClient.invalidateQueries({
queryKey: ['openflare', 'waf', 'rule-groups'],
}),
queryClient.invalidateQueries({
queryKey: ['openflare', 'config-versions', 'diff'],
}),
]);
},
onError: (error) => {
@@ -150,7 +164,10 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
const selectedSet = useMemo(() => new Set(selectedIDs), [selectedIDs]);
const ruleMap = useMemo(
() => new Map((wafQuery.data?.rule_groups ?? []).map((rule) => [rule.id, rule])),
() =>
new Map(
(wafQuery.data?.rule_groups ?? []).map((rule) => [rule.id, rule]),
),
[wafQuery.data?.rule_groups],
);
@@ -159,7 +176,7 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
setSelectedIDs((current) => arrayMove(current, from, to));
};
const handleDragEnd = ({active, over}: DragEndEvent) => {
const handleDragEnd = ({ active, over }: DragEndEvent) => {
if (!over || active.id === over.id) return;
setSelectedIDs((current) =>
reorderRuleIDs(current, Number(active.id), Number(over.id)),
@@ -168,13 +185,13 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
return (
<SectionShell
title="WAF"
description="全局规则组始终生效;这里可以为当前网站叠加自定义规则组。"
title='WAF'
description='全局规则组始终生效;这里可以为当前网站叠加自定义规则组。'
formId={proxyRouteFormIds.waf}
saving={wafMutation.isPending}
>
{wafQuery.isLoading ? (
<LoadingStateWithBorder description="加载 WAF 规则组..." />
<LoadingStateWithBorder description='加载 WAF 规则组...' />
) : wafQuery.isError ? (
<ErrorInline
message={getErrorMessage(wafQuery.error)}
@@ -183,39 +200,47 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
) : (
<form
id={proxyRouteFormIds.waf}
className="flex flex-col gap-5"
className='flex flex-col gap-5'
onSubmit={(event) => {
event.preventDefault();
wafMutation.mutate(selectedIDs);
}}
>
{wafQuery.data?.global_rule_group ? (
<div className="rounded-lg border bg-muted/30 p-4" data-testid="global-waf-rule">
<div className="flex items-center justify-between gap-3">
<div
className='rounded-lg border bg-muted/30 p-4'
data-testid='global-waf-rule'
>
<div className='flex items-center justify-between gap-3'>
<div>
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
<p className='text-[10px] font-semibold uppercase tracking-wider text-muted-foreground'>
Global Rule Group
</p>
<p className="mt-1 text-sm font-semibold">
<p className='mt-1 text-sm font-semibold'>
{wafQuery.data.global_rule_group.name}
</p>
</div>
<Badge variant="outline">始终生效</Badge>
<Badge variant='outline'>始终生效</Badge>
</div>
</div>
) : null}
<FieldSet>
<FieldLegend variant="label">选择自定义规则</FieldLegend>
<FieldDescription>选中的规则会按下方执行顺序依次运行。</FieldDescription>
<FieldGroup data-slot="checkbox-group" className="grid gap-3 md:grid-cols-2">
<FieldLegend variant='label'>选择自定义规则</FieldLegend>
<FieldDescription>
选中的规则会按下方执行顺序依次运行。
</FieldDescription>
<FieldGroup
data-slot='checkbox-group'
className='grid gap-3 md:grid-cols-2'
>
{(wafQuery.data?.rule_groups ?? []).map((group) => {
const checkboxID = `waf-rule-${group.id}`;
return (
<Field
key={group.id}
orientation="horizontal"
className="rounded-lg border p-4"
orientation='horizontal'
className='rounded-lg border p-4'
>
<Checkbox
id={checkboxID}
@@ -231,10 +256,12 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
/>
<FieldLabel
htmlFor={checkboxID}
className="min-w-0 cursor-pointer flex-col items-start gap-1"
className='min-w-0 cursor-pointer flex-col items-start gap-1'
>
<span className="truncate text-sm font-semibold">{group.name}</span>
<span className="text-xs text-muted-foreground">
<span className='truncate text-sm font-semibold'>
{group.name}
</span>
<span className='text-xs text-muted-foreground'>
{group.enabled ? '启用中' : '已停用'} ·{' '}
{group.graph.nodes.length} 个节点
</span>
@@ -246,9 +273,9 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
</FieldSet>
{selectedIDs.length > 0 ? (
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">执行顺序</p>
<p className="text-xs text-muted-foreground">
<div className='flex flex-col gap-2'>
<p className='text-sm font-medium'>执行顺序</p>
<p className='text-xs text-muted-foreground'>
自定义规则按此顺序执行,可拖动或使用上下移动按钮调整。
</p>
<DndContext
@@ -256,8 +283,11 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={selectedIDs} strategy={verticalListSortingStrategy}>
<div className="flex flex-col gap-2">
<SortableContext
items={selectedIDs}
strategy={verticalListSortingStrategy}
>
<div className='flex flex-col gap-2'>
{selectedIDs.map((id, index) => {
const rule = ruleMap.get(id);
return rule ? (
@@ -278,7 +308,7 @@ export function WafSection({ route, onSavingChange }: WafSectionProps) {
) : null}
{(wafQuery.data?.rule_groups ?? []).length === 0 ? (
<EmptyStateWithBorder description="暂无自定义 WAF 规则组" />
<EmptyStateWithBorder description='暂无自定义 WAF 规则组' />
) : null}
</form>
)}