|
1 | | -import React, { useCallback, useRef, useState } from 'react'; |
2 | | -import Chart, { ChartConfiguration } from 'chart.js'; |
| 1 | +import React, { useEffect, useRef } from 'react'; |
3 | 2 | import { ServerContext } from '@/state/server'; |
4 | | -import merge from 'deepmerge'; |
5 | | -import TitledGreyBox from '@/components/elements/TitledGreyBox'; |
6 | | -import { faEthernet, faMemory, faMicrochip } from '@fortawesome/free-solid-svg-icons'; |
7 | | -import tw from 'twin.macro'; |
8 | 3 | import { SocketEvent } from '@/components/server/events'; |
9 | 4 | import useWebsocketEvent from '@/plugins/useWebsocketEvent'; |
10 | | - |
11 | | -const chartDefaults = (ticks?: Chart.TickOptions): ChartConfiguration => ({ |
12 | | - type: 'line', |
13 | | - options: { |
14 | | - legend: { |
15 | | - display: false, |
16 | | - }, |
17 | | - tooltips: { |
18 | | - enabled: false, |
19 | | - }, |
20 | | - animation: { |
21 | | - duration: 0, |
22 | | - }, |
23 | | - elements: { |
24 | | - point: { |
25 | | - radius: 0, |
26 | | - }, |
27 | | - line: { |
28 | | - tension: 0.3, |
29 | | - backgroundColor: 'rgba(15, 178, 184, 0.45)', |
30 | | - borderColor: '#32D0D9', |
31 | | - }, |
32 | | - }, |
33 | | - scales: { |
34 | | - xAxes: [ { |
35 | | - ticks: { |
36 | | - display: false, |
37 | | - }, |
38 | | - gridLines: { |
39 | | - display: false, |
40 | | - }, |
41 | | - } ], |
42 | | - yAxes: [ { |
43 | | - gridLines: { |
44 | | - drawTicks: false, |
45 | | - color: 'rgba(229, 232, 235, 0.15)', |
46 | | - zeroLineColor: 'rgba(15, 178, 184, 0.45)', |
47 | | - zeroLineWidth: 3, |
48 | | - }, |
49 | | - ticks: merge(ticks || {}, { |
50 | | - fontSize: 10, |
51 | | - fontFamily: '"IBM Plex Mono", monospace', |
52 | | - fontColor: 'rgb(229, 232, 235)', |
53 | | - min: 0, |
54 | | - beginAtZero: true, |
55 | | - maxTicksLimit: 5, |
56 | | - }), |
57 | | - } ], |
58 | | - }, |
59 | | - }, |
60 | | - data: { |
61 | | - labels: Array(20).fill(''), |
62 | | - datasets: [ |
63 | | - { |
64 | | - fill: true, |
65 | | - data: Array(20).fill(0), |
66 | | - }, |
67 | | - ], |
68 | | - }, |
69 | | -}); |
70 | | - |
71 | | -type ChartState = [ (node: HTMLCanvasElement | null) => void, Chart | undefined ]; |
72 | | - |
73 | | -/** |
74 | | - * Creates an element ref and a chart instance. |
75 | | - */ |
76 | | -const useChart = (options?: Chart.TickOptions): ChartState => { |
77 | | - const [ chart, setChart ] = useState<Chart>(); |
78 | | - |
79 | | - const ref = useCallback<(node: HTMLCanvasElement | null) => void>(node => { |
80 | | - if (!node) return; |
81 | | - |
82 | | - const chart = new Chart(node.getContext('2d')!, chartDefaults(options)); |
83 | | - |
84 | | - setChart(chart); |
85 | | - }, []); |
86 | | - |
87 | | - return [ ref, chart ]; |
88 | | -}; |
89 | | - |
90 | | -const updateChartDataset = (chart: Chart | null | undefined, value: Chart.ChartPoint & number): void => { |
91 | | - if (!chart || !chart.data?.datasets) return; |
92 | | - |
93 | | - const data = chart.data.datasets[0].data!; |
94 | | - data.push(value); |
95 | | - data.shift(); |
96 | | - chart.update({ lazy: true }); |
97 | | -}; |
| 5 | +import { Line } from 'react-chartjs-2'; |
| 6 | +import { useChart, useChartTickLabel } from '@/components/server/console/chart'; |
| 7 | +import { bytesToHuman, toRGBA } from '@/helpers'; |
| 8 | +import { CloudDownloadIcon, CloudUploadIcon } from '@heroicons/react/solid'; |
| 9 | +import { theme } from 'twin.macro'; |
| 10 | +import ChartBlock from '@/components/server/console/ChartBlock'; |
| 11 | +import Tooltip from '@/components/elements/tooltip/Tooltip'; |
98 | 12 |
|
99 | 13 | export default () => { |
100 | 14 | const status = ServerContext.useStoreState(state => state.status.value); |
101 | 15 | const limits = ServerContext.useStoreState(state => state.server.data!.limits); |
102 | | - |
103 | 16 | const previous = useRef<Record<'tx' | 'rx', number>>({ tx: -1, rx: -1 }); |
104 | | - const [ cpuRef, cpu ] = useChart({ callback: (value) => `${value}% `, suggestedMax: limits.cpu }); |
105 | | - const [ memoryRef, memory ] = useChart({ callback: (value) => `${value}Mb `, suggestedMax: limits.memory }); |
106 | | - const [ txRef, tx ] = useChart({ callback: (value) => `${value}Kb/s ` }); |
107 | | - const [ rxRef, rx ] = useChart({ callback: (value) => `${value}Kb/s ` }); |
| 17 | + |
| 18 | + const cpu = useChartTickLabel('CPU', limits.cpu, '%'); |
| 19 | + const memory = useChartTickLabel('Memory', limits.memory, 'MB'); |
| 20 | + const network = useChart('Network', { |
| 21 | + sets: 2, |
| 22 | + options: { |
| 23 | + scales: { |
| 24 | + y: { |
| 25 | + ticks: { |
| 26 | + callback (value) { |
| 27 | + return bytesToHuman(typeof value === 'string' ? parseInt(value, 10) : value); |
| 28 | + }, |
| 29 | + }, |
| 30 | + }, |
| 31 | + }, |
| 32 | + }, |
| 33 | + callback (opts, index) { |
| 34 | + return { |
| 35 | + ...opts, |
| 36 | + label: !index ? 'Network In' : 'Network Out', |
| 37 | + borderColor: !index ? theme('colors.cyan.400') : theme('colors.yellow.400'), |
| 38 | + backgroundColor: toRGBA(!index ? theme('colors.cyan.700') : theme('colors.yellow.700'), 0.5), |
| 39 | + }; |
| 40 | + }, |
| 41 | + }); |
| 42 | + |
| 43 | + useEffect(() => { |
| 44 | + if (status === 'offline') { |
| 45 | + cpu.clear(); |
| 46 | + memory.clear(); |
| 47 | + network.clear(); |
| 48 | + } |
| 49 | + }, [ status ]); |
108 | 50 |
|
109 | 51 | useWebsocketEvent(SocketEvent.STATS, (data: string) => { |
110 | | - let stats: any = {}; |
| 52 | + let values: any = {}; |
111 | 53 | try { |
112 | | - stats = JSON.parse(data); |
| 54 | + values = JSON.parse(data); |
113 | 55 | } catch (e) { |
114 | 56 | return; |
115 | 57 | } |
116 | 58 |
|
117 | | - updateChartDataset(cpu, stats.cpu_absolute); |
118 | | - updateChartDataset(memory, Math.floor(stats.memory_bytes / 1024 / 1024)); |
119 | | - updateChartDataset(tx, previous.current.tx < 0 ? 0 : Math.max(0, stats.network.tx_bytes - previous.current.tx) / 1024); |
120 | | - updateChartDataset(rx, previous.current.rx < 0 ? 0 : Math.max(0, stats.network.rx_bytes - previous.current.rx) / 1024); |
| 59 | + cpu.push(values.cpu_absolute); |
| 60 | + memory.push(Math.floor(values.memory_bytes / 1024 / 1024)); |
| 61 | + network.push([ |
| 62 | + previous.current.tx < 0 ? 0 : Math.max(0, values.network.tx_bytes - previous.current.tx), |
| 63 | + previous.current.rx < 0 ? 0 : Math.max(0, values.network.rx_bytes - previous.current.rx), |
| 64 | + ]); |
121 | 65 |
|
122 | | - previous.current = { tx: stats.network.tx_bytes, rx: stats.network.rx_bytes }; |
| 66 | + previous.current = { tx: values.network.tx_bytes, rx: values.network.rx_bytes }; |
123 | 67 | }); |
124 | 68 |
|
125 | 69 | return ( |
126 | 70 | <> |
127 | | - <TitledGreyBox title={'Memory usage'} icon={faMemory}> |
128 | | - {status !== 'offline' ? |
129 | | - <canvas |
130 | | - id={'memory_chart'} |
131 | | - ref={memoryRef} |
132 | | - aria-label={'Server Memory Usage Graph'} |
133 | | - role={'img'} |
134 | | - /> |
135 | | - : |
136 | | - <p css={tw`text-xs text-neutral-400 text-center p-3`}> |
137 | | - Server is offline. |
138 | | - </p> |
139 | | - } |
140 | | - </TitledGreyBox> |
141 | | - <TitledGreyBox title={'CPU usage'} icon={faMicrochip}> |
142 | | - {status !== 'offline' ? |
143 | | - <canvas id={'cpu_chart'} ref={cpuRef} aria-label={'Server CPU Usage Graph'} role={'img'}/> |
144 | | - : |
145 | | - <p css={tw`text-xs text-neutral-400 text-center p-3`}> |
146 | | - Server is offline. |
147 | | - </p> |
148 | | - } |
149 | | - </TitledGreyBox> |
150 | | - <TitledGreyBox title={'Inbound Data'} icon={faEthernet}> |
151 | | - {status !== 'offline' ? |
152 | | - <canvas id={'rx_chart'} ref={rxRef} aria-label={'Server Inbound Data'} role={'img'}/> |
153 | | - : |
154 | | - <p css={tw`text-xs text-neutral-400 text-center p-3`}> |
155 | | - Server is offline. |
156 | | - </p> |
157 | | - } |
158 | | - </TitledGreyBox> |
159 | | - <TitledGreyBox title={'Outbound Data'} icon={faEthernet}> |
160 | | - {status !== 'offline' ? |
161 | | - <canvas id={'tx_chart'} ref={txRef} aria-label={'Server Outbound Data'} role={'img'}/> |
162 | | - : |
163 | | - <p css={tw`text-xs text-neutral-400 text-center p-3`}> |
164 | | - Server is offline. |
165 | | - </p> |
| 71 | + <ChartBlock title={'CPU Load'}> |
| 72 | + <Line {...cpu.props}/> |
| 73 | + </ChartBlock> |
| 74 | + <ChartBlock title={'Memory'}> |
| 75 | + <Line {...memory.props}/> |
| 76 | + </ChartBlock> |
| 77 | + <ChartBlock |
| 78 | + title={'Network'} |
| 79 | + legend={ |
| 80 | + <> |
| 81 | + <Tooltip arrow content={'Inbound'}> |
| 82 | + <CloudDownloadIcon className={'mr-2 w-4 h-4 text-yellow-400'}/> |
| 83 | + </Tooltip> |
| 84 | + <Tooltip arrow content={'Outbound'}> |
| 85 | + <CloudUploadIcon className={'w-4 h-4 text-cyan-400'}/> |
| 86 | + </Tooltip> |
| 87 | + </> |
166 | 88 | } |
167 | | - </TitledGreyBox> |
| 89 | + > |
| 90 | + <Line {...network.props}/> |
| 91 | + </ChartBlock> |
168 | 92 | </> |
169 | 93 | ); |
170 | 94 | }; |
0 commit comments