refactor: 优化探针详情页与流量统计页
- 探针详情: /admin/agents 前缀匹配宽屏布局;状态并入页头,返回列表改为名称旁小字链接,编辑探针移至页头右侧;基本信息/流量/防篡改/SSH 配置去卡片嵌套,内容扁平化 - 流量统计: 重构页面布局,配置与使用情况分栏;流量限额支持 GB/TB 单位切换与自动适配(后端字节存储无需改动);限额输入与单位下拉组合;使用情况改为卡片呈现
This commit is contained in:
1 parent
6c8d2f13d3
commit
7603906c36
7 files changed
+189
-189
No files matched your search
@@ -14,7 +14,7 @@ export interface Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PageHeaderProps {
|
interface PageHeaderProps {
|
||||||
title: string;
|
title: React.ReactNode;
|
||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
actions?: Action[];
|
actions?: Action[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ import type {User} from '@/types';
|
|||||||
const AdminLayout = () => {
|
const AdminLayout = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isWideCompactPage = [
|
// /admin/agents 前缀(含探针详情页)使用全宽布局
|
||||||
'/admin/agents',
|
const isWideCompactPage = location.pathname.startsWith('/admin/agents')
|
||||||
|
|| [
|
||||||
'/admin/monitors',
|
'/admin/monitors',
|
||||||
'/admin/ddns',
|
'/admin/ddns',
|
||||||
'/admin/api-keys',
|
'/admin/api-keys',
|
||||||
|
|||||||
@@ -1,31 +1,17 @@
|
|||||||
import {useState} from 'react';
|
import {Descriptions, Space, Spin, Tag} from 'antd';
|
||||||
import {App, Button, Card, Descriptions, Space, Spin, Tag} from 'antd';
|
import {Clock} from 'lucide-react';
|
||||||
import {Clock, Edit, RefreshCw} from 'lucide-react';
|
|
||||||
import {useQuery} from '@tanstack/react-query';
|
import {useQuery} from '@tanstack/react-query';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {getAgentForAdmin, getAgentLatestMetricsForAdmin, getTags} from '@/api/agent.ts';
|
import {getAgentForAdmin, getAgentLatestMetricsForAdmin} from '@/api/agent.ts';
|
||||||
import AgentEditModal from './AgentEditModal';
|
|
||||||
|
|
||||||
interface AgentBasicInfoProps {
|
interface AgentBasicInfoProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
||||||
const {message: messageApi} = App.useApp();
|
|
||||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
|
||||||
|
|
||||||
const {data: tags = []} = useQuery({
|
|
||||||
queryKey: ['admin', 'agents', 'tags'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const response = await getTags();
|
|
||||||
return response.data.tags || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: agent,
|
data: agent,
|
||||||
isLoading: agentLoading,
|
isLoading: agentLoading,
|
||||||
refetch: refetchAgent,
|
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['admin', 'agent', agentId],
|
queryKey: ['admin', 'agent', agentId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -38,7 +24,6 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
|||||||
const {
|
const {
|
||||||
data: latestMetrics,
|
data: latestMetrics,
|
||||||
isLoading: metricsLoading,
|
isLoading: metricsLoading,
|
||||||
refetch: refetchMetrics,
|
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['admin', 'agent', agentId, 'metrics', 'latest'],
|
queryKey: ['admin', 'agent', agentId, 'metrics', 'latest'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -48,11 +33,6 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
|||||||
enabled: !!agentId,
|
enabled: !!agentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRefresh = () => {
|
|
||||||
refetchAgent();
|
|
||||||
refetchMetrics();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (agentLoading || metricsLoading) {
|
if (agentLoading || metricsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
@@ -98,28 +78,7 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Card
|
<Descriptions column={{xs: 1, sm: 2, lg: 3, xl: 4}} bordered>
|
||||||
title="探针详细信息"
|
|
||||||
variant="outlined"
|
|
||||||
extra={
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
icon={<RefreshCw size={16}/>}
|
|
||||||
onClick={handleRefresh}
|
|
||||||
>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<Edit size={16}/>}
|
|
||||||
onClick={() => setEditModalVisible(true)}
|
|
||||||
>
|
|
||||||
编辑探针
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Descriptions column={{xs: 1, sm: 2, lg: 3}} bordered>
|
|
||||||
<Descriptions.Item label="探针名称">{agent.name || '-'}</Descriptions.Item>
|
<Descriptions.Item label="探针名称">{agent.name || '-'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="主机名">{agent.hostname || '-'}</Descriptions.Item>
|
<Descriptions.Item label="主机名">{agent.hostname || '-'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="探针ID">
|
<Descriptions.Item label="探针ID">
|
||||||
@@ -242,19 +201,6 @@ const AgentBasicInfo = ({agentId}: AgentBasicInfoProps) => {
|
|||||||
) : '-'}
|
) : '-'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 编辑模态框 */}
|
|
||||||
<AgentEditModal
|
|
||||||
open={editModalVisible}
|
|
||||||
agentId={agentId}
|
|
||||||
existingTags={tags}
|
|
||||||
onCancel={() => setEditModalVisible(false)}
|
|
||||||
onSuccess={() => {
|
|
||||||
setEditModalVisible(false);
|
|
||||||
refetchAgent();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import {useEffect, useState} from 'react';
|
|||||||
import {useNavigate, useParams, useSearchParams} from 'react-router-dom';
|
import {useNavigate, useParams, useSearchParams} from 'react-router-dom';
|
||||||
import type {TabsProps} from 'antd';
|
import type {TabsProps} from 'antd';
|
||||||
import {Alert, Spin, Tabs, Tag} from 'antd';
|
import {Alert, Spin, Tabs, Tag} from 'antd';
|
||||||
import {Activity, ArrowLeft, FileWarning, Lock, Shield, TrendingUp} from 'lucide-react';
|
import {Activity, ArrowLeft, Edit, FileWarning, Lock, Shield, TrendingUp} from 'lucide-react';
|
||||||
import {useQuery} from '@tanstack/react-query';
|
import {useQuery, useQueryClient} from '@tanstack/react-query';
|
||||||
import {getAgentForAdmin} from '@/api/agent.ts';
|
import {getAgentForAdmin, getTags} from '@/api/agent.ts';
|
||||||
import AgentBasicInfo from './AgentBasicInfo';
|
import AgentBasicInfo from './AgentBasicInfo';
|
||||||
import AgentAudit from './AgentAudit';
|
import AgentAudit from './AgentAudit';
|
||||||
|
import AgentEditModal from './AgentEditModal';
|
||||||
import TamperProtection from './TamperProtection';
|
import TamperProtection from './TamperProtection';
|
||||||
import SSHLoginMonitor from './SSHLoginMonitor';
|
import SSHLoginMonitor from './SSHLoginMonitor';
|
||||||
import TrafficStats from './TrafficStats';
|
import TrafficStats from './TrafficStats';
|
||||||
@@ -16,8 +17,10 @@ import {PagePanel} from '@/components/PagePanel';
|
|||||||
const AgentDetail = () => {
|
const AgentDetail = () => {
|
||||||
const {id} = useParams<{ id: string }>();
|
const {id} = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const [activeTab, setActiveTab] = useState<string>(searchParams.get('tab') || 'info');
|
const [activeTab, setActiveTab] = useState<string>(searchParams.get('tab') || 'info');
|
||||||
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
|
|
||||||
// 获取探针基本信息(用于显示头部卡片)
|
// 获取探针基本信息(用于显示头部卡片)
|
||||||
const {data: agent, isLoading} = useQuery({
|
const {data: agent, isLoading} = useQuery({
|
||||||
@@ -30,6 +33,15 @@ const AgentDetail = () => {
|
|||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取标签列表(编辑探针弹窗使用)
|
||||||
|
const {data: tags = []} = useQuery({
|
||||||
|
queryKey: ['admin', 'agents', 'tags'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await getTags();
|
||||||
|
return response.data.tags || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 同步 activeTab 到 URL
|
// 同步 activeTab 到 URL
|
||||||
const nextParams = new URLSearchParams(searchParams);
|
const nextParams = new URLSearchParams(searchParams);
|
||||||
@@ -136,29 +148,29 @@ const AgentDetail = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={agent.name || agent.hostname}
|
title={
|
||||||
|
<span className="flex flex-wrap items-center gap-2.5">
|
||||||
|
{agent.name || agent.hostname}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate('/admin/agents')}
|
||||||
|
className="flex cursor-pointer items-center gap-1 border-0 bg-transparent p-0 text-xs font-normal text-[#646a73] transition-colors hover:text-[#1677ff] dark:text-[#9ba1ab] dark:hover:text-[#75adff]"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={12}/>
|
||||||
|
<span>返回列表</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
extra={agent.status === 1 ? <Tag color="success">在线</Tag> : <Tag color="error">离线</Tag>}
|
||||||
actions={[{
|
actions={[{
|
||||||
key: 'back',
|
key: 'edit',
|
||||||
label: '返回列表',
|
label: '编辑探针',
|
||||||
icon: <ArrowLeft size={16}/>,
|
type: 'primary',
|
||||||
onClick: () => navigate('/admin/agents'),
|
icon: <Edit size={16}/>,
|
||||||
|
onClick: () => setEditModalVisible(true),
|
||||||
}]}
|
}]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PagePanel>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="grid h-10 w-10 place-items-center rounded-lg bg-blue-50 text-blue-600 dark:bg-blue-500/10 dark:text-blue-400">
|
|
||||||
<Activity size={21}/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium text-gray-800 dark:text-gray-100">当前连接状态</div>
|
|
||||||
<div className="mt-1">
|
|
||||||
{agent.status === 1 ? <Tag color="success">在线</Tag> : <Tag color="error">离线</Tag>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PagePanel>
|
|
||||||
|
|
||||||
{/* Tabs 内容 */}
|
{/* Tabs 内容 */}
|
||||||
<PagePanel>
|
<PagePanel>
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -167,6 +179,18 @@ const AgentDetail = () => {
|
|||||||
items={tabItems}
|
items={tabItems}
|
||||||
/>
|
/>
|
||||||
</PagePanel>
|
</PagePanel>
|
||||||
|
|
||||||
|
{/* 编辑探针模态框 */}
|
||||||
|
<AgentEditModal
|
||||||
|
open={editModalVisible}
|
||||||
|
agentId={id}
|
||||||
|
existingTags={tags}
|
||||||
|
onCancel={() => setEditModalVisible(false)}
|
||||||
|
onSuccess={() => {
|
||||||
|
setEditModalVisible(false);
|
||||||
|
queryClient.invalidateQueries({queryKey: ['admin', 'agent', id]});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {useEffect} from 'react';
|
import React, {useEffect} from 'react';
|
||||||
import {Alert, App, Button, Card, Form, Input, Space, Switch} from 'antd';
|
import {Alert, App, Button, Form, Input, Space, Spin, Switch} from 'antd';
|
||||||
import {Save, Terminal} from 'lucide-react';
|
import {Save, Terminal} from 'lucide-react';
|
||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||||
import type {SSHLoginConfig as SSHLoginConfigType} from '@/types';
|
import type {SSHLoginConfig as SSHLoginConfigType} from '@/types';
|
||||||
@@ -96,14 +96,12 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
|
|||||||
}, [config, form]);
|
}, [config, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div>
|
||||||
title={
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
|
||||||
<Terminal size={18}/>
|
<Terminal size={18}/>
|
||||||
<span>SSH 登录监控配置</span>
|
<span>SSH 登录监控配置</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
extra={
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<Save size={16}/>}
|
icon={<Save size={16}/>}
|
||||||
@@ -112,10 +110,13 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
|
|||||||
>
|
>
|
||||||
保存配置
|
保存配置
|
||||||
</Button>
|
</Button>
|
||||||
}
|
</div>
|
||||||
loading={isLoading}
|
{isLoading ? (
|
||||||
>
|
<div className="text-center py-12">
|
||||||
<Space direction="vertical" style={{width: '100%'}} size="large">
|
<Spin/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Space direction="vertical" style={{width: '100%'}} size="large" className="mt-4">
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
@@ -165,7 +166,8 @@ const SSHLoginConfig: React.FC<SSHLoginConfigProps> = ({agentId}) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import {Alert, App, Button, Card, Form, Input, List, Space, Switch} from 'antd';
|
import {Alert, App, Button, Form, Input, List, Space, Spin, Switch} from 'antd';
|
||||||
import {Plus, Save, Shield, Trash2} from 'lucide-react';
|
import {Plus, Save, Shield, Trash2} from 'lucide-react';
|
||||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||||
import {getTamperConfig, updateTamperConfig} from '@/api/tamper';
|
import {getTamperConfig, updateTamperConfig} from '@/api/tamper';
|
||||||
@@ -70,14 +70,12 @@ const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}
|
|||||||
}, [config, form]);
|
}, [config, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div>
|
||||||
title={
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
|
||||||
<Shield size={18}/>
|
<Shield size={18}/>
|
||||||
<span>防篡改保护配置</span>
|
<span>防篡改保护配置</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
extra={
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<Save size={16}/>}
|
icon={<Save size={16}/>}
|
||||||
@@ -86,10 +84,13 @@ const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}
|
|||||||
>
|
>
|
||||||
保存配置
|
保存配置
|
||||||
</Button>
|
</Button>
|
||||||
}
|
</div>
|
||||||
loading={isLoading}
|
{isLoading ? (
|
||||||
>
|
<div className="text-center py-12">
|
||||||
<Space orientation="vertical" style={{width: '100%'}} size="large">
|
<Spin/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Space orientation="vertical" style={{width: '100%'}} size="large" className="mt-4">
|
||||||
<Alert
|
<Alert
|
||||||
title="防篡改保护通过设置目录的不可变属性来防止文件被修改、删除或重命名。配置更新后将实时同步到探针。"
|
title="防篡改保护通过设置目录的不可变属性来防止文件被修改、删除或重命名。配置更新后将实时同步到探针。"
|
||||||
type="info"
|
type="info"
|
||||||
@@ -180,7 +181,8 @@ const TamperProtectionConfig: React.FC<TamperProtectionConfigProps> = ({agentId}
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import {
|
import {
|
||||||
Alert,
|
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
Col,
|
Col,
|
||||||
Descriptions,
|
|
||||||
Form,
|
Form,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Progress,
|
Progress,
|
||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
|
Spin,
|
||||||
Statistic,
|
Statistic,
|
||||||
Switch,
|
Switch,
|
||||||
Tag
|
Tag
|
||||||
@@ -33,6 +31,22 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [enabled, setEnabled] = useState(false);
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
const [limitUnit, setLimitUnit] = useState<'GB' | 'TB'>('GB');
|
||||||
|
|
||||||
|
const bytesPerUnit = limitUnit === 'TB' ? 1024 * 1024 * 1024 * 1024 : 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
// 切换单位时把已填写的数值换算到新单位
|
||||||
|
const handleUnitChange = (unit: 'GB' | 'TB') => {
|
||||||
|
if (unit === limitUnit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const factor = unit === 'TB' ? 1024 : 1 / 1024;
|
||||||
|
const values = form.getFieldsValue();
|
||||||
|
form.setFieldsValue({
|
||||||
|
trafficLimit: (values.trafficLimit || 0) * factor,
|
||||||
|
});
|
||||||
|
setLimitUnit(unit);
|
||||||
|
};
|
||||||
|
|
||||||
// 获取流量统计
|
// 获取流量统计
|
||||||
const {data: stats, isLoading} = useQuery<TrafficStatsType>({
|
const {data: stats, isLoading} = useQuery<TrafficStatsType>({
|
||||||
@@ -61,7 +75,7 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
|||||||
try {
|
try {
|
||||||
const values = enabled ? await form.validateFields() : form.getFieldsValue();
|
const values = enabled ? await form.validateFields() : form.getFieldsValue();
|
||||||
const limitValue = typeof values.trafficLimit === 'number' ? values.trafficLimit : 0;
|
const limitValue = typeof values.trafficLimit === 'number' ? values.trafficLimit : 0;
|
||||||
const limitBytes = enabled ? limitValue * 1024 * 1024 * 1024 : 0;
|
const limitBytes = enabled ? limitValue * bytesPerUnit : 0;
|
||||||
saveMutation.mutate({
|
saveMutation.mutate({
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
type: values.trafficType || 'recv',
|
type: values.trafficType || 'recv',
|
||||||
@@ -104,18 +118,24 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
|||||||
if (stats) {
|
if (stats) {
|
||||||
// 从服务器返回的 enabled 字段读取启用状态
|
// 从服务器返回的 enabled 字段读取启用状态
|
||||||
setEnabled(stats.enabled);
|
setEnabled(stats.enabled);
|
||||||
|
// 限额或已用量达到 1TB 时自动使用 TB 单位展示
|
||||||
|
const unit: 'GB' | 'TB' = (stats.limit > 0 && stats.limit >= 1024 * 1024 * 1024 * 1024)
|
||||||
|
|| (stats.used >= 1024 * 1024 * 1024 * 1024)
|
||||||
|
? 'TB'
|
||||||
|
: 'GB';
|
||||||
|
setLimitUnit(unit);
|
||||||
|
const divisor = unit === 'TB' ? 1024 * 1024 * 1024 * 1024 : 1024 * 1024 * 1024;
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
trafficType: stats.type || 'recv',
|
trafficType: stats.type || 'recv',
|
||||||
trafficLimit: stats.limit > 0 ? stats.limit / (1024 * 1024 * 1024) : 0,
|
trafficLimit: stats.limit > 0 ? stats.limit / divisor : 0,
|
||||||
trafficUsed: stats.used > 0 ? stats.used / (1024 * 1024 * 1024) : 0,
|
|
||||||
trafficResetDay: stats.resetDay || 0,
|
trafficResetDay: stats.resetDay || 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setEnabled(false);
|
setEnabled(false);
|
||||||
|
setLimitUnit('GB');
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
trafficType: 'recv',
|
trafficType: 'recv',
|
||||||
trafficLimit: 0,
|
trafficLimit: 0,
|
||||||
trafficUsed: 0,
|
|
||||||
trafficResetDay: 0,
|
trafficResetDay: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -144,43 +164,26 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space orientation="vertical" size="large" style={{width: '100%'}}>
|
|
||||||
<Alert
|
|
||||||
title="流量统计功能可以监控探针的流量使用情况。设置流量限额(大于0)将启用告警功能,超过阈值时会发送通知;设置为0则仅统计流量不发送告警。"
|
|
||||||
type="info"
|
|
||||||
showIcon
|
|
||||||
icon={<Activity size={16}/>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
{/* 左侧:流量配置 */}
|
{/* 左侧:流量配置 */}
|
||||||
<Col xs={24} lg={10}>
|
<Col xs={24} lg={10}>
|
||||||
<Card
|
<div className="flex items-center gap-2 mb-2 text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
|
||||||
title={
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Activity size={18}/>
|
<Activity size={18}/>
|
||||||
<span>流量统计配置</span>
|
<span>流量统计配置</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
{isLoading ? (
|
||||||
extra={
|
<div className="text-center py-12">
|
||||||
<Button
|
<Spin/>
|
||||||
type="primary"
|
</div>
|
||||||
icon={<Save size={16}/>}
|
) : (
|
||||||
onClick={handleSave}
|
<>
|
||||||
loading={saveMutation.isPending}
|
|
||||||
>
|
|
||||||
保存配置
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
|
className="mt-4"
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
trafficType: 'recv',
|
trafficType: 'recv',
|
||||||
trafficLimit: 0,
|
trafficLimit: 0,
|
||||||
trafficUsed: 0,
|
|
||||||
trafficResetDay: 0,
|
trafficResetDay: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -216,36 +219,32 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
|||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="流量限额"
|
label="流量限额"
|
||||||
|
extra={`设置流量限额(${limitUnit}),0表示仅统计不告警`}
|
||||||
|
>
|
||||||
|
<Space.Compact style={{width: '100%'}}>
|
||||||
|
<Form.Item
|
||||||
name="trafficLimit"
|
name="trafficLimit"
|
||||||
rules={[
|
noStyle
|
||||||
{type: 'number', min: 0, message: '流量限额不能小于0'},
|
rules={[{type: 'number', min: 0, message: '流量限额不能小于0'}]}
|
||||||
]}
|
|
||||||
extra="设置流量限额(GB),0表示仅统计不告警"
|
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
min={0}
|
min={0}
|
||||||
step={1}
|
step={limitUnit === 'TB' ? 0.5 : 1}
|
||||||
precision={0}
|
precision={limitUnit === 'TB' ? 2 : 0}
|
||||||
placeholder="请输入流量限额(GB)"
|
placeholder={`请输入流量限额`}
|
||||||
style={{width: '100%'}}
|
style={{width: '100%'}}
|
||||||
addonAfter="GB"
|
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Select
|
||||||
<Form.Item
|
value={limitUnit}
|
||||||
label="已使用流量"
|
onChange={handleUnitChange}
|
||||||
name="trafficUsed"
|
options={[
|
||||||
extra="当前已使用的流量(只读)"
|
{label: 'GB', value: 'GB'},
|
||||||
>
|
{label: 'TB', value: 'TB'},
|
||||||
<InputNumber
|
]}
|
||||||
min={0}
|
style={{width: 88}}
|
||||||
step={0.1}
|
|
||||||
precision={2}
|
|
||||||
placeholder="已使用流量(GB)"
|
|
||||||
style={{width: '100%'}}
|
|
||||||
addonAfter="GB"
|
|
||||||
disabled
|
|
||||||
/>
|
/>
|
||||||
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -268,19 +267,32 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<Save size={16}/>}
|
||||||
|
onClick={handleSave}
|
||||||
|
loading={saveMutation.isPending}
|
||||||
|
>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{/* 右侧:流量统计 */}
|
{/* 右侧:流量统计 */}
|
||||||
<Col xs={24} lg={14}>
|
<Col xs={24} lg={12}>
|
||||||
|
<div className={'max-w-[400px]'}>
|
||||||
|
<div className="rounded-md border border-[#e8ebf0] bg-white p-5 dark:border-[#272b33] dark:bg-[#1c2028]">
|
||||||
|
<div className="text-[15px] font-semibold text-[#1f2329] dark:text-[#e6e8ec]">
|
||||||
|
流量使用情况
|
||||||
|
</div>
|
||||||
{stats && stats.enabled ? (
|
{stats && stats.enabled ? (
|
||||||
<Card
|
<div className="mt-4 flex flex-col gap-5">
|
||||||
title="流量使用情况"
|
|
||||||
variant="outlined"
|
|
||||||
>
|
|
||||||
<Space orientation="vertical" size="large" style={{width: '100%'}}>
|
|
||||||
{/* 基础统计 */}
|
{/* 基础统计 */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||||
<Statistic
|
<Statistic
|
||||||
title="已使用流量"
|
title="已使用流量"
|
||||||
value={formatBytes(stats.used)}
|
value={formatBytes(stats.used)}
|
||||||
@@ -321,36 +333,50 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 详细信息 */}
|
{/* 详细信息 */}
|
||||||
<Descriptions bordered column={1} size="small">
|
<div className="text-[13px]">
|
||||||
<Descriptions.Item label="统计类型">
|
<div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
|
||||||
|
<span className="text-[#646a73] dark:text-[#9ba1ab]">统计类型</span>
|
||||||
|
<span className="text-[#1f2329] dark:text-[#e6e8ec]">
|
||||||
{stats.type === 'recv' ? '进站流量 (下载)' : stats.type === 'send' ? '出站流量 (上传)' : '全部流量 (上传+下载)'}
|
{stats.type === 'recv' ? '进站流量 (下载)' : stats.type === 'send' ? '出站流量 (上传)' : '全部流量 (上传+下载)'}
|
||||||
</Descriptions.Item>
|
</span>
|
||||||
<Descriptions.Item label="重置日期">
|
</div>
|
||||||
{stats.resetDay > 0 ? `每月 ${stats.resetDay} 号` : '不自动重置'}
|
<div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
|
||||||
</Descriptions.Item>
|
<span className="text-[#646a73] dark:text-[#9ba1ab]">当前周期</span>
|
||||||
<Descriptions.Item label="当前周期">
|
<span className="text-[#1f2329] dark:text-[#e6e8ec]">
|
||||||
{formatCurrentPeriod(stats.periodStart, stats.periodEnd, stats.resetDay)}
|
{formatCurrentPeriod(stats.periodStart, stats.periodEnd, stats.resetDay)}
|
||||||
</Descriptions.Item>
|
</span>
|
||||||
<Descriptions.Item label="距离重置">
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
|
||||||
|
<span className="text-[#646a73] dark:text-[#9ba1ab]">重置日期</span>
|
||||||
|
<span className="text-[#1f2329] dark:text-[#e6e8ec]">
|
||||||
|
{stats.resetDay > 0 ? `每月 ${stats.resetDay} 号` : '不自动重置'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
|
||||||
|
<span className="text-[#646a73] dark:text-[#9ba1ab]">距离重置</span>
|
||||||
|
<span className="text-[#1f2329] dark:text-[#e6e8ec]">
|
||||||
{stats.resetDay <= 0
|
{stats.resetDay <= 0
|
||||||
? '不自动重置'
|
? '不自动重置'
|
||||||
: stats.daysUntilReset > 0 ? `${stats.daysUntilReset} 天` : '今日重置'}
|
: stats.daysUntilReset > 0 ? `${stats.daysUntilReset} 天` : '今日重置'}
|
||||||
</Descriptions.Item>
|
</span>
|
||||||
{/* 告警状态 - 仅在有限额时显示 */}
|
</div>
|
||||||
{stats.limit > 0 && (
|
{stats.limit > 0 && (
|
||||||
<Descriptions.Item label="告警状态">
|
<div className="flex items-center justify-between gap-4 border-t border-[#e8ebf0] py-2.5 dark:border-[#272b33]">
|
||||||
<Space>
|
<span className="text-[#646a73] dark:text-[#9ba1ab]">告警状态</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
{stats.alerts.sent80 && <Tag color="orange">80%告警已发送</Tag>}
|
{stats.alerts.sent80 && <Tag color="orange">80%告警已发送</Tag>}
|
||||||
{stats.alerts.sent90 && <Tag color="orange">90%告警已发送</Tag>}
|
{stats.alerts.sent90 && <Tag color="orange">90%告警已发送</Tag>}
|
||||||
{stats.alerts.sent100 && <Tag color="red">100%告警已发送</Tag>}
|
{stats.alerts.sent100 && <Tag color="red">100%告警已发送</Tag>}
|
||||||
{!stats.alerts.sent80 && !stats.alerts.sent90 && !stats.alerts.sent100 && (
|
{!stats.alerts.sent80 && !stats.alerts.sent90 && !stats.alerts.sent100 && (
|
||||||
<Tag color="green">正常</Tag>
|
<Tag color="green">正常</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</span>
|
||||||
</Descriptions.Item>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Descriptions>
|
<div className="border-t border-[#e8ebf0] dark:border-[#272b33]"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
icon={<RotateCcw size={16}/>}
|
icon={<RotateCcw size={16}/>}
|
||||||
onClick={handleResetTraffic}
|
onClick={handleResetTraffic}
|
||||||
@@ -359,19 +385,18 @@ const TrafficStats: React.FC<TrafficStatsProps> = ({agentId}) => {
|
|||||||
>
|
>
|
||||||
立即重置流量
|
立即重置流量
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Card variant="outlined">
|
<div className="mt-4 rounded-lg border border-dashed border-[#d9dee7] py-10 text-center text-sm text-[#98a0ab] dark:border-[#30343d] dark:text-[#7d8590]">
|
||||||
<div className="text-center text-gray-400 py-12">
|
|
||||||
<Activity size={48} className="mx-auto mb-4 opacity-50"/>
|
<Activity size={48} className="mx-auto mb-4 opacity-50"/>
|
||||||
<p>请先启用流量统计功能</p>
|
<p>请先启用流量统计功能</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Space>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user