feat: server ops panel + dashboard server widgets
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 7s
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 7s
This commit is contained in:
parent
274a27a10a
commit
26f5750046
@ -10,6 +10,7 @@ import PageLoading from './components/PageLoading'
|
|||||||
import AdminLayout from './layouts/AdminLayout'
|
import AdminLayout from './layouts/AdminLayout'
|
||||||
|
|
||||||
const Login = lazy(() => import('./pages/Login'))
|
const Login = lazy(() => import('./pages/Login'))
|
||||||
|
const ServersPage = lazy(() => import("./pages/Servers"))
|
||||||
const AuditLogPage = lazy(() => import("./pages/AuditLog"))
|
const AuditLogPage = lazy(() => import("./pages/AuditLog"))
|
||||||
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
||||||
const UserManagement = lazy(() => import('./pages/UserManagement'))
|
const UserManagement = lazy(() => import('./pages/UserManagement'))
|
||||||
@ -80,6 +81,14 @@ function App() {
|
|||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="servers"
|
||||||
|
element={
|
||||||
|
<PermissionGuard requiredRole="SUPER_ADMIN">
|
||||||
|
<Suspense fallback={<PageLoading />}><ServersPage /></Suspense>
|
||||||
|
</PermissionGuard>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="audit"
|
path="audit"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import { RobotOutlined, DashboardOutlined,
|
import { CloudServerOutlined, RobotOutlined, DashboardOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
BookOutlined,
|
BookOutlined,
|
||||||
ImportOutlined,
|
ImportOutlined,
|
||||||
@ -47,6 +47,7 @@ export const adminMenuItems: AdminMenuItem[] = [
|
|||||||
{ path: '/ai-costs', name: 'AI 调用与成本', icon: <CloudOutlined /> },
|
{ path: '/ai-costs', name: 'AI 调用与成本', icon: <CloudOutlined /> },
|
||||||
{ path: '/files', name: '文件与 COS', icon: <FileOutlined /> },
|
{ path: '/files', name: '文件与 COS', icon: <FileOutlined /> },
|
||||||
{ path: '/settings', name: '系统配置', icon: <SettingOutlined />, requiredRole: 'ADMIN' },
|
{ path: '/settings', name: '系统配置', icon: <SettingOutlined />, requiredRole: 'ADMIN' },
|
||||||
|
{ path: '/servers', name: '服务器运维', icon: <CloudServerOutlined />, requiredRole: 'SUPER_ADMIN' },
|
||||||
{ path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' },
|
{ path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const breadcrumbMap: Record<string, string> = {
|
|||||||
'/ai-costs': 'AI 调用与成本',
|
'/ai-costs': 'AI 调用与成本',
|
||||||
'/files': '文件与 COS',
|
'/files': '文件与 COS',
|
||||||
'/settings': '系统配置',
|
'/settings': '系统配置',
|
||||||
|
'/servers': '服务器运维',
|
||||||
'/audit': '审计日志',
|
'/audit': '审计日志',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,14 @@ import * as echarts from 'echarts/core'
|
|||||||
import { LineChart, BarChart } from 'echarts/charts'
|
import { LineChart, BarChart } from 'echarts/charts'
|
||||||
import { GridComponent, TooltipComponent, TitleComponent, LegendComponent } from 'echarts/components'
|
import { GridComponent, TooltipComponent, TitleComponent, LegendComponent } from 'echarts/components'
|
||||||
import { CanvasRenderer } from 'echarts/renderers'
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
import { UserOutlined, BookOutlined, CloudOutlined, FileOutlined } from '@ant-design/icons'
|
import { UserOutlined, BookOutlined, CloudOutlined, FileOutlined, ClusterOutlined } from '@ant-design/icons'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import MetricCard from '@/components/MetricCard'
|
import MetricCard from '@/components/MetricCard'
|
||||||
|
import { Progress, Space } from 'antd'
|
||||||
|
const DText = Typography.Text
|
||||||
import EChartsChartContainer from '@/components/EChartsChartContainer'
|
import EChartsChartContainer from '@/components/EChartsChartContainer'
|
||||||
import { getDashboardStats } from '@/services/admin-api'
|
import { getDashboardStats } from '@/services/admin-api'
|
||||||
|
import { getServerMetrics } from '@/services/server-api'
|
||||||
|
|
||||||
echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, TitleComponent, LegendComponent, CanvasRenderer])
|
echarts.use([LineChart, BarChart, GridComponent, TooltipComponent, TitleComponent, LegendComponent, CanvasRenderer])
|
||||||
|
|
||||||
@ -27,6 +30,12 @@ export default function Dashboard() {
|
|||||||
staleTime: 60_000,
|
staleTime: 60_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { data: serverData } = useQuery({
|
||||||
|
queryKey: ['servers', 'metrics'],
|
||||||
|
queryFn: getServerMetrics,
|
||||||
|
refetchInterval: 10_000,
|
||||||
|
})
|
||||||
|
|
||||||
const userTrendOption = useMemo(() => ({
|
const userTrendOption = useMemo(() => ({
|
||||||
grid: { top: 20, right: 20, bottom: 20, left: 40 },
|
grid: { top: 20, right: 20, bottom: 20, left: 40 },
|
||||||
tooltip: { trigger: 'axis' as const },
|
tooltip: { trigger: 'axis' as const },
|
||||||
@ -56,6 +65,25 @@ export default function Dashboard() {
|
|||||||
<Col xs={24} lg={12}><EChartsChartContainer title="日活用户趋势(近 30 天)" loading={statsLoading} isEmpty={!stats?.userTrend?.length}><ReactEChartsCore echarts={echarts} option={userTrendOption} style={{ height: 300 }} notMerge lazyUpdate /></EChartsChartContainer></Col>
|
<Col xs={24} lg={12}><EChartsChartContainer title="日活用户趋势(近 30 天)" loading={statsLoading} isEmpty={!stats?.userTrend?.length}><ReactEChartsCore echarts={echarts} option={userTrendOption} style={{ height: 300 }} notMerge lazyUpdate /></EChartsChartContainer></Col>
|
||||||
<Col xs={24} lg={12}><EChartsChartContainer title="AI 调用趋势(近 30 天)" loading={statsLoading} isEmpty={!stats?.aiCallTrend?.length}><ReactEChartsCore echarts={echarts} option={aiCallTrendOption} style={{ height: 300 }} notMerge /></EChartsChartContainer></Col>
|
<Col xs={24} lg={12}><EChartsChartContainer title="AI 调用趋势(近 30 天)" loading={statsLoading} isEmpty={!stats?.aiCallTrend?.length}><ReactEChartsCore echarts={echarts} option={aiCallTrendOption} style={{ height: 300 }} notMerge /></EChartsChartContainer></Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||||
|
{serverData?.servers?.map(s => (
|
||||||
|
<Col xs={24} sm={12} lg={12} key={s.hostname}>
|
||||||
|
<div style={{ background: '#fff', borderRadius: 8, padding: '12px 16px', border: '1px solid #f0f0f0' }}>
|
||||||
|
<Space style={{ marginBottom: 8 }}><ClusterOutlined style={{ color: '#1677ff' }} /><DText strong>{s.name}</DText><DText type="secondary" style={{ fontSize: 12 }}>{s.network.ip}</DText></Space>
|
||||||
|
<Row gutter={12}>
|
||||||
|
<Col span={12}>
|
||||||
|
<DText type="secondary" style={{ fontSize: 11 }}>CPU {s.cpu.usagePercent}%</DText>
|
||||||
|
<Progress percent={s.cpu.usagePercent} size="small" strokeColor={s.cpu.usagePercent > 80 ? '#ff4d4f' : '#1677ff'} showInfo={false} />
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<DText type="secondary" style={{ fontSize: 11 }}>内存 {s.memory.percent}%</DText>
|
||||||
|
<Progress percent={s.memory.percent} size="small" strokeColor={s.memory.percent > 80 ? '#ff4d4f' : '#52c41a'} showInfo={false} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/pages/Servers.tsx
Normal file
75
src/pages/Servers.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { Card, Row, Col, Progress, Table, Tag, Typography, theme, Space } from 'antd'
|
||||||
|
import { CloudServerOutlined, DashboardOutlined } from '@ant-design/icons'
|
||||||
|
import { getServerMetrics, type ServerInfo } from '@/services/server-api'
|
||||||
|
|
||||||
|
const { Text, Title } = Typography
|
||||||
|
|
||||||
|
function ServerCard({ server }: { server: ServerInfo }) {
|
||||||
|
theme.useToken()
|
||||||
|
const cpuColor = server.cpu.usagePercent > 80 ? 'red' : server.cpu.usagePercent > 50 ? 'orange' : 'green'
|
||||||
|
const memColor = server.memory.percent > 80 ? 'red' : server.memory.percent > 50 ? 'orange' : 'green'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={<Space><CloudServerOutlined />{server.name}<Tag color="blue">{server.role}</Tag></Space>}
|
||||||
|
extra={<Text type="secondary">{server.network.ip}</Text>}
|
||||||
|
style={{ height: '100%' }}>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text type="secondary">CPU ({server.cpu.cores}核)</Text>
|
||||||
|
<Progress percent={server.cpu.usagePercent} strokeColor={cpuColor} size="small" />
|
||||||
|
<Text style={{ fontSize: 11 }} type="secondary">{server.cpu.model?.slice(0, 40)}</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text type="secondary">内存</Text>
|
||||||
|
<Progress percent={server.memory.percent} strokeColor={memColor} size="small" />
|
||||||
|
<Text style={{ fontSize: 11 }} type="secondary">{server.memory.used}/{server.memory.total}</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text type="secondary">磁盘</Text>
|
||||||
|
<Progress percent={server.disk.percent} strokeColor={server.disk.percent > 80 ? 'red' : 'blue'} size="small" />
|
||||||
|
<Text style={{ fontSize: 11 }} type="secondary">{server.disk.used}/{server.disk.total}</Text>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Text type="secondary">运行时间</Text>
|
||||||
|
<div><Text strong>{server.uptime}</Text></div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Table
|
||||||
|
dataSource={server.processes}
|
||||||
|
rowKey="pid"
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
columns={[
|
||||||
|
{ title: 'PID', dataIndex: 'pid', width: 70 },
|
||||||
|
{ title: 'CPU', dataIndex: 'cpu', width: 60 },
|
||||||
|
{ title: 'MEM', dataIndex: 'mem', width: 60 },
|
||||||
|
{ title: '命令', dataIndex: 'command', ellipsis: true },
|
||||||
|
]}
|
||||||
|
locale={{ emptyText: '暂无进程数据' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ServersPage() {
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ['servers', 'metrics'],
|
||||||
|
queryFn: getServerMetrics,
|
||||||
|
refetchInterval: 10_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Title level={5} style={{ marginTop: 0 }}><DashboardOutlined /> 服务器运维</Title>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{(data?.servers || []).map(s => (
|
||||||
|
<Col xs={24} lg={12} key={s.hostname}>
|
||||||
|
<ServerCard server={s} />
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
17
src/services/server-api.ts
Normal file
17
src/services/server-api.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { api } from './http-client'
|
||||||
|
|
||||||
|
export interface ServerInfo {
|
||||||
|
name: string
|
||||||
|
role: string
|
||||||
|
hostname: string
|
||||||
|
cpu: { model: string; cores: number; usagePercent: number; loadAvg: number[] }
|
||||||
|
memory: { total: string; used: string; free: string; percent: number }
|
||||||
|
disk: { total: string; used: string; free: string; percent: number }
|
||||||
|
uptime: string
|
||||||
|
network: { ip: string }
|
||||||
|
processes: { pid: number; cpu: string; mem: string; command: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerMetrics(): Promise<{ servers: ServerInfo[] }> {
|
||||||
|
return api.get('/admin-api/servers/metrics')
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user