/__mocks__/fileMock.js"
23 | }
24 | },
25 | "author": "",
26 | "license": "ISC",
27 | "dependencies": {
28 | "@apollo/client": "^3.4.10",
29 | "apollo-datasource-rest": "^3.2.0",
30 | "apollo-server-core": "^3.3.0",
31 | "apollo-server-express": "^3.3.0",
32 | "axios": "^0.21.1",
33 | "camelcase-keys": "^7.0.0",
34 | "express": "^4.17.1",
35 | "graphiql": "^1.4.2",
36 | "graphql": "^15.5.2",
37 | "graphql-tag": "^2.12.5",
38 | "jest-fetch-mock": "^3.0.3",
39 | "mdbreact": "^5.1.0",
40 | "node-fetch": "^2.6.1",
41 | "pg-promise": "^10.11.0",
42 | "prop-types": "^15.7.2",
43 | "react": "^17.0.2",
44 | "react-dom": "^17.0.2",
45 | "react-redux": "^7.2.4",
46 | "react-router": "^5.2.0",
47 | "react-router-dom": "^5.2.0",
48 | "react-table": "^7.7.0",
49 | "recharts": "^2.1.0",
50 | "redux": "^4.1.0",
51 | "redux-devtools-extension": "^2.13.9",
52 | "redux-thunk": "^2.3.0"
53 | },
54 | "devDependencies": {
55 | "@babel/core": "^7.14.8",
56 | "@babel/plugin-proposal-class-properties": "^7.14.5",
57 | "@babel/plugin-transform-async-to-generator": "^7.14.5",
58 | "@babel/plugin-transform-runtime": "^7.15.0",
59 | "@babel/preset-env": "^7.14.8",
60 | "@babel/preset-react": "^7.14.5",
61 | "@testing-library/dom": "^8.2.0",
62 | "@testing-library/jest-dom": "^5.14.1",
63 | "@testing-library/react": "^12.0.0",
64 | "@testing-library/user-event": "^13.2.1",
65 | "babel-core": "^6.26.3",
66 | "babel-loader": "^8.2.2",
67 | "babel-preset-env": "^1.7.0",
68 | "css-loader": "^6.2.0",
69 | "file-loader": "^6.2.0",
70 | "jest": "^27.1.1",
71 | "msw": "^0.35.0",
72 | "nodemon": "^2.0.12",
73 | "react-hot-loader": "^4.13.0",
74 | "sockjs": "^0.3.21",
75 | "style-loader": "^3.2.1",
76 | "supertest": "^6.1.6",
77 | "webpack": "^5.47.0",
78 | "webpack-cli": "^4.7.2",
79 | "webpack-dev-server": "^3.11.2"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/dashboard/client/components/Memory.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * *****************************************************************************
3 | * @description Component that renders Node memory usage
4 | * *****************************************************************************
5 | */
6 |
7 |
8 | import React, { useState, useEffect } from 'react';
9 | import {
10 | BarChart,
11 | Bar,
12 | Cell,
13 | XAxis,
14 | YAxis,
15 | CartesianGrid,
16 | Tooltip,
17 | Legend,
18 | ResponsiveContainer,
19 | } from 'recharts';
20 | import MemoryTooltip from './MemoryTooltip';
21 | import colors from '../assets/colors';
22 |
23 |
24 | const Memory = ({ nodeMemory, nodeNums }) => {
25 | const resultArr = [];
26 | const [result, setResult] = useState([]);
27 | const [render, setRender] = useState(false);
28 | if (nodeMemory.data) {
29 | const nodes = nodeMemory.data.result;
30 | nodes.forEach((node, i) => {
31 | // match length of instance to length of ip addresses in reference node list
32 | const len = nodeNums[0].length;
33 | const internal_ip = node.metric.instance.slice(0, len);
34 | // find position of node in reference list
35 | const position = nodeNums.findIndex((ip) => ip === internal_ip);
36 | const dataPoint = {};
37 |
38 |
39 | // builds a datapoint that has the correct node # & the % memory used data
40 | dataPoint.node = 'node' + (position + 1);
41 | dataPoint.ip = internal_ip;
42 | dataPoint.percentageMemoryUsed = (parseFloat(node.value[1])*100).toFixed(2);
43 | resultArr[position] = dataPoint;
44 | });
45 | //prevents recharts.js from causing infinite loop of re-renderes
46 | if (render === false) {
47 | setResult(resultArr);
48 | setRender(true);
49 | }
50 | }
51 |
52 | return (
53 |
54 |
Memory Usage
55 |
56 |
67 |
68 | {render && }
69 | {
70 | return `${tick}%`;
71 | }}
72 | />
73 |
74 |
75 | {resultArr.map((entry, index) => (
76 | |
77 | ))}
78 |
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | export default Memory;
86 |
87 |
--------------------------------------------------------------------------------
/dashboard/server/controllers/metricController.js:
--------------------------------------------------------------------------------
1 | /*
2 | * ******************************************************************************************
3 | * @description: Controller for node metrics using REST API (not in use, switched to GraphQL)
4 | * ******************************************************************************************
5 | */
6 |
7 |
8 |
9 | const fetch = require('node-fetch');
10 |
11 | const metricController = {};
12 |
13 | const currentDate = Math.floor(Date.now() / 1000);
14 | const startDate = currentDate - 21600;
15 |
16 | metricController.getTotalDisk = async (req, res, next) => {
17 | // get totalbytes==> disk usage will be (total-free) / total
18 | const totalQuery = `http://localhost:9090/api/v1/query?query=sum(node_filesystem_size_bytes)by(instance)`;
19 |
20 | try {
21 | const response = await fetch(totalQuery);
22 | res.locals.totalDisk = await response.json();
23 |
24 | return next();
25 | } catch (err) {
26 | return next(err);
27 | }
28 | };
29 |
30 | metricController.getFreeDisk = async (req, res, next) => {
31 | // get the free bytes: time series
32 | const freeQuery = `http://localhost:9090/api/v1/query_range?query=sum(node_filesystem_free_bytes)by(instance)&start=${startDate}&end=${currentDate}&step=1m`;
33 |
34 | // try/catch block to get free disk data bytes
35 | try {
36 | const response = await fetch(freeQuery);
37 | res.locals.freeDisk = await response.json();
38 |
39 | return next();
40 | } catch (err) {
41 | return next(err);
42 | }
43 | };
44 |
45 | metricController.getNodeCPU = async (req, res, next) => {
46 | const query = `http://localhost:9090/api/v1/query_range?query=sum(rate(container_cpu_usage_seconds_total{image!=%22%22}[1m]))by(instance)&start=${startDate}&end=${currentDate}&step=1m`;
47 |
48 | try {
49 | const response = await fetch(query);
50 | res.locals.nodeCPU = await response.json();
51 | return next();
52 | } catch (err) {
53 | return next(err);
54 | }
55 | };
56 |
57 | metricController.getNodeMemory = async (req, res, next) => {
58 | const query = `http://localhost:9090/api/v1/query?query=sum(container_memory_usage_bytes)by(instance)%20/%20sum(container_spec_memory_limit_bytes)%20by%20(instance)`;
59 |
60 | try {
61 | const response = await fetch(query);
62 | res.locals.nodeMemory = await response.json();
63 |
64 | return next();
65 | } catch (err) {
66 | return next(err);
67 | }
68 | };
69 |
70 | metricController.getClusterInfo = async (req, res, next) => {
71 | const query = `http://localhost:9090/api/v1/query?query=kube_node_info`;
72 |
73 | try {
74 | const response = await fetch(query);
75 | res.locals.clusterInfo = await response.json();
76 |
77 | return next();
78 | } catch (err) {
79 | return next(err);
80 | }
81 | };
82 |
83 | module.exports = metricController;
84 |
--------------------------------------------------------------------------------
/dashboard/client/components/PodMemoryCurrentComponent.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * *****************************************************************************
3 | * @description Chart component to render current memory usage of all pods in cluster
4 | * *****************************************************************************
5 | */
6 |
7 |
8 | import React, { useState, useEffect } from 'react';
9 | import {
10 | BarChart,
11 | Bar,
12 | Cell,
13 | XAxis,
14 | YAxis,
15 | CartesianGrid,
16 | Tooltip,
17 | Legend,
18 | ResponsiveContainer,
19 | } from 'recharts';
20 | import PodMemoryTooltip from './PodMemoryTooltip';
21 | import colors from '../assets/colors';
22 |
23 | const PodMemoryCurrentComponent = ({ podMemoryCurrent, podNums, clickedArray }) => {
24 | const [result, setResult] = useState([]); // data to pass to the chart
25 | const [render, setRender] = useState(false); // render state to allow recharts animation but prevent constant re-rendering
26 | let sortedData = []
27 |
28 | // check if current memory data has been received from query AND if podNums list contains pods
29 | if(podMemoryCurrent.data && Object.keys(podNums).length > 0) {
30 |
31 | const data = [];
32 | const podArray = podMemoryCurrent.data.result;
33 | for (let i = 0; i < podArray.length; i++) {
34 | const pod = {}; // create objects for each pod with relevant data from current memory query and pod number from podNums
35 | const podName = podArray[i].metric.pod;
36 | const newPodNumber = podNums[podName];
37 | if(newPodNumber) { // if pod exists in podNums (doesn't have a null node), assign values and push to data array
38 | pod.name = newPodNumber.name;
39 | pod.value = +((+(podArray[i].value[1]) / 1000000).toFixed(2)) ;
40 | pod.number = newPodNumber.number
41 | data.push(pod)
42 | }
43 | }
44 |
45 | sortedData = data.sort((a,b)=>(a.number > b.number) ? 1 : -1); // sort data array by pod number
46 |
47 | if (render === false) {
48 | setResult(sortedData); // set results with sorted data array
49 | setRender(true);
50 | }
51 | }
52 |
53 | return (
54 |
55 |
Pod Memory Usage
56 |
57 |
68 |
69 | {render && }
70 |
71 |
72 |
73 |
74 |
75 |
76 | );
77 | };
78 |
79 | export default PodMemoryCurrentComponent;
80 |
81 |
--------------------------------------------------------------------------------
/dashboard/client/components/CPU.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * *****************************************************************************
3 | * @description Component that renders Node CPU chart
4 | * *****************************************************************************
5 | */
6 |
7 | import React, { useState } from 'react';
8 | import {
9 | LineChart,
10 | Line,
11 | XAxis,
12 | YAxis,
13 | CartesianGrid,
14 | Tooltip,
15 | Legend,
16 | ResponsiveContainer,
17 | } from 'recharts';
18 | import TimeSeriesTooltip from './TimeSeriesTooltip';
19 | import colors from '../assets/colors';
20 |
21 | const CPU = (props) => {
22 | const resultArr = [];
23 | const lines = [];
24 | const [results, setResults] = useState([]);
25 | const [render, setRender] = useState(false);
26 | if (props.cpu.data) {
27 | const nodes = props.cpu.data.result;
28 | const nodeNums = props.nodeNums;
29 |
30 | // establishes a for loop based on length of first node
31 | nodes[0].values.forEach((x, i) => {
32 | const dataPoint = {};
33 | let current = new Date(x[0] * 1000);
34 | dataPoint.time = current.toLocaleString();
35 |
36 | for (let j = 0; j < nodes.length; j++) {
37 | // match length of instance to length of ip addresses in our reference node list
38 | const len = nodeNums[0].length;
39 | const internal_ip = nodes[j].metric.instance.slice(0, len);
40 | // find position of node in reference list
41 | const position = nodeNums.findIndex((ip) => ip === internal_ip);
42 | //create a datapoint with the correct node# (from reference list) and the relevant value
43 | dataPoint[`node${position + 1}`] = +(parseFloat(nodes[j].values[i][1])*100).toFixed(
44 | 2);
45 | }
46 | resultArr.push(dataPoint);
47 | });
48 | if (render === false) {
49 | setResults(resultArr);
50 | setRender(true);
51 | }
52 |
53 | //create line for CPU data for each node.
54 | for (let i = 0; i < nodes.length; i++) {
55 | lines.push(
56 |
63 | );
64 | }
65 | };
66 |
67 |
68 | return (
69 |
70 |
CPU Usage
71 |
72 |
83 |
84 |
89 | {
90 | return `${tick}%`;
91 | }}
92 | />
93 |
94 |
98 | {lines}
99 |
100 |
101 | );
102 | };
103 |
104 | export default CPU;
105 |
--------------------------------------------------------------------------------
/dashboard/client/components/PodCPU.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * *****************************************************************************
3 | * @description Component that renders Pod CPU usage chart
4 | * *****************************************************************************
5 | */
6 |
7 | import React, { useState } from 'react';
8 | import {
9 | LineChart,
10 | Line,
11 | XAxis,
12 | YAxis,
13 | CartesianGrid,
14 | Tooltip,
15 | Legend,
16 | ResponsiveContainer,
17 | } from 'recharts';
18 | import PodCpuToolTip from './PodCpuToolTip';
19 | import colors from '../assets/colors';
20 |
21 | const PodCPU = ({ clickedArray, timeWindow, step }) => {
22 | const [results, setResults] = useState([]);
23 | const [render, setRender] = useState(false);
24 | const [clickedLength, setClickedLength] = useState(0);
25 | const [timeWindowChange, setTimeWindowChange] = useState(timeWindow);
26 | const [stepChange, setStepChange] = useState(step);
27 | const lines = [];
28 | const resultArray = [];
29 |
30 | // clickedArray is the user selected list of pods, passed down from Pod Container
31 |
32 | if (clickedLength !== clickedArray.length) { // use clickedLength to trigger re-render when new clicked array is received
33 | if (clickedArray.length === 0) setClickedLength(0);
34 | setRender(false);
35 | }
36 |
37 | if (clickedArray.length > 0) {
38 | // create datapoint objects for each point in time series with values for first pod in clicked array
39 | clickedArray[0].cpuValues.forEach((x, i) => {
40 | const dataPoint = {};
41 | let time = new Date(x[0] * 1000);
42 | dataPoint.time = time.toLocaleString();
43 |
44 | // add values for other pods
45 | for (let j = 0; j < clickedArray.length; j++) {
46 | dataPoint[clickedArray[j].name] = +(
47 | parseFloat(clickedArray[j].cpuValues[i][1]) * 100
48 | ).toFixed(2);
49 | }
50 | resultArray.push(dataPoint); // results array contains datapoint objects
51 | });
52 |
53 | if (render === false) {
54 | setResults(resultArray);
55 | setClickedLength(clickedArray.length);
56 | setRender(true);
57 | }
58 |
59 | for (let i = 0; i < clickedArray.length; i++) {
60 | lines.push(
61 |
68 | );
69 | }
70 | }
71 |
72 | return (
73 |
74 |
CPU Usage
75 |
76 |
86 |
87 |
92 | {
95 | return `${tick}%`;
96 | }}
97 | />
98 |
99 |
100 | {lines}
101 |
102 |
103 | );
104 | };
105 |
106 | export default PodCPU;
107 |
--------------------------------------------------------------------------------
/dashboard/client/components/PodMemorySeriesComponent.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * *****************************************************************************
3 | * @description Linechart component to render time-series data of memory usage of selected pods
4 | * *****************************************************************************
5 | */
6 |
7 | import React, { useState } from 'react';
8 | import {
9 | LineChart,
10 | Line,
11 | XAxis,
12 | YAxis,
13 | CartesianGrid,
14 | Tooltip,
15 | Legend,
16 | ResponsiveContainer,
17 | } from 'recharts';
18 | import PodMemorySeriesTooltip from './PodMemorySeriesTooltip';
19 | import colors from '../assets/colors';
20 |
21 | const PodMemorySeriesComponent = ({ clickedArray }) => {
22 | const [results, setResults] = useState([]); // data to pass to chart component
23 | const [render, setRender] = useState(false); // render to track recharts animation without constant re-rendering
24 | const [clickedLength, setClickedLength] = useState(0);
25 | const lines = [];
26 | const resultArray = [];
27 |
28 | if (clickedLength !== clickedArray.length) { // if the length of the clickedarray changes allow re-render with new clickedarray
29 | if (clickedArray.length === 0) setClickedLength(0);
30 | setRender(false);
31 | }
32 |
33 |
34 |
35 | if (clickedArray.length > 0) {
36 | clickedArray[0].memorySeriesValues.forEach((x, i) => {
37 | const dataPoint = {}; // create datapoint object for each time/memory value of the first pod in clickedarray
38 | let time = new Date(x[0] * 1000);
39 | dataPoint.time = time.toLocaleString();
40 |
41 | for (let j = 0; j < clickedArray.length; j++) { // add values for other pods in the clickedarray
42 | dataPoint[clickedArray[j].name] = +(
43 | parseFloat(clickedArray[j].memorySeriesValues[i][1]) / 1000000
44 | ).toFixed(4);
45 | }
46 | resultArray.push(dataPoint); // push each datapoint to the resultarray
47 | });
48 |
49 | if (render === false) {
50 | setResults(resultArray); // set results with resultarray
51 | setClickedLength(clickedArray.length); // update clickedlength state to current clickedarray length
52 | setRender(true);
53 | }
54 |
55 | for (let i = 0; i < clickedArray.length; i++) {
56 | lines.push(
57 |
64 | );
65 | }
66 | }
67 |
68 | return (
69 |
70 |
Memory Usage
71 |
72 |
82 |
83 |
88 | {
91 | return `${tick}MB`;
92 | }}
93 | />
94 |
95 |
96 | {lines}
97 |
98 |
99 | );
100 | };
101 |
102 | export default PodMemorySeriesComponent;
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Periscope
2 |
3 |
4 |
5 |
6 |
7 | Periscope Dashboard
8 |
9 |
10 |
11 | []()
12 |
13 |
14 |
15 | ---
16 |
17 | Periscope is the dashboard solution for monitoring and tracking your Kubernetes pods & nodes.
18 |
19 |
20 | Visit us at getperiscopedashboard.com
21 |
22 |
23 |
24 |
25 | ## 📝 Table of Contents
26 | - [About](#about)
27 | - [Built Using](#built_using)
28 | - [Demo](#demo)
29 | - [Getting Started](#getting_started)
30 | - [Prerequisites](#prerequisites)
31 | - [Authors](#authors)
32 | - [Coming Soon](#coming_soon)
33 |
34 | ## 🧐 About
35 | Periscope is the dashboard solution for monitoring and tracking your Kubernetes pods & nodes.
36 |
37 | Periscope integrates with a Prometheus server and then displays the core metrics that any engineer needs to understand the state and health of their cluster.
38 | Engineers can see CPU, disk usage and memory usage across their cluster.
39 |
40 | The dashboard makes it easy to see troubling trends thereby providing developers with the information needed to make changes.
41 |
42 | ### ⛏️ Built Using
43 | - [Kubernetes](https://www.kubernetes.dev/)
44 | - [Prometheus|PromQL](https://prometheus.io/)
45 | - [React](https://reactjs.org)
46 | - [NodeJS|Express](https://expressjs.com/)
47 | - [Apollo GraphQL](https://www.apollographql.com/)
48 | - [React Router](https://reactrouter.com/)
49 | - [Locust](https://locust.io/)
50 | - [Recharts](https://recharts.org/en-US/)
51 | - [React Table](https://react-table.tanstack.com/)
52 | - [Webpack](https://webpack.js.org/)
53 | - [React Testing Library](https://testing-library.com/docs/react-testing-library/)
54 | - [Jest](https://jestjs.io/)
55 |
56 |
57 |
58 | ## 🎥 Demo
59 |
60 |
61 |
62 |
63 |
64 |
65 | ## 🏁 Getting Started
66 | Start by forking and cloning this repo.
67 |
68 | ### Prerequisites
69 | - Install the [kubectl](https://kubernetes.io/docs/tasks/tools) command line tools.
70 | - Host your Kubernetes cluster on a service like [GKE](https://cloud.google.com/kubernetes-engine) or [EKS](https://aws.amazon.com/eks/) or use [MiniKube](https://minikube.sigs.k8s.io/docs/start).
71 | - Install [the Prometheus server](https://prometheus-operator.dev/docs/prologue/quick-start/) in order to generate your metrics
72 | - Save your Prometheus server on the default namespace
73 | - Then build and run the dashboard!
74 |
75 | ## ✍️ Authors
76 | - Adda Kridler: [Github](https://github.com/addakridler) | [LinkedIn](https://www.linkedin.com/in/adda-kridler-23028887/)
77 | - Junie Hou: [Github](https://github.com/selilac) | [LinkedIn](https://www.linkedin.com/in/juniehou/)
78 | - Ronke Oyekunle: [Github](https://github.com/ronke11) | [LinkedIn](https://www.linkedin.com/in/royekunle)
79 | - Shawn Convery: [Github](https://github.com/smconvery) | [LinkedIn](https://www.linkedin.com/in/shawn-convery-459b79167/)
80 |
81 | ## 🎉 Coming Soon!
82 | - Set email / slack alerts for major changes in metrics
83 | - Ability to enter your own PromQL query
84 | - Log History
85 |
--------------------------------------------------------------------------------
/dashboard/client/container/NodeContainer.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * *****************************************************************************
3 | * @description Node dashboard page container
4 | * *****************************************************************************
5 | */
6 |
7 | import React, { useState, useEffect } from 'react';
8 | import Memory from '../components/Memory.jsx';
9 | import CPU from '../components/CPU.jsx';
10 | import ClusterInfo from '../components/ClusterInfo.jsx';
11 | import DiskUsage from '../components/DiskUsage.jsx';
12 | import loading from '../assets/loading.gif';
13 |
14 | const mainContainerGraphQL = () => {
15 | const [cpu, setCPU] = useState({});
16 | const [totalDisk, setTotalDisk] = useState({});
17 | const [freeDisk, setFreeDisk] = useState({});
18 | const [nodeMemory, setNodeMemory] = useState({});
19 | const [clusterInfo, setClusterInfo] = useState({});
20 | const [isLoading, setIsLoading] = useState(true);
21 | const [nodeNums, setNodeNums] = useState([]);
22 | const [called, setCalled] = useState(false);
23 |
24 | const sixHours = 21600;
25 | const endTime = Math.floor(Date.now() / 1000);;
26 | const startTime = endTime - sixHours;
27 |
28 |
29 | const step = '5m';
30 |
31 | //graphQL query
32 | const query = `{
33 | getFreeDiskSpace(startTime: "${startTime}", endTime: "${endTime}", step: "${step}") {
34 | data {
35 | result {
36 | metric {
37 | instance
38 | }
39 | values
40 | }
41 | }
42 | }
43 | getNodeCpu(startTime: "${startTime}", endTime: "${endTime}", step: "${step}") {
44 | data {
45 | result {
46 | metric {
47 | instance
48 | }
49 | values
50 | }
51 | }
52 | }
53 | getClusterInfo {
54 | data {
55 | result {
56 | metric {
57 | internal_ip
58 | node
59 | }
60 | value
61 | }
62 | }
63 | }
64 | getNodeMemory {
65 | data {
66 | result {
67 | metric {
68 | instance
69 | }
70 | value
71 | }
72 | }
73 | }
74 | getTotalDiskSpace {
75 | data {
76 | result {
77 | metric {
78 | instance
79 | }
80 | value
81 | }
82 | }
83 | }
84 | }`;
85 |
86 | //fetch request based on graphQL query
87 | useEffect(() => {
88 | fetch('/graphql', {
89 | method: 'POST',
90 | headers: {
91 | 'Content-Type': 'application/json',
92 | },
93 | body: JSON.stringify({
94 | query,
95 | }),
96 | })
97 | .then((res) => res.json())
98 | .then((res) => {
99 | const data = res.data;
100 | setCPU(data.getNodeCpu);
101 | setTotalDisk(data.getTotalDiskSpace);
102 | setFreeDisk(data.getFreeDiskSpace);
103 | setNodeMemory(data.getNodeMemory);
104 | setClusterInfo(data.getClusterInfo);
105 | setIsLoading(false);
106 | });
107 | }, []);
108 |
109 | // if data is loaded and data states are set, but called state is false
110 | if (!isLoading && !called) {
111 | const result = [];
112 | for (let i = 1; i <= clusterInfo.data.result.length; i++) {
113 | // create nodes 1 through x based on internal Ip addresses
114 | result.push(clusterInfo.data.result[i - 1].metric.internal_ip);
115 | }
116 | setNodeNums(result);
117 | setCalled(true);
118 | }
119 |
120 | //displays a loading gif if data pull isn't complete yet
121 | return isLoading ? (
122 |
123 | ) : (
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | );
139 | };
140 |
141 | export default mainContainerGraphQL;
142 |
--------------------------------------------------------------------------------
/dashboard/client/components/DiskUsage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import lineColors from '../assets/colors';
3 | /*
4 | * *****************************************************************************
5 | * @description Component that renders Node CPU chart
6 | * *****************************************************************************
7 | */
8 |
9 | import TimeSeriesTooltip from './TimeSeriesTooltip';
10 | import {
11 | LineChart,
12 | Line,
13 | XAxis,
14 | YAxis,
15 | CartesianGrid,
16 | Tooltip,
17 | Legend,
18 | ResponsiveContainer,
19 | } from 'recharts';
20 |
21 | const DiskUsage = (props) => {
22 |
23 | // nodes object ==> name of node: total diskSpace
24 | const nodes = {};
25 | const data = [];
26 | const lines = [];
27 | const [diskUsage, setDiskUsage] = useState([]);
28 | const [render, setRender] = useState(false);
29 | if (props.free.data) {
30 | const total = props.total.data?.result;
31 | const free = props.free.data?.result;
32 | const nodes = {};
33 | const nodeNums = props.nodeNums;
34 | // loop through freediskspace and get the times
35 |
36 | // loops through totalDiskSpace query and pushes the name of node and total diskspace of node into an object
37 | for (let i = 0; i < total.length; i++) {
38 | // push each node #: diskSpace
39 |
40 | // match length of instance to length of ip addresses in node list
41 | const len = nodeNums[0].length;
42 | const internal_ip = total[i].metric.instance.slice(0, len);
43 | // // find position of node in reference list
44 | const position = nodeNums.findIndex((ip) => ip === internal_ip);
45 |
46 | nodes[`node${position + 1}`] = total[i].value[1];
47 | }
48 |
49 | // loops through FreeDiskSpace and sends time and value @ time to new object
50 | for (let i = 0; i < free.length; i++) {
51 | const values = free[i].values;
52 | //find correct nodeNum
53 | // match length of instance to length of ip addresses in node list
54 | const len = nodeNums[0].length;
55 | const internal_ip = free[i].metric.instance.slice(0, len);
56 | // // find position of node in reference list
57 | const position = nodeNums.findIndex((ip) => ip === internal_ip);
58 |
59 | // grab all the times from the first index of the array
60 | if (i === 0) {
61 | for (let j = 0; j < values.length; j++) {
62 | const time = new Date(values[j][0] * 1000).toLocaleString();
63 | data.push({ time: time });
64 | }
65 | }
66 | // put the node # & it's value in each time object
67 | for (let k = 0; k < data.length; k++) {
68 | // (total size - value at each time) / total size
69 | const totalDisk = nodes[`node${position + 1}`];
70 | const freeDiskSpace = values[k][1];
71 | data[k][`node${position + 1}`] = (((totalDisk - freeDiskSpace) / totalDisk)*100).toFixed(2);
72 | }
73 | }
74 | //prevents recharts.js from creating infinite loop with re-renders.
75 | if (render === false) {
76 | setDiskUsage(data);
77 | setRender(true);
78 | }
79 |
80 | //adds a line in the graph for each node with total disk usage over time.
81 |
82 | for (let i = 0; i < total.length; i++) {
83 | lines.push(
84 |
91 | );
92 | }
93 | }
94 | return (
95 |
96 |
Disk Usage
97 |
109 |
110 |
111 |
116 | {
117 | return `${tick}%`;
118 | }}
119 | />
120 |
121 |
125 | {lines}
126 |
127 |
128 | );
129 | };
130 |
131 | export default DiskUsage;
132 |
--------------------------------------------------------------------------------
/dashboard/client/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
3 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
4 | background-color: #121212;
5 | color: gray;
6 | margin: 0px;
7 | }
8 |
9 | button {
10 | background-color: #0e4f8c;
11 | border: none;
12 | border-radius: 5px;
13 | color: whitesmoke;
14 | padding: 5px 10px;
15 | margin: 5px;
16 | font-weight: bold;
17 | }
18 |
19 | h2 {
20 | text-align: center;
21 | }
22 |
23 | .landing {
24 | display: flex;
25 | flex-direction: column;
26 | justify-content: center;
27 | align-items: center;
28 | height: 100vh;
29 | }
30 |
31 | .header {
32 | display: flex;
33 | justify-content: space-between;
34 | align-items: center;
35 | height: 30px;
36 | margin: 0px;
37 | padding: 10px;
38 | }
39 |
40 | .main-container {
41 | display: flex;
42 | justify-content: center;
43 | flex-wrap: wrap;
44 | }
45 |
46 | .components {
47 | border-color: #1f1b24;
48 | margin: 3px;
49 | border-radius: 10px;
50 | background-color: #1f1b24;
51 | text-align: center;
52 | }
53 |
54 | .dropdowns {
55 | display: flex;
56 | justify-content: flex-end;
57 | margin-bottom: 5px;
58 | margin-right: 30px;
59 | }
60 |
61 | .dropdowns button {
62 | background-color: #ff8505;
63 | border-radius: 0px;
64 | margin-bottom: 0px;
65 | }
66 |
67 | .dropdown-menu {
68 | /* border-radius: 0px 0px 5px 5px; */
69 | background-color: gray;
70 | opacity: 0.9;
71 | position: absolute;
72 | z-index: 2;
73 | margin: 0px 5px 5px;
74 | }
75 |
76 | .dropdown-item {
77 | font-weight: bold;
78 | text-decoration: none;
79 | color: black;
80 | }
81 |
82 | .dropdown-div {
83 | padding: 5px;
84 | color: black;
85 | font-weight: bold;
86 | }
87 |
88 | .dropdown-div:hover {
89 | background-color: whitesmoke;
90 | }
91 | .dropdown-div:active {
92 | color: #ff8505;
93 | }
94 |
95 | #timeMenu {
96 | width: 95.33px;
97 | }
98 |
99 | #stepMenu {
100 | width: 48.89px;
101 | }
102 |
103 | #podInfo {
104 | display: flex;
105 | flex-direction: column;
106 | justify-content: center;
107 | }
108 |
109 | .pod-info-rows {
110 | display: flex;
111 | flex-direction: column;
112 | justify-content: center;
113 | width: 750px;
114 | }
115 |
116 | .pod-container {
117 | display: flex;
118 | justify-content: center;
119 | flex-wrap: wrap;
120 | margin-top: 40px;
121 | }
122 |
123 | .chart-container {
124 | padding-bottom: 20px;
125 | }
126 | .table {
127 | display: flex;
128 | justify-content: center;
129 | padding-bottom: 20px;
130 | }
131 |
132 | td {
133 | margin: 0;
134 | padding-bottom: 10px;
135 | padding-left: 4px;
136 | padding-right: 4px;
137 | padding-top: 10px;
138 | /* border-bottom: 0.5px solid rgb(100, 100, 117, 0.5); */
139 | }
140 |
141 | tr:nth-of-type(odd) {
142 | background-color: #25212c;
143 | }
144 | tr > th {
145 | background-color: #1f1b24;
146 | padding-bottom: 10px;
147 | }
148 | .table-row {
149 | cursor: pointer;
150 | }
151 | .table-row:hover {
152 | color: #0e4f8c;
153 | }
154 |
155 | .column1 {
156 | width: 300px;
157 | text-overflow: scroll;
158 | overflow: hidden;
159 | }
160 | .column3 {
161 | width: 300px;
162 | text-overflow: scroll;
163 | overflow: hidden;
164 | }
165 |
166 | .table {
167 | margin: 0px 15px;
168 | }
169 | .pod-table td {
170 | padding: 5px;
171 | font-size: 12px;
172 | }
173 | .pod-table th {
174 | font-size: 12px;
175 | padding: 5px;
176 | }
177 |
178 | .recharts-wrapper {
179 | margin: 0 auto;
180 | }
181 |
182 | #CPU {
183 | width: 750px;
184 | }
185 |
186 | #disk-usage {
187 | width: 750px;
188 | }
189 |
190 | #clusterInfo {
191 | width: 500px;
192 | }
193 | #memory {
194 | width: 500px;
195 | }
196 | #logo {
197 | width: 75px;
198 | margin-right: -35px;
199 | margin-left: -5px;
200 | }
201 |
202 | #podMemory {
203 | padding-right: 25px;
204 | }
205 | #podCpu {
206 | padding-right: 25px;
207 | }
208 |
209 | #loading {
210 | display: block;
211 | margin-top: 100px;
212 | margin-left: auto;
213 | margin-right: auto;
214 | width: 50%;
215 | }
216 |
217 | @media screen and (max-width: 1280px) {
218 | .main-container {
219 | flex-direction: column;
220 | align-items: center;
221 | }
222 | #clusterInfo {
223 | width: 750px;
224 | order: 2;
225 | }
226 | #memory {
227 | width: 750px;
228 | order: 1;
229 | }
230 | #podInfo {
231 | order: 0;
232 | }
233 | #podCpu {
234 | width: 750px;
235 | order: 1;
236 | }
237 | #podMemory {
238 | width: 750px;
239 | order: 2;
240 | }
241 | #podMemoryBars {
242 | order: 3;
243 | }
244 |
245 |
246 |
247 | }
248 |
--------------------------------------------------------------------------------
/dashboard/client/components/PodInfoRows.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * *****************************************************************************
3 | * @description Information and selection of individual pods to be displayed in PodInfoTable;
4 | functions to select time and step range for time-series queries
5 | * *****************************************************************************
6 | */
7 |
8 |
9 | import React, { useState } from 'react';
10 | import PodInfoTableSetup from './PodInfoTableSetup.jsx';
11 |
12 | const PodInfoRows = ({
13 | clickedArray,
14 | setClickedArray,
15 | podNums,
16 | setStep,
17 | setTimeWindow,
18 | }) => {
19 | const [isTimeOpen, setIsTimeOpen] = useState(false); // state for time range dropdown
20 | const [isStepOpen, setIsStepOpen] = useState(false); // state for step range dropdown
21 |
22 | // on click function to select pods to add to clickedArray in podContainer
23 | const newClick = (arg) => {
24 | let found = false;
25 | const newClickedArray = clickedArray.slice(); // copy of current clickedArray to update
26 | for (let i = 0; i < newClickedArray.length; i++) {
27 | if (newClickedArray[i].podName === arg) { // if selected pod is already in clickedArray, remove it
28 | newClickedArray.splice(i, 1);
29 | setClickedArray(newClickedArray);
30 | found = true;
31 | break;
32 | }
33 | }
34 | if (!found) { // if selected pod is not in clickedArray, add it
35 | newClickedArray.push(podNums[arg]);
36 | setClickedArray(newClickedArray);
37 | }
38 | };
39 |
40 | // changes all colors of pods back to gray (unselects) once the time-range or time-step is changed
41 | function changeColorsBack() {
42 | const rows = document.querySelectorAll('.table-row');
43 | for (const row of rows) {
44 | if (row.style.color === 'orange') row.style.color = "gray";
45 | }
46 | }
47 |
48 | //time range variables for time range selection
49 | const oneHour = 3600;
50 | const sixHours = 21600;
51 | const twelveHours = 43200;
52 | const oneDay = 86400;
53 | const threeDays = 259200;
54 |
55 | const times = [oneHour, sixHours, twelveHours, oneDay, threeDays];
56 | const timeStrs = ['1hr', '6hrs', '12hrs', '1day', '3days'];
57 | const timeButtons = [];
58 | times.forEach((time, i) => {
59 | //create dropwdown items to select time range
60 | timeButtons.push(
61 | {
65 | setTimeWindow(time);
66 | setClickedArray([]);
67 | setIsTimeOpen(false);
68 | changeColorsBack();
69 | }}>
70 | {timeStrs[i]}
71 |
72 | );
73 | });
74 |
75 | const steps = ['5m', '15m', '30m', '1h']; // step range variables in array
76 | const stepButtons = [];
77 | steps.forEach((step, i) => {
78 | // create dropdown items to select step range
79 | stepButtons.push(
80 | {
84 | setStep(step);
85 | setClickedArray([]);
86 | setIsStepOpen(false);
87 | changeColorsBack();
88 | }}>
89 | {step}
90 |
91 | );
92 | });
93 |
94 | // functions to toggle time and step dropdowns
95 | const toggleStep = () => {
96 | if (isTimeOpen) setIsTimeOpen(false);
97 | isStepOpen ? setIsStepOpen(false) : setIsStepOpen(true);
98 | };
99 |
100 | const toggleTime = () => {
101 | if (isStepOpen) setIsStepOpen(false);
102 | isTimeOpen ? setIsTimeOpen(false) : setIsTimeOpen(true);
103 | };
104 |
105 |
106 |
107 | return (
108 |
109 |
110 |
111 | toggleTime()}>
112 | Time Range
113 |
114 | {isTimeOpen && (
115 |
118 | )}
119 |
120 |
121 | toggleStep()}>
122 | Step
123 |
124 | {isStepOpen && (
125 |
128 | )}
129 |
130 |
131 | {
134 | setClickedArray([]);
135 | changeColorsBack();
136 | }}>
137 | Unselect All
138 |
139 |
140 |
141 |
148 |
149 | );
150 | };
151 |
152 | export default PodInfoRows;
153 |
--------------------------------------------------------------------------------
/dashboard/client/container/PodContainer.jsx:
--------------------------------------------------------------------------------
1 | /*
2 | * *****************************************************************************
3 | * @description Pod dashboard page container
4 | * *****************************************************************************
5 | */
6 |
7 |
8 | import React, { useState, useEffect } from 'react';
9 | import PodMemoryCurrentComponent from '../components/PodMemoryCurrentComponent.jsx';
10 | import PodCPU from '../components/PodCPU.jsx';
11 | import PodInfoRows from '../components/PodInfoRows.jsx';
12 | import PodMemorySeriesComponent from '../components/PodMemorySeriesComponent.jsx';
13 |
14 | const PodContainer = () => {
15 | const [podCpu, setPodCpu] = useState({});
16 | const [podMemorySeries, setPodMemorySeries] = useState({});
17 | const [podMemoryCurrent, setPodMemoryCurrent] = useState({});
18 | const [podInfo, setPodInfo] = useState({});
19 | const [isLoading, setIsLoading] = useState(true);
20 | const [podNums, setPodNums] = useState({});
21 | const [called, setCalled] = useState(false);
22 | const [clickedArray, setClickedArray] = useState([]);
23 | const [timeWindow, setTimeWindow] = useState(21600);
24 | const [step, setStep] = useState('5m');
25 |
26 | // time variables for promQL query range
27 | const endTime = Math.floor(Date.now() / 1000);
28 | const startTime = endTime - timeWindow;
29 |
30 | // query to graphql server
31 | const query = `{
32 | getPodCpu(startTime: "${startTime}", endTime: "${endTime}", step: "${step}") {
33 | data {
34 | result {
35 | metric {
36 | pod
37 | }
38 | values
39 | }
40 | }
41 | }
42 | getPodMemorySeries(startTime: "${startTime}", endTime: "${endTime}", step: "${step}") {
43 | data {
44 | result {
45 | metric {
46 | pod
47 | }
48 | values
49 | }
50 | }
51 | }
52 | getPodMemoryCurrent {
53 | data {
54 | result {
55 | metric {
56 | pod
57 | }
58 | value
59 | }
60 | }
61 | }
62 | getPodInfo {
63 | data {
64 | result {
65 | metric {
66 | node
67 | pod
68 | pod_ip
69 | }
70 | }
71 | }
72 | }
73 | }`;
74 |
75 | // fetch to graphql backend, set state with resulting data
76 | useEffect(() => {
77 | fetch('/graphql', {
78 | method: 'POST',
79 | headers: {
80 | 'Content-Type': 'application/json',
81 | },
82 | body: JSON.stringify({
83 | query,
84 | }),
85 | })
86 | .then((res) => res.json())
87 | .then((res) => {
88 | const data = res.data;
89 | setPodCpu(data.getPodCpu);
90 | setPodMemorySeries(data.getPodMemorySeries);
91 | setPodMemoryCurrent(data.getPodMemoryCurrent);
92 | setPodInfo(data.getPodInfo);
93 | setIsLoading(false);
94 | setCalled(false); // reset called to false for updating with fresh data
95 | });
96 | }, [timeWindow, step]);
97 |
98 | // if data is loaded and data states are set, but called state is false
99 | if (!isLoading && !called) {
100 | const podInfoNumbers = {}; // empty object to store pod info with names
101 | let counter = 1; // counter to keep track of non-null pods
102 |
103 | for (let i = 0; i < podInfo.data.result.length; i++) {
104 | // create pods 1 through x based on pod names
105 | let pod = podInfo.data.result[i].metric;
106 | if (pod.node) { // skip pods with null nodes
107 | podInfoNumbers[pod.pod] = { // pod object with data
108 | node: pod.node,
109 | pod_ip: pod.pod_ip,
110 | name: `pod${counter}`,
111 | number: counter,
112 | podName: pod.pod
113 | };
114 | counter++; // counter to keep track of number of valid pods (no null nodes)
115 | }
116 | }
117 |
118 | for (let i = 0; i < podCpu.data.result.length; i++) {
119 | // update individual pod objects with cpu values and memory values
120 | let cpuPod = podCpu.data.result[i].metric.pod;
121 | if (podInfoNumbers[cpuPod]) podInfoNumbers[cpuPod].cpuValues = podCpu.data.result[i].values;
122 | let memPod = podMemorySeries.data.result[i].metric.pod;
123 | if (podInfoNumbers[memPod]) podInfoNumbers[memPod].memorySeriesValues = podMemorySeries.data.result[i].values;
124 | }
125 |
126 | setPodNums(podInfoNumbers);
127 | setCalled(true);
128 | }
129 |
130 |
131 | return (
132 |
133 |
136 |
139 |
142 |
145 |
146 | )
147 | };
148 |
149 | export default PodContainer;
150 |
--------------------------------------------------------------------------------
/dashboard/build/bundle.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | Copyright (c) 2017 Jed Watson.
9 | Licensed under the MIT License (MIT), see
10 | http://jedwatson.github.io/classnames
11 | */
12 |
13 | /*!
14 | Copyright (c) 2018 Jed Watson.
15 | Licensed under the MIT License (MIT), see
16 | http://jedwatson.github.io/classnames
17 | */
18 |
19 | /*! Conditions:: INITIAL */
20 |
21 | /*! Production:: $accept : expression $end */
22 |
23 | /*! Production:: css_value : ANGLE */
24 |
25 | /*! Production:: css_value : CHS */
26 |
27 | /*! Production:: css_value : EMS */
28 |
29 | /*! Production:: css_value : EXS */
30 |
31 | /*! Production:: css_value : FREQ */
32 |
33 | /*! Production:: css_value : LENGTH */
34 |
35 | /*! Production:: css_value : PERCENTAGE */
36 |
37 | /*! Production:: css_value : REMS */
38 |
39 | /*! Production:: css_value : RES */
40 |
41 | /*! Production:: css_value : SUB css_value */
42 |
43 | /*! Production:: css_value : TIME */
44 |
45 | /*! Production:: css_value : VHS */
46 |
47 | /*! Production:: css_value : VMAXS */
48 |
49 | /*! Production:: css_value : VMINS */
50 |
51 | /*! Production:: css_value : VWS */
52 |
53 | /*! Production:: css_variable : CSS_VAR LPAREN CSS_CPROP COMMA math_expression RPAREN */
54 |
55 | /*! Production:: css_variable : CSS_VAR LPAREN CSS_CPROP RPAREN */
56 |
57 | /*! Production:: expression : math_expression EOF */
58 |
59 | /*! Production:: math_expression : LPAREN math_expression RPAREN */
60 |
61 | /*! Production:: math_expression : NESTED_CALC LPAREN math_expression RPAREN */
62 |
63 | /*! Production:: math_expression : SUB PREFIX SUB NESTED_CALC LPAREN math_expression RPAREN */
64 |
65 | /*! Production:: math_expression : css_value */
66 |
67 | /*! Production:: math_expression : css_variable */
68 |
69 | /*! Production:: math_expression : math_expression ADD math_expression */
70 |
71 | /*! Production:: math_expression : math_expression DIV math_expression */
72 |
73 | /*! Production:: math_expression : math_expression MUL math_expression */
74 |
75 | /*! Production:: math_expression : math_expression SUB math_expression */
76 |
77 | /*! Production:: math_expression : value */
78 |
79 | /*! Production:: value : NUMBER */
80 |
81 | /*! Production:: value : SUB NUMBER */
82 |
83 | /*! Rule:: $ */
84 |
85 | /*! Rule:: (--[0-9a-z-A-Z-]*) */
86 |
87 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)% */
88 |
89 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)Hz\b */
90 |
91 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)\b */
92 |
93 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ch\b */
94 |
95 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)cm\b */
96 |
97 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)deg\b */
98 |
99 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dpcm\b */
100 |
101 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dpi\b */
102 |
103 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dppx\b */
104 |
105 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)em\b */
106 |
107 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ex\b */
108 |
109 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)grad\b */
110 |
111 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)in\b */
112 |
113 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)kHz\b */
114 |
115 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)mm\b */
116 |
117 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ms\b */
118 |
119 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)pc\b */
120 |
121 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)pt\b */
122 |
123 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)px\b */
124 |
125 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)rad\b */
126 |
127 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)rem\b */
128 |
129 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)s\b */
130 |
131 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)turn\b */
132 |
133 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vh\b */
134 |
135 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vmax\b */
136 |
137 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vmin\b */
138 |
139 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vw\b */
140 |
141 | /*! Rule:: ([a-z]+) */
142 |
143 | /*! Rule:: (calc) */
144 |
145 | /*! Rule:: (var) */
146 |
147 | /*! Rule:: , */
148 |
149 | /*! Rule:: - */
150 |
151 | /*! Rule:: \( */
152 |
153 | /*! Rule:: \) */
154 |
155 | /*! Rule:: \* */
156 |
157 | /*! Rule:: \+ */
158 |
159 | /*! Rule:: \/ */
160 |
161 | /*! Rule:: \s+ */
162 |
163 | /*! decimal.js-light v2.5.1 https://github.com/MikeMcl/decimal.js-light/LICENCE */
164 |
165 | /** @license React v0.20.2
166 | * scheduler.production.min.js
167 | *
168 | * Copyright (c) Facebook, Inc. and its affiliates.
169 | *
170 | * This source code is licensed under the MIT license found in the
171 | * LICENSE file in the root directory of this source tree.
172 | */
173 |
174 | /** @license React v16.13.1
175 | * react-is.production.min.js
176 | *
177 | * Copyright (c) Facebook, Inc. and its affiliates.
178 | *
179 | * This source code is licensed under the MIT license found in the
180 | * LICENSE file in the root directory of this source tree.
181 | */
182 |
183 | /** @license React v17.0.1
184 | * react-dom.production.min.js
185 | *
186 | * Copyright (c) Facebook, Inc. and its affiliates.
187 | *
188 | * This source code is licensed under the MIT license found in the
189 | * LICENSE file in the root directory of this source tree.
190 | */
191 |
192 | /** @license React v17.0.1
193 | * react.production.min.js
194 | *
195 | * Copyright (c) Facebook, Inc. and its affiliates.
196 | *
197 | * This source code is licensed under the MIT license found in the
198 | * LICENSE file in the root directory of this source tree.
199 | */
200 |
201 | /** @license React v17.0.2
202 | * react-dom.production.min.js
203 | *
204 | * Copyright (c) Facebook, Inc. and its affiliates.
205 | *
206 | * This source code is licensed under the MIT license found in the
207 | * LICENSE file in the root directory of this source tree.
208 | */
209 |
210 | /** @license React v17.0.2
211 | * react.production.min.js
212 | *
213 | * Copyright (c) Facebook, Inc. and its affiliates.
214 | *
215 | * This source code is licensed under the MIT license found in the
216 | * LICENSE file in the root directory of this source tree.
217 | */
218 |
219 | //! moment.js
220 |
221 | //! moment.js locale configuration
222 |
--------------------------------------------------------------------------------