feat(web): support batch traffic stats configuration

This commit is contained in:
dushixiang committed 2026-08-27 21:24:19 +08:00
1 parent 3efa6852ed
commit efa505f378
2 files changed
+228 -1

No files matched your search

+22 -1
View File
@@ -4,7 +4,7 @@ import {Link, useNavigate} from 'react-router-dom';
import type {MenuProps} from 'antd';
import {App, Button, Dropdown, Form, Input, Select, Space, Tag} from 'antd';
import type {ColumnsType} from 'antd/es/table';
import {Edit, Eye, EyeOff, FileWarning, GripVertical, Lock, MoreVertical, Play, Plus, PowerOff, RefreshCw, Shield, Tags, Trash2} from 'lucide-react';
import {Activity, Edit, Eye, EyeOff, FileWarning, GripVertical, Lock, MoreVertical, Play, Plus, PowerOff, RefreshCw, Shield, Tags, Trash2} from 'lucide-react';
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import type {DragEndEvent} from '@dnd-kit/core';
import {DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors} from '@dnd-kit/core';
@@ -28,6 +28,7 @@ import BatchTagsModal from './BatchTagsModal';
import BatchTamperProtectionModal from './BatchTamperProtectionModal';
import BatchSSHLoginConfigModal from './BatchSSHLoginConfigModal';
import BatchVisibilityModal from './BatchVisibilityModal';
import BatchTrafficStatsModal from './BatchTrafficStatsModal';
type SortableRowHook = ReturnType<typeof useSortable>;
@@ -103,6 +104,7 @@ const AgentList = () => {
const [batchTamperModalVisible, setBatchTamperModalVisible] = useState(false);
const [batchSSHModalVisible, setBatchSSHModalVisible] = useState(false);
const [batchVisibilityModalVisible, setBatchVisibilityModalVisible] = useState(false);
const [batchTrafficModalVisible, setBatchTrafficModalVisible] = useState(false);
const [editingAgentId, setEditingAgentId] = useState<string | undefined>(undefined);
// 过滤条件
@@ -279,6 +281,14 @@ const AgentList = () => {
setBatchVisibilityModalVisible(true);
};
const handleBatchTrafficConfig = () => {
if (selectedRowKeys.length === 0) {
messageApi.warning('请先选择要操作的探针');
return;
}
setBatchTrafficModalVisible(true);
};
// 前端过滤数据
const filteredAgents = useMemo(() => {
let result = agents || [];
@@ -635,6 +645,7 @@ const AgentList = () => {
const batchMenuItems: MenuProps['items'] = [
{key: 'tags', icon: <Tags size={15}/>, label: '修改标签', onClick: handleBatchTags},
{key: 'visibility', icon: <EyeOff size={15}/>, label: '修改可见性', onClick: handleBatchVisibility},
{key: 'traffic', icon: <Activity size={15}/>, label: '配置流量统计', onClick: handleBatchTrafficConfig},
{key: 'tamper', icon: <FileWarning size={15}/>, label: '配置防篡改保护', onClick: handleBatchTamperConfig},
{key: 'ssh', icon: <Lock size={15}/>, label: '配置 SSH 登录监控', onClick: handleBatchSSHConfig},
];
@@ -816,6 +827,16 @@ const AgentList = () => {
setSelectedRowKeys([]);
}}
/>
<BatchTrafficStatsModal
open={batchTrafficModalVisible}
agentIds={selectedRowKeys as string[]}
onCancel={() => setBatchTrafficModalVisible(false)}
onSuccess={() => {
setBatchTrafficModalVisible(false);
setSelectedRowKeys([]);
}}
/>
</div>
);
};
@@ -0,0 +1,206 @@
import {useEffect, useState} from 'react';
import {App, Form, InputNumber, Modal, Select, Space, Switch} from 'antd';
import {useMutation, useQueryClient} from '@tanstack/react-query';
import {updateTrafficConfig} from '@/api/agent';
import type {UpdateTrafficConfigRequest} from '@/types';
import {getErrorMessage} from '@/lib/utils';
type TrafficLimitUnit = 'GB' | 'TB';
interface BatchTrafficStatsModalProps {
open: boolean;
agentIds: string[];
onCancel: () => void;
onSuccess: () => void;
}
interface TrafficConfigFormValues {
enabled: boolean;
trafficType: UpdateTrafficConfigRequest['type'];
trafficLimit: number;
trafficResetDay: number;
}
const BYTES_PER_GB = 1024 * 1024 * 1024;
const BYTES_PER_TB = 1024 * BYTES_PER_GB;
const BatchTrafficStatsModal = ({open, agentIds, onCancel, onSuccess}: BatchTrafficStatsModalProps) => {
const {message: messageApi} = App.useApp();
const [form] = Form.useForm<TrafficConfigFormValues>();
const queryClient = useQueryClient();
const [limitUnit, setLimitUnit] = useState<TrafficLimitUnit>('GB');
const enabled = Form.useWatch('enabled', form) ?? false;
useEffect(() => {
if (!open) {
return;
}
form.setFieldsValue({
enabled: false,
trafficType: 'recv',
trafficLimit: 0,
trafficResetDay: 0,
});
setLimitUnit('GB');
}, [open, form]);
const handleUnitChange = (unit: TrafficLimitUnit) => {
if (unit === limitUnit) {
return;
}
const currentLimit = form.getFieldValue('trafficLimit') ?? 0;
form.setFieldValue(
'trafficLimit',
unit === 'TB' ? currentLimit / 1024 : currentLimit * 1024,
);
setLimitUnit(unit);
};
const batchMutation = useMutation({
mutationFn: async (payload: UpdateTrafficConfigRequest) => {
const results = await Promise.allSettled(
agentIds.map(agentId => updateTrafficConfig(agentId, payload)),
);
const failedCount = results.filter(result => result.status === 'rejected').length;
return {
total: agentIds.length,
successCount: agentIds.length - failedCount,
failedCount,
};
},
onSuccess: (result) => {
if (result.successCount === 0) {
messageApi.error(`所选 ${result.total} 个探针的流量统计配置均失败,请重试`);
return;
}
if (result.failedCount > 0) {
messageApi.warning(`已配置 ${result.successCount} 个探针,失败 ${result.failedCount} 个`);
} else {
messageApi.success(`成功配置 ${result.total} 个探针的流量统计`);
}
queryClient.invalidateQueries({queryKey: ['admin', 'agents']});
queryClient.invalidateQueries({queryKey: ['trafficStats']});
onSuccess();
},
onError: (error: unknown) => {
messageApi.error(getErrorMessage(error, '批量配置流量统计失败'));
},
});
const handleOk = async () => {
if (agentIds.length === 0) {
messageApi.warning('请先选择要操作的探针');
return;
}
try {
const values = await form.validateFields();
const bytesPerUnit = limitUnit === 'TB' ? BYTES_PER_TB : BYTES_PER_GB;
await batchMutation.mutateAsync({
enabled: values.enabled,
type: values.trafficType || 'recv',
limit: values.enabled ? Math.round((values.trafficLimit ?? 0) * bytesPerUnit) : 0,
resetDay: values.enabled ? (values.trafficResetDay ?? 0) : 0,
});
} catch (error: unknown) {
if (typeof error === 'object' && error !== null && 'errorFields' in error) {
return;
}
messageApi.error(getErrorMessage(error, '批量配置流量统计失败'));
}
};
return (
<Modal
title={`批量配置流量统计 (已选择 ${agentIds.length} 个探针)`}
open={open}
onOk={handleOk}
onCancel={onCancel}
confirmLoading={batchMutation.isPending}
width={640}
destroyOnHidden
>
<Form form={form} layout="vertical">
<Form.Item
label="启用流量统计"
name="enabled"
valuePropName="checked"
extra="开启后将监控所选探针的流量使用情况"
>
<Switch checkedChildren="已启用" unCheckedChildren="已禁用"/>
</Form.Item>
{enabled && (
<>
<Form.Item
label="统计类型"
name="trafficType"
rules={[{required: true, message: '请选择统计类型'}]}
extra="选择要统计的流量类型"
>
<Select
placeholder="请选择统计类型"
options={[
{label: '进站流量 (下载)', value: 'recv'},
{label: '出站流量 (上传)', value: 'send'},
{label: '全部流量 (上传+下载)', value: 'both'},
]}
/>
</Form.Item>
<Form.Item
label="流量限额"
extra={`设置流量限额 (${limitUnit}),0 表示仅统计不告警`}
>
<Space.Compact style={{width: '100%'}}>
<Form.Item
name="trafficLimit"
noStyle
rules={[{type: 'number', min: 0, message: '流量限额不能小于 0'}]}
>
<InputNumber
min={0}
step={limitUnit === 'TB' ? 0.5 : 1}
precision={limitUnit === 'TB' ? 2 : 0}
placeholder="请输入流量限额"
style={{width: '100%'}}
/>
</Form.Item>
<Select<TrafficLimitUnit>
value={limitUnit}
onChange={handleUnitChange}
options={[
{label: 'GB', value: 'GB'},
{label: 'TB', value: 'TB'},
]}
style={{width: 88}}
/>
</Space.Compact>
</Form.Item>
<Form.Item
label="流量重置日期"
name="trafficResetDay"
rules={[{required: true, message: '请选择流量重置日期'}]}
extra="每月的几号重置流量,0 表示不自动重置"
>
<Select
placeholder="请选择流量重置日期"
options={[
{label: '不自动重置', value: 0},
...Array.from({length: 31}, (_, index) => ({
label: `每月 ${index + 1} 号`,
value: index + 1,
})),
]}
/>
</Form.Item>
</>
)}
</Form>
</Modal>
);
};
export default BatchTrafficStatsModal;