diff --git a/package.json b/package.json
index 498c7afc4..6d2cbfa1d 100644
--- a/package.json
+++ b/package.json
@@ -6,12 +6,14 @@
"@fortawesome/react-fontawesome": "^0.1.4",
"@hot-loader/react-dom": "^16.8.6",
"axios": "^0.18.0",
+ "chart.js": "^2.8.0",
"classnames": "^2.2.6",
"date-fns": "^1.29.0",
"easy-peasy": "^3.0.2",
"events": "^3.0.0",
"formik": "^1.5.7",
"jquery": "^3.3.1",
+ "lodash-es": "^4.17.15",
"path": "^0.12.7",
"query-string": "^6.7.0",
"react": "^16.8.6",
@@ -39,10 +41,12 @@
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.6.0",
"@babel/runtime": "^7.6.0",
+ "@types/chart.js": "^2.8.5",
"@types/classnames": "^2.2.8",
"@types/events": "^3.0.0",
"@types/feather-icons": "^4.7.0",
"@types/lodash": "^4.14.119",
+ "@types/lodash-es": "^4.17.3",
"@types/node": "^12.6.9",
"@types/query-string": "^6.3.0",
"@types/react": "^16.8.19",
diff --git a/resources/scripts/components/elements/TitledGreyBox.tsx b/resources/scripts/components/elements/TitledGreyBox.tsx
new file mode 100644
index 000000000..ba5895471
--- /dev/null
+++ b/resources/scripts/components/elements/TitledGreyBox.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+
+interface Props {
+ icon?: IconProp;
+ title: string;
+ className?: string;
+ children: React.ReactNode;
+}
+
+export default ({ icon, title, children, className }: Props) => (
+
+
+
+ {icon && }{title}
+
+
+
+ {children}
+
+
+);
diff --git a/resources/scripts/components/server/ServerConsole.tsx b/resources/scripts/components/server/ServerConsole.tsx
index 672d654a6..2e10efcf9 100644
--- a/resources/scripts/components/server/ServerConsole.tsx
+++ b/resources/scripts/components/server/ServerConsole.tsx
@@ -4,19 +4,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faServer } from '@fortawesome/free-solid-svg-icons/faServer';
import { faCircle } from '@fortawesome/free-solid-svg-icons/faCircle';
import classNames from 'classnames';
-import styled from 'styled-components';
import { faMemory } from '@fortawesome/free-solid-svg-icons/faMemory';
import { faMicrochip } from '@fortawesome/free-solid-svg-icons/faMicrochip';
import { bytesToHuman } from '@/helpers';
import SuspenseSpinner from '@/components/elements/SuspenseSpinner';
+import TitledGreyBox from '@/components/elements/TitledGreyBox';
type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
-const GreyBox = styled.div`
- ${tw`mt-4 shadow-md bg-neutral-700 rounded p-3 flex text-xs`}
-`;
-
const ChunkedConsole = lazy(() => import('@/components/server/Console'));
+const ChunkedStatGraphs = lazy(() => import('@/components/server/StatGraphs'));
const StopOrKillButton = ({ onPress }: { onPress: (action: PowerAction) => void }) => {
const [ clicked, setClicked ] = useState(false);
@@ -80,46 +77,39 @@ export default () => {
return (
-
-
-
-
-
-
- {status}
-
-
-
- {bytesToHuman(memory)}
- / {server.limits.memory} MB
-
-
-
- {cpu.toFixed(2)} %
-
-
-
-
+
+
+
+
+ {status}
+
+
+
+ {bytesToHuman(memory)}
+ / {server.limits.memory} MB
+
+
+
+ {cpu.toFixed(2)} %
+
+
+
sendPowerCommand(action)}/>
-
-
-
-
-
-
+
+
+
+
+ {status !== 'offline' && }
+
+
);
};
diff --git a/resources/scripts/components/server/StatGraphs.tsx b/resources/scripts/components/server/StatGraphs.tsx
new file mode 100644
index 000000000..c0eea6789
--- /dev/null
+++ b/resources/scripts/components/server/StatGraphs.tsx
@@ -0,0 +1,171 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import Chart, { ChartConfiguration } from 'chart.js';
+import { ServerContext } from '@/state/server';
+import { bytesToMegabytes } from '@/helpers';
+import merge from 'lodash-es/merge';
+import TitledGreyBox from '@/components/elements/TitledGreyBox';
+import { faMemory } from '@fortawesome/free-solid-svg-icons/faMemory';
+import { faMicrochip } from '@fortawesome/free-solid-svg-icons/faMicrochip';
+
+const chartDefaults: ChartConfiguration = {
+ type: 'line',
+ options: {
+ legend: {
+ display: false,
+ },
+ tooltips: {
+ enabled: false,
+ },
+ animation: {
+ duration: 0,
+ },
+ hover: {
+ animationDuration: 0,
+ },
+ elements: {
+ point: {
+ radius: 0,
+ },
+ line: {
+ tension: 0.1,
+ backgroundColor: 'rgba(15, 178, 184, 0.45)',
+ borderColor: '#32D0D9',
+ },
+ },
+ scales: {
+ xAxes: [ {
+ ticks: {
+ display: false,
+ },
+ gridLines: {
+ display: false,
+ },
+ } ],
+ yAxes: [ {
+ gridLines: {
+ drawTicks: false,
+ color: 'rgba(229, 232, 235, 0.15)',
+ zeroLineColor: 'rgba(15, 178, 184, 0.45)',
+ zeroLineWidth: 3,
+ },
+ ticks: {
+ fontSize: 10,
+ fontFamily: '"IBM Plex Mono", monospace',
+ fontColor: 'rgb(229, 232, 235)',
+ min: 0,
+ beginAtZero: true,
+ maxTicksLimit: 5,
+ },
+ } ],
+ },
+ responsiveAnimationDuration: 0,
+ },
+};
+
+const createDefaultChart = (ctx: CanvasRenderingContext2D, options?: ChartConfiguration): Chart => new Chart(ctx, {
+ ...merge({}, chartDefaults, options),
+ data: {
+ labels: Array(20).fill(''),
+ datasets: [
+ {
+ fill: true,
+ data: Array(20).fill(0),
+ },
+ ],
+ },
+});
+
+export default () => {
+ const { limits } = ServerContext.useStoreState(state => state.server.data!);
+ const { connected, instance } = ServerContext.useStoreState(state => state.socket);
+
+ const [ memory, setMemory ] = useState
();
+ const [ cpu, setCpu ] = useState();
+
+ const memoryRef = useCallback<(node: HTMLCanvasElement | null) => void>(node => {
+ if (!node) {
+ return;
+ }
+
+ setMemory(createDefaultChart(node.getContext('2d')!, {
+ options: {
+ scales: {
+ yAxes: [ {
+ ticks: {
+ callback: (value) => `${value}Mb `,
+ suggestedMax: limits.memory,
+ },
+ } ],
+ },
+ },
+ }));
+ }, []);
+
+ const cpuRef = useCallback<(node: HTMLCanvasElement | null) => void>(node => {
+ if (!node) {
+ return;
+ }
+
+ setCpu(createDefaultChart(node.getContext('2d')!, {
+ options: {
+ scales: {
+ yAxes: [ {
+ ticks: {
+ callback: (value) => `${value}% `,
+ },
+ } ],
+ },
+ },
+ }));
+ }, []);
+
+ const statsListener = (data: string) => {
+ let stats: any = {};
+ try {
+ stats = JSON.parse(data);
+ } catch (e) {
+ return;
+ }
+
+ if (memory && memory.data.datasets) {
+ const data = memory.data.datasets[0].data!;
+
+ data.push(bytesToMegabytes(stats.memory_bytes));
+ data.shift();
+
+ memory.update();
+ }
+
+ if (cpu && cpu.data.datasets) {
+ const data = cpu.data.datasets[0].data!;
+
+ data.push(stats.cpu_absolute);
+ data.shift();
+
+ cpu.update();
+ }
+ };
+
+ useEffect(() => {
+ if (!connected || !instance) {
+ return;
+ }
+
+ instance.addListener('stats', statsListener);
+
+ return () => {
+ instance.removeListener('stats', statsListener);
+ };
+ }, [ connected, memory, cpu ]);
+
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/resources/scripts/helpers.ts b/resources/scripts/helpers.ts
index 020fe6020..79048f778 100644
--- a/resources/scripts/helpers.ts
+++ b/resources/scripts/helpers.ts
@@ -4,7 +4,8 @@ export function bytesToHuman (bytes: number): string {
}
const i = Math.floor(Math.log(bytes) / Math.log(1000));
-
// @ts-ignore
return `${(bytes / Math.pow(1000, i)).toFixed(2) * 1} ${['Bytes', 'kB', 'MB', 'GB', 'TB'][i]}`;
}
+
+export const bytesToMegabytes = (bytes: number) => Math.floor(bytes / 1000 / 1000);
diff --git a/resources/styles/components/miscellaneous.css b/resources/styles/components/miscellaneous.css
index f023a834b..9b2c4d39c 100644
--- a/resources/styles/components/miscellaneous.css
+++ b/resources/styles/components/miscellaneous.css
@@ -19,3 +19,7 @@ code.clean {
@apply .rounded-full .bg-neutral-500 .p-3;
}
}
+
+.grey-box {
+ @apply .mt-4 .shadow-md .bg-neutral-700 .rounded .p-3 .flex .text-xs;
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 8c30ac176..89cf42134 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -958,6 +958,10 @@
prop-types "^15.6.2"
scheduler "^0.13.6"
+"@types/chart.js@^2.8.5":
+ version "2.8.5"
+ resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.8.5.tgz#7d47cfd36f0a1c2c4ad6a585749bc68e8659492a"
+
"@types/classnames@^2.2.8":
version "2.2.8"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.8.tgz#17139e1e1104203572caa4368f6796f6225b70b4"
@@ -993,6 +997,16 @@
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
+"@types/lodash-es@^4.17.3":
+ version "4.17.3"
+ resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.3.tgz#87eb0b3673b076b8ee655f1890260a136af09a2d"
+ dependencies:
+ "@types/lodash" "*"
+
+"@types/lodash@*":
+ version "4.14.141"
+ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.141.tgz#d81f4d0c562abe28713406b571ffb27692a82ae6"
+
"@types/lodash@^4.14.119":
version "4.14.119"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39"
@@ -2143,6 +2157,26 @@ chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
+chart.js@^2.8.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.8.0.tgz#b703b10d0f4ec5079eaefdcd6ca32dc8f826e0e9"
+ dependencies:
+ chartjs-color "^2.1.0"
+ moment "^2.10.2"
+
+chartjs-color-string@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
+ dependencies:
+ color-name "^1.0.0"
+
+chartjs-color@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.3.0.tgz#0e7e1e8dba37eae8415fd3db38bf572007dd958f"
+ dependencies:
+ chartjs-color-string "^0.6.0"
+ color-convert "^0.5.3"
+
chokidar@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176"
@@ -2284,6 +2318,10 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
+color-convert@^0.5.3:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
+
color-convert@^1.8.2, color-convert@^1.9.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
@@ -4801,6 +4839,10 @@ lodash-es@^4.17.11:
version "4.17.11"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0"
+lodash-es@^4.17.15:
+ version "4.17.15"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
+
lodash._baseassign@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e"
@@ -5232,6 +5274,10 @@ mkdirp@0.5.x, mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~
dependencies:
minimist "0.0.8"
+moment@^2.10.2:
+ version "2.24.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"