From 29834a33f87fb8ca58c6f9060e9ce32e4e835b1d Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 29 Sep 2019 13:23:15 -0700 Subject: [PATCH] Add support for showing usage graphs on the console page --- package.json | 4 + .../components/elements/TitledGreyBox.tsx | 23 +++ .../components/server/ServerConsole.tsx | 93 +++++----- .../scripts/components/server/StatGraphs.tsx | 171 ++++++++++++++++++ resources/scripts/helpers.ts | 3 +- resources/styles/components/miscellaneous.css | 4 + yarn.lock | 46 +++++ 7 files changed, 292 insertions(+), 52 deletions(-) create mode 100644 resources/scripts/components/elements/TitledGreyBox.tsx create mode 100644 resources/scripts/components/server/StatGraphs.tsx 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 (
-
-
-
-

- {server.name} -

-
-
-

- -  {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"