Skip to content

Commit 29834a3

Browse files
committed
Add support for showing usage graphs on the console page
1 parent c66d2cd commit 29834a3

File tree

7 files changed

+291
-51
lines changed

7 files changed

+291
-51
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
"@fortawesome/react-fontawesome": "^0.1.4",
77
"@hot-loader/react-dom": "^16.8.6",
88
"axios": "^0.18.0",
9+
"chart.js": "^2.8.0",
910
"classnames": "^2.2.6",
1011
"date-fns": "^1.29.0",
1112
"easy-peasy": "^3.0.2",
1213
"events": "^3.0.0",
1314
"formik": "^1.5.7",
1415
"jquery": "^3.3.1",
16+
"lodash-es": "^4.17.15",
1517
"path": "^0.12.7",
1618
"query-string": "^6.7.0",
1719
"react": "^16.8.6",
@@ -39,10 +41,12 @@
3941
"@babel/preset-react": "^7.0.0",
4042
"@babel/preset-typescript": "^7.6.0",
4143
"@babel/runtime": "^7.6.0",
44+
"@types/chart.js": "^2.8.5",
4245
"@types/classnames": "^2.2.8",
4346
"@types/events": "^3.0.0",
4447
"@types/feather-icons": "^4.7.0",
4548
"@types/lodash": "^4.14.119",
49+
"@types/lodash-es": "^4.17.3",
4650
"@types/node": "^12.6.9",
4751
"@types/query-string": "^6.3.0",
4852
"@types/react": "^16.8.19",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import React from 'react';
2+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3+
import { IconProp } from '@fortawesome/fontawesome-svg-core';
4+
5+
interface Props {
6+
icon?: IconProp;
7+
title: string;
8+
className?: string;
9+
children: React.ReactNode;
10+
}
11+
12+
export default ({ icon, title, children, className }: Props) => (
13+
<div className={`rounded shadow-md bg-neutral-700 ${className}`}>
14+
<div className={'bg-neutral-900 rounded-t p-3 border-b border-black'}>
15+
<p className={'text-sm uppercase'}>
16+
{icon && <FontAwesomeIcon icon={icon} className={'mr-2 text-neutral-300'}/>}{title}
17+
</p>
18+
</div>
19+
<div className={'p-3'}>
20+
{children}
21+
</div>
22+
</div>
23+
);

resources/scripts/components/server/ServerConsole.tsx

Lines changed: 41 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
44
import { faServer } from '@fortawesome/free-solid-svg-icons/faServer';
55
import { faCircle } from '@fortawesome/free-solid-svg-icons/faCircle';
66
import classNames from 'classnames';
7-
import styled from 'styled-components';
87
import { faMemory } from '@fortawesome/free-solid-svg-icons/faMemory';
98
import { faMicrochip } from '@fortawesome/free-solid-svg-icons/faMicrochip';
109
import { bytesToHuman } from '@/helpers';
1110
import SuspenseSpinner from '@/components/elements/SuspenseSpinner';
11+
import TitledGreyBox from '@/components/elements/TitledGreyBox';
1212

1313
type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
1414

15-
const GreyBox = styled.div`
16-
${tw`mt-4 shadow-md bg-neutral-700 rounded p-3 flex text-xs`}
17-
`;
18-
1915
const ChunkedConsole = lazy(() => import('@/components/server/Console'));
16+
const ChunkedStatGraphs = lazy(() => import('@/components/server/StatGraphs'));
2017

2118
const StopOrKillButton = ({ onPress }: { onPress: (action: PowerAction) => void }) => {
2219
const [ clicked, setClicked ] = useState(false);
@@ -80,46 +77,39 @@ export default () => {
8077

8178
return (
8279
<div className={'my-10 flex'}>
83-
<div className={'flex-1 ml-4'}>
84-
<div className={'rounded shadow-md bg-neutral-700'}>
85-
<div className={'bg-neutral-900 rounded-t p-3 border-b border-black'}>
86-
<p className={'text-sm uppercase'}>
87-
<FontAwesomeIcon icon={faServer} className={'mr-1 text-neutral-300'}/> {server.name}
88-
</p>
89-
</div>
90-
<div className={'p-3'}>
91-
<p className={'text-xs uppercase'}>
92-
<FontAwesomeIcon
93-
icon={faCircle}
94-
fixedWidth={true}
95-
className={classNames('mr-1', {
96-
'text-red-500': status === 'offline',
97-
'text-yellow-500': [ 'running', 'offline' ].indexOf(status) < 0,
98-
'text-green-500': status === 'running',
99-
})}
100-
/>
101-
&nbsp;{status}
102-
</p>
103-
<p className={'text-xs mt-2'}>
104-
<FontAwesomeIcon
105-
icon={faMemory}
106-
fixedWidth={true}
107-
className={'mr-1'}
108-
/>
109-
&nbsp;{bytesToHuman(memory)}
110-
<span className={'text-neutral-500'}>/ {server.limits.memory} MB</span>
111-
</p>
112-
<p className={'text-xs mt-2'}>
113-
<FontAwesomeIcon
114-
icon={faMicrochip}
115-
fixedWidth={true}
116-
className={'mr-1'}
117-
/>
118-
&nbsp;{cpu.toFixed(2)} %
119-
</p>
120-
</div>
121-
</div>
122-
<GreyBox className={'justify-center'}>
80+
<div className={'w-1/4 ml-4'}>
81+
<TitledGreyBox title={server.name} icon={faServer}>
82+
<p className={'text-xs uppercase'}>
83+
<FontAwesomeIcon
84+
icon={faCircle}
85+
fixedWidth={true}
86+
className={classNames('mr-1', {
87+
'text-red-500': status === 'offline',
88+
'text-yellow-500': [ 'running', 'offline' ].indexOf(status) < 0,
89+
'text-green-500': status === 'running',
90+
})}
91+
/>
92+
&nbsp;{status}
93+
</p>
94+
<p className={'text-xs mt-2'}>
95+
<FontAwesomeIcon
96+
icon={faMemory}
97+
fixedWidth={true}
98+
className={'mr-1'}
99+
/>
100+
&nbsp;{bytesToHuman(memory)}
101+
<span className={'text-neutral-500'}>/ {server.limits.memory} MB</span>
102+
</p>
103+
<p className={'text-xs mt-2'}>
104+
<FontAwesomeIcon
105+
icon={faMicrochip}
106+
fixedWidth={true}
107+
className={'mr-1'}
108+
/>
109+
&nbsp;{cpu.toFixed(2)} %
110+
</p>
111+
</TitledGreyBox>
112+
<div className={'grey-box justify-center'}>
123113
<button
124114
className={'btn btn-secondary btn-xs mr-2'}
125115
disabled={status !== 'offline'}
@@ -140,13 +130,14 @@ export default () => {
140130
Restart
141131
</button>
142132
<StopOrKillButton onPress={action => sendPowerCommand(action)}/>
143-
</GreyBox>
133+
</div>
144134
</div>
145-
<SuspenseSpinner>
146-
<div className={'mx-4 w-3/4 mr-4'}>
135+
<div className={'flex-1 mx-4 mr-4'}>
136+
<SuspenseSpinner>
147137
<ChunkedConsole/>
148-
</div>
149-
</SuspenseSpinner>
138+
{status !== 'offline' && <ChunkedStatGraphs/>}
139+
</SuspenseSpinner>
140+
</div>
150141
</div>
151142
);
152143
};
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import React, { useCallback, useEffect, useState } from 'react';
2+
import Chart, { ChartConfiguration } from 'chart.js';
3+
import { ServerContext } from '@/state/server';
4+
import { bytesToMegabytes } from '@/helpers';
5+
import merge from 'lodash-es/merge';
6+
import TitledGreyBox from '@/components/elements/TitledGreyBox';
7+
import { faMemory } from '@fortawesome/free-solid-svg-icons/faMemory';
8+
import { faMicrochip } from '@fortawesome/free-solid-svg-icons/faMicrochip';
9+
10+
const chartDefaults: ChartConfiguration = {
11+
type: 'line',
12+
options: {
13+
legend: {
14+
display: false,
15+
},
16+
tooltips: {
17+
enabled: false,
18+
},
19+
animation: {
20+
duration: 0,
21+
},
22+
hover: {
23+
animationDuration: 0,
24+
},
25+
elements: {
26+
point: {
27+
radius: 0,
28+
},
29+
line: {
30+
tension: 0.1,
31+
backgroundColor: 'rgba(15, 178, 184, 0.45)',
32+
borderColor: '#32D0D9',
33+
},
34+
},
35+
scales: {
36+
xAxes: [ {
37+
ticks: {
38+
display: false,
39+
},
40+
gridLines: {
41+
display: false,
42+
},
43+
} ],
44+
yAxes: [ {
45+
gridLines: {
46+
drawTicks: false,
47+
color: 'rgba(229, 232, 235, 0.15)',
48+
zeroLineColor: 'rgba(15, 178, 184, 0.45)',
49+
zeroLineWidth: 3,
50+
},
51+
ticks: {
52+
fontSize: 10,
53+
fontFamily: '"IBM Plex Mono", monospace',
54+
fontColor: 'rgb(229, 232, 235)',
55+
min: 0,
56+
beginAtZero: true,
57+
maxTicksLimit: 5,
58+
},
59+
} ],
60+
},
61+
responsiveAnimationDuration: 0,
62+
},
63+
};
64+
65+
const createDefaultChart = (ctx: CanvasRenderingContext2D, options?: ChartConfiguration): Chart => new Chart(ctx, {
66+
...merge({}, chartDefaults, options),
67+
data: {
68+
labels: Array(20).fill(''),
69+
datasets: [
70+
{
71+
fill: true,
72+
data: Array(20).fill(0),
73+
},
74+
],
75+
},
76+
});
77+
78+
export default () => {
79+
const { limits } = ServerContext.useStoreState(state => state.server.data!);
80+
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
81+
82+
const [ memory, setMemory ] = useState<Chart>();
83+
const [ cpu, setCpu ] = useState<Chart>();
84+
85+
const memoryRef = useCallback<(node: HTMLCanvasElement | null) => void>(node => {
86+
if (!node) {
87+
return;
88+
}
89+
90+
setMemory(createDefaultChart(node.getContext('2d')!, {
91+
options: {
92+
scales: {
93+
yAxes: [ {
94+
ticks: {
95+
callback: (value) => `${value}Mb `,
96+
suggestedMax: limits.memory,
97+
},
98+
} ],
99+
},
100+
},
101+
}));
102+
}, []);
103+
104+
const cpuRef = useCallback<(node: HTMLCanvasElement | null) => void>(node => {
105+
if (!node) {
106+
return;
107+
}
108+
109+
setCpu(createDefaultChart(node.getContext('2d')!, {
110+
options: {
111+
scales: {
112+
yAxes: [ {
113+
ticks: {
114+
callback: (value) => `${value}% `,
115+
},
116+
} ],
117+
},
118+
},
119+
}));
120+
}, []);
121+
122+
const statsListener = (data: string) => {
123+
let stats: any = {};
124+
try {
125+
stats = JSON.parse(data);
126+
} catch (e) {
127+
return;
128+
}
129+
130+
if (memory && memory.data.datasets) {
131+
const data = memory.data.datasets[0].data!;
132+
133+
data.push(bytesToMegabytes(stats.memory_bytes));
134+
data.shift();
135+
136+
memory.update();
137+
}
138+
139+
if (cpu && cpu.data.datasets) {
140+
const data = cpu.data.datasets[0].data!;
141+
142+
data.push(stats.cpu_absolute);
143+
data.shift();
144+
145+
cpu.update();
146+
}
147+
};
148+
149+
useEffect(() => {
150+
if (!connected || !instance) {
151+
return;
152+
}
153+
154+
instance.addListener('stats', statsListener);
155+
156+
return () => {
157+
instance.removeListener('stats', statsListener);
158+
};
159+
}, [ connected, memory, cpu ]);
160+
161+
return (
162+
<div className={'flex mt-4'}>
163+
<TitledGreyBox title={'Memory usage'} icon={faMemory} className={'flex-1 mr-2'}>
164+
<canvas id={'memory_chart'} ref={memoryRef} aria-label={'Server Memory Usage Graph'} role={'img'}/>
165+
</TitledGreyBox>
166+
<TitledGreyBox title={'CPU usage'} icon={faMicrochip} className={'flex-1 ml-2'}>
167+
<canvas id={'cpu_chart'} ref={cpuRef} aria-label={'Server CPU Usage Graph'} role={'img'}/>
168+
</TitledGreyBox>
169+
</div>
170+
);
171+
};

resources/scripts/helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ export function bytesToHuman (bytes: number): string {
44
}
55

66
const i = Math.floor(Math.log(bytes) / Math.log(1000));
7-
87
// @ts-ignore
98
return `${(bytes / Math.pow(1000, i)).toFixed(2) * 1} ${['Bytes', 'kB', 'MB', 'GB', 'TB'][i]}`;
109
}
10+
11+
export const bytesToMegabytes = (bytes: number) => Math.floor(bytes / 1000 / 1000);

resources/styles/components/miscellaneous.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ code.clean {
1919
@apply .rounded-full .bg-neutral-500 .p-3;
2020
}
2121
}
22+
23+
.grey-box {
24+
@apply .mt-4 .shadow-md .bg-neutral-700 .rounded .p-3 .flex .text-xs;
25+
}

0 commit comments

Comments
 (0)