import React, { useEffect, useState } from 'react' import Icon from '@ant-design/icons' import * as echarts from 'echarts/core' import { TooltipComponent, TooltipComponentOption, GridComponent, GridComponentOption, ToolboxComponentOption, DataZoomComponentOption, ToolboxComponent, DataZoomComponent } from 'echarts/components' import { BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts' import { UniversalTransition } from 'echarts/features' import { SVGRenderer } from 'echarts/renderers' import { TopLevelFormatterParams } from 'echarts/types/dist/shared' import '@/assets/css/pages/system/statistics.scss' import { useUpdatedEffect } from '@/util/hooks' import { formatByteSize } from '@/util/common' import { getTimesBetweenTwoTimes, utcToLocalTime } from '@/util/datetime' import { r_sys_statistics_active, r_sys_statistics_cpu, r_sys_statistics_hardware, r_sys_statistics_online, r_sys_statistics_software, r_sys_statistics_storage } from '@/services/system' import Card from '@/components/common/Card' import FlexBox from '@/components/common/FlexBox' import FitFullscreen from '@/components/common/FitFullscreen' import HideScrollbar from '@/components/common/HideScrollbar' import LoadingMask from '@/components/common/LoadingMask' import Permission from '@/components/common/Permission' echarts.use([ TooltipComponent, ToolboxComponent, GridComponent, DataZoomComponent, BarChart, LineChart, SVGRenderer, UniversalTransition ]) type EChartsOption = echarts.ComposeOption< | TooltipComponentOption | ToolboxComponentOption | GridComponentOption | BarSeriesOption | DataZoomComponentOption | LineSeriesOption > const barDefaultSeriesOption: BarSeriesOption = { type: 'bar', stack: 'total', itemStyle: { color: (params) => { switch (params.seriesName) { case 'idle': case 'free': return '#F5F5F5' default: return params.color ?? echarts.color.random() } } } } const barEChartsBaseOption: EChartsOption = { tooltip: {}, xAxis: { show: false }, yAxis: { axisLine: { show: false }, axisLabel: { show: false }, axisTick: { show: false }, splitLine: { show: false }, axisPointer: { show: false } } } const getTooltipTimeFormatter = (format: string = 'yyyy-MM-DD HH:mm:ss') => { return (params: TopLevelFormatterParams) => `${utcToLocalTime( // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access params[0].data[0], format )}
${ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access params[0]['marker'] // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access }${params[0]['seriesName']}${ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access params[0].data[1] } ` } const lineEChartsBaseOption: EChartsOption = { tooltip: { trigger: 'axis' }, toolbox: { feature: { dataZoom: { yAxisIndex: 'none' }, restore: {}, saveAsImage: {} } }, xAxis: { type: 'time' }, yAxis: { type: 'value', interval: 1 }, dataZoom: [ { type: 'inside', start: 0, end: 100, minValueSpan: 2 * 60 * 60 * 1000 } ], series: [{}] } interface CommonCardProps extends React.PropsWithChildren { icon: IconComponent title: React.ReactNode loading?: boolean expand?: React.ReactNode } const CommonCard: React.FC = (props) => { return (
{props.title}
{props.expand}
) } const OnlineInfo: React.FC = () => { const onlineInfoDivRef = useRef(null) const onlineInfoEChartsRef = useRef(null) const [isLoading, setIsLoading] = useState(false) const [currentOnlineCount, setCurrentOnlineCount] = useState(-1) const [scope, setScope] = useState('WEAK') useUpdatedEffect(() => { const chartResizeObserver = new ResizeObserver(() => { onlineInfoEChartsRef.current?.resize() }) onlineInfoDivRef.current && chartResizeObserver.observe(onlineInfoDivRef.current) return () => { onlineInfoDivRef.current && chartResizeObserver.unobserve(onlineInfoDivRef.current) } }, [isLoading]) useUpdatedEffect(() => { getOnlineInfo() }, []) const handleOnScopeChange = (value: string) => { setScope(value) getOnlineInfo(value) } const handleOnRefresh = () => { getOnlineInfo() } const getOnlineInfo = (_scope: string = scope) => { if (isLoading) { return } setIsLoading(true) setCurrentOnlineCount(-1) void r_sys_statistics_online({ scope: _scope }).then((res) => { const response = res.data if (response.success) { const data = response.data if (data) { setIsLoading(false) setCurrentOnlineCount(data.current) setTimeout(() => { const dataList = getTimesBetweenTwoTimes( data.history[0].time, data.history[data.history.length - 1].time, 'minute' ).map((time) => [ time, data.history.find( (value) => value.time.substring(0, 16) === time.substring(0, 16) )?.record ?? 0 ]) onlineInfoEChartsRef.current = echarts.init( onlineInfoDivRef.current, null, { renderer: 'svg' } ) onlineInfoEChartsRef.current?.setOption({ ...lineEChartsBaseOption, tooltip: { ...lineEChartsBaseOption.tooltip, formatter: getTooltipTimeFormatter('yyyy-MM-DD HH:mm') }, xAxis: { ...lineEChartsBaseOption.xAxis }, series: [ { name: '在线人数', type: 'line', smooth: true, symbol: 'none', areaStyle: {}, data: dataList } ] }) }) } } }) } return ( 在线用户 {currentOnlineCount === -1 ? '获取中...' : `当前 ${currentOnlineCount}`} } loading={isLoading} expand={ <> 今天 最近7天 最近30天 最近3个月 最近12个月 最近2年 最近3年 最近5年 全部 } >
) } const ActiveInfo: React.FC = () => { const activeInfoDivRef = useRef(null) const activeInfoEChartsRef = useRef(null) const [isLoading, setIsLoading] = useState(false) const [scope, setScope] = useState('WEAK') useUpdatedEffect(() => { const chartResizeObserver = new ResizeObserver(() => { activeInfoEChartsRef.current?.resize() }) activeInfoDivRef.current && chartResizeObserver.observe(activeInfoDivRef.current) return () => { activeInfoDivRef.current && chartResizeObserver.unobserve(activeInfoDivRef.current) } }, [isLoading]) useUpdatedEffect(() => { getActiveInfo() }, []) const handleOnScopeChange = (value: string) => { setScope(value) getActiveInfo(value) } const handleOnRefresh = () => { getActiveInfo() } const getActiveInfo = (_scope: string = scope) => { if (isLoading) { return } setIsLoading(true) void r_sys_statistics_active({ scope: _scope }).then((res) => { const response = res.data if (response.success) { const data = response.data if (data) { setIsLoading(false) setTimeout(() => { const registerList = data.registerHistory.length ? getTimesBetweenTwoTimes( data.registerHistory[0].time, data.registerHistory[data.registerHistory.length - 1].time, 'day' ).map((time) => [ time, data.registerHistory.find( (value) => value.time.substring(0, 10) === time.substring(0, 10) )?.count ?? 0 ]) : [] const loginList = data.loginHistory.length ? getTimesBetweenTwoTimes( data.loginHistory[0].time, data.loginHistory[data.loginHistory.length - 1].time, 'day' ).map((time) => [ time, data.loginHistory.find( (value) => value.time.substring(0, 10) === time.substring(0, 10) )?.count ?? 0 ]) : [] activeInfoEChartsRef.current = echarts.init( activeInfoDivRef.current, null, { renderer: 'svg' } ) activeInfoEChartsRef.current?.setOption({ ...lineEChartsBaseOption, tooltip: { ...lineEChartsBaseOption.tooltip, formatter: getTooltipTimeFormatter('yyyy-MM-DD') }, dataZoom: [ { type: 'inside', start: 0, end: 100, minValueSpan: 2 * 24 * 60 * 60 * 1000 } ], series: [ { name: '注册人数', type: 'line', smooth: true, symbol: 'none', areaStyle: {}, data: registerList }, { name: '登录人数', type: 'line', smooth: true, symbol: 'none', areaStyle: {}, data: loginList } ] }) }) } } }) } return ( 用户活跃 } loading={isLoading} expand={ <> 最近7天 最近30天 最近3个月 最近12个月 最近2年 最近3年 最近5年 全部 } >
) } const SoftwareInfo: React.FC = () => { const [softwareInfoData, setSoftwareInfoData] = useState() useUpdatedEffect(() => { void r_sys_statistics_software().then((res) => { const response = res.data if (response.success) { response.data && setSoftwareInfoData(response.data) } else { void message.error('获取软件信息失败,请稍后重试') } }) }, []) return (
操作系统
位数
Java
Java 供应商
Runtime
JVM
JVM 供应商
操作系统启动时间
后端服务器启动时间
{softwareInfoData?.os}
{softwareInfoData?.bitness}
{`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}
{softwareInfoData?.javaVendor}
{`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}
{`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}
{softwareInfoData?.jvmVendor}
{softwareInfoData?.osBootTime && utcToLocalTime(softwareInfoData?.osBootTime)}
{softwareInfoData?.serverStartupTime && utcToLocalTime(softwareInfoData.serverStartupTime)}
) } const HardwareInfo: React.FC = () => { const [hardwareInfoData, setHardwareInfoData] = useState() useUpdatedEffect(() => { void r_sys_statistics_hardware().then((res) => { const response = res.data if (response.success) { response.data && setHardwareInfoData(response.data) } else { void message.error('获取硬件信息失败,请稍后重试') } }) }, []) return (
CPU
CPU 架构
微架构
64位
物理 CPU
物理核心
逻辑核心
内存
磁盘
{hardwareInfoData?.cpu}
{hardwareInfoData?.arch}
{hardwareInfoData?.microarchitecture}
{hardwareInfoData?.is64Bit ? '是' : '否'}
{hardwareInfoData?.cpuPhysicalPackageCount}
{hardwareInfoData?.cpuPhysicalProcessorCount}
{hardwareInfoData?.cpuLogicalProcessorCount}
{hardwareInfoData?.memories}
{hardwareInfoData?.disks}
) } const CPUInfo: React.FC = () => { const keyDivRef = useRef(null) const percentDivRef = useRef(null) const cpuInfoDivRef = useRef(null) const cpuInfoEChartsRef = useRef([]) const [isLoading, setIsLoading] = useState(true) const [refreshInterval, setRefreshInterval] = useState('5') const [cpuInfoEChartsOption, setCpuInfoEChartsOption] = useState([]) const cpuDefaultSeriesOption: BarSeriesOption = { ...barDefaultSeriesOption, tooltip: { valueFormatter: (value) => `${((value as number) * 100).toFixed(2)}%` } } useUpdatedEffect(() => { const chartResizeObserver = new ResizeObserver(() => { cpuInfoEChartsRef.current.forEach((value) => value.resize()) }) cpuInfoDivRef.current && chartResizeObserver.observe(cpuInfoDivRef.current) return () => { cpuInfoDivRef.current && chartResizeObserver.unobserve(cpuInfoDivRef.current) } }, [cpuInfoDivRef.current]) useUpdatedEffect(() => { const intervalId = setInterval(getCpuInfo(), parseInt(refreshInterval) * 1000) return () => { clearInterval(intervalId) } }, [refreshInterval]) const getCpuInfo = () => { void r_sys_statistics_cpu().then((res) => { const response = res.data if (response.success) { const data = response.data if (data) { if (isLoading) { setIsLoading(false) } setTimeout(() => { const dataList = data.processors.map((value) => cpuInfoVoToCpuInfoData(value) ) dataList.unshift(cpuInfoVoToCpuInfoData(data)) setCpuInfoEChartsOption( dataList.map((value, index) => ({ ...barEChartsBaseOption, yAxis: { ...barEChartsBaseOption.yAxis, data: [index === 0 ? '总占用' : `CPU ${index - 1}`] }, series: value })) ) if (percentDivRef.current) { percentDivRef.current.innerHTML = '' dataList.forEach((value) => { const percentElement = document.createElement('div') const idle = value.find((item) => item.name === 'idle')?.data[0] percentElement.innerText = idle !== undefined ? `${((1 - idle) * 100).toFixed(2)}%` : 'Unknown' percentDivRef.current?.appendChild(percentElement) }) } if (!cpuInfoEChartsRef.current.length) { keyDivRef.current && (keyDivRef.current.innerHTML = '') cpuInfoDivRef.current && (cpuInfoDivRef.current.innerHTML = '') for (let i = 0; i < dataList.length; i++) { const keyElement = document.createElement('div') keyElement.innerText = i === 0 ? '总占用' : `CPU ${i - 1}` keyDivRef.current?.appendChild(keyElement) const valueElement = document.createElement('div') cpuInfoDivRef.current?.appendChild(valueElement) cpuInfoEChartsRef.current.push( echarts.init(valueElement, null, { renderer: 'svg' }) ) } } }) } } }) return getCpuInfo } const cpuInfoVoToCpuInfoData = (cpuInfoVo: CpuInfoVo) => Object.entries(cpuInfoVo) .filter(([key]) => !['total', 'processors'].includes(key)) .map(([key, value]) => ({ ...cpuDefaultSeriesOption, name: key, data: [(value as number) / cpuInfoVo.total] })) .sort((a, b) => { const order = [ 'steal', 'irq', 'softirq', 'iowait', 'system', 'nice', 'user', 'idle' ] return order.indexOf(a.name) - order.indexOf(b.name) }) useEffect(() => { cpuInfoEChartsRef.current?.forEach((value, index) => { try { value.setOption(cpuInfoEChartsOption[index]) } catch (e) { /* empty */ } }) }, [cpuInfoEChartsOption]) return ( <> setRefreshInterval(value)} > 1秒 2秒 3秒 5秒 10秒 15秒 20秒 30秒 60秒 2分 3分 5分 10分 } > ) } const StorageInfo: React.FC = () => { const keyDivRef = useRef(null) const percentDivRef = useRef(null) const storageInfoDivRef = useRef(null) const storageInfoEChartsRef = useRef([]) const [isLoading, setIsLoading] = useState(true) const [refreshInterval, setRefreshInterval] = useState('5') const [storageInfoEChartsOption, setStorageInfoEChartsOption] = useState([]) const storageDefaultSeriesOption: BarSeriesOption = { ...barDefaultSeriesOption, tooltip: { valueFormatter: (value) => formatByteSize(value as number) } } useUpdatedEffect(() => { const chartResizeObserver = new ResizeObserver(() => { storageInfoEChartsRef.current.forEach((value) => value.resize()) }) storageInfoDivRef.current && chartResizeObserver.observe(storageInfoDivRef.current) return () => { storageInfoDivRef.current && chartResizeObserver.unobserve(storageInfoDivRef.current) } }, [storageInfoDivRef.current]) useUpdatedEffect(() => { const intervalId = setInterval(getStorageInfo(), parseInt(refreshInterval) * 1000) return () => { clearInterval(intervalId) } }, [refreshInterval]) const getStorageInfo = () => { void r_sys_statistics_storage().then((res) => { const response = res.data if (response.success) { const data = response.data if (data) { if (isLoading) { setIsLoading(false) } setTimeout(() => { const eChartsOptions = [ storageInfoVoToStorageEChartsOption( '物理内存', data.memoryTotal - data.memoryFree, data.memoryFree ), storageInfoVoToStorageEChartsOption( '虚拟内存', data.virtualMemoryInUse, data.virtualMemoryMax - data.virtualMemoryInUse ), storageInfoVoToStorageEChartsOption( 'swap', data.swapUsed, data.swapTotal - data.swapUsed ), storageInfoVoToStorageEChartsOption( 'jvm 内存', data.jvmTotal - data.jvmFree, data.jvmFree ) ] data.fileStores.forEach((value) => eChartsOptions.push( storageInfoVoToStorageEChartsOption( value.mount, value.total - value.free, value.free ) ) ) setStorageInfoEChartsOption(eChartsOptions) if (percentDivRef.current && keyDivRef.current) { keyDivRef.current.innerHTML = '' percentDivRef.current.innerHTML = '' eChartsOptions.forEach((value) => { const keyElement = document.createElement('div') const percentElement = document.createElement('div') keyElement.innerText = value.yAxis.data[0] percentElement.innerText = `${( (value.series[0].data[0] / (value.series[0].data[0] + value.series[1].data[0])) * 100 ).toFixed(2)}%` keyDivRef.current?.appendChild(keyElement) percentDivRef.current?.appendChild(percentElement) }) } if (!storageInfoEChartsRef.current.length) { storageInfoDivRef.current && (storageInfoDivRef.current.innerHTML = '') eChartsOptions.forEach(() => { const element = document.createElement('div') storageInfoDivRef.current?.appendChild(element) storageInfoEChartsRef.current.push( echarts.init(element, null, { renderer: 'svg' }) ) }) } }) } } }) return getStorageInfo } const storageInfoVoToStorageEChartsOption = (label: string, used: number, free: number) => ({ ...barEChartsBaseOption, xAxis: { ...barEChartsBaseOption.xAxis, max: used + free }, yAxis: { ...barEChartsBaseOption.yAxis, data: [label] }, series: [ { ...storageDefaultSeriesOption, name: 'used', data: [used] }, { ...storageDefaultSeriesOption, name: 'free', data: [free] } ] }) useEffect(() => { storageInfoEChartsRef.current?.forEach((value, index) => { try { value.setOption(storageInfoEChartsOption[index]) } catch (e) { /* empty */ } }) }, [storageInfoEChartsOption]) return ( <> setRefreshInterval(value)} > 1秒 2秒 3秒 5秒 10秒 15秒 20秒 30秒 60秒 2分 3分 5分 10分 } > ) } const Statistics: React.FC = () => { return ( <> ) } export default Statistics