From 2f581a040baf45c3171fa10a7d5fdc868bced87f Mon Sep 17 00:00:00 2001 From: Amy Yan Date: Wed, 20 Dec 2023 14:43:37 +1100 Subject: [PATCH 1/2] feat: implemented charts for seednode resources --- package-lock.json | 94 ++++++++++++++++++++++- package.json | 8 +- src/components/ResourceChart.tsx | 123 +++++++++++++++++++++++++++++++ src/pages/index.tsx | 85 +++++++++++++++------ src/theme/Root.tsx | 12 +++ 5 files changed, 297 insertions(+), 25 deletions(-) create mode 100644 src/components/ResourceChart.tsx create mode 100644 src/theme/Root.tsx diff --git a/package-lock.json b/package-lock.json index d694a8d..326f789 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,13 @@ "": { "name": "polykey-network-dashboard", "dependencies": { - "@docusaurus/plugin-client-redirects": "^3.0.1" + "@docusaurus/plugin-client-redirects": "^3.0.1", + "@tanstack/react-query": "^5.14.2", + "chart.js": "^4.4.1", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-zoom": "^2.0.1", + "date-fns": "^3.0.1", + "react-chartjs-2": "^5.2.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20230228.0", @@ -3569,6 +3575,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -4060,6 +4071,30 @@ "node": ">=14.16" } }, + "node_modules/@tanstack/query-core": { + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.14.2.tgz", + "integrity": "sha512-QmoJvC72sSWs3hgGis8JdmlDvqLfYGWUK4UG6OR9Q6t28JMN9m2FDwKPqoSJ9YVocELCSjMt/FGjEiLfk8000Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.14.2.tgz", + "integrity": "sha512-SbOzV7UBW8ED3tOnyn6kqNGscnOAfoxShYlbvaQo/5528mDZKpvrwoL/1du1/ukSC6RMAiKmx95SrYqlwPzWDw==", + "dependencies": { + "@tanstack/query-core": "5.14.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -5725,6 +5760,37 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chart.js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.1.tgz", + "integrity": "sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, + "node_modules/chartjs-adapter-date-fns": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz", + "integrity": "sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==", + "peerDependencies": { + "chart.js": ">=2.8.0", + "date-fns": ">=2.0.0" + } + }, + "node_modules/chartjs-plugin-zoom": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.0.1.tgz", + "integrity": "sha512-ogOmLu6e+Q7E1XWOCOz9YwybMslz9qNfGV2a+qjfmqJYpsw5ZMoRHZBUyW+NGhkpQ5PwwPA/+rikHpBZb7PZuA==", + "dependencies": { + "hammerjs": "^2.0.8" + }, + "peerDependencies": { + "chart.js": ">=3.2.0" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.12", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", @@ -6536,6 +6602,15 @@ "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", "dev": true }, + "node_modules/date-fns": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.0.1.tgz", + "integrity": "sha512-cr9igCUa0QSqgAMj7JOrYTY6Nh1rmyGrFDko7ADqfmaQqP/I2N4rlfrLl7AWuzDaoIpz6MNjoEcTPzgZYIrhnA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -8717,6 +8792,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -14333,6 +14416,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", diff --git a/package.json b/package.json index 67c3cab..b5f59ce 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,12 @@ "node": ">=18.0" }, "dependencies": { - "@docusaurus/plugin-client-redirects": "^3.0.1" + "@docusaurus/plugin-client-redirects": "^3.0.1", + "@tanstack/react-query": "^5.14.2", + "chart.js": "^4.4.1", + "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-zoom": "^2.0.1", + "date-fns": "^3.0.1", + "react-chartjs-2": "^5.2.0" } } diff --git a/src/components/ResourceChart.tsx b/src/components/ResourceChart.tsx new file mode 100644 index 0000000..202a315 --- /dev/null +++ b/src/components/ResourceChart.tsx @@ -0,0 +1,123 @@ +import * as React from 'react'; +import 'chartjs-adapter-date-fns'; +import { Line } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + TimeScale, + TimeSeriesScale, + Colors, +} from 'chart.js'; +import zoomPlugin from 'chartjs-plugin-zoom'; + +const registerList = [ + CategoryScale, + LinearScale, + PointElement, + LineElement, + TimeScale, + TimeSeriesScale, + Title, + Tooltip, + Legend, + Colors, + zoomPlugin, +]; + +function ResourceChart({ + data, + ...props +}: { + data: { + [nodeId: string]: { + timestamps: number[]; + values: number[]; + }; + }; +} & React.HTMLAttributes) { + const [isRegistered, setIsRegistered] = React.useState(false); + React.useEffect(() => { + ChartJS.register(...registerList); + setIsRegistered(true); + return () => { + setIsRegistered(false); + ChartJS.unregister(...registerList); + }; + }, []); + const timestamps = Object.values(data).at(0)?.timestamps ?? []; + return isRegistered ? ( + ({ + label: nodeId, + data: data.values, + })), + }} + options={{ + scales: { + x: { + type: 'time', + }, + y: { + ticks: { + callback: (value) => { + return value + '%'; + }, + }, + }, + }, + plugins: { + tooltip: { + callbacks: { + label: (item) => { + return item.formattedValue + '% Usage'; + }, + }, + }, + colors: { + enabled: true, + }, + zoom: { + zoom: { + pinch: { + enabled: true, + }, + wheel: { + enabled: true, + }, + mode: 'x', + }, + pan: { + enabled: true, + mode: 'x', + }, + limits: { + x: { + min: timestamps.at(-1), + max: timestamps.at(0), + }, + }, + }, + }, + elements: { + point: { + pointStyle: false, + }, + }, + }} + {...props} + /> + ) : ( + <> + ); +} + +export default ResourceChart; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 53068b9..8092418 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -2,32 +2,52 @@ import type { IpGeo } from '../types'; import * as React from 'react'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; +import { useQuery } from '@tanstack/react-query'; import NodeList from '../components/NodeList'; import Map from '../components/Map'; import SeednodeList from '../components/SeednodeList'; +import ResourceChart from '../components/ResourceChart'; export default function Home(): JSX.Element { const { siteConfig } = useDocusaurusContext(); - const [nodesGeo, setNodesGeo] = React.useState< - | { - [nodeId: string]: IpGeo; - } - | undefined - >(); - const [seednodes, setSeednodes] = React.useState<{ [nodeId: string]: any }>(); - React.useEffect(() => { - // Last 7 days - void fetch( - `${siteConfig.url}/api/nodes/geo?seek=${ - Date.now() - 1000 * 60 * 60 * 24 * 7 - }`, - ).then(async (data) => { - setNodesGeo(await data.json()); - }); - void fetch(`${siteConfig.url}/api/seednodes`).then(async (data) => { - setSeednodes(await data.json()); - }); - }, []); + const nodesGeoQuery = useQuery<{ [nodeId: string]: IpGeo }>({ + queryKey: ['nodesGeo'], + queryFn: () => + fetch( + `${siteConfig.url}/api/nodes/geo?seek=${ + Date.now() - 1000 * 60 * 60 * 24 * 7 + }`, + ).then((response) => response.json()), + refetchInterval: 60 * 1000, + }); + const seedNodesQuery = useQuery<{ [nodeId: string]: any }>({ + queryKey: ['seedNodes'], + queryFn: () => + fetch(`${siteConfig.url}/api/seednodes`).then((response) => + response.json(), + ), + }); + const resourceCpuQuery = useQuery<{ + [nodeId: string]: { values: Array; timestamps: Array }; + }>({ + queryKey: ['resourceCpu'], + queryFn: () => + fetch(`${siteConfig.url}/api/resource/cpu`).then((response) => + response.json(), + ), + refetchInterval: 60 * 1000, + }); + + const resourceMemoryQuery = useQuery<{ + [nodeId: string]: { values: Array; timestamps: Array }; + }>({ + queryKey: ['resourceMemory'], + queryFn: () => + fetch(`${siteConfig.url}/api/resource/memory`).then((response) => + response.json(), + ), + refetchInterval: 60 * 1000, + }); return (
- +

Seednodes:

- {seednodes != null ? : <>} + {seedNodesQuery.data != null ? ( + + ) : ( + <> + )} +

Seednodes Resources:

+ {resourceCpuQuery.data != null ? ( + + ) : ( + <> + )} + {resourceMemoryQuery.data != null ? ( + + ) : ( + <> + )}

Nodes:

- {nodesGeo != null ? : <>} + {nodesGeoQuery.data != null ? ( + + ) : ( + <> + )}
diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx new file mode 100644 index 0000000..869a5e7 --- /dev/null +++ b/src/theme/Root.tsx @@ -0,0 +1,12 @@ +import * as React from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +const queryClient = new QueryClient(); + +function Root({ children }) { + return ( + {children} + ); +} + +export default Root; From e03d9741ac3aa3623079bd442f1c898c7d5bf60e Mon Sep 17 00:00:00 2001 From: Amy Yan Date: Wed, 20 Dec 2023 16:00:00 +1100 Subject: [PATCH 2/2] feat: chart styling --- src/components/NodeCard.tsx | 37 +++++++++++++++++++ src/components/ResourceChart.tsx | 10 +++++- src/components/SeednodeList.tsx | 34 ------------------ src/pages/index.tsx | 62 ++++++++++++++++++-------------- 4 files changed, 81 insertions(+), 62 deletions(-) create mode 100644 src/components/NodeCard.tsx delete mode 100644 src/components/SeednodeList.tsx diff --git a/src/components/NodeCard.tsx b/src/components/NodeCard.tsx new file mode 100644 index 0000000..7911462 --- /dev/null +++ b/src/components/NodeCard.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import clsx from 'clsx'; + +const NodeCard = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { + nodeId: string; + remoteInfo?: { host: string; port: number }; + } + // Complains about props not being validated + // eslint-disable-next-line +>(({ className, nodeId, remoteInfo, ...props }, ref) => { + return ( +
+ Node ID: +
{nodeId}
+ {remoteInfo != null ? ( + <> + Address: +
+ {remoteInfo.host}:{remoteInfo.port} +
+ + ) : ( + <> + )} +
+ ); +}); + +NodeCard.displayName = 'SeednodeCard'; + +export default NodeCard; diff --git a/src/components/ResourceChart.tsx b/src/components/ResourceChart.tsx index 202a315..6ef3d6d 100644 --- a/src/components/ResourceChart.tsx +++ b/src/components/ResourceChart.tsx @@ -32,6 +32,7 @@ const registerList = [ function ResourceChart({ data, + title, ...props }: { data: { @@ -40,6 +41,7 @@ function ResourceChart({ values: number[]; }; }; + title?: string; } & React.HTMLAttributes) { const [isRegistered, setIsRegistered] = React.useState(false); React.useEffect(() => { @@ -53,6 +55,7 @@ function ResourceChart({ const timestamps = Object.values(data).at(0)?.timestamps ?? []; return isRegistered ? ( { - return item.formattedValue + '% Usage'; + return item.formattedValue + '%'; }, }, }, @@ -113,6 +120,7 @@ function ResourceChart({ }, }, }} + height={200} {...props} /> ) : ( diff --git a/src/components/SeednodeList.tsx b/src/components/SeednodeList.tsx deleted file mode 100644 index c2c3da6..0000000 --- a/src/components/SeednodeList.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import clsx from 'clsx'; - -const SeednodeList = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & { - seedNodes: { [nodeId: string]: { host: string; port: number } }; - } - // Complains about props not being validated - // eslint-disable-next-line ->(({ className, seedNodes, ...props }, ref) => { - return ( -
- {seedNodes != null ? ( - Object.entries(seedNodes).map(([nodeId, data]) => ( -
- Node ID: -
{nodeId}
- Address: -
- {data.host}:{data.port} -
-
- )) - ) : ( - <> - )} -
- ); -}); - -SeednodeList.displayName = 'SeednodeList'; - -export default SeednodeList; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 8092418..cf36dfc 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -3,10 +3,9 @@ import * as React from 'react'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import { useQuery } from '@tanstack/react-query'; -import NodeList from '../components/NodeList'; import Map from '../components/Map'; -import SeednodeList from '../components/SeednodeList'; import ResourceChart from '../components/ResourceChart'; +import NodeCard from '../components/NodeCard'; export default function Home(): JSX.Element { const { siteConfig } = useDocusaurusContext(); @@ -37,7 +36,6 @@ export default function Home(): JSX.Element { ), refetchInterval: 60 * 1000, }); - const resourceMemoryQuery = useQuery<{ [nodeId: string]: { values: Array; timestamps: Array }; }>({ @@ -62,30 +60,40 @@ export default function Home(): JSX.Element { -
-

Seednodes:

- {seedNodesQuery.data != null ? ( - - ) : ( - <> - )} -

Seednodes Resources:

- {resourceCpuQuery.data != null ? ( - - ) : ( - <> - )} - {resourceMemoryQuery.data != null ? ( - - ) : ( - <> - )} -

Nodes:

- {nodesGeoQuery.data != null ? ( - - ) : ( - <> - )} +
+

Seed Nodes

+
+ {seedNodesQuery.data != null ? ( + Object.entries(seedNodesQuery.data).map(([nodeId, data]) => ( + + )) + ) : ( + <> + )} +
+
+
+ {resourceCpuQuery.data != null ? ( + + ) : ( + <> + )} +
+
+ {resourceMemoryQuery.data != null ? ( + + ) : ( + <> + )} +
+