Skip to content

Commit 182507f

Browse files
committed
Upgrade charts to ChartJS 3 and improve UI for them
1 parent 980f828 commit 182507f

File tree

8 files changed

+269
-179
lines changed

8 files changed

+269
-179
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
"@tailwindcss/line-clamp": "^0.4.0",
1313
"axios": "^0.21.1",
1414
"boring-avatars": "^1.7.0",
15-
"chart.js": "^2.8.0",
15+
"chart.js": "^3.8.0",
1616
"classnames": "^2.3.1",
1717
"codemirror": "^5.57.0",
1818
"date-fns": "^2.16.1",
1919
"debounce": "^1.2.0",
20-
"deepmerge": "^4.2.2",
20+
"deepmerge-ts": "^4.2.1",
2121
"easy-peasy": "^4.0.1",
2222
"events": "^3.0.0",
2323
"formik": "^2.2.6",
@@ -28,6 +28,7 @@
2828
"qrcode.react": "^1.0.1",
2929
"query-string": "^6.7.0",
3030
"react": "^16.14.0",
31+
"react-chartjs-2": "^4.2.0",
3132
"react-copy-to-clipboard": "^5.0.2",
3233
"react-dom": "npm:@hot-loader/react-dom",
3334
"react-fast-compare": "^3.2.0",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import classNames from 'classnames';
3+
import styles from '@/components/server/console/style.module.css';
4+
5+
interface ChartBlockProps {
6+
title: string;
7+
legend?: React.ReactNode;
8+
children: React.ReactNode;
9+
}
10+
11+
export default ({ title, legend, children }: ChartBlockProps) => (
12+
<div className={classNames(styles.chart_container, 'group')}>
13+
<div className={'flex items-center justify-between px-4 py-2'}>
14+
<h3 className={'font-header transition-colors duration-100 group-hover:text-gray-50'}>
15+
{title}
16+
</h3>
17+
{legend &&
18+
<p className={'text-sm flex items-center'}>
19+
{legend}
20+
</p>
21+
}
22+
</div>
23+
<div className={'z-10 ml-2'}>
24+
{children}
25+
</div>
26+
</div>
27+
);

resources/scripts/components/server/console/ServerDetailsBlock.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
8585
</StatBlock>
8686
<StatBlock
8787
icon={faMicrochip}
88-
title={'CPU'}
88+
title={'CPU Load'}
8989
color={getBackgroundColor(stats.cpu, limits.cpu)}
90-
description={limits.memory
90+
description={limits.cpu
9191
? `This server is allowed to use up to ${limits.cpu}% of the host's available CPU resources.`
9292
: 'No CPU limit has been configured for this server.'
9393
}
Lines changed: 70 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,170 +1,94 @@
1-
import React, { useCallback, useRef, useState } from 'react';
2-
import Chart, { ChartConfiguration } from 'chart.js';
1+
import React, { useEffect, useRef } from 'react';
32
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';
83
import { SocketEvent } from '@/components/server/events';
94
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';
9812

9913
export default () => {
10014
const status = ServerContext.useStoreState(state => state.status.value);
10115
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
102-
10316
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 ]);
10850

10951
useWebsocketEvent(SocketEvent.STATS, (data: string) => {
110-
let stats: any = {};
52+
let values: any = {};
11153
try {
112-
stats = JSON.parse(data);
54+
values = JSON.parse(data);
11355
} catch (e) {
11456
return;
11557
}
11658

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+
]);
12165

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 };
12367
});
12468

12569
return (
12670
<>
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+
</>
16688
}
167-
</TitledGreyBox>
89+
>
90+
<Line {...network.props}/>
91+
</ChartBlock>
16892
</>
16993
);
17094
};

0 commit comments

Comments
 (0)