Complete main UI #37

Merged
FatttSnake merged 192 commits from FatttSnake into dev 2024-02-23 16:31:17 +08:00
10 changed files with 1057 additions and 994 deletions
Showing only changes of commit d67aea2895 - Show all commits

View File

@@ -1,993 +0,0 @@
import React, { useEffect, useState } from 'react'
import Icon from '@ant-design/icons'
import * as echarts from 'echarts/core'
import {
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
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 { CallbackDataParams } 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,
LegendComponent,
DataZoomComponent,
BarChart,
LineChart,
SVGRenderer,
UniversalTransition
])
type EChartsOption = echarts.ComposeOption<
| TooltipComponentOption
| ToolboxComponentOption
| GridComponentOption
| LegendComponentOption
| 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: CallbackDataParams[]) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
`${utcToLocalTime(params[0].data[0], format)}<br>${params
.map(
(param) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`<span style="display: flex; justify-content: space-between;"><span>${param.marker}${param.seriesName}</span><span style="font-weight: bold; margin-left: 16px;">${param.data[1]}</span></span>`
)
.join('')}`
}
const lineEChartsBaseOption: EChartsOption = {
tooltip: {
trigger: 'axis'
},
legend: {},
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<CommonCardProps> = (props) => {
return (
<Card style={{ overflow: 'visible' }}>
<FlexBox className={'common-card'}>
<FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} />
<div className={'title'}>{props.title}</div>
{props.expand}
</FlexBox>
<LoadingMask
hidden={!props.loading}
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
>
{props.children}
</LoadingMask>
</FlexBox>
</Card>
)
}
const OnlineInfo: React.FC = () => {
const onlineInfoDivRef = useRef<HTMLDivElement>(null)
const onlineInfoEChartsRef = useRef<echarts.EChartsType | null>(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 (
<CommonCard
icon={IconOxygenOnline}
title={
<>
<FlexBox gap={10} direction={'horizontal'}>
<span style={{ whiteSpace: 'nowrap' }}>线</span>
<AntdTag>
{currentOnlineCount === -1 ? '获取中...' : `当前 ${currentOnlineCount}`}
</AntdTag>
</FlexBox>
</>
}
loading={isLoading}
expand={
<>
<AntdSelect
value={scope}
onChange={handleOnScopeChange}
disabled={isLoading}
style={{ width: '8em' }}
>
<AntdSelect.Option key={'DAY'}></AntdSelect.Option>
<AntdSelect.Option key={'WEAK'}>7</AntdSelect.Option>
<AntdSelect.Option key={'MONTH'}>30</AntdSelect.Option>
<AntdSelect.Option key={'QUARTER'}>3</AntdSelect.Option>
<AntdSelect.Option key={'YEAR'}>12</AntdSelect.Option>
<AntdSelect.Option key={'TWO_YEARS'}>2</AntdSelect.Option>
<AntdSelect.Option key={'THREE_YEARS'}>3</AntdSelect.Option>
<AntdSelect.Option key={'FIVE_YEARS'}>5</AntdSelect.Option>
<AntdSelect.Option key={'ALL'}></AntdSelect.Option>
</AntdSelect>
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
<Icon component={IconOxygenRefresh} />
</AntdButton>
</>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<div className={'big-chart'} ref={onlineInfoDivRef} />
</FlexBox>
</CommonCard>
)
}
const ActiveInfo: React.FC = () => {
const activeInfoDivRef = useRef<HTMLDivElement>(null)
const activeInfoEChartsRef = useRef<echarts.EChartsType | null>(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
])
: []
const verifyList = data.verifyHistory.length
? getTimesBetweenTwoTimes(
data.verifyHistory[0].time,
data.verifyHistory[data.verifyHistory.length - 1].time,
'day'
).map((time) => [
time,
data.verifyHistory.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,
useUTC: true,
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
},
{
name: '验证账号人数',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {},
data: verifyList
}
]
})
})
}
}
})
}
return (
<CommonCard
icon={IconOxygenAnalysis}
title={
<>
<FlexBox gap={10} direction={'horizontal'}>
<span style={{ whiteSpace: 'nowrap' }}></span>
</FlexBox>
</>
}
loading={isLoading}
expand={
<>
<AntdSelect
value={scope}
onChange={handleOnScopeChange}
disabled={isLoading}
style={{ width: '8em' }}
>
<AntdSelect.Option key={'WEAK'}>7</AntdSelect.Option>
<AntdSelect.Option key={'MONTH'}>30</AntdSelect.Option>
<AntdSelect.Option key={'QUARTER'}>3</AntdSelect.Option>
<AntdSelect.Option key={'YEAR'}>12</AntdSelect.Option>
<AntdSelect.Option key={'TWO_YEARS'}>2</AntdSelect.Option>
<AntdSelect.Option key={'THREE_YEARS'}>3</AntdSelect.Option>
<AntdSelect.Option key={'FIVE_YEARS'}>5</AntdSelect.Option>
<AntdSelect.Option key={'ALL'}></AntdSelect.Option>
</AntdSelect>
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
<Icon component={IconOxygenRefresh} />
</AntdButton>
</>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<div className={'big-chart'} ref={activeInfoDivRef} />
</FlexBox>
</CommonCard>
)
}
const SoftwareInfo: React.FC = () => {
const [softwareInfoData, setSoftwareInfoData] = useState<SoftwareInfoVo>()
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 (
<CommonCard
icon={IconOxygenSoftware}
title={'软件信息'}
loading={softwareInfoData === undefined}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'}>
<div></div>
<div></div>
<div>Java</div>
<div>Java </div>
<div>Runtime</div>
<div>JVM</div>
<div>JVM </div>
<div></div>
<div></div>
</FlexBox>
<FlexBox className={'value'}>
<div title={softwareInfoData?.os}>{softwareInfoData?.os}</div>
<div title={softwareInfoData?.bitness.toString()}>
{softwareInfoData?.bitness}
</div>
<div
title={`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}
>{`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}</div>
<div title={softwareInfoData?.javaVendor}>{softwareInfoData?.javaVendor}</div>
<div
title={`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}
>{`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}</div>
<div
title={`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}
>{`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}</div>
<div title={softwareInfoData?.jvmVendor}>{softwareInfoData?.jvmVendor}</div>
<div>
{softwareInfoData?.osBootTime &&
utcToLocalTime(softwareInfoData?.osBootTime)}
</div>
<div>
{softwareInfoData?.serverStartupTime &&
utcToLocalTime(softwareInfoData.serverStartupTime)}
</div>
</FlexBox>
</FlexBox>
</CommonCard>
)
}
const HardwareInfo: React.FC = () => {
const [hardwareInfoData, setHardwareInfoData] = useState<HardwareInfoVo>()
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 (
<CommonCard
icon={IconOxygenHardware}
title={'硬件信息'}
loading={hardwareInfoData === undefined}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'}>
<div>CPU</div>
<div>CPU </div>
<div></div>
<div>64</div>
<div> CPU</div>
<div></div>
<div></div>
<div></div>
<div></div>
</FlexBox>
<FlexBox className={'value'}>
<div title={hardwareInfoData?.cpu}>{hardwareInfoData?.cpu}</div>
<div title={hardwareInfoData?.arch}>{hardwareInfoData?.arch}</div>
<div title={hardwareInfoData?.microarchitecture}>
{hardwareInfoData?.microarchitecture}
</div>
<div title={hardwareInfoData?.is64Bit ? '是' : '否'}>
{hardwareInfoData?.is64Bit ? '是' : '否'}
</div>
<div title={hardwareInfoData?.cpuPhysicalPackageCount.toString()}>
{hardwareInfoData?.cpuPhysicalPackageCount}
</div>
<div title={hardwareInfoData?.cpuPhysicalProcessorCount.toString()}>
{hardwareInfoData?.cpuPhysicalProcessorCount}
</div>
<div title={hardwareInfoData?.cpuLogicalProcessorCount.toString()}>
{hardwareInfoData?.cpuLogicalProcessorCount}
</div>
<div title={hardwareInfoData?.memories}>{hardwareInfoData?.memories}</div>
<div title={hardwareInfoData?.disks}>{hardwareInfoData?.disks}</div>
</FlexBox>
</FlexBox>
</CommonCard>
)
}
const CPUInfo: React.FC = () => {
const keyDivRef = useRef<HTMLDivElement>(null)
const percentDivRef = useRef<HTMLDivElement>(null)
const cpuInfoDivRef = useRef<HTMLDivElement>(null)
const cpuInfoEChartsRef = useRef<echarts.EChartsType[]>([])
const [isLoading, setIsLoading] = useState(true)
const [refreshInterval, setRefreshInterval] = useState('5')
const [cpuInfoEChartsOption, setCpuInfoEChartsOption] = useState<EChartsOption[]>([])
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 (
<>
<CommonCard
icon={IconOxygenCpu}
title={'CPU 信息'}
loading={isLoading}
expand={
<AntdSelect
value={refreshInterval}
onChange={(value) => setRefreshInterval(value)}
>
<AntdSelect.Option key={1}>1</AntdSelect.Option>
<AntdSelect.Option key={2}>2</AntdSelect.Option>
<AntdSelect.Option key={3}>3</AntdSelect.Option>
<AntdSelect.Option key={5}>5</AntdSelect.Option>
<AntdSelect.Option key={10}>10</AntdSelect.Option>
<AntdSelect.Option key={15}>15</AntdSelect.Option>
<AntdSelect.Option key={20}>20</AntdSelect.Option>
<AntdSelect.Option key={30}>30</AntdSelect.Option>
<AntdSelect.Option key={60}>60</AntdSelect.Option>
<AntdSelect.Option key={120}>2</AntdSelect.Option>
<AntdSelect.Option key={180}>3</AntdSelect.Option>
<AntdSelect.Option key={300}>5</AntdSelect.Option>
<AntdSelect.Option key={600}>10</AntdSelect.Option>
</AntdSelect>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'} ref={keyDivRef} />
<FlexBox className={'value-chart'} ref={cpuInfoDivRef} />
<FlexBox className={'value-percent'} ref={percentDivRef} />
</FlexBox>
</CommonCard>
</>
)
}
const StorageInfo: React.FC = () => {
const keyDivRef = useRef<HTMLDivElement>(null)
const percentDivRef = useRef<HTMLDivElement>(null)
const storageInfoDivRef = useRef<HTMLDivElement>(null)
const storageInfoEChartsRef = useRef<echarts.EChartsType[]>([])
const [isLoading, setIsLoading] = useState(true)
const [refreshInterval, setRefreshInterval] = useState('5')
const [storageInfoEChartsOption, setStorageInfoEChartsOption] = useState<EChartsOption[]>([])
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 (
<>
<CommonCard
icon={IconOxygenMemory}
title={'内存信息'}
loading={isLoading}
expand={
<AntdSelect
value={refreshInterval}
onChange={(value) => setRefreshInterval(value)}
>
<AntdSelect.Option key={1}>1</AntdSelect.Option>
<AntdSelect.Option key={2}>2</AntdSelect.Option>
<AntdSelect.Option key={3}>3</AntdSelect.Option>
<AntdSelect.Option key={5}>5</AntdSelect.Option>
<AntdSelect.Option key={10}>10</AntdSelect.Option>
<AntdSelect.Option key={15}>15</AntdSelect.Option>
<AntdSelect.Option key={20}>20</AntdSelect.Option>
<AntdSelect.Option key={30}>30</AntdSelect.Option>
<AntdSelect.Option key={60}>60</AntdSelect.Option>
<AntdSelect.Option key={120}>2</AntdSelect.Option>
<AntdSelect.Option key={180}>3</AntdSelect.Option>
<AntdSelect.Option key={300}>5</AntdSelect.Option>
<AntdSelect.Option key={600}>10</AntdSelect.Option>
</AntdSelect>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'} ref={keyDivRef} />
<FlexBox className={'value-chart'} ref={storageInfoDivRef} />
<FlexBox className={'value-percent'} ref={percentDivRef} />
</FlexBox>
</CommonCard>
</>
)
}
const Statistics: React.FC = () => {
return (
<>
<FitFullscreen data-component={'system-statistics'}>
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}>
<FlexBox direction={'horizontal'} className={'root-content'}>
<Permission operationCode={'system:statistics:query:usage'}>
<OnlineInfo />
<ActiveInfo />
</Permission>
<Permission operationCode={'system:statistics:query:base'}>
<HardwareInfo />
<SoftwareInfo />
</Permission>
<Permission operationCode={'system:statistics:query:real'}>
<CPUInfo />
<StorageInfo />
</Permission>
</FlexBox>
</HideScrollbar>
</FitFullscreen>
</>
)
}
export default Statistics

View File

@@ -0,0 +1,192 @@
import React from 'react'
import Icon from '@ant-design/icons'
import * as echarts from 'echarts/core'
import { useUpdatedEffect } from '@/util/hooks'
import { getTimesBetweenTwoTimes } from '@/util/datetime'
import { r_sys_statistics_active } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { getTooltipTimeFormatter, lineEChartsBaseOption } from '@/pages/system/statistics/shared'
import { CommonCard } from '@/pages/system/statistics'
const ActiveInfo: React.FC = () => {
const activeInfoDivRef = useRef<HTMLDivElement>(null)
const activeInfoEChartsRef = useRef<echarts.EChartsType | null>(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
])
: []
const verifyList = data.verifyHistory.length
? getTimesBetweenTwoTimes(
data.verifyHistory[0].time,
data.verifyHistory[data.verifyHistory.length - 1].time,
'day'
).map((time) => [
time,
data.verifyHistory.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,
useUTC: true,
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
},
{
name: '验证账号人数',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {},
data: verifyList
}
]
})
})
}
}
})
}
return (
<CommonCard
icon={IconOxygenAnalysis}
title={
<>
<FlexBox gap={10} direction={'horizontal'}>
<span style={{ whiteSpace: 'nowrap' }}></span>
</FlexBox>
</>
}
loading={isLoading}
expand={
<>
<AntdSelect
value={scope}
onChange={handleOnScopeChange}
disabled={isLoading}
style={{ width: '8em' }}
>
<AntdSelect.Option key={'WEAK'}>7</AntdSelect.Option>
<AntdSelect.Option key={'MONTH'}>30</AntdSelect.Option>
<AntdSelect.Option key={'QUARTER'}>3</AntdSelect.Option>
<AntdSelect.Option key={'YEAR'}>12</AntdSelect.Option>
<AntdSelect.Option key={'TWO_YEARS'}>2</AntdSelect.Option>
<AntdSelect.Option key={'THREE_YEARS'}>3</AntdSelect.Option>
<AntdSelect.Option key={'FIVE_YEARS'}>5</AntdSelect.Option>
<AntdSelect.Option key={'ALL'}></AntdSelect.Option>
</AntdSelect>
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
<Icon component={IconOxygenRefresh} />
</AntdButton>
</>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<div className={'big-chart'} ref={activeInfoDivRef} />
</FlexBox>
</CommonCard>
)
}
export default ActiveInfo

View File

@@ -0,0 +1,182 @@
import React from 'react'
import * as echarts from 'echarts/core'
import { BarSeriesOption } from 'echarts/charts'
import { useUpdatedEffect } from '@/util/hooks'
import { r_sys_statistics_cpu } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import {
barDefaultSeriesOption,
barEChartsBaseOption,
EChartsOption
} from '@/pages/system/statistics/shared'
import { CommonCard } from '@/pages/system/statistics'
const CPUInfo: React.FC = () => {
const keyDivRef = useRef<HTMLDivElement>(null)
const percentDivRef = useRef<HTMLDivElement>(null)
const cpuInfoDivRef = useRef<HTMLDivElement>(null)
const cpuInfoEChartsRef = useRef<echarts.EChartsType[]>([])
const [isLoading, setIsLoading] = useState(true)
const [refreshInterval, setRefreshInterval] = useState('5')
const [cpuInfoEChartsOption, setCpuInfoEChartsOption] = useState<EChartsOption[]>([])
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 (
<>
<CommonCard
icon={IconOxygenCpu}
title={'CPU 信息'}
loading={isLoading}
expand={
<AntdSelect
value={refreshInterval}
onChange={(value) => setRefreshInterval(value)}
>
<AntdSelect.Option key={1}>1</AntdSelect.Option>
<AntdSelect.Option key={2}>2</AntdSelect.Option>
<AntdSelect.Option key={3}>3</AntdSelect.Option>
<AntdSelect.Option key={5}>5</AntdSelect.Option>
<AntdSelect.Option key={10}>10</AntdSelect.Option>
<AntdSelect.Option key={15}>15</AntdSelect.Option>
<AntdSelect.Option key={20}>20</AntdSelect.Option>
<AntdSelect.Option key={30}>30</AntdSelect.Option>
<AntdSelect.Option key={60}>60</AntdSelect.Option>
<AntdSelect.Option key={120}>2</AntdSelect.Option>
<AntdSelect.Option key={180}>3</AntdSelect.Option>
<AntdSelect.Option key={300}>5</AntdSelect.Option>
<AntdSelect.Option key={600}>10</AntdSelect.Option>
</AntdSelect>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'} ref={keyDivRef} />
<FlexBox className={'value-chart'} ref={cpuInfoDivRef} />
<FlexBox className={'value-percent'} ref={percentDivRef} />
</FlexBox>
</CommonCard>
</>
)
}
export default CPUInfo

View File

@@ -0,0 +1,65 @@
import React from 'react'
import { useUpdatedEffect } from '@/util/hooks'
import { r_sys_statistics_hardware } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { CommonCard } from '@/pages/system/statistics'
const HardwareInfo: React.FC = () => {
const [hardwareInfoData, setHardwareInfoData] = useState<HardwareInfoVo>()
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 (
<CommonCard
icon={IconOxygenHardware}
title={'硬件信息'}
loading={hardwareInfoData === undefined}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'}>
<div>CPU</div>
<div>CPU </div>
<div></div>
<div>64</div>
<div> CPU</div>
<div></div>
<div></div>
<div></div>
<div></div>
</FlexBox>
<FlexBox className={'value'}>
<div title={hardwareInfoData?.cpu}>{hardwareInfoData?.cpu}</div>
<div title={hardwareInfoData?.arch}>{hardwareInfoData?.arch}</div>
<div title={hardwareInfoData?.microarchitecture}>
{hardwareInfoData?.microarchitecture}
</div>
<div title={hardwareInfoData?.is64Bit ? '是' : '否'}>
{hardwareInfoData?.is64Bit ? '是' : '否'}
</div>
<div title={hardwareInfoData?.cpuPhysicalPackageCount.toString()}>
{hardwareInfoData?.cpuPhysicalPackageCount}
</div>
<div title={hardwareInfoData?.cpuPhysicalProcessorCount.toString()}>
{hardwareInfoData?.cpuPhysicalProcessorCount}
</div>
<div title={hardwareInfoData?.cpuLogicalProcessorCount.toString()}>
{hardwareInfoData?.cpuLogicalProcessorCount}
</div>
<div title={hardwareInfoData?.memories}>{hardwareInfoData?.memories}</div>
<div title={hardwareInfoData?.disks}>{hardwareInfoData?.disks}</div>
</FlexBox>
</FlexBox>
</CommonCard>
)
}
export default HardwareInfo

View File

@@ -0,0 +1,149 @@
import React from 'react'
import Icon from '@ant-design/icons'
import * as echarts from 'echarts/core'
import { useUpdatedEffect } from '@/util/hooks'
import { getTimesBetweenTwoTimes } from '@/util/datetime'
import { r_sys_statistics_online } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { getTooltipTimeFormatter, lineEChartsBaseOption } from '@/pages/system/statistics/shared'
import { CommonCard } from '@/pages/system/statistics'
const OnlineInfo: React.FC = () => {
const onlineInfoDivRef = useRef<HTMLDivElement>(null)
const onlineInfoEChartsRef = useRef<echarts.EChartsType | null>(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 (
<CommonCard
icon={IconOxygenOnline}
title={
<>
<FlexBox gap={10} direction={'horizontal'}>
<span style={{ whiteSpace: 'nowrap' }}>线</span>
<AntdTag>
{currentOnlineCount === -1 ? '获取中...' : `当前 ${currentOnlineCount}`}
</AntdTag>
</FlexBox>
</>
}
loading={isLoading}
expand={
<>
<AntdSelect
value={scope}
onChange={handleOnScopeChange}
disabled={isLoading}
style={{ width: '8em' }}
>
<AntdSelect.Option key={'DAY'}></AntdSelect.Option>
<AntdSelect.Option key={'WEAK'}>7</AntdSelect.Option>
<AntdSelect.Option key={'MONTH'}>30</AntdSelect.Option>
<AntdSelect.Option key={'QUARTER'}>3</AntdSelect.Option>
<AntdSelect.Option key={'YEAR'}>12</AntdSelect.Option>
<AntdSelect.Option key={'TWO_YEARS'}>2</AntdSelect.Option>
<AntdSelect.Option key={'THREE_YEARS'}>3</AntdSelect.Option>
<AntdSelect.Option key={'FIVE_YEARS'}>5</AntdSelect.Option>
<AntdSelect.Option key={'ALL'}></AntdSelect.Option>
</AntdSelect>
<AntdButton title={'刷新'} onClick={handleOnRefresh} disabled={isLoading}>
<Icon component={IconOxygenRefresh} />
</AntdButton>
</>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<div className={'big-chart'} ref={onlineInfoDivRef} />
</FlexBox>
</CommonCard>
)
}
export default OnlineInfo

View File

@@ -0,0 +1,70 @@
import React from 'react'
import { useUpdatedEffect } from '@/util/hooks'
import { utcToLocalTime } from '@/util/datetime'
import { r_sys_statistics_software } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import { CommonCard } from '@/pages/system/statistics'
const SoftwareInfo: React.FC = () => {
const [softwareInfoData, setSoftwareInfoData] = useState<SoftwareInfoVo>()
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 (
<CommonCard
icon={IconOxygenSoftware}
title={'软件信息'}
loading={softwareInfoData === undefined}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'}>
<div></div>
<div></div>
<div>Java</div>
<div>Java </div>
<div>Runtime</div>
<div>JVM</div>
<div>JVM </div>
<div></div>
<div></div>
</FlexBox>
<FlexBox className={'value'}>
<div title={softwareInfoData?.os}>{softwareInfoData?.os}</div>
<div title={softwareInfoData?.bitness.toString()}>
{softwareInfoData?.bitness}
</div>
<div
title={`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}
>{`${softwareInfoData?.javaVersion} (${softwareInfoData?.javaVersionDate})`}</div>
<div title={softwareInfoData?.javaVendor}>{softwareInfoData?.javaVendor}</div>
<div
title={`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}
>{`${softwareInfoData?.javaRuntime} (build ${softwareInfoData?.javaRuntimeVersion})`}</div>
<div
title={`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}
>{`${softwareInfoData?.jvm} (build ${softwareInfoData?.jvmVersion}, ${softwareInfoData?.jvmInfo})`}</div>
<div title={softwareInfoData?.jvmVendor}>{softwareInfoData?.jvmVendor}</div>
<div>
{softwareInfoData?.osBootTime &&
utcToLocalTime(softwareInfoData?.osBootTime)}
</div>
<div>
{softwareInfoData?.serverStartupTime &&
utcToLocalTime(softwareInfoData.serverStartupTime)}
</div>
</FlexBox>
</FlexBox>
</CommonCard>
)
}
export default SoftwareInfo

View File

@@ -0,0 +1,201 @@
import React, { useEffect, useState } from 'react'
import * as echarts from 'echarts/core'
import { BarSeriesOption } from 'echarts/charts'
import { formatByteSize } from '@/util/common'
import { useUpdatedEffect } from '@/util/hooks'
import { r_sys_statistics_storage } from '@/services/system'
import FlexBox from '@/components/common/FlexBox'
import {
barDefaultSeriesOption,
barEChartsBaseOption,
EChartsOption
} from '@/pages/system/statistics/shared'
import { CommonCard } from '@/pages/system/statistics'
const StorageInfo: React.FC = () => {
const keyDivRef = useRef<HTMLDivElement>(null)
const percentDivRef = useRef<HTMLDivElement>(null)
const storageInfoDivRef = useRef<HTMLDivElement>(null)
const storageInfoEChartsRef = useRef<echarts.EChartsType[]>([])
const [isLoading, setIsLoading] = useState(true)
const [refreshInterval, setRefreshInterval] = useState('5')
const [storageInfoEChartsOption, setStorageInfoEChartsOption] = useState<EChartsOption[]>([])
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 (
<>
<CommonCard
icon={IconOxygenMemory}
title={'内存信息'}
loading={isLoading}
expand={
<AntdSelect
value={refreshInterval}
onChange={(value) => setRefreshInterval(value)}
>
<AntdSelect.Option key={1}>1</AntdSelect.Option>
<AntdSelect.Option key={2}>2</AntdSelect.Option>
<AntdSelect.Option key={3}>3</AntdSelect.Option>
<AntdSelect.Option key={5}>5</AntdSelect.Option>
<AntdSelect.Option key={10}>10</AntdSelect.Option>
<AntdSelect.Option key={15}>15</AntdSelect.Option>
<AntdSelect.Option key={20}>20</AntdSelect.Option>
<AntdSelect.Option key={30}>30</AntdSelect.Option>
<AntdSelect.Option key={60}>60</AntdSelect.Option>
<AntdSelect.Option key={120}>2</AntdSelect.Option>
<AntdSelect.Option key={180}>3</AntdSelect.Option>
<AntdSelect.Option key={300}>5</AntdSelect.Option>
<AntdSelect.Option key={600}>10</AntdSelect.Option>
</AntdSelect>
}
>
<FlexBox className={'card-content'} direction={'horizontal'}>
<FlexBox className={'key'} ref={keyDivRef} />
<FlexBox className={'value-chart'} ref={storageInfoDivRef} />
<FlexBox className={'value-percent'} ref={percentDivRef} />
</FlexBox>
</CommonCard>
</>
)
}
export default StorageInfo

View File

@@ -0,0 +1,69 @@
import React from 'react'
import Icon from '@ant-design/icons'
import '@/assets/css/pages/system/statistics.scss'
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'
import OnlineInfo from '@/pages/system/statistics/OnlineInfo'
import ActiveInfo from '@/pages/system/statistics/ActiveInfo'
import SoftwareInfo from '@/pages/system/statistics/SoftwareInfo'
import HardwareInfo from '@/pages/system/statistics/HardwareInfo'
import CPUInfo from '@/pages/system/statistics/CPUInfo'
import StorageInfo from '@/pages/system/statistics/StorageInfo'
interface CommonCardProps extends React.PropsWithChildren {
icon: IconComponent
title: React.ReactNode
loading?: boolean
expand?: React.ReactNode
}
export const CommonCard: React.FC<CommonCardProps> = (props) => {
return (
<Card style={{ overflow: 'visible' }}>
<FlexBox className={'common-card'}>
<FlexBox direction={'horizontal'} className={'head'}>
<Icon component={props.icon} className={'icon'} />
<div className={'title'}>{props.title}</div>
{props.expand}
</FlexBox>
<LoadingMask
hidden={!props.loading}
maskContent={<AntdSkeleton active paragraph={{ rows: 6 }} />}
>
{props.children}
</LoadingMask>
</FlexBox>
</Card>
)
}
const Statistics: React.FC = () => {
return (
<>
<FitFullscreen data-component={'system-statistics'}>
<HideScrollbar isShowVerticalScrollbar autoHideWaitingTime={500}>
<FlexBox direction={'horizontal'} className={'root-content'}>
<Permission operationCode={'system:statistics:query:usage'}>
<OnlineInfo />
<ActiveInfo />
</Permission>
<Permission operationCode={'system:statistics:query:base'}>
<HardwareInfo />
<SoftwareInfo />
</Permission>
<Permission operationCode={'system:statistics:query:real'}>
<CPUInfo />
<StorageInfo />
</Permission>
</FlexBox>
</HideScrollbar>
</FitFullscreen>
</>
)
}
export default Statistics

View File

@@ -0,0 +1,128 @@
import * as echarts from 'echarts/core'
import {
DataZoomComponent,
DataZoomComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
ToolboxComponent,
ToolboxComponentOption,
TooltipComponent,
TooltipComponentOption
} from 'echarts/components'
import { BarChart, BarSeriesOption, LineChart, LineSeriesOption } from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
import { UniversalTransition } from 'echarts/features'
import { CallbackDataParams } from 'echarts/types/dist/shared'
import { utcToLocalTime } from '@/util/datetime.tsx'
echarts.use([
TooltipComponent,
ToolboxComponent,
GridComponent,
LegendComponent,
DataZoomComponent,
BarChart,
LineChart,
SVGRenderer,
UniversalTransition
])
export type EChartsOption = echarts.ComposeOption<
| TooltipComponentOption
| ToolboxComponentOption
| GridComponentOption
| LegendComponentOption
| BarSeriesOption
| DataZoomComponentOption
| LineSeriesOption
>
export 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()
}
}
}
}
export const barEChartsBaseOption: EChartsOption = {
tooltip: {},
xAxis: {
show: false
},
yAxis: {
axisLine: {
show: false
},
axisLabel: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: false
},
axisPointer: {
show: false
}
}
}
export const getTooltipTimeFormatter = (format: string = 'yyyy-MM-DD HH:mm:ss') => {
return (params: CallbackDataParams[]) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
`${utcToLocalTime(params[0].data[0], format)}<br>${params
.map(
(param) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`<span style="display: flex; justify-content: space-between;"><span>${param.marker}${param.seriesName}</span><span style="font-weight: bold; margin-left: 16px;">${param.data[1]}</span></span>`
)
.join('')}`
}
export const lineEChartsBaseOption: EChartsOption = {
tooltip: {
trigger: 'axis'
},
legend: {},
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: [{}]
}

View File

@@ -15,7 +15,7 @@ const system: RouteJsonObject[] = [
path: 'statistics', path: 'statistics',
absolutePath: '/system/statistics', absolutePath: '/system/statistics',
id: 'system-statistics', id: 'system-statistics',
component: React.lazy(() => import('@/pages/system/Statistics')), component: React.lazy(() => import('@/pages/system/statistics')),
name: '系统概况', name: '系统概况',
icon: React.lazy(() => import('~icons/oxygen/chart.jsx')), icon: React.lazy(() => import('~icons/oxygen/chart.jsx')),
menu: true, menu: true,