├── .DS_Store ├── .babelrc ├── .eslintrc.json ├── .gitignore ├── README.md ├── __tests__ ├── client │ └── Home.test.js └── server │ └── Controllers.js ├── client ├── .DS_Store ├── Components │ ├── App.jsx │ ├── Dashboard │ │ ├── ClusterView │ │ │ ├── CPUChart.jsx │ │ │ ├── ClusterDashboard.jsx │ │ │ ├── ClusterFilteringOptions.jsx │ │ │ ├── MemoryChart.jsx │ │ │ └── NetworkTransmitted.jsx │ │ ├── Dashboard.jsx │ │ ├── DashboardHeader.jsx │ │ ├── Home.jsx │ │ ├── NavBarItems │ │ │ ├── ClusterView.jsx │ │ │ ├── Navbar.jsx │ │ │ ├── NodeView.jsx │ │ │ ├── PodView.jsx │ │ │ └── Settings.jsx │ │ ├── NodeView │ │ │ ├── CPUusage.jsx │ │ │ ├── DiskSpace.jsx │ │ │ ├── NodeDashboard.jsx │ │ │ ├── NodesTable.jsx │ │ │ └── Speedometer.jsx │ │ └── PodView │ │ │ ├── BarChart.jsx │ │ │ ├── Doughnut.jsx │ │ │ ├── PodDashboard.jsx │ │ │ └── PodsTable.jsx │ ├── GetStarted │ │ ├── GetStartedBody.jsx │ │ └── GetStartedPage.jsx │ ├── LandingPage │ │ ├── Body.jsx │ │ ├── Footer.jsx │ │ ├── GithubLink.jsx │ │ ├── Header.jsx │ │ ├── LandingPageContainer.jsx │ │ └── Logo.jsx │ ├── LogIn │ │ └── LogInPage.jsx │ ├── NotFound.jsx │ └── componetMap.md ├── assets │ ├── ShipShapeBlackLogo.png │ ├── ShipShapeDashboard.png │ ├── ShipShapeLogo.png │ ├── ShipShapeWhiteLogo.png │ ├── StopProblemsBefore.png │ ├── TrackWhatMatters.png │ ├── brian.jpg │ ├── ozi.jpeg │ ├── rebeccaschell.jpg │ └── whit.png ├── helpers.js ├── index.html ├── index.js └── styles │ └── styles.scss ├── package.json ├── server ├── graphQL │ ├── datasources │ │ ├── dataSources.js │ │ ├── nodeConstructor.js │ │ ├── podConstructor.js │ │ └── prometheusAPI.js │ ├── demoData │ │ ├── CpuUsage.json │ │ ├── FreeMemory.json │ │ ├── NetworkTransmitted.json │ │ ├── demoData.js │ │ ├── nodeData.json │ │ └── podData.json │ ├── directives │ │ └── directives.js │ ├── resolvers │ │ └── resolvers.js │ ├── schema.js │ └── typeDef │ │ └── typeDef.js ├── k8sApi.js ├── restAPI │ ├── controller │ │ ├── nodeController.js │ │ ├── podController.js │ │ └── prometheusController.js │ └── router │ │ ├── metricsServerRouter.js │ │ └── prometheusRouter.js └── server.js └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2021": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "extends": [ 10 | "airbnb", 11 | "plugin:react/recommended" 12 | ], 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 12, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react" 22 | ], 23 | "rules": { 24 | "arrow-parens": "off", 25 | "consistent-return": "off", 26 | "func-names": "off", 27 | "no-console": "off", 28 | "radix": "off", 29 | "react/button-has-type": "off", 30 | "react/destructuring-assignment": "warn", 31 | "react/prop-types": "off", 32 | "no-param-reassign": ["warn", { "props": false }], 33 | "prefer-object-spread": "off", 34 | "linebreak-style": "off", 35 | "arrow-body-style": "off", 36 | "import/extensions": "off", 37 | "no-plusplus": ["warn", { "allowForLoopAfterthoughts": true }] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .env 4 | build 5 | sample-service.yaml 6 | .vscode 7 | workspace.code-workspace -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 | 5 | Logo 6 | 7 | 8 |

ShipShape

9 | 10 |

11 | Kubernetes Cluster Monitoring Made Easy 12 |

13 | getinshipshape.io 14 |
15 |
16 | 17 |

18 |

19 | 20 | 21 |
22 | Table of Contents 23 |
    24 |
  1. 25 | About The Project 26 | 29 |
  2. 30 |
  3. 31 | Demo 32 |
  4. 33 |
  5. 34 | Getting Started 35 | 40 |
  6. 41 |
  7. Contributors
  8. 42 |
  9. Looking Ahead
  10. 43 |
44 |
45 | 46 | 47 | 48 | ## About The Project 49 | 50 | ShipShape is an open-source Web App for Kubernetes monitoring. ShipShape allows Users to track numerous Kubernetes Cluster metrics with easy to interpret graphs at varying levels of granularity. 51 | 52 | Key features include: 53 | 54 | 55 | 56 | - A facilitated access of internal Kubernetes environment without having to expose it to outside traffic 57 | - Instructions for auto-deployment of Prometheus for time-series metrics 58 | - GraphQL service to abstract PromQL queries for frontend developers 59 | - There are different dashboard views of Kubernetes clusters (cluster, node, and pod view) 60 | - Graphs that display important metrics 61 | - Dynamic time-range and filtering selection for cluster metric data 62 | 63 | ### Built With 64 | 65 | - [Kubernetes](https://kubernetes.io/) 66 | - [Prometheus](https://prometheus.io/) 67 | - [React](https://reactjs.org/) 68 | - [Chart JS](https://www.chartjs.org/) 69 | - [Node JS/Express](https://nodejs.dev) 70 | - [Apollo GraphQL](https://www.apollographql.com/docs/apollo-server) 71 | - [Webpack](https://webpack.js.org/) 72 | - [Jest](https://jestjs.io/) 73 | 74 | ## Demo 75 | 76 | 77 | ![Demo Gif](https://s6.gifyu.com/images/DemoGif-3.gif) 78 | 79 | 80 | ## Getting Started 81 | 82 | To get a local copy up and running, follow these steps: 83 | 84 | ### Prerequisites 85 | 86 | 1. Installed on your local machine, [kubectl](https://kubernetes.io/docs/tasks/tools) and [helm](https://helm.sh/docs/intro/install) command line tools. 87 | 2. Have a hosted Kubernetes Cluster on a service like [EKS](https://aws.amazon.com/eks/), [GKE](https://cloud.google.com/kubernetes-engine), or [MiniKube](https://minikube.sigs.k8s.io/docs/start). 88 | 3. A metrics server installed inside the cluster, if it is not standard with your cluster service. For metrics server deployment, see our [Getting Started](https://www.getinshipshape.io/getStarted) for instructions on setting this up. 89 | 4. Ensure that your local kubeconfig is setup to access the cluster you wish to monitor. You can check that by running the following command and checking the cluster info displayed in your shell. 90 | 91 | ``` 92 | kubectl config view 93 | ``` 94 | 95 | 5. Expose metrics using Prometheus from a Kubernetes cluster. This can be done easily via Helm once the above steps are complete. 96 | 97 | ``` 98 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 99 | ``` 100 | 101 | ``` 102 | helm upgrade -i prometheus prometheus-community/prometheus --namespace prometheus 103 | ``` 104 | 105 | ShipShape requires that you have a running Kubernetes cluster and your local kubeconfig file setup to access it. You can check your configuration by running 106 | 107 | ``` 108 | kubectl config view 109 | ``` 110 | 111 | ### Installing ShipShape 112 | 113 | Before running ShipShape the first time, you'll need to pull the codebase down to your local machine and compile the bundle.js for optimal performance. 114 | 115 | 1. Fork and/or clone this repo to get started 116 | 117 | ``` 118 | git clone https://github.com/oslabs-beta/ShipShape.git 119 | ``` 120 | 121 | 2. Cd into the cloned Directory 122 | 123 | ``` 124 | cd ShipShape 125 | ``` 126 | 127 | 3. Npm install in the ShipShape root directory 128 | 129 | ``` 130 | npm install 131 | ``` 132 | 133 | 4. Npm run build 134 | 135 | ``` 136 | npm run build 137 | ``` 138 | 139 | ### Running ShipShape 140 | 141 | After installation is complete, you can start here to boot up ShipShape in the future. 142 | 143 | 1. Start the server 144 | 145 | ``` 146 | npm start 147 | ``` 148 | 149 | 2. Navigate to http://localhost:3000 150 | 3. Click "Dashboard" to view your metrics! 151 | 152 | ### Demo Mode 153 | 154 | To see a working demo of ShipShape: 155 | 156 | Navigate to the ShipShape demo dashbaord! 157 | 158 | 159 | 160 | ## Contributors 161 | 162 | Brian Barr - [GitHub](https://github.com/BarrBrian/) - [LinkedIn](https://www.linkedin.com/in/barrbrian/) 163 | 164 | Ozi Oztourk - [GitHub](https://github.com/ozi-oztrk/) - [LinkedIn](https://www.linkedin.com/in/ozi-oztourk/) 165 | 166 | Rebecca Schell - [GitHub](https://github.com/rschelly/) - [LinkedIn](https://www.linkedin.com/in/rschelly/) 167 | 168 | Whit Rooke - [GitHub](https://github.com/Whitrooke) - [LinkedIn](https://www.linkedin.com/in/whit-rooke) 169 | 170 | ## Looking Ahead 171 | 172 | ShipShape is currently in Alpha. Here's some features we hope to have implemented in future versions: 173 | 174 | - More options for which metrics you monitor 175 | - More filtering options for “Cluster View” metrics with dynamic PromQL queries 176 | - Impment a state management framework (Redux/Context API) 177 | - UI Optimization for faster rendering 178 | - Prometheus Auto-Deployment to streamline setup 179 | - Organization and user permissions to share access to your metrics with employees without sharing your provider access keys 180 | 181 | ### Have an idea to make ShipShape even better? 182 | 183 | If you'd like to contributee to ShipShape, please open a PR from your personal fork, or shoot us an email at ShipShapeK8S@gmail.com and our team will work with you to get the feature implemented. 184 | 185 | ## License 186 | 187 | This product is licensed under the MIT License without restriction. 188 | -------------------------------------------------------------------------------- /__tests__/client/Home.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Home from '../../client/Components/Dashboard/Home.jsx'; 3 | import { mount, shallow } from 'enzyme'; 4 | // configure and adapter is necessary for Enzyme. 5 | import { configure } from 'enzyme'; 6 | import Adapter from 'enzyme-adapter-react-16'; 7 | 8 | 9 | configure({ adapter: new Adapter() }); 10 | 11 | // test cases starts here 12 | 13 | describe('Home Component Tests', () => { 14 | it('should contain/render Navbar component', () => { 15 | const wrapper = shallow(); 16 | expect(wrapper.text()).toContain(''); 17 | }) 18 | 19 | it('should have div element with className of homeContainer', () => { 20 | const wrapper = shallow(); 21 | expect(wrapper.find('div').hasClass('homeContainer')).toEqual(true); 22 | }) 23 | 24 | it('should have contain dashboardHeader', () => { 25 | const wrapper = shallow(); 26 | expect(wrapper.text()).toContain(''); 27 | }) 28 | }) -------------------------------------------------------------------------------- /__tests__/server/Controllers.js: -------------------------------------------------------------------------------- 1 | import nodeController from '../../server/restAPI/controller/nodeController.js'; 2 | import podController from '../../server/restAPI/controller/podController.js'; 3 | // skip the namespace controller as it is currently not in use 4 | 5 | describe('nodeController tests', () => { 6 | // initialize next to be a function that solely ends the middleware, 7 | // the req/res objects should still persist in the local memory of the 'it' statement 8 | // thus we don't need the normal full express specific next() functionality 9 | // will test that with supertest 10 | const next = () => undefined; 11 | let req; 12 | let res; 13 | // resert the req and res objects before each test 14 | beforeEach(() => { 15 | req = { body: {} }; 16 | res = { locals: {} }; 17 | }); 18 | 19 | describe('getNodeMetrics', () => { 20 | it('should have something on res.locals.nodeMetrics', async () => { 21 | await nodeController.getNodeMetrics(req, res, next); 22 | expect(res.locals.nodeMetrics).toBeTruthy(); 23 | }); 24 | it('res.locals.nodes has the expected data form', async () => { 25 | await nodeController.getNodeMetrics(req, res, next); 26 | const responseJSON = res.locals.nodeMetrics.response.toJSON(); 27 | expect(responseJSON).toHaveProperty('body'); 28 | expect(responseJSON.body).toHaveProperty('items'); 29 | expect(Array.isArray(responseJSON.body.items)).toBeTruthy(); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/.DS_Store -------------------------------------------------------------------------------- /client/Components/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Route, Switch } from 'react-router-dom'; 3 | import LandingPageContainer from './LandingPage/LandingPageContainer.jsx'; 4 | import Home from './Dashboard/Home.jsx'; 5 | import GetStartedPage from './GetStarted/GetStartedPage.jsx'; 6 | import NotFound from './NotFound.jsx'; 7 | 8 | /* 9 | * 10 | * This is the main app component that renders everythig in ShipShape. 11 | * We use react router to serve the different pages of the website. 12 | * 13 | */ 14 | 15 | function App() { 16 | console.log('nothing to see here, get out of our console'); 17 | console.log(` 18 | ____ _ _ _ ___ ____ _ _ ____ ___ ____ 19 | [__ |__| | |__] [__ |__| |__| |__] |___ 20 | ___] | | | | ___] | | | | | |___ `); 21 | return ( 22 |
23 | 24 | {/* switch statement that chooses the component to render based on the path */} 25 | 26 | } /> 27 | } /> 28 | } /> 29 | 30 | 31 | 32 |
33 | ); 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /client/Components/Dashboard/ClusterView/CPUChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Bar } from "react-chartjs-2"; 3 | import { fetchChartData } from '../../../helpers.js' 4 | 5 | /* 6 | This is a line chart we imported from chart.js to display the CPU usage 7 | of the whole cluster over time. The data from the cluster query is being 8 | passed down from the cluster dashboard component ***or home component if 9 | we use context api*** and is used here to populate the data points on the line 10 | graph. 11 | 12 | We wanted to make this a streaming live data chart initally, but never 13 | got that fully Implemented. 14 | */ 15 | 16 | const colors = ["rgb(160, 192, 206)", "rgb(38,84,121)", "rgb(207, 225, 232)"]; 17 | 18 | 19 | const StreamingCpuChart = ({ chartDurationHours }) => { 20 | const [chartData, setChartData] = useState({}); 21 | 22 | async function chart(){ 23 | const data = await fetchChartData('cpuUsage', chartDurationHours, '2m') 24 | setChartData(data); 25 | } 26 | 27 | useEffect(() => { 28 | chart(); 29 | }, [chartDurationHours]); 30 | 31 | return ( 32 |
33 | 53 |
54 | ); 55 | }; 56 | 57 | export default StreamingCpuChart; 58 | -------------------------------------------------------------------------------- /client/Components/Dashboard/ClusterView/ClusterDashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import CpuChart from './CPUChart.jsx'; 3 | import MemoryChart from './MemoryChart.jsx'; 4 | import NetworkTransmitted from './NetworkTransmitted.jsx'; 5 | import ClusterFilteringOptions from './ClusterFilteringOptions.jsx'; 6 | 7 | /* 8 | This is the view displayed when cluster view is clicked on the nav bar. 9 | The cluster view displays the metrics of the cluster as a whole. This 10 | cluster dashboard displays 3 line charts that display the CPU usage over 11 | time, the memory usage over time, and the network pressure over time. 12 | 13 | We are storing the data recieved from the query, passing it down to each of the 14 | graphs. 15 | 16 | NOTE: we may move the query to the home component if we impliment context api 17 | */ 18 | 19 | const ClusterDashboard = () => { 20 | const [chartDurationHours, setChartDurationHours] = useState(6); 21 | 22 | return ( 23 |
24 | 28 | 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default ClusterDashboard; 36 | -------------------------------------------------------------------------------- /client/Components/Dashboard/ClusterView/ClusterFilteringOptions.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button, TextField } from '@material-ui/core'; 3 | 4 | export default function ClusterFilteringOptions({ chartDurationHours, setChartDurationHours }) { 5 | const [errorMessage, setErroMessage] = useState(''); 6 | 7 | function submitHandler(e) { 8 | e.preventDefault(); 9 | if (errorMessage) return; 10 | const { value } = e.target[1]; 11 | setChartDurationHours(value); 12 | } 13 | 14 | function checkForError(event) { 15 | const { value } = event.target; 16 | 17 | const num = parseInt(value, 10); 18 | const isValidInput = (num > 0 && num <= 36) && (num === Number(value)); 19 | 20 | if (isValidInput && errorMessage) setErroMessage(''); 21 | if (!isValidInput && !errorMessage) setErroMessage('Hours must be an positive integer between 0 and 36'); 22 | } 23 | 24 | return ( 25 | 26 |
submitHandler(e)}> 27 |
28 | 29 |
30 |
31 | checkForError(e)} 38 | helperText={errorMessage} 39 | /> 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /client/Components/Dashboard/ClusterView/MemoryChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import { fetchChartData } from '../../../helpers.js'; 4 | 5 | /* 6 | This is a line chart we imported from chart.js to display the Memory usage 7 | of the whole cluster over time. The data from the cluster query is being 8 | passed down from the cluster dashboard component ***or home component if 9 | we use context api*** and is used here to populate the data points on the line 10 | graph. 11 | 12 | We wanted to make this a streaming live data chart initally, but never 13 | got that fully Implemented. 14 | */ 15 | 16 | const MemoryChart = ({ chartDurationHours }) => { 17 | const [chartData, setChartData] = useState({}); 18 | 19 | async function chart() { 20 | const data = await fetchChartData('freeMemory', chartDurationHours, '5m'); 21 | setChartData(data); 22 | } 23 | 24 | useEffect(() => { 25 | chart(); 26 | }, [chartDurationHours]); 27 | 28 | return ( 29 |
30 | 63 |
64 | ); 65 | }; 66 | 67 | export default MemoryChart; 68 | -------------------------------------------------------------------------------- /client/Components/Dashboard/ClusterView/NetworkTransmitted.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import { fetchChartData } from '../../../helpers.js'; 4 | 5 | /* 6 | This is a line chart we imported from chart.js to display the Network Pressure 7 | of the whole cluster over time. The data from the cluster query is being 8 | passed down from the cluster dashboard component ***or home component if 9 | we use context api*** and is used here to populate the data points on the line 10 | graph. 11 | 12 | We wanted to make this a streaming live data chart initally, but never 13 | got that fully Implemented. 14 | */ 15 | 16 | const NetworkTransmitted = ({ chartDurationHours }) => { 17 | const [chartData, setChartData] = useState({}); 18 | 19 | async function chart() { 20 | const data = await fetchChartData('networkTransmitted', chartDurationHours, '5m'); 21 | setChartData(data); 22 | } 23 | 24 | useEffect(() => { 25 | chart(); 26 | }, [chartDurationHours]); 27 | 28 | return ( 29 |
30 | 63 |
64 | ); 65 | }; 66 | 67 | export default NetworkTransmitted; 68 | -------------------------------------------------------------------------------- /client/Components/Dashboard/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NodeDashboard from './NodeView/NodeDashboard.jsx'; 3 | import PodDashboard from './PodView/PodDashboard.jsx'; 4 | import ClusterDashboard from './ClusterView/ClusterDashboard.jsx'; 5 | 6 | /* 7 | This is the dashboard view hub that will render whichever dashboard view the user clicks on 8 | from the nav bar, using the props "view" that is being passed down from the home component. 9 | The state of the view string is altered in the nav bar, and that change is reflected here 10 | and determins which dashboard view will be rendered. 11 | */ 12 | 13 | const Dashboard = ({ view }) => { 14 | const renderSwitch = () => { 15 | switch (view) { 16 | case 'pod': 17 | return ; 18 | case 'node': 19 | return ; 20 | case 'cluser': 21 | return ; 22 | default: 23 | return ; 24 | } 25 | }; 26 | 27 | return ( 28 |
29 | {renderSwitch()} 30 |
31 | ); 32 | }; 33 | 34 | export default Dashboard; 35 | -------------------------------------------------------------------------------- /client/Components/Dashboard/DashboardHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | /* 5 | This is the header that sits at the top of the dashboard home page. 6 | it has links to our github repo and clicking help will take users 7 | to our geting started page to view some Instructions for how to get 8 | started 9 | */ 10 | 11 | const DashboardHeader = () => { 12 | return ( 13 |
14 | 15 | Help 16 | 17 | 18 | Github Logo 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default DashboardHeader; 30 | -------------------------------------------------------------------------------- /client/Components/Dashboard/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Dashboard from './Dashboard.jsx'; 3 | import DashboardHeader from './DashboardHeader.jsx'; 4 | import Navbar from './NavBarItems/Navbar.jsx'; 5 | 6 | /* 7 | This is the top most component of our dashboard. From here, everything is rendered, 8 | including a header, a nav bar, and the actual metric monitoring dashboards. We set state 9 | here to see which 'view' of the dashboard the user has clicked on, and pass that state 10 | to both the nav bar and the dashboard components. When a user clicks on a different view 11 | in the nav bar, the state string set here gets update, and that update is reflected on the 12 | dashboad. 13 | */ 14 | 15 | const Home = () => { 16 | const [dashboardView, setdashboardView] = useState('pod'); 17 | function changeView(string) { 18 | setdashboardView(string); 19 | } 20 | return ( 21 |
22 | 23 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default Home; 30 | -------------------------------------------------------------------------------- /client/Components/Dashboard/NavBarItems/ClusterView.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { FaAngleDown } from "react-icons/fa"; 3 | 4 | /* 5 | This is the tab on the nav bar for the cluster view. When it is clicked, 6 | the dashboard will change to display metrics and graphs for the whole 7 | cluster. 8 | The click handler function is passed down from the nar bar, so when 9 | cluster view is clicked, the handler updates state to the string 'cluster' 10 | so the dashboard knows to render the cluster view. 11 | */ 12 | 13 | const ClusterView = ({ handler }) => { 14 | 15 | return ( 16 |
17 | 21 |
22 | ); 23 | 24 | }; 25 | 26 | export default ClusterView; -------------------------------------------------------------------------------- /client/Components/Dashboard/NavBarItems/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PodView from './PodView.jsx'; 3 | import NodeView from './NodeView.jsx'; 4 | import ClusterView from './ClusterView.jsx'; 5 | import Settings from './Settings.jsx'; 6 | import PodsTable from '../PodView/PodsTable.jsx'; 7 | 8 | /* 9 | This is the main nav bar that sits on the left of the screen and allows users 10 | to toggle between different views of their Kubernetes Cluster, from pod to node 11 | to the cluster as a whole. 12 | We set up a handler function in the home component and pass it down to the nav bar 13 | so a user can update the state depending on which dashboard view they click on from 14 | the nav bar. 15 | */ 16 | 17 | const Navbar = ({ handler }) => { 18 | return ( 19 |
20 |
21 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 |
35 |
36 | ); 37 | }; 38 | 39 | export default Navbar; 40 | -------------------------------------------------------------------------------- /client/Components/Dashboard/NavBarItems/NodeView.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { FaAngleDown } from "react-icons/fa"; 3 | 4 | /* 5 | This is the tab on the nav bar for the node view. When it is clicked, 6 | the dashboard will change to display metrics and graphs for the whole 7 | node. 8 | The click handler function is passed down from the nar bar, so when 9 | node view is clicked, the handler updates state to the string 'node' 10 | so the dashboard knows to render the node view. 11 | */ 12 | 13 | 14 | const NodeView = ({ handler }) => { 15 | 16 | return ( 17 |
18 | 22 |
23 | ); 24 | }; 25 | 26 | export default NodeView; 27 | -------------------------------------------------------------------------------- /client/Components/Dashboard/NavBarItems/PodView.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { FaAngleDown } from "react-icons/fa"; 3 | 4 | /* 5 | This is the tab on the nav bar for the pod view. When it is clicked, 6 | the dashboard will change to display metrics and graphs for the whole 7 | pod. 8 | The click handler function is passed down from the nar bar, so when 9 | pod view is clicked, the handler updates state to the string 'pod' 10 | so the dashboard knows to render the pod view. 11 | */ 12 | 13 | const podView = ({ handler }) => { 14 | return ( 15 |
16 | 20 |
21 | ); 22 | }; 23 | 24 | export default podView; 25 | -------------------------------------------------------------------------------- /client/Components/Dashboard/NavBarItems/Settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { FaCog } from "react-icons/fa"; 3 | import { Link } from "react-router-dom"; 4 | 5 | /* 6 | Currently empty, will eventually be populated with instructions for how to use ShipShape 7 | */ 8 | 9 | const Settings = () => { 10 | return ( 11 |
12 | 13 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | export default Settings; 23 | -------------------------------------------------------------------------------- /client/Components/Dashboard/NodeView/CPUusage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import AnimatedNumber from 'animated-number-react'; 3 | import { StaticRouter } from 'react-router'; 4 | 5 | /* 6 | This is a static number that displays the CPU usage of a selected node. 7 | 8 | */ 9 | 10 | const CPUusage = ({ selectedNodeData }) => { 11 | const [state, setState] = useState(0); 12 | 13 | let cpuUsage; 14 | if (selectedNodeData.status) { 15 | const allLetters = /[a-z|%]*/gi; 16 | cpuUsage = selectedNodeData.status.usagePercent.cpuCores.replace(allLetters, ''); 17 | } 18 | 19 | useEffect(() => { 20 | setState(Number(cpuUsage)); 21 | }); 22 | 23 | return ( 24 |
25 |

CPU Usage

26 |
27 | v.toFixed(0)} 30 | duration={1000} 31 | frameStyle={perc => ( 32 | { opacity: perc / 100 } 33 | )} 34 | style={ 35 | { 36 | fontSize: 200, 37 | } 38 | } 39 | /> 40 |

m

41 |
42 |
43 | ); 44 | }; 45 | 46 | export default CPUusage; 47 | -------------------------------------------------------------------------------- /client/Components/Dashboard/NodeView/DiskSpace.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import AnimatedNumber from 'animated-number-react'; 3 | 4 | /* 5 | This is a static number that displays the disk space of a selected node. 6 | 7 | Currently, it is using dummy data 8 | */ 9 | 10 | const DiskSpace = ({ selectedNodeData }) => { 11 | const [state, setState] = useState(0); 12 | 13 | let ephemeralStorage; 14 | if (selectedNodeData.status) { 15 | const allLetters = /[a-z|%]*/gi; 16 | ephemeralStorage = selectedNodeData.status.allocatable.ephemeralStorage.replace(allLetters, ''); 17 | } 18 | 19 | useEffect(() => { 20 | setState(Number(ephemeralStorage) / 1000000); 21 | }); 22 | 23 | return ( 24 |
25 |

Disk Space

26 |
27 | v.toFixed(0)} 30 | duration={1000} 31 | frameStyle={(perc) => ({ opacity: perc / 100 })} 32 | style={{ 33 | fontSize: 200, 34 | }} 35 | /> 36 |

mB

37 |
38 |
39 | ); 40 | }; 41 | 42 | export default DiskSpace; 43 | -------------------------------------------------------------------------------- /client/Components/Dashboard/NodeView/NodeDashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { filter, find } from 'lodash'; 3 | import NodesTable from './NodesTable.jsx'; 4 | import Speedometer from './Speedometer.jsx'; 5 | import DiskSpace from './DiskSpace.jsx'; 6 | import CPUusage from './CPUusage.jsx'; 7 | 8 | /* 9 | This is the view displayed when node view is clicked on the nav bar. 10 | The node view displays the metrics of a kubernetes cluster at a node level. 11 | This node dashboard displays 4 items, a modified doughnut 'speedometer' graph 12 | that monitors memory usage by the selected node, and a table of all the node 13 | in the cluster. 14 | 15 | We are storing the data recieved from the query, the selected node, and the 16 | data of that selected node here in state and passing it down to each of the 17 | graphs and the table. 18 | 19 | NOTE: we may move the query to the home component if we impliment context api 20 | */ 21 | 22 | const NodeDashboard = () => { 23 | const [data, setData] = useState([]); 24 | const [nodeSelected, setNodeSelected] = useState(); 25 | const [selectedNodeData, setSelectedNodeData] = useState({}); 26 | 27 | function changeNode(nodeName) { 28 | setNodeSelected(nodeName); 29 | const selectedNode = filter(data, { metadata: { name: nodeName } })[0]; 30 | setSelectedNodeData(selectedNode); 31 | } 32 | 33 | async function fetchData() { 34 | const result = await fetch('/graphql', { 35 | method: 'post', 36 | headers: { 37 | 'Content-Type': 'application/json', 38 | }, 39 | body: JSON.stringify({ 40 | query: ` 41 | { 42 | nodes{ 43 | metadata{ 44 | name 45 | creationTimestamp 46 | } 47 | status{ 48 | allocatable{ 49 | cpu 50 | memory 51 | ephemeralStorage 52 | } 53 | usage{ 54 | cpu 55 | memory 56 | } 57 | usagePercent{ 58 | cpu 59 | cpuCores 60 | memory 61 | memoryBytes 62 | } 63 | } 64 | } 65 | } 66 | `, 67 | }), 68 | }) 69 | .then((res) => res.json()) 70 | .then((res) => { 71 | const { nodes } = res.data; 72 | const firstNodeName = nodes[0].metadata.name; 73 | const nodeData = filter(nodes, { 74 | metadata: { name: firstNodeName }, 75 | })[0]; 76 | setSelectedNodeData(nodeData); 77 | setData(res.data.nodes); 78 | }) 79 | .catch((err) => console.log(err)); 80 | 81 | return function cleanup() { 82 | AbortController.abort(); 83 | }; 84 | } 85 | 86 | useEffect(() => fetchData(), []); 87 | return ( 88 |
89 | 90 | 91 | 92 | 93 |
94 | ); 95 | }; 96 | 97 | export default NodeDashboard; 98 | -------------------------------------------------------------------------------- /client/Components/Dashboard/NodeView/NodesTable.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { DataGrid } from '@material-ui/data-grid'; 3 | 4 | /* 5 | This is the node table that stores all the information for each node in a cluster with 6 | a few important metrics displayed as well as the node name and its id. We imported a 7 | DataGrid from Material UI and populated it with node info from the query made in the node 8 | dashboard component ***or maybe home component if we use context api** 9 | 10 | We also passed down the changeNode function from the node dashboard so when a user clicks 11 | on a node in the table, the graphs change to reflect the data from the selected node. 12 | 13 | The GetTimeFromStart functions calculates the lifespan of a node, giving us infromation 14 | about how long a node has been running, giving us the age of our node. 15 | 16 | <<<<<<< HEAD 17 | ======= 18 | NOTE: add more columns and data 19 | >>>>>>> main 20 | */ 21 | 22 | const NodesTable = ({ data, changeNode }) => { 23 | const columns = [ 24 | { 25 | field: 'id', headerName: 'ID', width: 70, headerAlign: 'center', align: 'center', 26 | }, 27 | { 28 | field: 'Name', headerName: 'Name', width: 300, headerAlign: 'center', align: 'left', 29 | }, 30 | { 31 | field: 'Age', 32 | headerName: 'Age', 33 | description: 'This column has a value getter and is not sortable.', 34 | sortable: false, 35 | width: 160, 36 | headerAlign: 'center', 37 | align: 'center', 38 | // valueGetter: (params) => 39 | // `${params.getValue('firstName') || ''} ${params.getValue('lastName') || ''}`, 40 | }, 41 | { 42 | field: 'CPU', headerName: 'CPU(Corses)', width: 180, headerAlign: 'center', align: 'center', 43 | }, 44 | { 45 | field: 'Memory', headerName: 'Memory(Bytes)', width: 180, headerAlign: 'center', align: 'center', 46 | }, 47 | { 48 | field: 'DiskCapacity', 49 | headerName: 'Disk Capacity', 50 | type: 'number', 51 | width: 130, 52 | headerAlign: 'center', 53 | align: 'center', 54 | }, 55 | ]; 56 | 57 | const rows = []; 58 | 59 | const getTimeFromStart = (time) => { 60 | let ms = new Date() - new Date(time); 61 | const daysFactor = 86400000; 62 | const hoursFactor = 3600000; 63 | const minutesFactor = 60000; 64 | 65 | const days = Math.floor(ms / daysFactor); 66 | ms %= daysFactor; 67 | const hours = Math.floor(ms / hoursFactor); 68 | ms %= hoursFactor; 69 | const minutes = Math.floor(ms / minutesFactor); 70 | ms %= minutesFactor; 71 | 72 | return `${days ? (`${days}D`) : ''} ${hours}:${(minutes > 9) ? minutes : `0${minutes}`}`; 73 | }; 74 | 75 | if (data) { 76 | for (let i = 0; i < data.length; i++) { 77 | const { status } = data[i]; 78 | 79 | const node = { 80 | id: i, 81 | Name: data[i].metadata.name, 82 | Age: data[i].metadata.creationTimestamp ? getTimeFromStart(data[i].metadata.creationTimestamp) : 'undeployed', 83 | CPU: `${status.usage.cpu} / ${status.allocatable.cpu}`, 84 | Memory: `${status.usage.memory} / ${status.allocatable.memory}`, 85 | DiskCapacity: `${status.allocatable.ephemeralStorage}`, 86 | }; 87 | 88 | rows.push(node); 89 | } 90 | } 91 | 92 | return ( 93 |
94 | changeNode(row.Name)} /> 95 |
96 | ); 97 | }; 98 | 99 | export default NodesTable; 100 | -------------------------------------------------------------------------------- /client/Components/Dashboard/NodeView/Speedometer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Doughnut } from 'react-chartjs-2'; 3 | 4 | /* 5 | This is a doughnut Chart JS imported from chart.js and altered to only show half 6 | of the circle, giving the appearance of a speedometer. This graph displays the 7 | selected node memory usage of each node out of the total avaiable memroy. The 8 | selectedNodeData is being passed down from the node dashboard component, so 9 | the speedometer graph will update based on which node a user selects from the 10 | nodes table. 11 | 12 | If a node's memory usage exceeds the total available memory space, the avaiable 13 | memory section on the graph will display in red. 14 | */ 15 | 16 | const Speedometer = ({ selectedNodeData }) => { 17 | const [chartData, setChartData] = useState({}); 18 | const allLetters = /[a-z|%]*/gi; 19 | const memory = selectedNodeData.status ? selectedNodeData.status.usagePercent.memory.replace(allLetters, '') : undefined; 20 | 21 | if (selectedNodeData.status) { 22 | if (selectedNodeData.metadata.name !== chartData.nodeName) { 23 | const allLetters = /[a-z|%]*/gi; 24 | const memory = selectedNodeData.status.usagePercent.memory.replace(allLetters, ''); 25 | let backgroundColor = 'rgb(160,192,206)'; 26 | let memoryLabel = 'Remaining'; 27 | if (memory > 100) { 28 | backgroundColor = 'rgb(181,1,0)'; 29 | memoryLabel = 'Memory Exceeded'; 30 | } 31 | setChartData({ 32 | labels: ['Memory Used', memoryLabel], 33 | datasets: [ 34 | { 35 | label: `${memory}% Memory in Use`, 36 | data: [memory, 100 - memory], 37 | backgroundColor: [ 38 | 'rgb(38,84,121)', 39 | backgroundColor, 40 | ], 41 | hoverOffset: 4, 42 | }, 43 | ], 44 | nodeName: selectedNodeData.metadata.name, 45 | }); 46 | } 47 | } 48 | 49 | function chart() { 50 | setChartData({ 51 | labels: ['Memory Used', 'Remaining'], 52 | datasets: [ 53 | { 54 | label: 'My First Dataset', 55 | data: [75, 25], 56 | backgroundColor: [ 57 | 'rgb(38,84,121)', 58 | 'rgb(160,192,206)', 59 | ], 60 | hoverOffset: 4, 61 | }, 62 | ], 63 | }); 64 | } 65 | 66 | useEffect(() => { 67 | chart(); 68 | }, []); 69 | 70 | return ( 71 |
72 | 91 |
92 | ); 93 | }; 94 | 95 | export default Speedometer; 96 | -------------------------------------------------------------------------------- /client/Components/Dashboard/PodView/BarChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Bar } from 'react-chartjs-2'; 3 | 4 | /* 5 | This is a bar chart imported in from chart.js to display the memory usage 6 | across all pods in a cluster. It is not dependent on a specific pod the 7 | user clicks on, it displays the memory usage info for all the pods at once. 8 | 9 | The memory metric uses a letter/string as a unit, so we used regex to get rid 10 | of this string unit so we could just get the raw numbers representing memory usage. 11 | 12 | The inital state is set to the default value of Loading while the fetch request 13 | for the data is pending. Once the fetch has finished, the state will update 14 | with the data and the grpah will reflect this change. 15 | */ 16 | 17 | const BarChart = ({ data }) => { 18 | const [chartData, setChartData] = useState({ 19 | labels: ['loading...'], 20 | datasets: [ 21 | { 22 | label: 'LOADING...', 23 | data: [0], 24 | backgroundColor: [ 25 | // 'rgb(144, 112, 140)' 26 | 'rgb(38,84,121)', 27 | ], 28 | borderWidth: 5, 29 | }, 30 | ], 31 | }); 32 | 33 | const labels = []; const 34 | memoryUsage = []; 35 | 36 | if (data[0]) { 37 | data.forEach((container, index) => { 38 | const allLetters = /[a-z]*/gi; 39 | let memory = 0; 40 | if (container.spec.containers[1]) { 41 | const memory2 = container.spec.containers[1].usage.memory.replace(allLetters, ''); 42 | memory = Number(container.spec.containers[0].usage.memory.replace(allLetters, '')) + Number(memory2); 43 | } else { 44 | memory = Number(container.spec.containers[0].usage.memory.replace(allLetters, '')); 45 | } 46 | memoryUsage.push(memory); 47 | labels.push(`Pod ${index}`); 48 | }); 49 | } 50 | 51 | function chart() { 52 | setChartData({ 53 | labels, 54 | datasets: [ 55 | { 56 | label: 'Memory Usage by Pod', 57 | data: memoryUsage, 58 | backgroundColor: [ 59 | // 'rgb(144, 112, 140)' 60 | 'rgb(38,84,121)', 61 | 'rgb(38,84,121)', 62 | 'rgb(38,84,121)', 63 | 'rgb(38,84,121)', 64 | 'rgb(38,84,121)', 65 | 'rgb(38,84,121)', 66 | 'rgb(38,84,121)', 67 | 'rgb(38,84,121)', 68 | 'rgb(38,84,121)', 69 | 'rgb(38,84,121)', 70 | 'rgb(38,84,121)', 71 | 'rgb(38,84,121)', 72 | 'rgb(38,84,121)', 73 | ], 74 | borderWidth: 2, 75 | maxBarThickness: 50, 76 | }, 77 | ], 78 | }); 79 | } 80 | 81 | useEffect(() => { 82 | chart(); 83 | }, [data]); 84 | 85 | return ( 86 |
87 | 116 |
117 | ); 118 | }; 119 | 120 | export default BarChart; 121 | -------------------------------------------------------------------------------- /client/Components/Dashboard/PodView/Doughnut.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Doughnut } from 'react-chartjs-2'; 3 | 4 | /* 5 | This is a doughnut Chart JS imported from chart.js that displays the selected 6 | pods memory usage by container. The selectedPodData is being passed down from 7 | the pod dashboard component, so the doughnut graph will update based on which 8 | pod a user selects from the pod table. 9 | */ 10 | 11 | const DoughnutChart = ({ selectedPodData }) => { 12 | const [chartData, setChartData] = useState({}); 13 | 14 | if (selectedPodData.spec) { 15 | if (chartData.labels[0] !== selectedPodData.spec.containers[0].name) { 16 | const { containers } = selectedPodData.spec; 17 | 18 | const labels = []; 19 | const memoryUsage = []; 20 | 21 | containers.forEach(cont => { 22 | const allLetters = /[a-z]*/gi; 23 | let { memory } = cont.usage; 24 | if (memory) memory = memory.replace(allLetters, ''); 25 | labels.push(cont.name); 26 | memoryUsage.push(memory); 27 | }); 28 | 29 | setChartData({ 30 | labels, 31 | datasets: [ 32 | { 33 | label: 'Memory Usage by Container', 34 | data: memoryUsage, 35 | backgroundColor: [ 36 | 'rgb(160, 192, 206)', 37 | 'rgb(38, 84, 121)', 38 | 'rgb(147, 176, 189)', 39 | 'rgb(127, 152, 163)', 40 | 'rgb(103, 131, 143)', 41 | 'rgb(78, 113, 128)', 42 | 'rgb(47, 91, 110)', 43 | 'rgb(29, 82, 105)', 44 | 'rgb(38, 84, 121)', 45 | 'rgb(23, 61, 105)', 46 | 'rgb(15, 46, 82)', 47 | 'rgb(8, 31, 58)', 48 | 'rgb(3, 17, 32)', 49 | 'rgb(1, 5, 10)', 50 | 'rgb(0, 0, 0)', 51 | ], 52 | hoverOffset: 4, 53 | }, 54 | ], 55 | }); 56 | } 57 | } 58 | 59 | function chart() { 60 | setChartData({ 61 | labels: ['Loading...'], 62 | datasets: [ 63 | { 64 | label: 'My First Dataset', 65 | data: [0], 66 | backgroundColor: [ 67 | 'rgb(38,84,121)', 68 | 'rgb(160,192,206)', 69 | 'rgb(38,84,121)', 70 | 'rgb(160,192,206)', 71 | 'rgb(38,84,121)', 72 | 'rgb(160,192,206)', 73 | 'rgb(38,84,121)', 74 | 'rgb(160,192,206)', 75 | 'rgb(38,84,121)', 76 | 'rgb(160,192,206)', 77 | 'rgb(38,84,121)', 78 | 'rgb(160,192,206)', 79 | ], 80 | hoverOffset: 4, 81 | }, 82 | ], 83 | }); 84 | } 85 | 86 | useEffect(() => { 87 | chart(); 88 | }, [selectedPodData]); 89 | 90 | return ( 91 |
92 | 109 |
110 | ); 111 | }; 112 | 113 | export default DoughnutChart; 114 | -------------------------------------------------------------------------------- /client/Components/Dashboard/PodView/PodDashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | // import LineChart from './LineChart.jsx'; 3 | import { filter } from 'lodash'; 4 | import BarChart from './BarChart.jsx'; 5 | // import HeatMap from './HeatMap.jsx'; 6 | import PodsTable from './PodsTable.jsx'; 7 | import DoughnutChart from './Doughnut.jsx'; 8 | 9 | /* 10 | This is the view displayed when pod view is clicked on the nav bar. 11 | The pod view displays the metrics of a kubernetes cluster at a pod level. 12 | This pod dashboard displays 3 graphs, a bar chart that displays memory usage 13 | for all the pods in the cluster, a doughnut graph that monitors memory 14 | usage by the selected pod's containers, and a table of all the pods in the cluster. 15 | 16 | We are storing the data recieved from the query, the selected pod, and the 17 | data of that selected pod here in state and passing it down to each of the 18 | graphs and the table. 19 | 20 | NOTE: we may move the query to the home component if we impliment context api 21 | */ 22 | 23 | const PodDashboard = () => { 24 | const [data, setData] = useState([]); 25 | const [selectedPodData, setSelectedPodData] = useState({}); 26 | 27 | function changePod(podName) { 28 | const selectPod = filter(data, { metadata: { name: podName } })[0]; 29 | setSelectedPodData(selectPod); 30 | } 31 | 32 | async function fetchData() { 33 | const abortController = new AbortController(); 34 | const { signal } = abortController; 35 | const result = await fetch('/graphql', { 36 | signal, 37 | method: 'post', 38 | headers: { 39 | 'Content-Type': 'application/json', 40 | }, 41 | body: JSON.stringify({ 42 | query: ` 43 | { 44 | getPods{ 45 | metadata{ 46 | name 47 | namespace 48 | labels{ 49 | app 50 | } 51 | } 52 | status{ 53 | phase 54 | podIP 55 | startTime 56 | } 57 | spec{ 58 | nodeName 59 | containers{ 60 | name 61 | usage { 62 | cpu 63 | memory 64 | } 65 | } 66 | } 67 | } 68 | } 69 | `, 70 | }), 71 | }) 72 | .then((res) => res.json()) 73 | .then((res) => { 74 | const pods = res.data.getPods; 75 | const firstPodName = pods[0].metadata.name; 76 | const podData = filter(pods, { metadata: { name: firstPodName } })[0]; 77 | setSelectedPodData(podData); 78 | setData(pods); 79 | }) 80 | .catch((err) => console.log(err)); 81 | 82 | return function cleanup() { 83 | AbortController.abort(); 84 | }; 85 | } 86 | 87 | useEffect(() => { 88 | fetchData(); 89 | }, []); 90 | 91 | return ( 92 |
93 | 94 | 95 | 99 |
100 | ); 101 | }; 102 | 103 | export default PodDashboard; 104 | -------------------------------------------------------------------------------- /client/Components/Dashboard/PodView/PodsTable.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { DataGrid } from '@material-ui/data-grid'; 3 | 4 | /* 5 | This is the pod table that stores all the information for each pod in a cluster with 6 | a few important metrics displayed as well as the pod name and its id. We imported a 7 | DataGrid from Material UI and populated it with pod info from the query made in the pod 8 | dashboard component ***or maybe home component if we use context api** 9 | 10 | We also passed down the changePod function from the pod dashboard so when a user clicks 11 | on a pod in the table, the graphs change to reflect the data from the selected pod. 12 | 13 | The GetTimeFromStart functions calculates the lifespan of a pod, giving us infromation 14 | about how long a pod has been running, giving us the age of our pod. 15 | 16 | <<<<<<< HEAD 17 | ======= 18 | NOTE: add more columns and data 19 | >>>>>>> main 20 | */ 21 | 22 | const PodsTable = ({ data, changePod }) => { 23 | const columns = [ 24 | { 25 | field: 'id', 26 | headerName: 'ID', 27 | width: 70, 28 | headerAlign: 'center', 29 | align: 'center', 30 | }, 31 | { 32 | field: 'Name', 33 | headerName: 'Name', 34 | width: 300, 35 | headerAlign: 'center', 36 | align: 'left', 37 | }, 38 | { 39 | field: 'Status', 40 | headerName: 'Status', 41 | width: 130, 42 | headerAlign: 'center', 43 | align: 'center', 44 | }, 45 | { 46 | field: 'containerCount', 47 | headerName: 'Container Count', 48 | type: 'number', 49 | width: 180, 50 | headerAlign: 'center', 51 | align: 'center', 52 | hide: false, 53 | }, 54 | { 55 | field: 'Age', 56 | headerName: 'Age', 57 | description: 'This column has a value getter and is not sortable.', 58 | sortable: false, 59 | width: 160, 60 | headerAlign: 'center', 61 | align: 'center', 62 | }, 63 | { 64 | field: 'nodeName', 65 | headerName: 'Node Name', 66 | width: 300, 67 | headerAlign: 'center', 68 | align: 'center', 69 | }, 70 | { 71 | field: 'podIp', 72 | headerName: 'Pod Ip', 73 | width: 180, 74 | headerAlign: 'center', 75 | align: 'center', 76 | hide: true, 77 | }, 78 | ]; 79 | const rows = []; 80 | 81 | if (data) { 82 | for (let i = 0; i < data.length; i++) { 83 | const getTimeFromStart = (time) => { 84 | let ms = new Date() - new Date(time); 85 | const daysFactor = 86400000; 86 | const hoursFactor = 3600000; 87 | const minutesFactor = 60000; 88 | 89 | const days = Math.floor(ms / daysFactor); 90 | ms %= daysFactor; 91 | const hours = Math.floor(ms / hoursFactor); 92 | ms %= hoursFactor; 93 | let minutes = Math.floor(ms / minutesFactor); 94 | minutes = (minutes < 10) ? `0${minutes}` : minutes; 95 | ms %= minutesFactor; 96 | 97 | return `${days ? `${days}D` : ''} ${hours}:${minutes}`; 98 | }; 99 | 100 | const pod = { 101 | id: i, 102 | Status: data[i].status.phase, 103 | Name: data[i].metadata.name, 104 | Age: data[i].status.startTime 105 | ? getTimeFromStart(data[i].status.startTime) 106 | : 'undeployed', 107 | containerCount: data[i].spec.containers.length, 108 | podIp: data[i].status.podIP, 109 | nodeName: data[i].spec.nodeName, 110 | }; 111 | rows.push(pod); 112 | } 113 | } 114 | 115 | return ( 116 |
117 | changePod(row.Name)} 122 | pageSize={10} 123 | /> 124 |
125 | ); 126 | }; 127 | 128 | export default PodsTable; 129 | -------------------------------------------------------------------------------- /client/Components/GetStarted/GetStartedBody.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-one-expression-per-line */ 2 | /* eslint-disable max-len */ 3 | /* eslint-disable react/no-unescaped-entities */ 4 | import React from "react"; 5 | import { Link } from "react-router-dom"; 6 | 7 | /* 8 | This is the body get started page, it is bascially just a page of instruction 9 | to help users get started using ShipShape. 10 | */ 11 | 12 | export default function GetStartedBody() { 13 | return ( 14 |
15 |
16 |

Getting Started with ShipShape

17 |
18 | If you're new here, here's some quick instructions on how to get 19 | started! Then we can begin the process of monitoring your cluster. Don't 20 | forget, for full functionality, make sure you have a metrics server 21 | deployed on your cluster. (Help with that coming soon!) If your 22 | kubeconfig is pointing propely to your cluster, allowing you to use your 23 | kubectl commands on your command line, then you're all set! Head on over 24 | to the Dashboard tab and you'll see your metrics. If not, make sure that 25 | your cluster administrator has allowed you a user account with access to 26 | cluster kubeclt commands. If you can access the cluster using kubectl 27 | commands you're good to go! This app automatically connects to your 28 | cluster using your kubeConfig file, so there's no need to create an 29 | account, or worry about us storing your data, this application only 30 | reads it! Once you are on your ShipSape dashboard, you'll start off 31 | looking at metrics for your pods, just click any of them in the table at 32 | the bottom of the screen to see their individual metrics. If you want a 33 | more hardware focused look, check out the Node View by clicking its 34 | button on the Navigation Bar! 35 |
36 |
37 | There are several different key metrics ShipShape will help you look out 38 | for regarding the health and functionality of your clister, such as 39 | memory usage or pod efficiency. Information about your cluster's 40 | performance metrics should be nicely displayed in a series of graphs and 41 | tables to help you get a better overview of how your cluster is doing. 42 | Take a look around and see how ShipShape can help you keep on top of 43 | Kubernetes! 44 |
45 |
46 | In order get the basic metrics out of this application, make sure that you have deployed a metrics 47 | server as well as prometheus. To deploy a metrics server, open up a command line with 48 | access to the cluster and run this command, 49 |
50 |
"kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml" 51 |
52 |
53 | For even further monitoring capabilities, 54 | download and configure helm to be able to deploy helm charts to your 55 | cluster. The Helm installation docs can be found{" "} 56 | here! 57 |
58 |
59 | Once installed, download/upgrade the Prometheus helm chart with this command. 60 |
61 |
"helm repo add prometheus-community https://prometheus-community.github.io/helm-charts" 62 |
63 |
64 | Next, you'll want to create a namespace prometheus, to deploy prometheus inside. 65 |
66 | And finally, deploy Prometheus to your cluster with 67 |
68 |
"helm upgrade -i prometheus prometheus-community/prometheus --namespace prometheus "" 69 |
70 |
71 | This will deploy Prometheus to your cluster under the namespace, Prometheus. 72 |
73 |
74 | Any comments or concerns? The ShipShape dev team would love to hear from 75 | you! Email ShipShapeK8S@gmail.com to get in touch! 76 |
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /client/Components/GetStarted/GetStartedPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from '../LandingPage/Header.jsx'; 3 | import GetStartedBody from './GetStartedBody.jsx'; 4 | 5 | /* 6 | This is the top level component of the Get Started page of the site. It borrows 7 | the headerfrom the landing page, and renders a get stated body component to house 8 | all the info a user might to get started using ShipShape 9 | */ 10 | 11 | export default function GetStartedPage() { 12 | return ( 13 |
14 |
15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /client/Components/LandingPage/Body.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Fade from 'react-reveal/Fade'; 4 | import Pulse from 'react-reveal/Pulse'; 5 | import Jump from 'react-reveal/Jump'; 6 | 7 | export default function Body() { 8 | return ( 9 |
10 |
11 | 12 |

Welcome to ShipShape!

13 |

A better way to monitor your Kubernetes Cluster

14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | Kubernetes Dashboard 29 |
30 |
31 |
32 | 33 |

About Us

34 |
35 |
36 |
37 |

Real Time Monitoring

38 |

39 | Real Time monitoring the metrics that matter the most in your 40 | Kubernetes Cluster 41 | {' '} 42 |

43 |
44 |
45 |

Send and Store

46 |

47 | Continuously send and store the data related to CPU, Disk, and 48 | Memory usage 49 |

50 |
51 |
52 |

Easy Visualization

53 |

54 | View your cluster like never before with our clear and concise 55 | dashborad 56 |

57 |
58 |
59 |
60 |
61 |
62 | 63 |

Track What Matters Most

64 |
65 |
66 |
67 | trackwhatmatters 68 |
69 |
70 | tostopproblemsbefore 71 |
72 |
73 | 74 |

To Catch Problems Before They Happen

75 |
76 |
77 |
78 |
79 | 80 |

More Information

81 |
82 |

83 | ShipShape is a Kubernetes monitoring tool designed to help you 84 | visualize the most important metrics of your Cluster at various levels 85 | of granularity. ShipShape can also track long-term performance, help 86 | debug errors, and offer potential configuration optimization 87 | suggestions. 88 |
89 | ShipShape will connect to and monitor the real time metrics of a 90 | Kubernetes Cluster using a variety of graphs and the clusters 91 | components to assess overall and pod/node-specific health. We do this by 92 | connecting to the Metrics Server of a Kubernetes cluster and 93 | continuously send and store the data related to CPU, Disk, and Memory 94 | usage to get a look at the metrics over time. We then present these 95 | metrics in an easy to understand, actionable graphic display on your 96 | personal ShipShape dashboard. 97 |
98 |

99 |
100 |
101 |
102 | 103 |
Meet The ShipShape Team
104 |
105 |
106 |
107 | brianbarr 108 |

Brian Barr

109 | Github 110 | LinkedIn 111 |
112 |
113 | ozioztourk 114 |

Ozi Oztourk

115 | Github 116 | LinkedIn 117 |
118 |
119 | rebeccaschell 124 |

Rebecca Schell

125 | Github 126 | LinkedIn 127 |
128 |
129 | brianbarr 130 |

Whit Rooke

131 | Github 132 | LinkedIn 133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 | ); 189 | } 190 | -------------------------------------------------------------------------------- /client/Components/LandingPage/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Logo from "./Logo.jsx"; 3 | 4 | export default function Footer() { 5 | return ( 6 |
7 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/Components/LandingPage/GithubLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function GithubLink() { 4 | return ( 5 |
6 | 7 | shipshapelogo 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /client/Components/LandingPage/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Logo from './Logo.jsx'; 3 | import GithubLink from './GithubLink.jsx'; 4 | 5 | export default function Header() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/Components/LandingPage/LandingPageContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Header from './Header.jsx'; 3 | import Footer from './Footer.jsx'; 4 | import Body from './Body.jsx'; 5 | 6 | /* 7 | This is the landing page for our website, which renders components 8 | for the header, body of the page, and the footer. This is the top 9 | level component for all things related to the landing page. 10 | */ 11 | 12 | 13 | export default function LandingPageContainer() { 14 | return ( 15 |
16 |
17 | 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /client/Components/LandingPage/Logo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Logo() { 4 | return ( 5 |
6 | ShipShape Logo 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/Components/LogIn/LogInPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Avatar from '@material-ui/core/Avatar'; 3 | import Button from '@material-ui/core/Button'; 4 | import CssBaseline from '@material-ui/core/CssBaseline'; 5 | import TextField from '@material-ui/core/TextField'; 6 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | import Link from '@material-ui/core/Link'; 9 | import Grid from '@material-ui/core/Grid'; 10 | import Box from '@material-ui/core/Box'; 11 | import LockOutlinedIcon from '@material-ui/icons/LockOutlined'; 12 | import Typography from '@material-ui/core/Typography'; 13 | import { makeStyles } from '@material-ui/core/styles'; 14 | import Container from '@material-ui/core/Container'; 15 | 16 | /* 17 | This is the beginnings of our login page. We imported a login template from 18 | Material UI, though we had plans to impliment Oauth at some point. In the end we 19 | decided not to proceed with giving users the option to make and account/ 20 | log in with ShipShape at all, since this feature doesn't actually seem all 21 | that necessary to use and experience all ShipShpae has to offer. Instead of 22 | saving any information that might make a user want to keep secure and in an 23 | account, ShipShape will just read and retrun metrics. nothing is saved or 24 | stored at any point, so there is not real motivation for making an account. 25 | */ 26 | 27 | function Copyright() { 28 | return ( 29 | 30 | {'Copyright © '} 31 | 32 | ShipShape 33 | 34 | 35 | {new Date().getFullYear()} 36 | 37 | 38 | ); 39 | } 40 | 41 | const useStyles = makeStyles((theme) => ({ 42 | paper: { 43 | marginTop: theme.spacing(8), 44 | display: 'flex', 45 | flexDirection: 'column', 46 | alignItems: 'center', 47 | }, 48 | avatar: { 49 | margin: theme.spacing(1), 50 | backgroundColor: 'pink', 51 | }, 52 | form: { 53 | width: '100%', // Fix IE 11 issue. 54 | marginTop: theme.spacing(1), 55 | }, 56 | submit: { 57 | margin: theme.spacing(3, 0, 2), 58 | }, 59 | })); 60 | 61 | export default function SignIn() { 62 | const classes = useStyles(); 63 | 64 | return ( 65 |
66 | 67 | 68 |
69 | 70 | 71 | 72 | 73 | Sign in 74 | 75 |
76 | 87 | 98 | } 100 | label="Remember me" 101 | /> 102 | 111 | 112 | 113 | 114 | Forgot password? 115 | 116 | 117 | 118 | 119 | Don't have an account? Sign Up 120 | 121 | 122 | 123 | 124 |
125 | 126 | 127 | 128 |
129 |
130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /client/Components/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ClusterDashboard = () => { 4 | return ( 5 |
6 |

404 Page Not Found

7 |

Click here to get your bearings straight

8 |
9 | ); 10 | }; 11 | 12 | export default ClusterDashboard; 13 | -------------------------------------------------------------------------------- /client/Components/componetMap.md: -------------------------------------------------------------------------------- 1 | |App 2 | | Landing Page 3 | | Landing Page Container 4 | | Header 5 | |Logo 6 | |GitHub Link 7 | |Body 8 | |Footer 9 | 10 | | Get Started 11 | | Header <- component borrowed from landing page 12 | | Get Started Body 13 | 14 | | Log In 15 | | Log In Page 16 | 17 | | Dashboard 18 | | Home 19 | | Dashboard Header 20 | | Navbar 21 | | Pod View 22 | | Node View 23 | | Cluster View 24 | | Settings 25 | | Dashboard 26 | | Pod Dashboard 27 | | Doughnut Chart 28 | | Bar Chart 29 | | Pods Table 30 | | Node Dashboard 31 | | CPU Usage 32 | | Speedometer 33 | | Disk Space 34 | | Node Table 35 | | Cluster Dashboard 36 | | Streaming Memory Chart 37 | | Streaming CPU Chart 38 | | Streaming Network Pressure 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /client/assets/ShipShapeBlackLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/ShipShapeBlackLogo.png -------------------------------------------------------------------------------- /client/assets/ShipShapeDashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/ShipShapeDashboard.png -------------------------------------------------------------------------------- /client/assets/ShipShapeLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/ShipShapeLogo.png -------------------------------------------------------------------------------- /client/assets/ShipShapeWhiteLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/ShipShapeWhiteLogo.png -------------------------------------------------------------------------------- /client/assets/StopProblemsBefore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/StopProblemsBefore.png -------------------------------------------------------------------------------- /client/assets/TrackWhatMatters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/TrackWhatMatters.png -------------------------------------------------------------------------------- /client/assets/brian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/brian.jpg -------------------------------------------------------------------------------- /client/assets/ozi.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/ozi.jpeg -------------------------------------------------------------------------------- /client/assets/rebeccaschell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/rebeccaschell.jpg -------------------------------------------------------------------------------- /client/assets/whit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ShipShape/59c3d685c0a1f175f91fc9daad1dc4a020341569/client/assets/whit.png -------------------------------------------------------------------------------- /client/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | const colors = ['rgb(38,84,121)', 3 | 'rgb(160, 192, 206)', 4 | 'rgb(207, 225, 232)', 5 | 'rgb(160, 192, 206)', 6 | 'rgb(38, 84, 121)', 7 | 'rgb(147, 176, 189)', 8 | 'rgb(127, 152, 163)', 9 | 'rgb(103, 131, 143)', 10 | 'rgb(78, 113, 128)', 11 | 'rgb(47, 91, 110)', 12 | ]; 13 | 14 | export function fetchChartData(queryType, hours = 6, step = '2m') { 15 | const end = new Date(); 16 | const start = new Date(end - 3600000 * hours); 17 | 18 | const query = ` 19 | { 20 | ${queryType}(start: "${start.toISOString()}", end: "${end.toISOString()}", step: "${step}"){ 21 | timestamps 22 | seriesLabels 23 | seriesValues 24 | } 25 | } 26 | `; 27 | 28 | return fetch('/graphql', { 29 | method: 'post', 30 | headers: { 31 | 'Content-Type': 'application/json', 32 | }, 33 | body: JSON.stringify({ 34 | query, 35 | }), 36 | }) 37 | .then((res) => res.json()) 38 | .then(({ data }) => { 39 | const result = data[queryType]; 40 | const labels = result.timestamps; 41 | const datasets = []; 42 | result.seriesLabels.slice(0, 10).forEach((label, i) => { 43 | datasets.push({ 44 | label, 45 | data: result.seriesValues[i], 46 | backgroundColor: colors[i % colors.length], 47 | }); 48 | }); 49 | return { labels, datasets }; 50 | }) 51 | .catch((err) => console.log(err)); 52 | } 53 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ShipShape 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './Components/App.jsx'; 4 | // eslint-disable-next-line no-unused-vars 5 | import styles from './styles/styles.scss'; 6 | 7 | render( 8 | , 9 | document.getElementById('root'), 10 | ); 11 | -------------------------------------------------------------------------------- /client/styles/styles.scss: -------------------------------------------------------------------------------- 1 | $mainTextColor: rgb(244, 246, 247); 2 | $darkGray: rgb(185, 194, 193); 3 | $lightBlue: rgb(160, 192, 206); 4 | $next: rgb(0, 0, 0); 5 | $darkBlue: rgb(38, 84, 121); 6 | $secondTextColor: rgb(38, 84, 121); 7 | 8 | @import url("https://fonts.googleapis.com/css2?family=Comfortaa:wght@300;400;500;600;700&display=swap"); 9 | @import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@100&display=swap"); 10 | 11 | * { 12 | margin: 0; 13 | padding: 0; 14 | box-sizing: border-box; 15 | font-family: Montserrat; 16 | font-weight: 800; 17 | } 18 | 19 | body { 20 | background-color: rgb(160, 192, 206); 21 | color: $mainTextColor; 22 | } 23 | 24 | /***************************************************************** 25 | 26 | Landing Page 27 | 28 | *****************************************************************/ 29 | 30 | .LandingPageContainer { 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: space-between; 34 | position: relative; 35 | } 36 | 37 | .headerDiv { 38 | height: 70px; 39 | display: flex; 40 | justify-content: space-between; 41 | flex-shrink: 0; 42 | position: relative; 43 | } 44 | 45 | .gitHubHeader, 46 | .logo { 47 | margin: auto 5px; 48 | color: $mainTextColor; 49 | } 50 | 51 | .logo { 52 | margin-left: 45%; 53 | position: absolute; 54 | margin-left: auto; 55 | margin-right: auto; 56 | left: 0; 57 | right: 0; 58 | text-align: center; 59 | } 60 | 61 | .gitHubHeader { 62 | position: absolute; 63 | right: 5; 64 | top: 10; 65 | } 66 | 67 | .bodyDiv { 68 | display: flex; 69 | flex-direction: column; 70 | align-items: center; 71 | } 72 | 73 | #intro { 74 | color: white; 75 | display: flex; 76 | flex-direction: column; 77 | align-items: center; 78 | justify-content: center; 79 | background: transparent; 80 | padding-top: 10vh; 81 | } 82 | #intro > h1 { 83 | font-size: 5em; 84 | text-align: center; 85 | } 86 | 87 | #intro > h2 { 88 | font-size: 3em; 89 | text-align: center; 90 | } 91 | 92 | #intro > h2, 93 | h1, 94 | button { 95 | margin-bottom: 1rem; 96 | } 97 | 98 | .getStartedButton { 99 | height: 40px; 100 | width: 200px; 101 | font-size: 0.75em; 102 | border-radius: 5px; 103 | box-shadow: 1px 1px 1px black; 104 | color: $mainTextColor; 105 | margin: 10px; 106 | margin-bottom: 30px; 107 | background: rgba(38, 84, 121, 0.3); 108 | padding-bottom: 15px; 109 | padding-top: 5px; 110 | border-bottom-width: 15px; 111 | border-bottom-style: solid; 112 | border: 1px solid rgba(255, 255, 255, 0.514); 113 | } 114 | .getStartedButton:hover { 115 | color: $lightBlue; 116 | cursor: pointer; 117 | background-color: rgba(38, 84, 121, 0.6); 118 | border: 1px solid rgba(0, 0, 0, 0.322); 119 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.308); 120 | } 121 | 122 | .imageContainer { 123 | border: 2px solid rgba(0, 0, 0, 0.466); 124 | max-width: 65%; 125 | max-height: 700px; 126 | border-radius: 10px; 127 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.438); 128 | } 129 | 130 | .imageContainer > img { 131 | background-size: cover; 132 | background-repeat: no-repeat; 133 | width: 100%; 134 | border-radius: 10px; 135 | } 136 | 137 | #aboutUs { 138 | margin-top: 5rem; 139 | display: flex; 140 | flex-direction: column; 141 | align-items: center; 142 | width: 80%; 143 | } 144 | 145 | #aboutUs > h3 { 146 | margin-top: 1rem; 147 | margin-bottom: 1rem; 148 | color: $mainTextColor; 149 | font-size: 2.5rem; 150 | } 151 | 152 | .cardContainer { 153 | display: flex; 154 | justify-content: space-evenly; 155 | margin-top: 1rem; 156 | } 157 | 158 | .aboutUsCards { 159 | display: flex; 160 | flex-direction: column; 161 | flex-wrap: wrap; 162 | width: 30%; 163 | color: $mainTextColor; 164 | background-color: rgba(125, 162, 179, 0.137); 165 | border: 1px solid $darkBlue; 166 | border-radius: 10px; 167 | padding: 1rem; 168 | font-size: 1.5rem; 169 | } 170 | 171 | .aboutUsCards:hover { 172 | transform: translateX(-1%); 173 | background-color: rgba(125, 162, 179, 0.6); 174 | cursor: default; 175 | } 176 | 177 | .aboutUsCards > h4 { 178 | margin-bottom: 1rem; 179 | font-size: 1.55em; 180 | text-align: center; 181 | color: inherit; 182 | } 183 | 184 | .aboutUsCards > p { 185 | text-align: center; 186 | } 187 | 188 | .features { 189 | width: 80%; 190 | display: flex; 191 | flex-wrap: wrap; 192 | margin-top: 4rem; 193 | margin-bottom: 4rem; 194 | } 195 | 196 | .featurePicture > img { 197 | height: 500px; 198 | width: 600px; 199 | margin: 2rem; 200 | border-radius: 10px; 201 | box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.2); 202 | } 203 | 204 | .featureText { 205 | font-size: 4em; 206 | text-align: center; 207 | width: 40%; 208 | height: 40%; 209 | color: $mainTextColor; 210 | padding-top: 12%; 211 | } 212 | 213 | #topText { 214 | padding-top: 17%; 215 | animation: 3s slidein-left; 216 | } 217 | 218 | #bottomText { 219 | animation: 3s slidein-right; 220 | } 221 | 222 | .moreInfo { 223 | width: 100%; 224 | display: flex; 225 | flex-direction: column; 226 | justify-content: center; 227 | align-items: center; 228 | margin: 2rem; 229 | } 230 | .moreInfo > p, 231 | h3 { 232 | padding: 15px; 233 | } 234 | 235 | .moreInfo > h3 { 236 | font-size: 2.75rem; 237 | } 238 | 239 | .moreInfo > p { 240 | font-size: 2rem; 241 | text-align: center; 242 | line-height: 1.5em; 243 | } 244 | 245 | // @keyframes slidein-left { 246 | // 0% { 247 | // transform: translateX(-100%); 248 | // } 249 | // 100% { 250 | // transform: translateX(0); 251 | // } 252 | // } 253 | 254 | // @keyframes slidein-right { 255 | // 0% { 256 | // transform: translateX(100%); 257 | // } 258 | // 100% { 259 | // transform: translateX(0); 260 | // } 261 | // } 262 | 263 | .teamContainer { 264 | display: flex; 265 | flex-direction: column; 266 | align-items: center; 267 | justify-content: space-evenly; 268 | width: 100%; 269 | height: 80vh; 270 | padding: 20rem auto; 271 | } 272 | 273 | .teamCards { 274 | display: flex; 275 | flex-direction: column; 276 | align-items: center; 277 | } 278 | 279 | .profileContainer { 280 | display: flex; 281 | justify-content: space-evenly; 282 | width: 100%; 283 | } 284 | 285 | .teamContainerHeader { 286 | text-align: center; 287 | font-size: 3em; 288 | color: $mainTextColor; 289 | } 290 | 291 | .teamPicture { 292 | width: 15rem; 293 | height: 15rem; 294 | border-radius: 50%; 295 | border: 1px solid black; 296 | background-size: cover; 297 | object-fit: cover; 298 | box-shadow: 2px 2px 2px #23232394; 299 | } 300 | 301 | .teamPicture > img:hover { 302 | background: rgba(255, 255, 255, 0.363); 303 | } 304 | 305 | .teamCards > p, 306 | a { 307 | padding: 5px; 308 | font-size: 1.75em; 309 | text-decoration: none; 310 | } 311 | .teamCards > p { 312 | color: $mainTextColor; 313 | font-weight: 800; 314 | } 315 | .teamCards > a { 316 | color: $secondTextColor; 317 | } 318 | a:hover { 319 | color: $mainTextColor; 320 | } 321 | 322 | .footerDiv { 323 | height: 5rem; 324 | width: 100%; 325 | display: flex; 326 | justify-content: center; 327 | align-items: center; 328 | background: inherit; 329 | } 330 | 331 | footer { 332 | position: absolute; 333 | bottom: 0; 334 | } 335 | 336 | //------------------------------------------------------------------------------------// 337 | //fancy bubbles 338 | //------------------------------------------------------------------------------------// 339 | 340 | $bubble-count: 50; 341 | $sway-type: "sway-left-to-right", "sway-right-to-left"; 342 | 343 | @function random_range($min, $max) { 344 | $rand: random(); 345 | $random_range: $min + floor($rand * (($max - $min) + 1)); 346 | @return $random_range; 347 | } 348 | 349 | @function sample($list) { 350 | @return nth($list, random(length($list))); 351 | } 352 | 353 | .bubbles { 354 | position: relative; 355 | width: 100%; 356 | height: 100%; 357 | overflow: hidden; 358 | display: flex; 359 | flex-direction: column; 360 | align-items: center; 361 | padding-bottom: 50px; 362 | border-top: 1px solid $mainTextColor; 363 | } 364 | 365 | .bubble { 366 | position: absolute; 367 | left: var(--bubble-left-offset); 368 | bottom: -75%; 369 | display: block; 370 | width: var(--bubble-radius); 371 | height: var(--bubble-radius); 372 | border-radius: 50%; 373 | animation: float-up var(--bubble-float-duration) var(--bubble-float-delay) 374 | ease-in infinite; 375 | 376 | &::before { 377 | position: absolute; 378 | content: ""; 379 | top: 0; 380 | left: 0; 381 | width: 100%; 382 | height: 100%; 383 | background: rgb(238, 243, 255); 384 | background: radial-gradient( 385 | circle, 386 | rgba(238, 243, 255, 0.5) 0%, 387 | rgba(222, 230, 250, 0.5) 33%, 388 | rgba(164, 191, 255, 0.5) 100% 389 | ); 390 | border-radius: inherit; 391 | animation: var(--bubble-sway-type) var(--bubble-sway-duration) 392 | var(--bubble-sway-delay) ease-in-out alternate infinite; 393 | } 394 | 395 | @for $i from 0 through $bubble-count { 396 | &:nth-child(#{$i}) { 397 | --bubble-left-offset: #{random_range(0vw, 100vw)}; 398 | --bubble-radius: #{random_range(1vw, 10vw)}; 399 | --bubble-float-duration: #{random_range(6s, 12s)}; 400 | --bubble-sway-duration: #{random_range(4s, 6s)}; 401 | --bubble-float-delay: #{random_range(0s, 4s)}; 402 | --bubble-sway-delay: #{random_range(0s, 4s)}; 403 | --bubble-sway-type: #{sample($sway-type)}; 404 | } 405 | } 406 | } 407 | 408 | @keyframes float-up { 409 | to { 410 | transform: translateY(-175vh); 411 | } 412 | } 413 | 414 | @keyframes sway-left-to-right { 415 | from { 416 | transform: translateX(-100%); 417 | } 418 | 419 | to { 420 | transform: translateX(100%); 421 | } 422 | } 423 | 424 | @keyframes sway-right-to-left { 425 | from { 426 | transform: translateX(100%); 427 | } 428 | 429 | to { 430 | transform: translateX(-100%); 431 | } 432 | } 433 | 434 | /***************************************************************** 435 | 436 | Get Started Page 437 | 438 | *****************************************************************/ 439 | 440 | .getStartedBody { 441 | display: flex; 442 | flex-direction: column; 443 | align-items: center; 444 | 445 | h1 { 446 | font-size: 2em; 447 | padding-top: 10px; 448 | } 449 | 450 | #infoBox { 451 | height: 65vh; 452 | width: 80%; 453 | background-color: rgba(0, 0, 0, 0.103); 454 | border: 1px solid rgb(38, 0, 80); 455 | border-radius: 10px; 456 | font-size: 1.5em; 457 | line-height: 2rem; 458 | text-align: center; 459 | padding: 5px; 460 | overflow-y: scroll; 461 | box-shadow: 2px 2px 2px black; 462 | margin-top: 70px; 463 | } 464 | #infobox::-webkit-scrollbar { 465 | display: none; 466 | } 467 | #helm-install-here { 468 | font-size: 1em; 469 | color: $darkBlue; 470 | } 471 | 472 | #helm-install-here:hover { 473 | color: white; 474 | } 475 | } 476 | 477 | /***************************************************************** 478 | 479 | Log In Page (not in use) 480 | 481 | *****************************************************************/ 482 | 483 | #LogInContainer { 484 | background-color: $lightBlue; 485 | width: 100vw; 486 | height: 100vh; 487 | display: grid; 488 | place-content: center; 489 | } 490 | 491 | /***************************************************************** 492 | 493 | Dashboard 494 | 495 | *****************************************************************/ 496 | 497 | .homeContainer { 498 | background-color: rgb(255, 255, 255); 499 | width: 100vw; 500 | height: 100vh; 501 | display: grid; 502 | grid-template-columns: repeat(14, 1fr); 503 | grid-template-rows: repeat(12, 1fr); 504 | } 505 | 506 | .dashboardHeader { 507 | grid-column: 3 / 15; 508 | grid-row: 1 / 2; 509 | background-color: $lightBlue; 510 | display: flex; 511 | justify-content: flex-end; 512 | align-items: center; 513 | padding-right: 1rem; 514 | border-bottom: 1px solid $darkBlue; 515 | } 516 | 517 | .dashboardHeader > p { 518 | font-weight: bold; 519 | } 520 | 521 | .dashboardHeader > #help { 522 | text-decoration: none; 523 | color: inherit; 524 | color: $darkBlue; 525 | padding: 0.6rem 1.5rem; 526 | margin-right: 7px; 527 | border-radius: 5px; 528 | font-size: 1rem; 529 | font-weight: bold; 530 | } 531 | 532 | #import { 533 | color: $darkBlue; 534 | padding: 0.3rem; 535 | border-radius: 5px; 536 | } 537 | 538 | #import:hover, 539 | #help:hover { 540 | cursor: pointer; 541 | color: $mainTextColor; 542 | } 543 | 544 | // navbar starts ------------------// 545 | 546 | .navbarContainer { 547 | grid-column: 1/3; 548 | grid-row: 1/13; 549 | background-color: $lightBlue; 550 | display: flex; 551 | flex-direction: column; 552 | border-right: 1px solid $darkBlue; 553 | } 554 | 555 | #dashboardLogo { 556 | margin-right: 0.3rem; 557 | } 558 | 559 | .logoHeader { 560 | display: flex; 561 | justify-content: center; 562 | align-items: center; 563 | margin-top: 1rem; 564 | margin-bottom: 5rem; 565 | } 566 | 567 | .shipshape { 568 | font-size: 1.5em; 569 | font-weight: 900; 570 | } 571 | 572 | .subjectContainer { 573 | display: flex; 574 | flex-direction: column; 575 | position: relative; 576 | height: 100%; 577 | } 578 | 579 | .subjects { 580 | font-weight: bold; 581 | font-size: 1.2rem; 582 | } 583 | 584 | .mainDashboard { 585 | grid-column: 3 / 15; 586 | grid-row: 2 / 13; 587 | background-color: $mainTextColor; 588 | display: grid; 589 | grid-template-columns: repeat(12, 1fr); 590 | grid-template-rows: repeat(12, 1fr); 591 | padding: 0.5rem; 592 | gap: 1rem; 593 | position: relative; 594 | } 595 | 596 | .podDashboard, 597 | .nodeDashboard, 598 | .clusterDashboard { 599 | grid-column: 1 / 15; 600 | grid-row: 1 / 15; 601 | background-color: $mainTextColor; 602 | display: grid; 603 | grid-template-columns: repeat(12, 1fr); 604 | grid-template-rows: repeat(12, 1fr); 605 | padding: 0.5rem; 606 | gap: 1rem; 607 | position: relative; 608 | } 609 | 610 | .duration-select { 611 | grid-column: 1 / 13; 612 | grid-row: 1/ 2; 613 | border: 1px solid rgba(14, 13, 13, 0.3); 614 | border-radius: 5px; 615 | box-shadow: rgba(149, 157, 165, 0.411) 0px 8px 24px; 616 | display: flex; 617 | align-items: center; 618 | gap: 1rem; 619 | padding-left: 2rem; 620 | vertical-align: baseline; 621 | height: 5em; 622 | } 623 | 624 | .StreamingNetworkPressure { 625 | grid-column: 1 / 13; 626 | grid-row: 2/ 6; 627 | border: 1px solid rgba(14, 13, 13, 0.3); 628 | border-radius: 5px; 629 | box-shadow: rgba(149, 157, 165, 0.411) 0px 8px 24px; 630 | } 631 | 632 | .StreamingMemoryChart { 633 | grid-column: 1 / 13; 634 | grid-row: 6 / 10; 635 | border: 1px solid rgba(0, 0, 0, 0.3); 636 | border-radius: 5px; 637 | box-shadow: rgba(149, 157, 165, 0.411) 0px 8px 24px; 638 | } 639 | 640 | .StreamingCpuChart { 641 | grid-column: 1 / 13; 642 | grid-row: 10 / 15; 643 | border: 1px solid rgba(0, 0, 0, 0.3); 644 | border-radius: 5px; 645 | } 646 | 647 | .streams > canvas { 648 | max-width: 100% !important; 649 | max-height: 100% !important; 650 | } 651 | 652 | .barChart { 653 | grid-column: 1 / 7; 654 | grid-row: 1 / 7; 655 | border: 1px solid rgba(0, 0, 0, 0.3); 656 | border-radius: 5px; 657 | box-shadow: rgba(149, 157, 165, 0.411) 0px 8px 24px; 658 | } 659 | 660 | .doughnutContainer { 661 | grid-column: 7 / 13; 662 | grid-row: 1 / 7; 663 | border: 1px solid rgba(0, 0, 0, 0.3); 664 | border-radius: 5px; 665 | box-shadow: rgba(149, 157, 165, 0.411) 0px 8px 24px; 666 | } 667 | 668 | .podsTable, 669 | .nodesTable { 670 | grid-column: 1 / 13; 671 | grid-row: 7 / 13; 672 | border: 1px solid rgba(0, 0, 0, 0.3); 673 | border-radius: 5px; 674 | box-shadow: rgba(149, 157, 165, 0.411) 0px 8px 24px; 675 | color: rgb(0, 0, 0) !important; 676 | font-weight: bold !important; 677 | overflow: scroll; 678 | } 679 | 680 | .podsTable::-webkit-scrollbar { 681 | display: none; 682 | } 683 | 684 | .pods { 685 | color: black !important; 686 | font-weight: bold !important; 687 | } 688 | 689 | .btnDashboard { 690 | display: flex; 691 | justify-content: space-between; 692 | align-items: center; 693 | background-color: white; 694 | color: #6a8693; 695 | font-weight: bold; 696 | padding: 1rem 1rem; 697 | width: 100%; 698 | font-size: 1.2rem; 699 | margin-bottom: 0; 700 | box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.342); 701 | transition: 0.3s; 702 | &:hover { 703 | background-color: rgba(38, 84, 121, 0.527); 704 | color: $mainTextColor; 705 | border: 1px solid rgba(0, 0, 0, 0.466); 706 | } 707 | } 708 | 709 | .settings { 710 | position: absolute; 711 | bottom: 20; 712 | width: 100%; 713 | } 714 | 715 | .dropDown { 716 | background-color: white; 717 | color: black; 718 | text-decoration: none; 719 | list-style-type: none; 720 | margin-top: 0; 721 | padding-left: 2.99rem; 722 | } 723 | 724 | .dropDown > ul { 725 | list-style-type: none; 726 | } 727 | 728 | .speedometerContainer { 729 | grid-column: 5 / 9; 730 | grid-row: 1 / 7; 731 | border: 1px solid rgba(0, 0, 0, 0.3); 732 | border-radius: 5px; 733 | box-shadow: rgba(149, 157, 165, 0.411) 0px 8px 24px; 734 | } 735 | 736 | .diskSpaceContainer { 737 | color: $lightBlue; 738 | text-align: center; 739 | display: grid; 740 | place-content: center; 741 | grid-column: 1 / 5; 742 | grid-row: 1 / 7; 743 | border: 1px solid rgba(0, 0, 0, 0.3); 744 | border-radius: 5px; 745 | box-shadow: rgba(149, 157, 165, 0.411) 0px 8px 24px; 746 | 747 | h2 { 748 | font-size: 2em; 749 | font-weight: 800; 750 | color: $darkBlue; 751 | } 752 | h1 { 753 | font-size: 3em; 754 | } 755 | 756 | span { 757 | font-size: 3em; 758 | border-bottom: 1px solid $lightBlue; 759 | box-shadow: 0 4px 2px -2px rgba(160, 192, 206, 0.4); 760 | } 761 | } 762 | 763 | .cpuUSageContainer { 764 | color: $lightBlue; 765 | text-align: center; 766 | display: grid; 767 | place-content: center; 768 | grid-column: 9 / 13; 769 | grid-row: 1 / 7; 770 | border: 1px solid rgba(0, 0, 0, 0.3); 771 | border-radius: 5px; 772 | box-shadow: rgba(149, 157, 165, 0.411) 0px 8px 24px; 773 | 774 | h2 { 775 | font-size: 2em; 776 | font-weight: 800; 777 | color: $darkBlue; 778 | } 779 | h1 { 780 | font-size: 3em; 781 | } 782 | 783 | span { 784 | font-size: 3em; 785 | border-bottom: 1px solid $lightBlue; 786 | box-shadow: 0 4px 2px -2px rgba(160, 192, 206, 0.4); 787 | } 788 | } 789 | 790 | /* Number animations for CPU/Disk Space in Nodeview components starts here */ 791 | 792 | .animatedNumber { 793 | display: flex; 794 | text-align: center; 795 | justify-content: center; 796 | } 797 | 798 | .animatedNumber > P { 799 | font-size: 3em; 800 | border-bottom: 1px solid $lightBlue; 801 | // box-shadow: inset 0 0 10px 0 #5f5e5e41; 802 | box-shadow: 0 4px 2px -2px rgba(160, 192, 206, 0.4); 803 | } 804 | 805 | /* Ends here */ 806 | 807 | /***************************************************************** 808 | 809 | Media Querys 810 | 811 | *****************************************************************/ 812 | 813 | @media only screen and (max-width: 900px) { 814 | .aboutUs { 815 | width: 100%; 816 | display: flex; 817 | flex-direction: column; 818 | align-items: center; 819 | } 820 | 821 | .cardContainer { 822 | display: flex; 823 | flex-direction: column; 824 | align-items: center; 825 | } 826 | 827 | .aboutUsCards { 828 | width: 100%; 829 | margin-bottom: 1rem; 830 | display: flex; 831 | flex-direction: column; 832 | align-items: center; 833 | } 834 | 835 | .features { 836 | display: flex; 837 | flex-direction: column; 838 | align-items: center; 839 | justify-content: center; 840 | text-align: center; 841 | } 842 | 843 | .featureText, 844 | #topText, 845 | #bottomText { 846 | display: flex; 847 | justify-content: center; 848 | text-align: center; 849 | width: 100%; 850 | p { 851 | // width: 1500px !important; 852 | // font-size: 2em !important; 853 | // color: pink !important; 854 | font-size: 0.85em; 855 | } 856 | } 857 | 858 | // #topText { 859 | // width: 1000px !important; 860 | // } 861 | 862 | .featurePicture { 863 | width: 90%; 864 | } 865 | 866 | .featurePicture > img { 867 | width: 100%; 868 | // height: 50%; 869 | } 870 | } 871 | 872 | /* Large devices (laptops/desktops, 992px and up) */ 873 | @media only screen and (max-width: 1120px) { 874 | .aboutUs { 875 | width: 60%; 876 | display: flex; 877 | flex-direction: column; 878 | align-items: center; 879 | } 880 | 881 | .cardContainer { 882 | display: flex; 883 | flex-direction: column; 884 | align-items: center; 885 | } 886 | 887 | .aboutUsCards { 888 | width: 60%; 889 | margin-bottom: 1rem; 890 | } 891 | } 892 | 893 | @media (max-width: 1360px) { 894 | .shipshape { 895 | font-size: 1.1em; 896 | } 897 | } 898 | 899 | @media (max-width: 1111px) { 900 | .shipshape { 901 | display: none; 902 | } 903 | } 904 | 905 | @media only screen and (max-width: 500px) { 906 | .LandingPageContainer { 907 | display: flex; 908 | flex-direction: column; 909 | justify-content: center; 910 | align-items: center; 911 | } 912 | 913 | .headerDiv { 914 | width: 100%; 915 | padding: 50px; 916 | display: flex; 917 | justify-items: center; 918 | align-items: center; 919 | } 920 | 921 | .logo > img { 922 | width: 70px; 923 | height: 60px; 924 | } 925 | 926 | #githubImage { 927 | } 928 | 929 | .bodyDiv { 930 | width: 95%; 931 | display: flex; 932 | justify-content: center; 933 | align-items: center; 934 | } 935 | 936 | #intro { 937 | padding-top: 10; 938 | display: flex; 939 | flex-direction: column; 940 | justify-content: center; 941 | align-items: center; 942 | text-align: center; 943 | h1 { 944 | font-size: 2em; 945 | } 946 | 947 | h2 { 948 | font-size: 1em; 949 | } 950 | } 951 | 952 | #buttonContainer { 953 | display: flex; 954 | 955 | button { 956 | width: 40vw; 957 | font-size: 0.5em; 958 | text-align: center; 959 | display: flex; 960 | justify-content: center; 961 | align-items: center; 962 | } 963 | } 964 | 965 | .getStartedButton { 966 | display: flex; 967 | justify-content: center; 968 | align-items: center; 969 | } 970 | 971 | .imageContainer { 972 | width: 350px; 973 | } 974 | 975 | #dashImage { 976 | width: 100%; 977 | } 978 | 979 | #aboutUs { 980 | margin-top: 15px; 981 | display: flex; 982 | flex-direction: column; 983 | justify-content: center; 984 | align-items: center; 985 | text-align: center; 986 | h3 { 987 | padding: 2px 2px; 988 | font-size: 2em; 989 | } 990 | 991 | h4 { 992 | font-size: 1em; 993 | } 994 | 995 | p { 996 | font-size: 0.8em; 997 | } 998 | } 999 | 1000 | .aboutUsCards { 1001 | width: 90%; 1002 | } 1003 | 1004 | .cardContainer { 1005 | margin-top: 5px; 1006 | } 1007 | 1008 | .features { 1009 | display: flex; 1010 | justify-content: center; 1011 | align-items: center; 1012 | text-align: center; 1013 | margin-top: 0; 1014 | } 1015 | 1016 | #topText, 1017 | #bottomText { 1018 | font-size: 2em; 1019 | text-align: center; 1020 | } 1021 | 1022 | #bottomText { 1023 | margin-top: -15px; 1024 | } 1025 | 1026 | .featurePicture { 1027 | width: 90%; 1028 | img { 1029 | width: 95%; 1030 | height: 150px; 1031 | margin: auto 0; 1032 | margin-bottom: 10px; 1033 | margin-top: 10px; 1034 | } 1035 | } 1036 | 1037 | .last { 1038 | width: 90%; 1039 | img { 1040 | width: 95%; 1041 | height: 150px; 1042 | margin: auto 0; 1043 | margin-top: 10px; 1044 | } 1045 | } 1046 | 1047 | .moreInfo { 1048 | margin-top: -20px; 1049 | display: flex; 1050 | justify-content: center; 1051 | align-items: center; 1052 | text-align: center; 1053 | h3 { 1054 | font-size: 2em; 1055 | } 1056 | 1057 | p { 1058 | font-size: 1em; 1059 | } 1060 | } 1061 | 1062 | .bubbles, 1063 | .teamContainer { 1064 | font-size: 0.75em; 1065 | height: 70%; 1066 | padding: 2rem; 1067 | display: flex; 1068 | justify-content: space-between; 1069 | } 1070 | 1071 | .teamPicture { 1072 | width: 60px; 1073 | height: 50px; 1074 | } 1075 | 1076 | .head { 1077 | font-size: 1.5rem; 1078 | } 1079 | 1080 | .teamContainerHeader { 1081 | margin-bottom: 10px; 1082 | } 1083 | 1084 | .profileContainer { 1085 | } 1086 | 1087 | .teamCards > p { 1088 | font-size: 1em; 1089 | } 1090 | } 1091 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shipshape", 3 | "version": "1.0.0", 4 | "description": "Kubernetes Cluster Visualizer and Management Tool", 5 | "main": "client/index.js", 6 | "scripts": { 7 | "test": "jest --verbose", 8 | "start": "cross-env NODE_ENV=production node server/server.js", 9 | "build": "cross-env NODE_ENV=production webpack", 10 | "dev": "concurrently \"cross-env NODE_ENV=development webpack serve\" \"cross-env NODE_ENV=development nodemon server/server.js\"", 11 | "demo-start": "cross-env NODE_ENV=production DEMO_MODE=true node server/server.js", 12 | "demo-dev": "concurrently \"cross-env NODE_ENV=development DEMO_MODE=true webpack serve\" \"cross-env NODE_ENV=development DEMO_MODE=true nodemon server/server.js\"", 13 | "lint": "./node_modules/.bin/eslint ." 14 | }, 15 | "keywords": [ 16 | "Kubernetes", 17 | "Visualizer", 18 | "React", 19 | "Express" 20 | ], 21 | "author": "Brian Barr, Ozi Ozturk, Rebecca Schell, Whit Rooke", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@babel/preset-env": "^7.14.2", 25 | "@fortawesome/fontawesome-free": "^5.15.3", 26 | "@kubernetes/client-node": "^0.14.3", 27 | "@material-ui/core": "^4.11.4", 28 | "@material-ui/data-grid": "^4.0.0-alpha.28", 29 | "@material-ui/icons": "^4.11.2", 30 | "@mui-treasury/styles": "^1.13.1", 31 | "@testing-library/jest-dom": "^5.12.0", 32 | "Utils": "^1.0.39", 33 | "animate.css": "^4.1.1", 34 | "animated-number-react": "^0.1.2", 35 | "apollo-datasource-rest": "^0.14.0", 36 | "apollo-server": "^2.25.0", 37 | "apollo-server-express": "^2.18.0-rc.1", 38 | "axios": "^0.21.1", 39 | "babel-jest": "^27.0.2", 40 | "body-parser": "^1.19.0", 41 | "chart.js": "^3.3.0", 42 | "chartjs-adapter-luxon": "^1.0.0", 43 | "chartjs-gauge": "^0.3.0", 44 | "chartjs-plugin-streaming": "^2.0.0-beta.3", 45 | "dotenv": "^9.0.2", 46 | "express": "^4.17.1", 47 | "express-graphql": "^0.12.0", 48 | "graphql": "^15.5.0", 49 | "graphql-tools": "^7.0.5", 50 | "jest": "^27.0.3", 51 | "lodash": "^4.17.21", 52 | "luxon": "^1.27.0", 53 | "mongoose": "^5.12.8", 54 | "node-cmd": "^4.0.0", 55 | "node-fetch": "^2.6.1", 56 | "path": "^0.12.7", 57 | "react": "^17.0.2", 58 | "react-animate-on-scroll": "^2.1.5", 59 | "react-chartjs-2": "^3.0.3", 60 | "react-dom": "^17.0.2", 61 | "react-icons": "^4.2.0", 62 | "react-reveal": "^1.2.2", 63 | "react-router-dom": "^5.2.0", 64 | "react-test-renderer": "^17.0.2", 65 | "regenerator-runtime": "^0.13.7", 66 | "sass": "^1.32.12", 67 | "styled-components": "^5.3.0", 68 | "supertest": "^6.1.3", 69 | "table-parser": "^1.0.1" 70 | }, 71 | "devDependencies": { 72 | "@babel/core": "^7.13.16", 73 | "@babel/plugin-transform-runtime": "^7.14.3", 74 | "@babel/preset-env": "^7.14.2", 75 | "@babel/preset-react": "^7.13.13", 76 | "@testing-library/react": "^11.2.7", 77 | "babel": "^6.23.0", 78 | "babel-loader": "^8.2.2", 79 | "babel-polyfill": "^6.26.0", 80 | "concurrently": "^6.1.0", 81 | "cross-env": "^7.0.3", 82 | "css-loader": "^5.2.4", 83 | "enzyme": "^3.11.0", 84 | "enzyme-adapter-react-16": "^1.15.6", 85 | "eslint": "^7.27.0", 86 | "eslint-config-airbnb": "^18.2.1", 87 | "eslint-plugin-import": "^2.23.4", 88 | "eslint-plugin-jsx-a11y": "^6.4.1", 89 | "eslint-plugin-react": "^7.24.0", 90 | "file-loader": "^6.2.0", 91 | "jest-dom": "^4.0.0", 92 | "nodemon": "^2.0.7", 93 | "sass-loader": "^11.1.0", 94 | "style-loader": "^2.0.0", 95 | "webpack": "^5.34.0", 96 | "webpack-cli": "^4.6.0", 97 | "webpack-dev-server": "^3.11.2" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /server/graphQL/datasources/dataSources.js: -------------------------------------------------------------------------------- 1 | const PrometheusAPI = require('./prometheusAPI'); 2 | 3 | const memory = { 4 | isPrometheusUp: { check: false }, 5 | }; 6 | 7 | // eslint-disable-next-line arrow-body-style 8 | module.exports = () => { 9 | return { 10 | prometheusAPI: new PrometheusAPI(memory), 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /server/graphQL/datasources/nodeConstructor.js: -------------------------------------------------------------------------------- 1 | const cmd = require('node-cmd'); 2 | const Parser = require('table-parser'); 3 | 4 | const Node = {}; 5 | 6 | Node.getPercentages = async function(node = '') { 7 | const percents = cmd.runSync(`kubectl top node ${node}`); 8 | const percentObj = Parser.parse(percents.data); 9 | return percentObj; 10 | }; 11 | 12 | Node.getNodeMetrics = async function(node = '') { 13 | const rawMetrics = cmd.runSync(`kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes/${node}`); 14 | const regex = '///'; 15 | const metrics = rawMetrics.data.replace(regex, ''); 16 | return await JSON.parse(metrics); 17 | }; 18 | 19 | module.exports = Node; -------------------------------------------------------------------------------- /server/graphQL/datasources/podConstructor.js: -------------------------------------------------------------------------------- 1 | const cmd = require('node-cmd'); 2 | 3 | const Pod = {}; 4 | 5 | Pod.getMetrics = async function (namespace = undefined, pod = undefined) { 6 | let rawMetrics; 7 | if (!namespace || !pod) rawMetrics = cmd.runSync('kubectl get --raw /apis/metrics.k8s.io/v1beta1/pods/'); 8 | else rawMetrics = cmd.runSync(`kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/${namespace}/pods/${pod}`); 9 | const regex = '///'; 10 | const metrics = rawMetrics.data.replace(regex, ''); 11 | return JSON.parse(metrics); 12 | }; 13 | 14 | module.exports = Pod; 15 | -------------------------------------------------------------------------------- /server/graphQL/datasources/prometheusAPI.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process') 2 | const {RESTDataSource} = require('apollo-datasource-rest'); 3 | 4 | const prometheusURL = 'http://127.0.0.1:9090/api/v1/'; 5 | 6 | class PrometheusAPI extends RESTDataSource{ 7 | 8 | constructor({ isPrometheusUp }){ 9 | super() 10 | this.baseURL = prometheusURL; 11 | this.portAttempts = 0; 12 | this.isPrometheusUp = isPrometheusUp; 13 | this.portPrometheus = this.portPrometheus.bind(this); 14 | } 15 | 16 | async getCpuUsageSecondsRateByName(startDateTime, endDateTime, step){ 17 | const checkProm = await this.portPrometheus() 18 | if(!checkProm) return console.log('Error with Pometheus Configuration'); 19 | 20 | let query = `query_range?query=sum(rate(container_cpu_usage_seconds_total{container_name!="POD",namespace!=""}[2m])) by (namespace)`; 21 | query += `&start=${startDateTime}&end=${endDateTime}&step=${step}`; 22 | const data = await this.get(query).then( ({ data }) => data.result); 23 | 24 | return this.formatResponseObject(data); 25 | } 26 | 27 | async getClusterFreeMemory(startDateTime, endDateTime, step){ 28 | const checkProm = await this.portPrometheus() 29 | if(!checkProm) return console.log('Error with Pometheus Configuration'); 30 | let query = `query_range?query=sum(rate(node_memory_MemFree_bytes[2m]))`; 31 | query += `&start=${startDateTime}&end=${endDateTime}&step=${step}`; 32 | const data = await this.get(query).then(({ data }) => data.result); 33 | 34 | return this.formatResponseObject(data) 35 | } 36 | 37 | //http://localhost:9090/api/v1/query_range?query=sum(rate(node_network_transmit_bytes[…]2021-05-26T20:55:06.753Z&end=2021-05-28T20:55:05.208Z&step=1m 38 | 39 | async getNetworkTransmitBytes(startDateTime, endDateTime, step){ 40 | const checkProm = await this.portPrometheus() 41 | 42 | if(!checkProm) return console.log('Error with Pometheus Configuration'); 43 | let query = `query_range?query=sum(rate(node_network_transmit_bytes_total[2m]))`; 44 | query += `&start=${startDateTime}&end=${endDateTime}&step=${step}`; 45 | const data = await this.get(query).then(({ data }) => data.result).catch(err => console.log(err)); 46 | 47 | return this.formatResponseObject(data) 48 | } 49 | 50 | 51 | formatResponseObject(data){ 52 | try{ 53 | const res = { 54 | timestamps: [], 55 | seriesLabels: [], 56 | seriesValues: [], 57 | }; 58 | 59 | //helper function to convert the Prometheus MS timestamp to HH:MM 60 | const timeFilter = /[0-9][0-9]:[0-9][0-9]/ 61 | const msToTimestamp = (ms) => new Date(1000 * ms).toISOString().match(timeFilter)[0]; 62 | 63 | //pop the last series off the query response to extract timestamp and groupBy label 64 | const initialSet = data.pop(); 65 | const groupByLabel = Object.keys(initialSet.metric)[0]; 66 | 67 | // add this last series to the response object arrays 68 | res.timestamps = initialSet.values.map(vals => msToTimestamp(vals[0])); 69 | res.seriesLabels.push(initialSet.metric[groupByLabel] || 'Cluster') 70 | res.seriesValues.push(initialSet.values.map(vals => vals[1])) 71 | 72 | // for each remaining dataset, push the series label and array of datapoints onto the res object 73 | data.forEach(dataset => { 74 | res.seriesLabels.push(dataset.metric[groupByLabel]); // add a new dataseries label to our res 75 | res.seriesValues.push(dataset.values.map(vals => vals[1])); // add the dataseries to our res 76 | }); 77 | 78 | //return the constructed response 79 | return res; 80 | } catch(err){ 81 | console.log(err); 82 | } 83 | } 84 | 85 | portPrometheus = () => new Promise((resolve, reject) => { 86 | 87 | if(this.isPrometheusUp.check) resolve(true); 88 | 89 | try{ 90 | const process = spawn('kubectl', ['--namespace=prometheus', 'port-forward', 'deploy/prometheus-server', '9090']) 91 | this.portAttempts += 1 92 | 93 | process.stdout.on('data', (data) => { 94 | // console.log(`stdout: ${data}`); 95 | this.isPrometheusUp.check = true; 96 | resolve(true) 97 | }) 98 | 99 | process.stderr.on('data', async (err) => { 100 | // console.log(`stderr: ${err}`); 101 | }) 102 | 103 | process.on('close', (code) => { 104 | if(code === 1) resolve(this.isPrometheusUp.check) 105 | else { 106 | console.log(`child process exited with code ${code}`); 107 | this.isPrometheusUp.check = false; 108 | } 109 | }) 110 | }catch(err){ 111 | console.log(err); 112 | } 113 | 114 | }); 115 | } 116 | 117 | module.exports = PrometheusAPI; -------------------------------------------------------------------------------- /server/graphQL/demoData/CpuUsage.json: -------------------------------------------------------------------------------- 1 | {"timestamps":["19:34","19:36","19:38","19:40","19:42","19:44","19:46","19:48","19:50","19:52","19:54","19:56","19:58","20:00","20:02","20:04","20:06","20:08","20:10","20:12","20:14","20:16","20:18","20:20","20:22","20:24","20:26","20:28","20:30","20:32","20:34","20:36","20:38","20:40","20:42","20:44","20:46","20:48","20:50","20:52","20:54","20:56","20:58","21:00","21:02","21:04","21:06","21:08","21:10","21:12","21:14","21:16","21:18","21:20","21:22","21:24","21:26","21:28","21:30","21:32","21:34","21:36","21:38","21:40","21:42","21:44","21:46","21:48","21:50","21:52","21:54","21:56","21:58","22:00","22:02","22:04","22:06","22:08","22:10","22:12","22:14","22:16","22:18","22:20","22:22","22:24","22:26","22:28","22:30","22:32","22:34","22:36","22:38","22:40","22:42","22:44","22:46","22:48","22:50","22:52","22:54","22:56","22:58","23:00","23:02","23:04","23:06","23:08","23:10","23:12","23:14","23:16","23:18","23:20","23:22","23:24","23:26","23:28","23:30","23:32","23:34","23:36","23:38","23:40","23:42","23:44","23:46","23:48","23:50","23:52","23:54","23:56","23:58","00:00","00:02","00:04","00:06","00:08","00:10","00:12","00:14","00:16","00:18","00:20","00:22","00:24","00:26","00:28","00:30","00:32","00:34","00:36","00:38","00:40","00:42","00:44","00:46","00:48","00:50","00:52","00:54","00:56","00:58","01:00","01:02","01:04","01:06","01:08","01:10","01:12","01:14","01:16","01:18","01:20","01:22","01:24","01:26","01:28","01:30","01:32","01:34","01:36","01:38","01:40","01:42","01:44","01:46","01:48","01:50","01:52","01:54","01:56","01:58","02:00","02:02","02:04","02:06","02:08","02:10","02:12","02:14","02:16","02:18","02:20","02:22","02:24","02:26","02:28","02:30","02:32","02:34","02:36","02:38","02:40","02:42","02:44","02:46","02:48","02:50","02:52","02:54","02:56","02:58","03:00","03:02","03:04","03:06","03:08","03:10","03:12","03:14","03:16","03:18","03:20","03:22","03:24","03:26","03:28","03:30","03:32","03:34","03:36","03:38","03:40","03:42","03:44","03:46","03:48","03:50","03:52","03:54","03:56","03:58","04:00","04:02","04:04","04:06","04:08","04:10","04:12","04:14","04:16","04:18","04:20","04:22","04:24","04:26","04:28","04:30","04:32","04:34","04:36","04:38","04:40","04:42","04:44","04:46","04:48","04:50","04:52","04:54","04:56","04:58","05:00","05:02","05:04","05:06","05:08","05:10","05:12","05:14","05:16","05:18","05:20","05:22","05:24","05:26","05:28","05:30","05:32","05:34","05:36","05:38","05:40","05:42","05:44","05:46","05:48","05:50","05:52","05:54","05:56","05:58","06:00","06:02","06:04","06:06","06:08","06:10","06:12","06:14","06:16","06:18","06:20","06:22","06:24","06:26","06:28","06:30","06:32","06:34","06:36","06:38","06:40","06:42","06:44","06:46","06:48","06:50","06:52","06:54","06:56","06:58","07:00","07:02","07:04","07:06","07:08","07:10","07:12","07:14","07:16","07:18","07:20","07:22","07:24","07:26","07:28","07:30","07:32","07:34","07:36","07:38","07:40","07:42","07:44","07:46","07:48","07:50","07:52","07:54","07:56","07:58","08:00","08:02","08:04","08:06","08:08","08:10","08:12","08:14","08:16","08:18","08:20","08:22","08:24","08:26","08:28","08:30","08:32","08:34","08:36","08:38","08:40","08:42","08:44","08:46","08:48","08:50","08:52","08:54","08:56","08:58","09:00","09:02","09:04","09:06","09:08","09:10","09:12","09:14","09:16","09:18","09:20","09:22","09:24","09:26","09:28","09:30","09:32","09:34","09:36","09:38","09:40","09:42","09:44","09:46","09:48","09:50","09:52","09:54","09:56","09:58","10:00","10:02","10:04","10:06","10:08","10:10","10:12","10:14","10:16","10:18","10:20","10:22","10:24","10:26","10:28","10:30","10:32","10:34","10:36","10:38","10:40","10:42","10:44","10:46","10:48","10:50","10:52","10:54","10:56","10:58","11:00","11:02","11:04","11:06","11:08","11:10","11:12","11:14","11:16","11:18","11:20","11:22","11:24","11:26","11:28","11:30","11:32","11:34","11:36","11:38","11:40","11:42","11:44","11:46","11:48","11:50","11:52","11:54","11:56","11:58","12:00","12:02","12:04","12:06","12:08","12:10","12:12","12:14","12:16","12:18","12:20","12:22","12:24","12:26","12:28","12:30","12:32","12:34","12:36","12:38","12:40","12:42","12:44","12:46","12:48","12:50","12:52","12:54","12:56","12:58","13:00","13:02","13:04","13:06","13:08","13:10","13:12","13:14","13:16","13:18","13:20","13:22","13:24","13:26","13:28","13:30","13:32","13:34","13:36","13:38","13:40","13:42","13:44","13:46","13:48","13:50","13:52","13:54","13:56","13:58","14:00","14:02","14:04","14:06","14:08","14:10","14:12","14:14","14:16","14:18","14:20","14:22","14:24","14:26","14:28","14:30","14:32","14:34","14:36","14:38","14:40","14:42","14:44","14:46","14:48","14:50","14:52","14:54","14:56","14:58","15:00","15:02","15:04","15:06","15:08","15:10","15:12","15:14","15:16","15:18","15:20","15:22","15:24","15:26","15:28","15:30","15:32","15:34","15:36","15:38","15:40","15:42","15:44","15:46","15:48","15:50","15:52","15:54","15:56","15:58","16:00","16:02","16:04","16:06","16:08","16:10","16:12","16:14","16:16","16:18","16:20","16:22","16:24","16:26","16:28","16:30","16:32","16:34","16:36","16:38","16:40","16:42","16:44","16:46","16:48","16:50","16:52","16:54","16:56","16:58","17:00","17:02","17:04","17:06","17:08","17:10","17:12","17:14","17:16","17:18","17:20","17:22","17:24","17:26","17:28","17:30","17:32","17:34","17:36","17:38","17:40","17:42","17:44","17:46","17:48","17:50","17:52","17:54","17:56","17:58","18:00","18:02","18:04","18:06","18:08","18:10","18:12","18:14","18:16","18:18","18:20","18:22","18:24","18:26","18:28","18:30","18:32","18:34","18:36","18:38","18:40","18:42","18:44","18:46","18:48","18:50","18:52","18:54","18:56","18:58","19:00","19:02","19:04","19:06","19:08","19:10","19:12","19:14","19:16","19:18","19:20","19:22","19:24","19:26","19:28","19:30","19:32","19:34","19:36","19:38","19:40","19:42","19:44","19:46","19:48","19:50","19:52","19:54","19:56","19:58","20:00","20:02","20:04","20:06","20:08","20:10","20:12","20:14","20:16","20:18","20:20","20:22","20:24","20:26","20:28"],"seriesLabels":["prometheus","kube-system"],"seriesValues":[["0.0017374533520085633","0.004144749433558235","0.019120339678475803","0.011701511803156518","0.015476048226924272","0.008654825384808634","0.015576394695588662","0.008883315396266626","0.01141306630350034","0.015931663167725457","0.013977530204476746","0.009479829971427054","0.016715742558959107","0.016255107628188353","0.007792183984839642","0.015267736586240754","0.009791650563746234","0.012382985320266328","0.0111077203767667","0.01599029135401939","0.015596370254859965","0.014389913927518193","0.01504042276048521","0.012674597943256078","0.01443618532573964","0.01862555434521293","0.008994090052698724","0.016626428886479482","0.012865735805203907","0.012944483503214005","0.009326590254402248","0.014982461023028068","0.009607121255511046","0.011962487904659161","0.012174157028289737","0.018142413516834297","0.010405868654905252","0.01142061573146944","0.01579121696786214","0.011527688801583823","0.01366413955050301","0.005227039298664253","0.01844372053626407","0.015996920553083497","0.004674724701994179","0.019798897070254753","0.019127636997637844","0.019770858799657887","0.011631792382861316","0.01725671269035944","0.019367806917036455","0.01588360277870967","0.01812877315032707","0.02040085437878778","0.012403244131613023","0.019278680375894554","0.009543512316209821","0.007508096377038961","0.015354192630142038","0.007214216518864501","0.018529076487336912","0.007761116911658574","0.019071835275740967","0.020297335310612175","0.018590090494624224","0.011218746101197614","0.01622607474751456","0.019556052272160937","0.018240263533340337","0.020837656123336365","0.012880883841613417","0.013529044654087689","0.019437021833621542","0.015182574385828358","0.01890518480298872","0.0195945832417982","0.019263901855107925","0.012857608784562047","0.010744217310024777","0.018944756658255534","0.021437334473843994","0.010624244074536542","0.016029668021155626","0.012936443206451697","0.01981273400422205","0.020699798378464102","0.018274228213777922","0.0118428068108997","0.01889112248654017","0.012342628466228073","0.012465970651296048","0.01514537495902164","0.009479926842332063","0.0184187391153098","0.01585204053897142","0.009153672140539314","0.010004588291006776","0.01468401680422962","0.013664976005457345","0.01698194405981991","0.01950434611740789","0.021324544905406476","0.008143801266522603","0.042281588480446435","0.02087398330908953","0.016755984509285345","0.012634034557216192","0.019601711992005368","0.01694882799211752","0.016910464053110145","0.021456712611543426","0.012572562400950489","0.01828648845977936","0.02015090211361002","0.017206066365182227","0.012310307110783212","0.010587157266715656","0.010368566043210205","0.018041606047551565","0.011317953648706015","0.009840196649329587","0.011050037486773393","0.017195699936837096","0.013422434310663478","0.009795009181009565","0.017561410504178254","0.009768653353298627","0.013828368358127632","0.010437923700598806","0.017376706767926964","0.012369603708747038","0.01566117844522706","0.01226605617387441","0.009959502036572847","0.01261422944860465","0.014918880319665631","0.016925798093302396","0.014611668819214498","0.02109596739315191","0.014498909099230127","0.02082589499572771","0.004599730083387597","0.010986097000278364","0.017867959272448494","0.009702945617675176","0.017462636503725305","0.015465062110274465","0.004864480747189799","0.016380757786091275","0.009633355094400801","0.015281010755567577","0.014481216659769047","0.012604048172088855","0.010515039494313067","0.009677121038077718","0.01387753913700554","0.014402830048159024","0.013397735232994458","0.011702481985480703","0.01479355665689158","0.015087265200994853","0.011065109550947954","0.015988567714883377","0.05030883999151251","0.024359256574204485","0.021036947311528442","0.017667286577129","0.012305878968590603","0.007973493106862422","0.01885091270710038","0.011004882482950255","0.010980844129271312","0.01866202832111919","0.010905559338893788","0.019928565539744453","0.01644638546176866","0.020498024816075808","0.021747607974013615","0.018613078590104776","0.018792942788037312","0.013148909937503133","0.019698955773695258","0.021244785274795105","0.019878039280991994","0.01267980172910802","0.011814401658006811","0.01713677812432694","0.012544105921250413","0.010602842398599892","0.011990204486910112","0.011960690384693171","0.011437107357471707","0.016483075813202956","0.011491219353976945","0.012233912204041844","0.006980230349754563","0.015232023269391364","0.01816119540018962","0.012209489195279496","0.019908846427106473","0.012409925613075573","0.019955941392051913","0.019377386745845337","0.020478932493569413","0.01833508998469229","0.021004475252395082","0.005276479757828842","0.015861778957573545","0.018806090474558723","0.020498836728887155","0.015742240371148002","0.020932285394475086","0.011273420487412242","0.010952258438464143","0.01621816390541801","0.011086328548626501","0.017103273521102837","0.017874437604767503","0.021066422283958556","0.004877926424293211","0.015405405444461524","0.011102803337712802","0.011082503549603825","0.0564969520940609","0.010420684725696261","0.016150248056965463","0.017364332590620372","0.014633207142308771","0.013437257224726851","0.012343599256452675","0.01690236846849643","0.01660853545399631","0.014393564637274063","0.010498908990045406","0.015104819183801733","0.02033532033390568","0.019028503200893197","0.008919574072779048","0.012716400043786037","0.01274417785430534","0.018540205710345408","0.018732101052197137","0.01688800952448683","0.016033228895016325","0.013125398411966518","0.01339105545294093","0.013499465642635156","0.01505717015303994","0.016477596974983844","0.013598952591262432","0.016969550980295286","0.00901622064466172","0.011213026806231404","0.02113960135152818","0.0035313760192267705","0.01571115733868907","0.010140126124983814","0.01989621665290189","0.01649341415373167","0.012384003720552447","0.01580288255656262","0.018733815480993232","0.014252930579797709","0.011071199996115832","0.010935483413690007","0.014243863460249428","0.011546060677444644","0.015773402174872095","0.011954326952859386","0.01797089229232936","0.01985446973875728","0.012377300258417715","0.019698209753554116","0.019523440055228155","0.01268542229045557","0.008937559942622847","0.0179874297075483","0.01950216475847662","0.0127310402380026","0.02032022590632248","0.020915885901305338","0.02191768142268565","0.018881063981262734","0.12468866425367428","0.00956735867565479","0.012140169183979964","0.01613644877390218","0.01531175616534059","0.009152570264194538","0.01623966123791065","0.015872333197371993","0.016203620871199833","0.008300788239266313","0.00711175796595907","0.01574496444180706","0.016952436974272084","0.016260287327006726","0.01494313306773083","0.009913903887283727","0.013036461939912088","0.018102210900225773","0.014429562300185444","0.01682034507589082","0.008333385706231674","0.01675779177907497","0.005591099526173969","0.016144491929250812","0.016100389690915456","0.008893574189984441","0.014664081303926216","0.015690084672243525","0.009567803048896805","0.013323099820750625","0.011381118181812314","0.015244262392240053","0.01281664090142885","0.008629534604588571","0.016185008078890326","0.01863371886110452","0.008563534396410246","0.011335873370024771","0.02032223396550197","0.01596870248181639","0.012433566833962722","0.015500648283275792","0.01302948719930024","0.015402325773997324","0.0160970087788657","0.01592769684585434","0.009653911286149425","0.016915756173298567","0.019402854409522136","0.019811708424805136","0.004486779457212793","0.014314538899403588","0.017668532124450034","0.021975365164358062","0.02167656760130234","0.004463837162946282","0.01746871476007853","0.017340869148727848","0.003485329796135227","0.01441555255051052","0.04694217197082402","0.017845078457834743","0.020322762421692734","0.018302271543984047","0.012620727631621668","0.009284004309875086","0.02021299732401195","0.00470406629093166","0.01638230697321955","0.010181204239921913","0.015121128774793935","0.011123362325722363","0.009889495620880862","0.016601363709330557","0.014623367633147684","0.013085441283391402","0.017807433720510003","0.012262197640005317","0.008833520371040645","0.017720516274626934","0.019235398931788394","0.022088747937645464","0.018950753151895452","0.012134173194920264","0.019710447830711273","0.019555093759275916","0.02050162215516137","0.013377714756032465","0.017076797893249535","0.011050331650456918","0.01012249248265865","0.01869712129450224","0.01318987326938944","0.01969147072197374","0.0216736575177212","0.011860017391086178","0.014013102211544696","0.017831895668692747","0.01885970643460935","0.011444105126271371","0.018047073752440797","0.012505061761612918","0.01166735284236646","0.011526658361766945","0.009198703614406452","0.016884673672266898","0.01745471822474767","0.018892445816900805","0.01912650929400127","0.004833764312399974","0.014952914472709807","0.020065623486295897","0.021296015646517938","0.01861965607752634","0.022088336494496805","0.01916109098538531","0.020465689334218422","0.017006359655153645","0.013554733368542895","0.012158657872456582","0.052875753897668853","0.009687654733050038","0.014444249540692268","0.012971702340657781","0.012908780731484298","0.008903919583998216","0.01612387481166936","0.008250130820189945","0.013194103671726576","0.01842123131126755","0.008831881963702175","0.01294482427936257","0.019685910180247273","0.01197897784878678","0.017974963590852332","0.010550262774773627","0.015499072814104444","0.013846938661394054","0.009010220148078816","0.015339269461802835","0.014298798133283079","0.009913152561699761","0.015502008491317516","0.011269652288806881","0.011089331308875025","0.01859834973825905","0.013574316480836337","0.008472486224789706","0.015025934846477726","0.012773166592869097","0.009907495815693538","0.012334206029518482","0.014457304640052966","0.0167349128998129","0.014866245991026171","0.013019164346662032","0.012237075962356049","0.010315184392505743","0.014975097968787907","0.011952835743674266","0.011051746125712664","0.01267262970932045","0.015648483283910684","0.004287980942316568","0.018955866799617596","0.01130844797731125","0.016389444389918982","0.01233976989501712","0.008955320194704438","0.009971822550783082","0.01993976513332043","0.017417809138016462","0.013604916860234255","0.0095577478430604","0.017884528208911076","0.01717160389118774","0.017662028895623925","0.012945696867522912","0.014565183949174339","0.008912239986719543","0.04716968857649485","0.012677174394167259","0.011598007325956448","0.017320846724563802","0.020478989760490052","0.0051550541627144635","0.01700175806157749","0.010306939793358328","0.01779221040623007","0.021145718898914846","0.004872207440357597","0.015582133721168928","0.016575134338077585","0.018681419174587436","0.01964206614041254","0.020564824634488783","0.004307188872745378","0.013578807176562978","0.017316246357675247","0.013970837520253635","0.010838102415721716","0.018279927708529885","0.017323456538374093","0.003783843056657895","0.018622023903462803","0.019969796058796747","0.019699252058159974","0.019685254280659994","0.004425903728735203","0.015675185938928134","0.012938648054120198","0.012761037228902879","0.012132659681303521","0.011489159547417638","0.019465946254338496","0.012996507472883044","0.01736319490696852","0.01927729136096532","0.019450933656894445","0.013441722746444945","0.020170219551901535","0.012163579946703775","0.01246040415798448","0.01016970387305031","0.018553364955003135","0.010203205625044655","0.013925566559263925","0.017336076735687697","0.018828330032743847","0.005394156909954316","0.013848643797977083","0.018930507723533847","0.004729076723649639","0.01584165782772432","0.013065920429236714","0.00909544334230851","0.016720132805197623","0.012844188891230575","0.017718619831322786","0.009596389713727563","0.043553945173853297","0.022349234404842374","0.012002461269971072","0.0184733574220183","0.011677773262214693","0.01897371811492651","0.0210370374586108","0.01353362462553076","0.011176417528101293","0.015599950004232845","0.007613774840357468","0.01685058103428193","0.01805483419657765","0.013602941896981196","0.01695897876560335","0.01246878553665925","0.019765737371755215","0.005336755768082411","0.019026094045070267","0.02041635987942077","0.012623519289027359","0.012522068608516113","0.018309243024122367","0.013641485471711874","0.00907920962486854","0.017798163252907794","0.02066143075850676","0.005651781484396817","0.015050464387140892","0.010207962676699205","0.019192498960787632","0.01835599300736193","0.011800640319937558","0.018280090106069766","0.013144418160465694","0.00784327265016298","0.017935631353002936","0.021302422682166127","0.011374941496013086","0.013927384983742084","0.012310660893927827","0.010777129842837869","0.01835494847719091","0.013494152807865995","0.010416611656675982","0.01578470997634448","0.01873143642124103","0.013007180946024327","0.01737714099312808","0.018682244222521335","0.00428123714531817","0.01571298546717408","0.01795428346184137","0.010285445148588148","0.015405067450131774","0.017894858576745325","0.009636476544528191","0.01892325871269592","0.013361467277816074","0.013948229379803162","0.03630481149188765","0.014246916920239367","0.019817473935459774","0.01901383826190092","0.016244749552252732","0.008601569394227604","0.015610957414651896","0.01340713010635972","0.013155056655281938","0.015794178825483382","0.019284460781905443","0.016997376434821687","0.01836996240448394","0.013163371455348204","0.009602799275301385","0.020342382541929386","0.019153306486429527","0.015955282269490445","0.013932345162641583","0.018810614161840055","0.01356075191262995","0.016886909692916976","0.015845721141433268","0.015524300891997975","0.015987638290701982","0.016321149806144845","0.021585051902295063","0.012803023808225171","0.01922671170759187","0.013542696609502454","0.02065299497930445","0.01956388163810259","0.0035415912138287856","0.016151842400748143","0.007645184064069978","0.01785692511387261","0.020007236814523134","0.011843691749803773","0.011329548662006202","0.015185804346542573","0.017351929410544788","0.018280509313381273","0.012444060754352193","0.018015646508846846","0.01777288101853276","0.01873214299364928","0.022995184921951323","0.019068899428172678","0.004799210367621339","0.01891508064805862","0.012269116533056314","0.018325590469776377","0.021446047959649966","0.019790372217171517","0.015170299435446807","0.01835270254949003","0.01575783451207946","0.015982548371995368","0.010997669915138023","0.021382444301224577","0.07387782374905985","0.00980439392014326","0.017086811399960208","0.015921931429789413","0.009347670200230336","0.02032938495168855","0.012913394305431779","0.016361808743632546","0.019542409022828642","0.014318895041439449","0.018807228137433407","0.012592378601436579","0.013519512695940702","0.020723350979768383","0.004852946499885723","0.01801467815179467","0.005667550802604468","0.013859910684046425","0.018701774665427422","0.015216225008116998","0.010882346304866189","0.019443406915082652","0.017834990019643483","0.011970631309340039","0.020576785013439406","0.011188600173087615","0.007791756987186523","0.017257185050181163","0.005220011893893464","0.016581189862110376","0.005099569167487066","0.01961376294997873","0.017323465682046037","0.005648412832766739","0.018866933733303375","0.0198094350643548","0.014286541901832291","0.011949114620158649","0.01975963918875428","0.013897657792378117","0.018047640957336897","0.01910882695095206","0.019267451408111123","0.004641870981541405","0.018293001222246075","0.013036763938789135","0.010747819942322665","0.011718393003319035","0.01970198284556531","0.022453706583502552","0.02044893687108314","0.010594628583379066","0.01241213360917889","0.017930386688000535","0.01965785637511562","0.017797864806352606","0.010564005385606163","0.012415410305990495","0.01617579396730911","0.021652046486062202","0.03782949742346453","0.016421863933977213","0.009732044545383","0.016248780002221155","0.009961500711569376","0.011436244577262396","0.015358888559982688","0.016617957416236645","0.011046617461004774","0.011685900131828462","0.018702908377647556","0.009135306792699149","0.016207250361980768","0.013737965935741477","0.012657134480782964","0.009189041543040266","0.016139702265318866","0.009158258848778062","0.016612055561106516","0.014414042966098538","0.013350950555242243","0.01377674116434406","0.014937464970699584","0.014613530292026724","0.01377088016134445","0.011804410154164885","0.011449850087189586","0.01127193464435887","0.01725732494945394","0.004766600877358858","0.01839020853750229","0.012918913387665215","0.01284828186976797","0.019093084308093675","0.0170675887753556","0.00481901623208205","0.020231151151813016","0.01867747655865387","0.00885044397524013","0.015843899834963242","0.01716313595343393","0.004971340912285976","0.020121414991528006","0.01690493804407717","0.011537471735263103"],["0.003533989457508691","0.05028539373770204","0.04937638343623915","0.04873897221043962","0.04010342127632776","0.04724297017532976","0.05024612343191488","0.0380612148125387","0.047954776409226435","0.0466461423211271","0.04640714783027991","0.04280253019328759","0.046409504373042074","0.051243732983521424","0.04804088290850129","0.04403046936325802","0.043624499936932376","0.05041208270482822","0.04709601520133966","0.04251751391008766","0.045043697951527","0.04752634086704805","0.04370280948346287","0.044497673266788475","0.04898648540212329","0.03860586294754421","0.04457471466307209","0.05213697646230785","0.051510125440742564","0.04223548783256108","0.05690116668928581","0.051104718136199725","0.04813474061523245","0.04009141900548522","0.05004747292780102","0.04761475215464231","0.0438031378433663","0.0486731283206987","0.04276432084139922","0.04859939196596861","0.043244732973550616","0.04717213300478481","0.0455804976931952","0.05009313290029982","0.048550062390600335","0.04621826811567587","0.04618258643429124","0.046407529573919665","0.04924356722302471","0.04806796610249752","0.05180213775812553","0.040681054487965294","0.049002348872533914","0.045906393040174236","0.045903314170570565","0.05044201684589265","0.044925727654452255","0.04629675822187541","0.04725463365348061","0.05215036362935355","0.04407565464343604","0.049318205488943216","0.0466660195437724","0.04836311436554138","0.04736504513157754","0.04693228871205711","0.045732586282488406","0.050035871508679663","0.05048978479492894","0.052258720895079554","0.04900798642419548","0.04254621011785414","0.048707226010758366","0.049941406992857144","0.044950795477094636","0.04230239246828255","0.04847027243343948","0.04773167324547548","0.04540409133763462","0.05184958374155118","0.049895797017160105","0.04662809159407141","0.04626360470406703","0.04455890481711307","0.0461117532280656","0.044405462900061476","0.049076971090337485","0.04077500257876688","0.04620580940885138","0.047511426227172526","0.04922648490008692","0.04762334987004147","0.04982833127039534","0.0527123993975396","0.04697125106210698","0.04417892038622212","0.0511208684922701","0.050459684592513154","0.04431674398389018","0.0496159683263046","0.049943877990396","0.05055632772354478","0.047629519221256626","0.046052573701728876","0.047394069511793493","0.04957333573151615","0.048622518817738214","0.05013543703026864","0.051316360465155614","0.04533354576568852","0.049922142405100234","0.03859551026723771","0.0492999033537373","0.04565215104891714","0.04607122539504299","0.046324562870648164","0.04880402123347753","0.04816217174635902","0.04935086017806395","0.05238969902040948","0.05043883050391266","0.04818192439102534","0.04374765455877044","0.048436280612124814","0.040512345962250264","0.04905694689373994","0.05074153760976415","0.04880277160331598","0.04467496599164173","0.03941478273332739","0.04952149762580043","0.04766203716337704","0.040613322679428486","0.05093865144001723","0.04614365767874218","0.04072994240370461","0.049594028171385904","0.043593903483752916","0.05217506831637391","0.04649428181492502","0.04255634468054158","0.043749980748705966","0.04885132519778036","0.04162365051314588","0.04949560391230032","0.04723254848181043","0.04698786200412405","0.04784433505697366","0.04717825224322757","0.047948911443174785","0.04725166302740936","0.045980552024378556","0.04335118364968403","0.04725049575310369","0.043179047492585707","0.04892024593643574","0.04696930464648119","0.049049828563039875","0.048856800457715156","0.04443669486002501","0.0541321341485998","0.04228036124436892","0.047253685003300745","0.05073173375985102","0.04745622427919392","0.05085355771507133","0.04480955732259937","0.044954373202048176","0.04817686309428733","0.05096729594152034","0.05024064337704698","0.04771525791074608","0.05174977989424148","0.04601771825357154","0.054196237202738876","0.041918223063112874","0.0480847561536162","0.04783454724147474","0.04964273987040982","0.04599162520845879","0.054346257657101285","0.04919102346507157","0.051248233135528495","0.04528856099451231","0.04921357794989257","0.045965482355436044","0.03981439911030223","0.047866076713220095","0.04586883818357162","0.04575814703765635","0.052566314769806055","0.048123744394534124","0.04631422221256516","0.047278879173071914","0.0451152369465408","0.05027530232528351","0.04149730285583844","0.053591181960308196","0.04440048771748084","0.05352706707611195","0.04849543679384971","0.05037841802119718","0.038754970753612616","0.0522490619764492","0.04596059332809901","0.051501279552771215","0.046298909930599694","0.04642333072438831","0.046112967076336706","0.04498344477245164","0.051788579075453554","0.047748303976037965","0.04418986285056315","0.04148886806546682","0.04682086764416193","0.047788965283516865","0.0505368021147701","0.04038227844566457","0.04848298836598215","0.04723598055451735","0.050235398709406846","0.049746275919732075","0.050763673030274604","0.047302892890677614","0.044448790343199034","0.048917100610060923","0.047800415449146816","0.046584437927746795","0.050509025686552814","0.046239115964443474","0.04660902224271292","0.052732838239305094","0.045208609623191835","0.054152436432131024","0.05019549072929076","0.04603818194074326","0.047971233733497064","0.04906194488075217","0.04782677733191022","0.05262298131225552","0.04874975174066175","0.04494352433404699","0.04735782396868938","0.05345620196855532","0.04600913578799246","0.04733179337232012","0.051324049173041494","0.04600800902112596","0.04817512453082875","0.050965950619374686","0.03509260072839023","0.0504410071000529","0.05071516330184855","0.0383226658176584","0.046535443299941734","0.04661359540161917","0.047586735433067934","0.048858180587963075","0.05042332366226695","0.0498748487106732","0.04165858167568708","0.0506801164369404","0.048953174043308734","0.04976776900425335","0.05018457798103124","0.050917173652809705","0.044592607781657266","0.04988381263156886","0.04361207711979772","0.053348933371810604","0.04697071787295741","0.04387675692556532","0.036977674999611305","0.04579701438510721","0.04525122149779339","0.05324624154563808","0.04586768231764508","0.045523835957364735","0.04966650763485678","0.04266575149703035","0.041421387811672226","0.04575627832659769","0.04184361445685141","0.045646320873641434","0.046761423309461654","0.04491635903647592","0.051400819830615875","0.04354031927417659","0.04435074953263129","0.04839494238181419","0.04695636005023371","0.04602797847588559","0.0451329683649755","0.04512894889951158","0.05075914508745263","0.050019411765675544","0.04316742369562308","0.050270307624857596","0.04464069570621771","0.047864746274532265","0.05342590588426379","0.051917669190625765","0.04503224386643563","0.051221316529517565","0.0468555245010409","0.05584541258558676","0.04630797380176779","0.0479724951193861","0.044184353063625004","0.047749470870277","0.04389853046293846","0.054206152724418685","0.044813894001971305","0.04882474247512843","0.04559362592367814","0.04245719491267865","0.047428344895123965","0.048015484413433035","0.05386901355864143","0.041133355893936935","0.04561319925297312","0.047609668846902535","0.049907344580487664","0.0454310948180179","0.05347035136164882","0.04914217638737072","0.04427208831556614","0.04566165234406183","0.04760283444883962","0.05413423482105637","0.0466832050032646","0.05027491762721986","0.04767212803395421","0.047143370485677315","0.042105783825558525","0.0432684819333606","0.05070892385625357","0.049725365887944586","0.048188808100992866","0.04575589720572691","0.04915790165542216","0.04836631071625845","0.052827127674826314","0.05085291580748979","0.04682200233033878","0.04970940694254417","0.04582230302226853","0.04763740170317271","0.051250062232750054","0.04578876895436295","0.05033748622632175","0.04381287265714982","0.048807430814314025","0.04591432065932541","0.04891573057122096","0.04764345087719116","0.04541386870506952","0.047074557458614115","0.04889732292962924","0.04663183136500297","0.05180464535402601","0.05173638628183123","0.050170755588701134","0.04473899947267896","0.04687437047096928","0.05030002946331351","0.05103396275885561","0.04916787775514642","0.04365433897862447","0.04869523096698755","0.04790460259807533","0.041728607569532075","0.04805053377097583","0.047504437835489036","0.045434172538311245","0.04918432612496789","0.04620339335544162","0.046610473067356814","0.05005803573256337","0.04688327217456495","0.04108621152424804","0.04612975578367986","0.04588292882003325","0.05002239885523998","0.047641899615309735","0.047387451838786065","0.047320995285045304","0.05068358775940973","0.05201151059572706","0.041713769341067825","0.0504967302350004","0.0490324857465771","0.04881562838265401","0.04793313990081491","0.04460057860051698","0.04906908630402031","0.04757477368892197","0.051127682884994065","0.04451568117818822","0.05348247833614489","0.04472699336333901","0.04932571684230758","0.05086463116919129","0.04679389688544405","0.04854957144465044","0.04953934909279196","0.04939808293540579","0.040742778899158116","0.053576236201652826","0.04185482165785724","0.04663082030634912","0.04313054206438774","0.04696024720837748","0.047014274348109966","0.03811104007918893","0.05000683139448968","0.04799403627110325","0.04342668043213616","0.053629537353931736","0.04286938673051309","0.05244765563515946","0.04482630860281109","0.050530346961451984","0.047000508716502694","0.045543564283747555","0.047416470737764296","0.044704398070463114","0.04570425689001258","0.04618300104825756","0.04827843884804958","0.04702056432622863","0.044888272212642154","0.047438015471349466","0.04007018138958246","0.04586112627382214","0.050266148344925446","0.04283942380353264","0.0508052759893413","0.04823804113759988","0.04070070176042165","0.04770346060487222","0.04739201130369904","0.044769225681387644","0.04799099351780093","0.043686659017698165","0.052642858016359845","0.046238377998383415","0.05232780517480664","0.04531236951031213","0.04566863681742511","0.05399207574005188","0.04454704880957796","0.05221484046180965","0.04681735702879946","0.04600815644308433","0.044781599522227857","0.048004495041650184","0.04224603485898149","0.04848632924216516","0.05014975165909466","0.05064578689309538","0.04031985743258422","0.04884836201447721","0.050647763103810264","0.053379544821720346","0.043776376529596206","0.04836442013609544","0.043663817098678864","0.045442588243273276","0.04811982918829556","0.04648132124947636","0.04858544685805692","0.04596356493730548","0.047243069850733614","0.04858266440013235","0.046896269680190804","0.050849976540263804","0.042385344680715996","0.05199775935995063","0.046951654092195115","0.05168903570871805","0.04874359797228143","0.038488386745360925","0.04774277967825721","0.04888278901658949","0.052321573171625405","0.05307638352462017","0.040198738850126106","0.044831012254594585","0.045759987201757814","0.04999740937207551","0.049542064945087026","0.05227590019891278","0.047303839623544786","0.04626168965219331","0.050651955709606754","0.051310657199696914","0.04498495463184708","0.04974381472991075","0.04958907833076175","0.05274107691895389","0.051995381718357325","0.04234705886340792","0.04584935576817873","0.04797553624017741","0.05130745850959984","0.04925865041680764","0.04032064957286523","0.04911565703471483","0.0451613977187152","0.053786466040304244","0.0492295314743849","0.04709311327355222","0.04814002307723653","0.04889930192207311","0.04781448596508733","0.04453894719679503","0.04151951893930649","0.049030210975940756","0.049270638618466735","0.047365641341960404","0.047349224053247534","0.04463965027524558","0.05254154144759462","0.03964822993135625","0.05327240323901903","0.04890766694614932","0.049578360747896254","0.04759161363364222","0.04559007338172613","0.04768538691432921","0.0483791458598958","0.0503705102190818","0.04959801151028848","0.04505440172443028","0.05308574701835265","0.045667505237051584","0.04502847772345931","0.04584317865192433","0.04680676742772457","0.05269881396559616","0.05122595778514647","0.04630284635238567","0.049238564871773784","0.044309035943587545","0.04524709872284411","0.04039075530764207","0.04645874771823308","0.04990428988535261","0.04661951099412288","0.048022240469342066","0.047586800520952224","0.050810246207245224","0.04823347105897569","0.04716664429826452","0.05331270286359761","0.05004088771834243","0.039407914902245905","0.052984702063815406","0.04713889567308018","0.05065789688834515","0.045788841588504985","0.047400730913776494","0.050750147527861576","0.04359772877081344","0.043725896118436185","0.04771417847108317","0.04733327150617064","0.051694960347408246","0.04294167631879139","0.04857964892309423","0.04955995387575815","0.04947195629286579","0.04136917672668434","0.05200386235795367","0.047384439503566474","0.04911970264690678","0.042955669897469025","0.05419968891123354","0.04654623632425386","0.04886875598889278","0.04875110618205092","0.04306470789284217","0.04741824447417406","0.04202541630922171","0.04642185226531286","0.045804460695494355","0.05029588207771095","0.04929540983048927","0.04491558192064795","0.04225175951471279","0.041397868107134965","0.048267626945120115","0.05542798112477515","0.04163480189797873","0.050208464882724665","0.0446069430535744","0.05036747206643711","0.045771665898096815","0.04802894807792448","0.05051420542944843","0.05288468482439539","0.04351838736684304","0.05011365019156252","0.04496718733681547","0.04705381072786448","0.053022414256442725","0.04758045998440112","0.04598774159442801","0.04705357658291981","0.048779867299627126","0.04301864760343718","0.04891934627773036","0.04077433054598352","0.0473060610768422","0.045491936413990484","0.04415615706878261","0.04794658436956068","0.04891142508163003","0.0486943859949627","0.05297629661855289","0.044655106371018646","0.05093567663243946","0.040845168905303174","0.04375066934181482","0.04845879772250517","0.04703696194143271","0.046232497532653405","0.04457971679523852","0.05211395097213672","0.052897498356365674","0.04975182537208347","0.04520769132319516","0.04448676104989268","0.04460606455268224","0.04560561109837459","0.04913909670480481","0.04360688535078112","0.04836590942803556","0.05111932379724206","0.044104189629186046","0.04696703463212053","0.05098304954620736","0.04719790620383038","0.05122798422952291","0.04228061743726815","0.05060444145427497","0.0482455412358424","0.04479473961875506","0.05010497170250437","0.04747142159306018","0.04802419354447612","0.04514719854271412","0.04448407102832789","0.05244932511512701","0.04992676745351565","0.04727658868649167","0.05028193296757006","0.04844346328148016","0.047168263802506265","0.05427977802027494","0.04243141805765962","0.04708932450554812","0.046667960395688025","0.05039187177284854","0.04993836778322347","0.04828839457712491","0.04459494328014061","0.04348842490335427","0.049453686026797575","0.04921292742365237","0.046703843910723686","0.04556727560983043","0.05114454439012664","0.048847162881237756","0.046522908693277985","0.0474473124990698","0.048122574019010336","0.049951404975845506","0.039752327106719956","0.042238031254183725","0.048728636598559326","0.04152626964918878","0.05379793965815374","0.0513351823505035","0.04366139437447353","0.052972389549024626","0.044985550320232014","0.04620573422692874","0.04371949657426764","0.05077714711397436","0.04449099468782666","0.040381385859590536","0.04754209707750896","0.04140858829329867","0.04515402250839304","0.046055384055716936","0.04305922819201528","0.048359886767273395","0.04705936883329201","0.03960829707986783","0.04690420371493724","0.04725949414379789","0.04900692605257987","0.044661447247411896","0.04941405786923551","0.04881911245848272","0.04223685399669105","0.04516299978652419","0.05027783999375195","0.051431364129296865","0.04529196715928124","0.047978442895854234","0.044468657138553545","0.05139934516196061","0.04771459391158685","0.048689347525400974","0.05086364346826629","0.0465964136347839","0.04319961842803359","0.05356811937980809","0.048405898148707764","0.048483404201948355","0.0449241706251315","0.04913940568825707","0.050351306322861396","0.04676622828328552","0.04829285041647848","0.0516531870431172","0.0477147027420442","0.05375592886556302","0.04892374804665959","0.046004343776844946","0.04927044460696028","0.04466647337640839","0.046375263965920455","0.04093947897957111","0.048068339494231754","0.049185323186551916","0.05164701252656837","0.049588222502809455","0.04761676315221673","0.048821026011825","0.04679926334306225","0.04607446503133184","0.04229212730792625","0.05445759361200931","0.032420979254558925"]]} -------------------------------------------------------------------------------- /server/graphQL/demoData/FreeMemory.json: -------------------------------------------------------------------------------- 1 | {"timestamps":["19:38","19:43","19:48","19:53","19:58","20:03","20:08","20:13","20:18","20:23","20:28","20:33","20:38","20:43","20:48","20:53","20:58","21:03","21:08","21:13","21:18","21:23","21:28","21:33","21:38","21:43","21:48","21:53","21:58","22:03","22:08","22:13","22:18","22:23","22:28","22:33","22:38","22:43","22:48","22:53","22:58","23:03","23:08","23:13","23:18","23:23","23:28","23:33","23:38","23:43","23:48","23:53","23:58","00:03","00:08","00:13","00:18","00:23","00:28","00:33","00:38","00:43","00:48","00:53","00:58","01:03","01:08","01:13","01:18","01:23","01:28","01:33","01:38","01:43","01:48","01:53","01:58","02:03","02:08","02:13","02:18","02:23","02:28","02:33","02:38","02:43","02:48","02:53","02:58","03:03","03:08","03:13","03:18","03:23","03:28","03:33","03:38","03:43","03:48","03:53","03:58","04:03","04:08","04:13","04:18","04:23","04:28","04:33","04:38","04:43","04:48","04:53","04:58","05:03","05:08","05:13","05:18","05:23","05:28","05:33","05:38","05:43","05:48","05:53","05:58","06:03","06:08","06:13","06:18","06:23","06:28","06:33","06:38","06:43","06:48","06:53","06:58","07:03","07:08","07:13","07:18","07:23","07:28","07:33","07:38","07:43","07:48","07:53","07:58","08:03","08:08","08:13","08:18","08:23","08:28","08:33","08:38","08:43","08:48","08:53","08:58","09:03","09:08","09:13","09:18","09:23","09:28","09:33","09:38","09:43","09:48","09:53","09:58","10:03","10:08","10:13","10:18","10:23","10:28","10:33","10:38","10:43","10:48","10:53","10:58","11:03","11:08","11:13","11:18","11:23","11:28","11:33","11:38","11:43","11:48","11:53","11:58","12:03","12:08","12:13","12:18","12:23","12:28","12:33","12:38","12:43","12:48","12:53","12:58","13:03","13:08","13:13","13:18","13:23","13:28","13:33","13:38","13:43","13:48","13:53","13:58","14:03","14:08","14:13","14:18","14:23","14:28","14:33","14:38","14:43","14:48","14:53","14:58","15:03","15:08","15:13","15:18","15:23","15:28","15:33","15:38","15:43","15:48","15:53","15:58","16:03","16:08","16:13","16:18","16:23","16:28","16:33","16:38","16:43","16:48","16:53","16:58","17:03","17:08","17:13","17:18","17:23","17:28","17:33","17:38","17:43","17:48","17:53","17:58","18:03","18:08","18:13","18:18","18:23","18:28","18:33","18:38","18:43","18:48","18:53","18:58","19:03","19:08","19:13","19:18","19:23","19:28","19:33","19:38","19:43","19:48","19:53","19:58","20:03","20:08","20:13","20:18","20:23","20:28"],"seriesLabels":["Cluster"],"seriesValues":[["15340407.466666667","13423752.533333331","6152533.333333333","9630173.866666667","7833531.733333332","3369164.8","10159786.666666666","9208217.6","10234538.666666666","8468889.599999998","5065318.4","10150024.533333331","6736964.266666666","5838438.399999999","6601454.933333333","9154901.333333332","3015065.6","10824499.2","14072490.666666668","9942425.6","5071872","9901192.533333333","7402222.933333332","12237824","11477538.133333333","5603464.533333333","9265356.799999999","7325559.466666666","11712989.866666665","8782370.133333333","12197546.666666666","5709824","9509478.4","9547161.6","9972394.666666666","3899460.266666667","5123891.2","10620928","9216273.066666666","7617126.399999999","9020074.666666666","11080089.6","12525021.866666667","5068117.333333334","12400025.6","6618112","4249122.133333333","10173098.666666666","7420313.6","10131114.666666668","9765478.399999999","10018338.133333333","5632204.799999999","9781657.6","10114867.2","1825587.2000000002","7189708.8","9365503.999999998","6301627.733333332","7771682.133333332","9131766.146243367","10436471.466666667","11285504","11152657.066666666","13752729.600000001","6936302.933333334","5827037.866666667","13247419.733333334","12533009.066666666","12318651.733333334","11308782.933333334","12881373.866666667","12982886.4","10713019.733333332","11043976.533333333","11269324.8","9753531.733333332","14129834.666666666","6569642.666666665","13390233.600000001","18511667.2","4147609.6000000006","12942745.6","14695697.066666666","9674069.333333332","16979558.4","5061495.466666667","8892757.333333334","16105813.333333332","9299012.266666666","12157132.799999997","13576533.333333332","10107221.333333332","8394342.4","9766365.866666667","9581363.2","13259161.6","7461546.666666666","13226871.466666665","13581585.066666666","11450982.4","15331123.2","17034240","7642316.800000001","17139575.46666667","13166933.333333332","18398276.266666666","9533440.02731213","16637474.133333333","13400064","20364288","16575624.533333333","16663893.333333334","17018743.466666665","16985838.933333334","9718374.399999999","10067080.533333333","13252130.133333333","17087897.6","17139097.6","10130090.666666666","20669781.333333332","13710131.2","16500599.466666667","9386803.2","9504699.733333334","9197021.866666665","13101260.8","19977284.266666666","16752776.533333333","12895709.866666665","9538286.933333334","13086583.466666667","16555212.8","16106427.733333334","8795613.866666665","12237960.533333331","13123584","20597896.533333335","16659524.266666668","17428138.666666668","16556168.533333333","6212471.466666667","13221683.2","17153911.466666665","16272861.866666663","9704448","20193348.266666666","19997286.400000002","15797111.466666667","9203302.399999999","9160157.866666665","12712004.266666668","5658350.933333334","16094822.399999999","2079061.3333333333","15982318.933333334","9163298.133333333","19113779.2","16276206.933333334","5396275.2","13919641.6","12414020.266666666","12754534.4","19724014.933333334","15759701.333333332","8766190.933333334","12270250.666666666","22481715.200000003","5588377.600000001","11951718.399999999","12458734.933333332","8132130.133333333","18915328","8920312.17953197","11534267.733333332","8130218.666666667","8342186.666666667","15046860.799999997","18189585.066666666","11586082.133333333","11520409.6","11393774.933333334","8350651.733333332","8303957.333333332","8056422.399999999","16401749.333333332","9754487.466666667","22674090.666666664","9315669.333333334","9688337.066666665","12688452.266666666","18906248.533333335","15656960","12915507.2","12586325.333333332","12561476.266666666","8968942.933333334","18661853.866666667","12032750.933333332","15534216.533333331","11534540.8","8674986.666666666","9309183.999999998","8663722.666666666","11651822.933333334","14501341.866666665","14768605.866666667","19072136.53333333","13164817.066666666","21284454.4","11184810.666666666","8112810.666666666","11217032.533333333","11462724.266666666","10969497.600000001","8259310.933333334","8512785.066666666","11926801.066666666","17325806.933333334","8050756.266666667","14491852.8","8168584.533333332","17508420.266666666","14334361.6","10352776.533333331","7317777.066666666","13996919.466666667","19872836.266666666","10983970.133333333","7139191.466666667","7385224.533333333","10720256","11704115.200000001","9027584","8641740.799999999","17926417.066666666","17382946.133333333","11351244.8","11407837.866666665","8330171.733333334","8326963.199999999","14567765.333333332","8531217.066666666","17682227.200000003","8424038.399999999","11161463.466666667","20003362.133333333","15333717.333333334","10801766.399999999","17244091.73333333","13480482.133333331","19665100.8","13779968","11105143.466666667","1874807.4666666668","16905830.4","8009454.933333334","5031526.4","8447317.333333332","10541329.066666666","10420155.733333334","11088622.933333332","14232439.466666665","5338316.8","10551432.533333333","13524172.8","19411353.6","14848000","16114619.733333332","16274295.466666667","10673152","4660019.2","13267558.399999999","7411848.533333334","13160311.466666665","4770474.666666666","10683665.066666666","12444262.4","9611741.866666667","9454592","8553949.866666665","5942067.199999999","13455906.133333331","8607129.6","7812710.4","5757132.799999999","13575304.533333335","14360507.733333332","11376435.2","10336324.266666668","14077542.4","5170995.2","13737233.066666665","10409301.333333332","10949358.933333334","12905472","8035123.199999999","4474333.866666666"]]} -------------------------------------------------------------------------------- /server/graphQL/demoData/NetworkTransmitted.json: -------------------------------------------------------------------------------- 1 | {"timestamps":["19:38","19:43","19:48","19:53","19:58","20:03","20:08","20:13","20:18","20:23","20:28","20:33","20:38","20:43","20:48","20:53","20:58","21:03","21:08","21:13","21:18","21:23","21:28","21:33","21:38","21:43","21:48","21:53","21:58","22:03","22:08","22:13","22:18","22:23","22:28","22:33","22:38","22:43","22:48","22:53","22:58","23:03","23:08","23:13","23:18","23:23","23:28","23:33","23:38","23:43","23:48","23:53","23:58","00:03","00:08","00:13","00:18","00:23","00:28","00:33","00:38","00:43","00:48","00:53","00:58","01:03","01:08","01:13","01:18","01:23","01:28","01:33","01:38","01:43","01:48","01:53","01:58","02:03","02:08","02:13","02:18","02:23","02:28","02:33","02:38","02:43","02:48","02:53","02:58","03:03","03:08","03:13","03:18","03:23","03:28","03:33","03:38","03:43","03:48","03:53","03:58","04:03","04:08","04:13","04:18","04:23","04:28","04:33","04:38","04:43","04:48","04:53","04:58","05:03","05:08","05:13","05:18","05:23","05:28","05:33","05:38","05:43","05:48","05:53","05:58","06:03","06:08","06:13","06:18","06:23","06:28","06:33","06:38","06:43","06:48","06:53","06:58","07:03","07:08","07:13","07:18","07:23","07:28","07:33","07:38","07:43","07:48","07:53","07:58","08:03","08:08","08:13","08:18","08:23","08:28","08:33","08:38","08:43","08:48","08:53","08:58","09:03","09:08","09:13","09:18","09:23","09:28","09:33","09:38","09:43","09:48","09:53","09:58","10:03","10:08","10:13","10:18","10:23","10:28","10:33","10:38","10:43","10:48","10:53","10:58","11:03","11:08","11:13","11:18","11:23","11:28","11:33","11:38","11:43","11:48","11:53","11:58","12:03","12:08","12:13","12:18","12:23","12:28","12:33","12:38","12:43","12:48","12:53","12:58","13:03","13:08","13:13","13:18","13:23","13:28","13:33","13:38","13:43","13:48","13:53","13:58","14:03","14:08","14:13","14:18","14:23","14:28","14:33","14:38","14:43","14:48","14:53","14:58","15:03","15:08","15:13","15:18","15:23","15:28","15:33","15:38","15:43","15:48","15:53","15:58","16:03","16:08","16:13","16:18","16:23","16:28","16:33","16:38","16:43","16:48","16:53","16:58","17:03","17:08","17:13","17:18","17:23","17:28","17:33","17:38","17:43","17:48","17:53","17:58","18:03","18:08","18:13","18:18","18:23","18:28","18:33","18:38","18:43","18:48","18:53","18:58","19:03","19:08","19:13","19:18","19:23","19:28","19:33","19:38","19:43","19:48","19:53","19:58","20:03","20:08","20:13","20:18","20:23","20:28"],"seriesLabels":["Cluster"],"seriesValues":[["39268.416666666664","38866.6","39703.666666666664","39465.649999999994","38925.55","38845.700000000004","39105.5","39555.849999999984","39213.21666666666","39087.600000000006","38996.86666666667","39166.73333333334","38871.58333333333","39292.649999999994","39297.76666666667","38952.366666666654","39619.799999999996","39142.06666666668","40201.066666666666","39156.03333333333","39068.383333333324","39436.383333333346","38877.91666666666","38944.2","39187.31666666667","38952.51666666667","39071.11666666667","39374.13333333333","38959.33333333333","39230.63333333334","39899.899999999994","39262.59999999999","39027.40000000001","39071.133333333346","38966.51666666666","39347.9","39179.71666666666","39028.93333333332","39210.21666666667","39452.33333333334","39185.8","39709.86666666667","39782.64999999999","39399.666666666664","39604.81666666666","39284.13333333333","39530.75","39392.33333333333","39268.35","39411.88333333334","39460.31666666665","39744.433333333334","39386.09999999999","39428.56666666666","39305.20000000001","39254.01666666668","39548.149999999994","39281.066666666666","39876.88333333334","39542.15","39308.069771963455","40121.88333333333","39694.9","41584.600000000006","39647.38333333334","39759.433333333334","39593.08333333332","39443.75","39722.00000000001","39574.09999999999","39520.99999999999","39865.03333333333","40179.61666666667","39664.850000000006","39424.95","39964.33333333333","39523.516666666656","39438.53333333334","39748.316666666666","39425.31666666666","39922.69999999999","39894.11666666666","39701.53333333333","40166.25","40172.98333333334","39872.88333333333","40313.61666666666","39765.78333333334","39512.45","39919.333333333336","39969.8","39725.45","39826.333333333336","39933.100000000006","39736.43333333334","39778.78333333334","39833.1","40046.66666666665","39657.19999999999","39625.450000000004","40043.49999999999","39973.21666666666","39832.18333333333","40053.71666666666","39711.783333333326","39704.98333333333","40261.68333333334","39938.18368673734","39787.13333333334","39871.066666666666","39747.399999999994","40017.08333333333","39609.866666666654","39722.583333333336","40270.20000000001","39621.1","39721.88333333333","40005.55","40294.566666666666","39816.700000000004","39968.41666666667","39818.86666666668","39389.78333333334","39560.33333333333","39896.68333333333","39590.06666666667","39713.9","39678.51666666668","40320.35","40154.03333333333","39704.96666666667","39486.86666666667","39592.333333333336","39553.01666666668","39553.916666666664","39677.58333333334","39469.51666666666","39590.21666666667","39498.36666666667","40038.93333333332","39780.28333333333","39633.74999999999","40124.5","39396.433333333334","39185.21666666666","39783.166666666664","39645.38333333333","39299.48333333333","39286.55","39601.11666666667","39742.69999999999","39599","39153.23333333334","39734.58333333332","39456.283333333326","38909.86666666666","39948.049999999996","39411.63333333333","39398.68333333333","39258.23333333334","39209.36666666667","39542.466666666674","39497.05","39312.21666666666","39539.71666666667","39325.5","39286.39999999999","39059.983333333344","39092.6","39038.216666666674","39812.48333333334","38798.61666666667","39473.48333333332","39458.616666666654","39011.44634753428","39038.36666666667","39278.96666666666","38836.583333333336","38774.98333333333","39442.21666666667","38949.48333333333","38942.999999999985","39294.45","39246.88333333333","39846.316666666666","39384.83333333333","39197.56666666666","39271.216666666674","39125.73333333332","38989.83333333333","39201.94999999999","39170.600000000006","38784.383333333346","38867.83333333332","39637.81666666668","39339.21666666667","38964.249999999985","39136.16666666666","39462.649999999994","38925.283333333326","39117.633333333324","39750.75","38700.25","39128.933333333334","39058.56666666667","38958.48333333332","39321.333333333336","39394.53333333333","39015.11666666667","38869.133333333324","39706.18333333334","39071.46666666666","39162","39067.35","38946.43333333334","39560.23333333334","38927.399999999994","39120.88333333334","39520.200000000004","39121.999999999985","38694.73333333332","39418.70000000001","39065.08333333332","39210.5","39271.50000000001","39262.46666666667","39448.21666666665","39219.48333333332","39840.79999999999","40132.983333333344","39512.75","39287.65","39670.46666666666","39753.883333333324","39643.93333333334","39712.24999999999","39795.916666666664","39496.16666666666","40201.983333333344","40322.13333333334","40693.799999999996","41465.61666666667","40428.200000000004","40295.850000000006","40176.166666666686","40085.23333333334","40313.36666666667","40687.13333333333","40406.66666666666","40712.483333333344","40591.88333333334","41432.56666666667","40366.816666666666","40399.03333333333","41888.333333333336","41627.86666666666","41225.600000000006","40793.01666666666","43343.04999999999","41129.66666666666","41292.51666666666","42163.649999999994","40733.96666666667","40709.716666666674","41701.166666666664","40411.78333333334","40804.38333333333","40290.36666666668","40884.88333333333","40758.21666666667","40601.483333333344","40890.15000000001","40818.399999999994","40785.566666666666","40614.71666666665","40523.7","40501.46666666665","40561.73333333332","40520.5","40427.799999999996","42489.666666666664","40471.68333333334","40703.3","40639.916666666686","40652.183333333334","41007.61666666667","40042.66666666666","40447.35","40837.23333333332","40339.53333333335","40286.71666666666","40351.94999999999","40097.366666666676","40483.499999999985","40677.13333333334","40118.76666666666","40545.28333333333","40211.083333333314","39967.649999999994"]]} -------------------------------------------------------------------------------- /server/graphQL/demoData/demoData.js: -------------------------------------------------------------------------------- 1 | const cpuUsage = require('./CpuUsage.json'); 2 | const freeMemory = require('./FreeMemory.json'); 3 | const networkTransmitted = require('./NetworkTransmitted.json'); 4 | const nodes = require('./nodeData.json'); 5 | const pods = require('./podData.json'); 6 | 7 | const timeSeriesDemo = { 8 | cpuUsage, 9 | freeMemory, 10 | networkTransmitted, 11 | }; 12 | 13 | const demoTimeSeries = (start, end, type) => { 14 | const startDate = new Date(start); 15 | const endDate = new Date(end); 16 | const hours = (endDate - startDate) / 3600000; 17 | const numOfResults = hours * 30; 18 | const data = timeSeriesDemo[type]; 19 | const timestamps = data.timestamps.slice(0, numOfResults); 20 | const seriesLabels = data.seriesLabels.slice(); 21 | const seriesValues = data.seriesValues.map(series => series.slice(0, numOfResults)); 22 | return { timestamps, seriesLabels, seriesValues }; 23 | }; 24 | 25 | module.exports = { 26 | cpuUsage: (start, end) => demoTimeSeries(start, end, 'cpuUsage'), 27 | freeMemory: (start, end) => demoTimeSeries(start, end, 'freeMemory'), 28 | networkTransmitted: (start, end) => demoTimeSeries(start, end, 'networkTransmitted'), 29 | nodes, 30 | pods, 31 | }; 32 | -------------------------------------------------------------------------------- /server/graphQL/demoData/nodeData.json: -------------------------------------------------------------------------------- 1 | [{"metadata":{"name":"ip-192-168-15-117.us-east-2.compute.internal","creationTimestamp":"2021-05-20T19:45:04Z"},"status":{"allocatable":{"cpu":"940m","memory":"592036Ki","ephemeralStorage":"76224326324"},"usage":{"cpu":"47962470n","memory":"461828Ki"},"usagePercent":{"cpu":"5%","cpuCores":"48m","memory":"78%","memoryBytes":"451Mi"}}},{"metadata":{"name":"ip-192-168-5-18.us-east-2.compute.internal","creationTimestamp":"2021-05-20T19:45:04Z"},"status":{"allocatable":{"cpu":"940m","memory":"592036Ki","ephemeralStorage":"76224326324"},"usage":{"cpu":"41088608n","memory":"485680Ki"},"usagePercent":{"cpu":"4%","cpuCores":"42m","memory":"82%","memoryBytes":"474Mi"}}},{"metadata":{"name":"ip-192-168-56-91.us-east-2.compute.internal","creationTimestamp":"2021-05-20T19:45:07Z"},"status":{"allocatable":{"cpu":"940m","memory":"592036Ki","ephemeralStorage":"76224326324"},"usage":{"cpu":"42955330n","memory":"488656Ki"},"usagePercent":{"cpu":"4%","cpuCores":"43m","memory":"82%","memoryBytes":"477Mi"}}},{"metadata":{"name":"ip-192-168-62-206.us-east-2.compute.internal","creationTimestamp":"2021-05-20T19:45:08Z"},"status":{"allocatable":{"cpu":"940m","memory":"592036Ki","ephemeralStorage":"76224326324"},"usage":{"cpu":"42812316n","memory":"453964Ki"},"usagePercent":{"cpu":"4%","cpuCores":"43m","memory":"76%","memoryBytes":"443Mi"}}},{"metadata":{"name":"ip-192-168-79-108.us-east-2.compute.internal","creationTimestamp":"2021-05-20T19:45:03Z"},"status":{"allocatable":{"cpu":"940m","memory":"592036Ki","ephemeralStorage":"76224326324"},"usage":{"cpu":"49957488n","memory":"622308Ki"},"usagePercent":{"cpu":"5%","cpuCores":"50m","memory":"105%","memoryBytes":"607Mi"}}},{"metadata":{"name":"ip-192-168-80-204.us-east-2.compute.internal","creationTimestamp":"2021-05-20T19:45:04Z"},"status":{"allocatable":{"cpu":"940m","memory":"592036Ki","ephemeralStorage":"76224326324"},"usage":{"cpu":"45673722n","memory":"478688Ki"},"usagePercent":{"cpu":"4%","cpuCores":"46m","memory":"80%","memoryBytes":"467Mi"}}},{"metadata":{"name":"ip-192-168-86-244.us-east-2.compute.internal","creationTimestamp":"2021-05-20T19:45:06Z"},"status":{"allocatable":{"cpu":"940m","memory":"592036Ki","ephemeralStorage":"76224326324"},"usage":{"cpu":"44203269n","memory":"463136Ki"},"usagePercent":{"cpu":"4%","cpuCores":"45m","memory":"78%","memoryBytes":"452Mi"}}}] 2 | -------------------------------------------------------------------------------- /server/graphQL/demoData/podData.json: -------------------------------------------------------------------------------- 1 | [{"metadata":{"name":"aws-node-5ldd6","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.5.18","startTime":"2021-05-20T19:45:07Z"},"spec":{"nodeName":"ip-192-168-5-18.us-east-2.compute.internal","containers":[{"name":"aws-node","usage":{"cpu":"3104798n","memory":"43796Ki"}}, {"name":"aws-node2","usage":{"cpu":"2395139n","memory":"7220Ki"}},{"name":"aws-node3","usage":{"cpu":"65139n","memory":"6220Ki"}}]}},{"metadata":{"name":"aws-node-d2sxh","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.62.206","startTime":"2021-05-20T19:45:29Z"},"spec":{"nodeName":"ip-192-168-62-206.us-east-2.compute.internal","containers":[{"name":"aws-node","usage":{"cpu":"3365139n","memory":"37220Ki"}}]}},{"metadata":{"name":"aws-node-fgwjl","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.80.204","startTime":"2021-05-20T19:45:06Z"},"spec":{"nodeName":"ip-192-168-80-204.us-east-2.compute.internal","containers":[{"name":"aws-node","usage":{"cpu":"2671359n","memory":"45536Ki"}}]}},{"metadata":{"name":"aws-node-hh9fw","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.15.117","startTime":"2021-05-20T19:45:11Z"},"spec":{"nodeName":"ip-192-168-15-117.us-east-2.compute.internal","containers":[{"name":"aws-node","usage":{"cpu":"2140724n","memory":"43748Ki"}}]}},{"metadata":{"name":"aws-node-lhpzk","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.79.108","startTime":"2021-05-20T19:45:10Z"},"spec":{"nodeName":"ip-192-168-79-108.us-east-2.compute.internal","containers":[{"name":"aws-node","usage":{"cpu":"2566659n","memory":"47252Ki"}}]}},{"metadata":{"name":"aws-node-q8cdl","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.86.244","startTime":"2021-05-20T19:45:16Z"},"spec":{"nodeName":"ip-192-168-86-244.us-east-2.compute.internal","containers":[{"name":"aws-node","usage":{"cpu":"1583872n","memory":"43248Ki"}}]}},{"metadata":{"name":"aws-node-rndmt","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.56.91","startTime":"2021-05-20T19:45:44Z"},"spec":{"nodeName":"ip-192-168-56-91.us-east-2.compute.internal","containers":[{"name":"aws-node","usage":{"cpu":"3331458n","memory":"35156Ki"}}]}},{"metadata":{"name":"coredns-56b458df85-85l5q","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.89.29","startTime":"2021-05-20T19:45:54Z"},"spec":{"nodeName":"ip-192-168-86-244.us-east-2.compute.internal","containers":[{"name":"coredns","usage":{"cpu":"1655406n","memory":"12324Ki"}}, {"name":"coredns2","usage":{"cpu":"6555406n","memory":"9324Ki"}}]}},{"metadata":{"name":"coredns-56b458df85-nrhq2","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.4.47","startTime":"2021-05-20T19:45:54Z"},"spec":{"nodeName":"ip-192-168-15-117.us-east-2.compute.internal","containers":[{"name":"coredns","usage":{"cpu":"1564762n","memory":"12968Ki"}}]}},{"metadata":{"name":"kube-proxy-6vds4","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.5.18","startTime":"2021-05-20T19:45:07Z"},"spec":{"nodeName":"ip-192-168-5-18.us-east-2.compute.internal","containers":[{"name":"kube-proxy","usage":{"cpu":"46897n","memory":"18836Ki"}},{"name":"kube-proxy2","usage":{"cpu":"46897n","memory":"11836Ki"}},{"name":"kube-proxy3","usage":{"cpu":"46897n","memory":"8836Ki"}},{"name":"kube-proxy4","usage":{"cpu":"46897n","memory":"12836Ki"}}]}},{"metadata":{"name":"kube-proxy-f2cvk","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.86.244","startTime":"2021-05-20T19:45:16Z"},"spec":{"nodeName":"ip-192-168-86-244.us-east-2.compute.internal","containers":[{"name":"kube-proxy","usage":{"cpu":"167346n","memory":"18996Ki"}}]}},{"metadata":{"name":"kube-proxy-hwfzd","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.80.204","startTime":"2021-05-20T19:45:06Z"},"spec":{"nodeName":"ip-192-168-80-204.us-east-2.compute.internal","containers":[{"name":"kube-proxy","usage":{"cpu":"36491n","memory":"18504Ki"}}]}},{"metadata":{"name":"kube-proxy-jv6lj","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.62.206","startTime":"2021-05-20T19:45:29Z"},"spec":{"nodeName":"ip-192-168-62-206.us-east-2.compute.internal","containers":[{"name":"kube-proxy","usage":{"cpu":"23947n","memory":"17704Ki"}}]}},{"metadata":{"name":"kube-proxy-x9ndv","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.56.91","startTime":"2021-05-20T19:45:44Z"},"spec":{"nodeName":"ip-192-168-56-91.us-east-2.compute.internal","containers":[{"name":"kube-proxy","usage":{"cpu":"46178n","memory":"18000Ki"}}]}},{"metadata":{"name":"kube-proxy-zk6kb","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.15.117","startTime":"2021-05-20T19:45:11Z"},"spec":{"nodeName":"ip-192-168-15-117.us-east-2.compute.internal","containers":[{"name":"kube-proxy","usage":{"cpu":"47125n","memory":"18968Ki"}}]}},{"metadata":{"name":"kube-proxy-zzjp5","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.79.108","startTime":"2021-05-20T19:45:10Z"},"spec":{"nodeName":"ip-192-168-79-108.us-east-2.compute.internal","containers":[{"name":"kube-proxy","usage":{"cpu":"31789n","memory":"17848Ki"}}]}},{"metadata":{"name":"metrics-server-5fbdc54f8c-8dhp6","namespace":"kube-system","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.89.186","startTime":"2021-05-20T19:47:10Z"},"spec":{"nodeName":"ip-192-168-80-204.us-east-2.compute.internal","containers":[{"name":"metrics-server","usage":{"cpu":"2320946n","memory":"20492Ki"}}]}},{"metadata":{"name":"prometheus-alertmanager-ccf8f68cd-f4gzm","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.49.200","startTime":"2021-06-01T19:33:14Z"},"spec":{"nodeName":"ip-192-168-62-206.us-east-2.compute.internal","containers":[{"name":"prometheus-alertmanager","usage":{"cpu":"539474n","memory":"9120Ki"}},{"name":"prometheus-alertmanager-configmap-reload","usage":{"cpu":"0","memory":"2008Ki"}}]}},{"metadata":{"name":"prometheus-kube-state-metrics-685b975bb7-jt8g8","namespace":"prometheus","labels":{"app":null}},"status":{"phase":"Running","podIP":"192.168.47.180","startTime":"2021-06-01T19:33:08Z"},"spec":{"nodeName":"ip-192-168-56-91.us-east-2.compute.internal","containers":[{"name":"kube-state-metrics","usage":{"cpu":"366781n","memory":"8660Ki"}}]}},{"metadata":{"name":"prometheus-node-exporter-746hc","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.62.206","startTime":"2021-06-01T19:33:08Z"},"spec":{"nodeName":"ip-192-168-62-206.us-east-2.compute.internal","containers":[{"name":"prometheus-node-exporter","usage":{"cpu":"0","memory":"11404Ki"}}]}},{"metadata":{"name":"prometheus-node-exporter-9ptzw","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.80.204","startTime":"2021-06-01T19:33:08Z"},"spec":{"nodeName":"ip-192-168-80-204.us-east-2.compute.internal","containers":[{"name":"prometheus-node-exporter","usage":{"cpu":"0","memory":"7088Ki"}}]}},{"metadata":{"name":"prometheus-node-exporter-kbr6f","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.15.117","startTime":"2021-06-01T19:33:08Z"},"spec":{"nodeName":"ip-192-168-15-117.us-east-2.compute.internal","containers":[{"name":"prometheus-node-exporter","usage":{"cpu":"0","memory":"7436Ki"}}]}},{"metadata":{"name":"prometheus-node-exporter-l4pbd","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.5.18","startTime":"2021-06-01T19:33:08Z"},"spec":{"nodeName":"ip-192-168-5-18.us-east-2.compute.internal","containers":[{"name":"prometheus-node-exporter","usage":{"cpu":"14154n","memory":"7512Ki"}}]}},{"metadata":{"name":"prometheus-node-exporter-nv9vb","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.79.108","startTime":"2021-06-01T19:33:08Z"},"spec":{"nodeName":"ip-192-168-79-108.us-east-2.compute.internal","containers":[{"name":"prometheus-node-exporter","usage":{"cpu":"1289031n","memory":"13028Ki"}}]}},{"metadata":{"name":"prometheus-node-exporter-qx6gl","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.56.91","startTime":"2021-06-01T19:33:08Z"},"spec":{"nodeName":"ip-192-168-56-91.us-east-2.compute.internal","containers":[{"name":"prometheus-node-exporter","usage":{"cpu":"0","memory":"7436Ki"}}]}},{"metadata":{"name":"prometheus-node-exporter-z2n8x","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.86.244","startTime":"2021-06-01T19:33:08Z"},"spec":{"nodeName":"ip-192-168-86-244.us-east-2.compute.internal","containers":[{"name":"prometheus-node-exporter","usage":{"cpu":"0","memory":"7472Ki"}}]}},{"metadata":{"name":"prometheus-pushgateway-74cb65b858-55tmj","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.4.236","startTime":"2021-06-01T19:33:08Z"},"spec":{"nodeName":"ip-192-168-5-18.us-east-2.compute.internal","containers":[{"name":"prometheus-pushgateway","usage":{"cpu":"61134n","memory":"7584Ki"}}]}},{"metadata":{"name":"prometheus-server-d9fb67455-svs57","namespace":"prometheus","labels":{"app":"prometheus"}},"status":{"phase":"Running","podIP":"192.168.90.34","startTime":"2021-06-01T19:33:14Z"},"spec":{"nodeName":"ip-192-168-79-108.us-east-2.compute.internal","containers":[{"name":"prometheus-server-configmap-reload","usage":{"cpu":"0","memory":"4460Ki"}},{"name":"prometheus-server","usage":{"cpu":"5499722n","memory":"237100Ki"}}]}}] 2 | -------------------------------------------------------------------------------- /server/graphQL/directives/directives.js: -------------------------------------------------------------------------------- 1 | // schema directives can be placed here 2 | 3 | module.exports = () => {}; 4 | -------------------------------------------------------------------------------- /server/graphQL/resolvers/resolvers.js: -------------------------------------------------------------------------------- 1 | const { isObject, find } = require('lodash'); 2 | const k8sApi = require('../../k8sApi'); 3 | const podData = require('../datasources/podConstructor') 4 | const nodeData = require('../datasources/nodeConstructor'); 5 | const demo = require('../demoData/demoData'); 6 | 7 | // set to true to use mockData instead of pulling real k8s cluster data 8 | const demoMode = process.env.DEMO_MODE; 9 | 10 | // helper function that acts as Object.assign but deeply 11 | const mergeDeep = (target, source) => { 12 | for (const key in source) { 13 | if (isObject(source[key])) { 14 | if (!target[key]) Object.assign(target, { [key]: {} }); 15 | mergeDeep(target[key], source[key]); 16 | } else { 17 | Object.assign(target, { [key]: source[key] }); 18 | } 19 | } 20 | return target; 21 | }; 22 | 23 | /* 24 | * TODO: Implement pagination: https://www.apollographql.com/docs/tutorial/resolvers/#paginate-results 25 | * TODO: Abstract mergeDeep function 26 | * TODO: Cleamup messy merge below 27 | */ 28 | 29 | /** Useful Docs: 30 | * On accessing grandparents in resolvers https://github.com/graphql/graphql-js/issues/1098 31 | * a 32 | */ 33 | 34 | module.exports = { 35 | Query: { 36 | getPods: async (parent, args, context, info) => { 37 | /** NEW CODE */ 38 | if (demoMode) return demo.pods; 39 | 40 | const podsApi = (await k8sApi.listPodForAllNamespaces()).response.body.items; 41 | const podsCmd = (await podData.getMetrics()).items; 42 | 43 | // Brute force approach to merging these two datasources by cycling through to match on pod name and container name 44 | // should be refactored using only forEach, or better yet a findOne style method would improve performance; 45 | // I also feel like we should fold container status into this query as well 46 | // question though - how closely do we want to match original data source? This could allow us to build a graphQL tool 47 | // maybe use a lodash function https://lodash.com/docs/4.17.15; this will likely have a similar time complexity, 48 | // but it will be more declarative and easier to read. 49 | 50 | const podArray = []; 51 | podsApi.forEach((pod) => { 52 | const mergedPod = podsCmd.reduce((original, metrics) => { 53 | if (original.metadata.name == metrics.metadata.name) { 54 | original.spec.containers.forEach(container => { 55 | metrics.containers.reduce((origCont, metricCont) => { 56 | if (origCont.name === metricCont.name) return mergeDeep(origCont, metricCont); 57 | return origCont; 58 | }, container); 59 | }); 60 | } 61 | return original; 62 | }, pod); 63 | podArray.push(mergedPod); 64 | }); 65 | return podArray; 66 | }, 67 | nodes: async (parent, args, context, info) => { 68 | if (demoMode) return demo.nodes; 69 | 70 | const nodes = (await k8sApi.listNode('default')).response.body.items; 71 | const allNodePercentages = (await nodeData.getPercentages()); 72 | const allNodeMetrics = (await nodeData.getNodeMetrics()).items; 73 | 74 | nodes.forEach(node => { 75 | const nodePercent = find(allNodePercentages, { NAME: [node.metadata.name] }); 76 | node.status.usagePercent = { 77 | cpu: nodePercent['CPU%'][0], 78 | memory: nodePercent['MEMORY%'][0], 79 | cpuCores: nodePercent['CPU(cores)'][0], 80 | memoryBytes: nodePercent['MEMORY(bytes)'][0], 81 | }; 82 | 83 | const nodeMetrics = find(allNodeMetrics, { metadata: { name: node.metadata.name } }); 84 | node.status.usage = nodeMetrics.usage; 85 | 86 | node.status.allocatable.ephemeralStorage = node.status.allocatable['ephemeral-storage']; 87 | }); 88 | return nodes; 89 | }, 90 | cpuUsage: async (parent, { start, end, step }, { dataSources }, info) => { 91 | if (demoMode) return demo.cpuUsage(start, end); 92 | const startTime = new Date(start).toISOString(); 93 | const endTime = new Date(end).toISOString(); 94 | return dataSources.prometheusAPI.getCpuUsageSecondsRateByName(startTime, endTime, step); 95 | }, 96 | freeMemory: async (parent, { start, end, step }, { dataSources }, info) => { 97 | if (demoMode) return demo.freeMemory(start, end); 98 | const startTime = new Date(start).toISOString(); 99 | const endTime = new Date(end).toISOString(); 100 | return dataSources.prometheusAPI.getClusterFreeMemory(startTime, endTime, step); 101 | }, 102 | networkTransmitted: async (parent, { start, end, step }, { dataSources }, info) => { 103 | if (demoMode) return demo.networkTransmitted(start, end); 104 | const startTime = new Date(start).toISOString(); 105 | const endTime = new Date(end).toISOString(); 106 | return dataSources.prometheusAPI.getNetworkTransmitBytes(startTime, endTime, step); 107 | }, 108 | }, 109 | }; 110 | -------------------------------------------------------------------------------- /server/graphQL/schema.js: -------------------------------------------------------------------------------- 1 | // const { makeExecutableSchema } = require('apollo-server') 2 | const typeDefs = require('./typeDef/typeDef'); 3 | const resolvers = require('./resolvers/resolvers'); 4 | const dataSources = require('./datasources/dataSources'); 5 | const schemaDirectives = require('./directives/directives'); 6 | 7 | module.exports = { 8 | typeDefs, 9 | resolvers, 10 | schemaDirectives, 11 | dataSources, 12 | }; -------------------------------------------------------------------------------- /server/graphQL/typeDef/typeDef.js: -------------------------------------------------------------------------------- 1 | const { gql } = require('apollo-server-express'); 2 | 3 | module.exports = gql` 4 | 5 | type NodeInfo { 6 | machineID: String 7 | systemUUID: String 8 | bootID: String 9 | kernelVersion: String 10 | osImage: String 11 | containerRuntimeVersion: String 12 | kubeletVersion: String 13 | kubeProxyVersion: String 14 | operatingSystem: String 15 | architecture: String 16 | } 17 | 18 | type Image { sizeBytes: Int names: [String ] } 19 | 20 | type KubeletEndpoint { Port: Int } 21 | 22 | type DaemonEndpoints { kubeletEndpoint: KubeletEndpoint } 23 | 24 | type Addresses { 25 | type: String 26 | address: String 27 | } 28 | 29 | # the resources a given node has to allocate to pods and containers running on it 30 | type Allocatable { 31 | attachablevolumesgcepd: String 32 | cpu: String 33 | ephemeralStorage: String 34 | hugepages2Mi: String 35 | memory: String 36 | pods: String 37 | } 38 | 39 | type Capacity { 40 | attachablevolumesgcepd: String 41 | cpu: String 42 | ephemeralstorage: String 43 | hugepages2Mi: String 44 | memory: String 45 | pods: String 46 | } 47 | 48 | # the status of a specific node 49 | type NodeStatus { 50 | images: [Image] 51 | nodeName: String 52 | nodeInfo: NodeInfo 53 | daemonEndpoints: DaemonEndpoints 54 | addresses: [Addresses ] 55 | conditions: [ Conditions ] 56 | allocatable: Allocatable 57 | capacity: Capacity 58 | usage: NodeUsage 59 | usagePercent: NodeUsagePercent 60 | } 61 | 62 | type NodeUsage { 63 | cpu: String 64 | memory: String 65 | } 66 | 67 | # node metrics and their usage as a percent of availability 68 | type NodeUsagePercent { 69 | cpu: String 70 | cpuCores: String 71 | memory: String 72 | memoryBytes:String 73 | } 74 | 75 | type NodeSpec { 76 | podCIDR: String 77 | providerID: String 78 | podCIDRs: [String ] 79 | } 80 | 81 | type Annotations { 82 | containergoogleapiscominstance_id: String 83 | csivolumekubernetesionodeid: String 84 | nodealphakubernetesiottl: String 85 | nodegkeiolastappliednodelabels: String 86 | volumeskubernetesiocontrollermanagedattachdetach: String 87 | } 88 | 89 | # metadata of a specific node 90 | type NodeMetadata { 91 | name: String 92 | selfLink: String 93 | uid: String 94 | resourceVersion: String 95 | creationTimestamp: String 96 | managedFields: [ManagedFields ] 97 | annotations: Annotations 98 | } 99 | 100 | type Node { 101 | metadata: NodeMetadata 102 | status: NodeStatus 103 | spec: NodeSpec 104 | } 105 | 106 | 107 | type ManagedFields { 108 | manager: String 109 | operation: String 110 | apiVersion: String 111 | time: String 112 | fieldsType: String 113 | } 114 | 115 | type Running { 116 | startedAt: String 117 | } 118 | 119 | type State { 120 | running: Running 121 | } 122 | 123 | # Status of a specific container 124 | type ContainerStatus { 125 | name: String 126 | ready: Boolean 127 | restartCount: Int 128 | image: String 129 | imageID: String 130 | containerID: String 131 | started: Boolean 132 | state: State 133 | } 134 | 135 | type PodIPs { 136 | ip: String 137 | } 138 | 139 | # Condition of the Pod, includes reason & message if not "Running" 140 | type Conditions { 141 | type: String 142 | # current status of the pod 143 | status: String 144 | lastProbeTime: String 145 | lastTransitionTime: String 146 | lastHeartbeatTime: String 147 | # reason the pod is not running 148 | reason: String 149 | # message about why the pod is not running 150 | message: String 151 | } 152 | 153 | # the status of a specific pod 154 | type PodStatus { 155 | phase: String 156 | conditions: [Conditions] 157 | hostIP: String 158 | podIP: String 159 | podIPs: [PodIPs ] 160 | startTime: String 161 | containerStatuses: [ContainerStatus] 162 | qosClass: String 163 | } 164 | 165 | type Tolerations { 166 | key: String 167 | operator: String 168 | effect: String 169 | tolerationSeconds: Int 170 | } 171 | 172 | type VolumeMounts { 173 | name: String 174 | readOnly: Boolean 175 | mountPath: String 176 | } 177 | 178 | # individual container metadata and metrics 179 | type Container { 180 | name: String 181 | podName: String 182 | namespace: String 183 | image: String 184 | terminationMessagePath: String 185 | terminationMessagePolicy: String 186 | imagePullPolicy: String 187 | volumeMounts: [VolumeMounts ] 188 | usage: ContainerUsage 189 | } 190 | 191 | # tells you how much cpu and memory a container is using 192 | type ContainerUsage{ 193 | cpu: String 194 | memory: String 195 | } 196 | 197 | type Secret { 198 | secretName: String 199 | defaultMode: Int 200 | } 201 | 202 | type Volumes { 203 | name: String secret: Secret 204 | } 205 | 206 | # Technical specifications about a pod 207 | type PodSpec { 208 | restartPolicy: String 209 | terminationGracePeriodSeconds: Int 210 | dnsPolicy: String 211 | serviceAccountName: String 212 | serviceAccount: String 213 | nodeName: String 214 | schedulerName: String 215 | priority: Int 216 | enableServiceLinks: Boolean 217 | preemptionPolicy: String 218 | tolerations: [Tolerations ] 219 | containers: [Container] 220 | volumes: [Volumes ] 221 | 222 | } 223 | 224 | type OwnerReferences { 225 | apiVersion: String 226 | kind: String 227 | name: String 228 | uid: String 229 | controller: Boolean 230 | blockOwnerDeletion: Boolean 231 | } 232 | 233 | # Labels applied to the pods 234 | type Labels { 235 | # the application the pod is a part of 236 | app: String 237 | podtemplatehash: String 238 | } 239 | 240 | # descriptive information about the pod 241 | type PodMetadata { 242 | name: String 243 | generateName: String 244 | namespace: String 245 | selfLink: String 246 | uid: String 247 | resourceVersion: String 248 | creationTimestamp: String 249 | managedFields: [ManagedFields ] 250 | ownerReferences: [OwnerReferences ] 251 | labels: Labels 252 | } 253 | 254 | # an individual pod object with all nested data objects 255 | type Pod { 256 | metadata: PodMetadata 257 | status: PodStatus 258 | spec: PodSpec 259 | } 260 | 261 | # a standard time series object buil t from a PromQL query 262 | type TimeSeries { 263 | # the timestamps for the datapoints stored in seriesValues 264 | timestamps: [String]! 265 | # the names used to subdivide the metrics being queried 266 | seriesLabels: [String]! 267 | # an array of arrays containg the actual values of each series. These correspond to the label in parallel order 268 | seriesValues: [[String]]! 269 | } 270 | 271 | # the schema allows the following query 272 | type Query { 273 | # query will return an array of all pods 274 | getPods: [Pod] 275 | # query will return an array of all nodes 276 | nodes: [Node] 277 | # Cluster's cpu usages (cores) over time 278 | cpuUsage(start:String!, end:String!, step:String!): TimeSeries 279 | # Cluster's available memrory (bytes) over time 280 | freeMemory(start:String!, end:String!, step:String!): TimeSeries 281 | # Cluster's network transmitted (bytes) over time 282 | networkTransmitted(start:String!, end:String!, step:String!): TimeSeries 283 | } 284 | `; 285 | -------------------------------------------------------------------------------- /server/k8sApi.js: -------------------------------------------------------------------------------- 1 | const k8s = require('@kubernetes/client-node'); 2 | 3 | // initialization and standardization of the configuration file 4 | const kubeConfig = new k8s.KubeConfig(); 5 | kubeConfig.loadFromDefault(); 6 | 7 | // initialization of the K8s api object 8 | const k8sApi = kubeConfig.makeApiClient(k8s.CoreV1Api); 9 | 10 | module.exports = k8sApi; 11 | -------------------------------------------------------------------------------- /server/restAPI/controller/nodeController.js: -------------------------------------------------------------------------------- 1 | const cmd = require('node-cmd'); 2 | const Node = require('../../graphQL/datasources/nodeConstructor'); 3 | 4 | const nodeController = {}; 5 | 6 | // this should eventually be abstracted to use the constructor function like NodePercents 7 | nodeController.getNodeMetrics = async function(req, res, next) { 8 | const rawMetrics = cmd.runSync('kubectl get --raw /apis/metrics.k8s.io/v1beta1/nodes/'); 9 | const regex = '///'; 10 | const metrics = rawMetrics.data.replace(regex, ''); 11 | res.locals.nodeMetrics = JSON.parse(metrics); 12 | return next(); 13 | }; 14 | 15 | nodeController.getNodePercents = async function(req, res, next) { 16 | res.locals.nodePercents = await Node.getPercentages(); 17 | return next(); 18 | }; 19 | 20 | module.exports = nodeController; 21 | -------------------------------------------------------------------------------- /server/restAPI/controller/podController.js: -------------------------------------------------------------------------------- 1 | const k8sApi = require('../../k8sApi'); 2 | const Pod = require('../../graphQL/datasources/podConstructor'); 3 | 4 | const podController = {}; 5 | 6 | podController.getPodsRaw = async function(req, res, next) { 7 | console.log('in the podController'); 8 | const data = (await k8sApi.listPodForAllNamespaces()).response.body.items; 9 | console.log(data); 10 | res.locals.pods = data; 11 | return next(); 12 | }; 13 | 14 | podController.getPodMetrics = async function(req, res, next) { 15 | res.locals.podMetrics = await Pod.getMetrics(); 16 | return next(); 17 | }; 18 | 19 | module.exports = podController; 20 | -------------------------------------------------------------------------------- /server/restAPI/controller/prometheusController.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const { spawn } = require('child_process'); 3 | 4 | const prometheusURL = 'http://127.0.0.1:9090/api/v1/'; 5 | 6 | const prometheusController = {}; 7 | 8 | /** 9 | * Stretch Feature: install prometheus programmatically for users 10 | * TODO: Add instructions for installing helm to the readme 11 | * TODO: Automatically download helm chart for prometheus 12 | * TODO: Run Helm Chart to Create Prometheus Deployment inside the K8S Cluster 13 | * TODO: Check if all the Prometheus Pods are Running? 14 | */ 15 | prometheusController.isUp = async (req, res, next) => { 16 | const queryStr = `${prometheusURL}query?query=up`; 17 | 18 | try { 19 | const response = await fetch(queryStr); 20 | res.locals.query = await response.json(); 21 | return next(); 22 | } catch (err) { 23 | return next(err); 24 | } 25 | }; 26 | 27 | prometheusController.portPrometheus = (req, res, next) => { 28 | try { 29 | const process = spawn('kubectl', ['--namespace=default', 'port-forward', 'deploy/prometheus-server', '9090']) 30 | 31 | process.stdout.on('data', data => { 32 | console.log(`stdout: ${data}`); 33 | }); 34 | 35 | process.stderr.on('data', (data) => { 36 | console.log(`stderr: ${data}`); 37 | }); 38 | 39 | process.on('close', (code) => { 40 | if (code === 1) console.log('PROMETHEUS ALREADY IN USE NUM NUM'); 41 | console.log(`child process exited with code ${code}`); 42 | }); 43 | 44 | return next(); 45 | } catch (err) { 46 | return next(err); 47 | } 48 | }; 49 | 50 | module.exports = prometheusController; 51 | -------------------------------------------------------------------------------- /server/restAPI/router/metricsServerRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const nodeController = require('../controller/nodeController'); 3 | const podController = require('../controller/podController'); 4 | 5 | const metricsServerRouter = express.Router(); 6 | 7 | metricsServerRouter.get('/pods', podController.getPodMetrics, (req, res) => { 8 | res.status(200).json(res.locals.podMetrics); 9 | }); 10 | 11 | metricsServerRouter.get('/nodesPercent', nodeController.getNodePercents, (req, res) => { 12 | res.status(200).send(res.locals.nodePercents); 13 | }); 14 | 15 | module.exports = metricsServerRouter; -------------------------------------------------------------------------------- /server/restAPI/router/prometheusRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const prometheusController = require('../controller/prometheusController'); 3 | 4 | const prometheusRouter = express.Router(); 5 | 6 | // route to deploy prometheus onto the Cluster 7 | prometheusRouter.post('/install', (req, res) =>{ 8 | res.status(200).json('Prometheus Fired Up') 9 | }); 10 | 11 | // trial route to get data from prometheus after nodeporting 12 | prometheusRouter.get('/up', prometheusController.isUp, (req, res) => { 13 | res.status(200).json(res.locals.query); 14 | }); 15 | 16 | prometheusRouter.get('/port', prometheusController.portPrometheus, (req, res) => { 17 | res.status(200).send('port on'); 18 | }); 19 | 20 | module.exports = prometheusRouter; 21 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ApolloServer } = require('apollo-server-express'); 3 | const path = require('path'); 4 | const gqlSchema = require('./graphQL/schema'); 5 | 6 | // initialize express server 7 | const app = express(); 8 | 9 | // routers for various function 10 | const metricsServerRouter = require('./restAPI/router/metricsServerRouter'); 11 | const prometheusRouter = require('./restAPI/router/prometheusRouter'); 12 | 13 | const PORT = 3000; 14 | 15 | // Serve all compiled files when running the production build 16 | app.use(express.static(path.resolve(__dirname, '../client'))); 17 | app.use('/build', express.static(path.join(__dirname, '../build'))); 18 | 19 | // Parse json & urlencoded body content from incoming requests and place it in req.body 20 | app.use(express.json()); 21 | app.use(express.urlencoded({ extended: true })); 22 | 23 | app.use('/metrics', metricsServerRouter); 24 | app.use('/prometheus', prometheusRouter); 25 | 26 | // Wrap the express server in an ApolloServer to include Apollo GraphQL functionality 27 | // https://www.apollographql.com/docs/apollo-server/api/apollo-server/#applymiddleware 28 | const apollo = new ApolloServer(gqlSchema); 29 | apollo.applyMiddleware({ app }); 30 | 31 | // Catchall Routes, GET request 404s handled on cleint-side by React Router & 32 | // All remained 404s are handled server-side 33 | app.get('/*', (req, res) => res.sendFile(path.join(__dirname, '../client/index.html'))); 34 | app.use('/*', (req, res) => res.status(404).send('Resource Not Found')); 35 | 36 | // Global Error Handler for Express 37 | // eslint-disable-next-line no-unused-vars 38 | app.use((err, req, res, next) => { 39 | const defaultErr = { 40 | log: 'Express error handler caught unknown middleware error', 41 | status: 500, 42 | message: { err: 'An error occurred' }, 43 | }; 44 | const errorObj = Object.assign({}, defaultErr, err); 45 | console.log(errorObj.log); 46 | return res.status(errorObj.status).json(errorObj.message); 47 | }); 48 | 49 | app.listen(PORT, () => { 50 | if (process.env.DEMO_MODE) console.log('~~~ D E M O M O D E A C T I V A T E D ~~~'); 51 | console.log(`Chillin' on port ${PORT} 😎`); 52 | }); 53 | 54 | // if testing using supertest 55 | module.exports = app; 56 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | 5 | mode: process.env.NODE_ENV, 6 | entry: ['babel-polyfill', path.resolve(__dirname, 'client/index.js')], 7 | output: { 8 | // path 9 | path: path.resolve(__dirname, 'build'), 10 | // file name 11 | filename: 'bundle.js', 12 | }, 13 | devServer: { 14 | publicPath: '/build', 15 | proxy: { 16 | '/': 'http://localhost:3000', 17 | }, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(js|jsx)$/, 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'babel-loader', 26 | options: { 27 | presets: ['@babel/preset-env', '@babel/preset-react'], 28 | }, 29 | }, 30 | }, 31 | { 32 | test: /\.(sa|sc|c)ss$/i, 33 | exclude: /node_modules/, 34 | use: [ 35 | // Creates `style` nodes from JS strings 36 | 'style-loader', 37 | // Translates CSS into CommonJS 38 | 'css-loader', 39 | // Translates sass into CommonJS 40 | 'sass-loader', 41 | ], 42 | }, 43 | { 44 | test: /\.(gif|png|jpe?g|svg)$/i, 45 | use: { 46 | loader: 'file-loader', 47 | }, 48 | }, 49 | ], 50 | }, 51 | } --------------------------------------------------------------------------------