├── coverage ├── lcov.info ├── coverage-final.json ├── lcov-report │ ├── favicon.png │ ├── sort-arrow-sprite.png │ ├── prettify.css │ ├── block-navigation.js │ ├── index.html │ ├── routes │ │ ├── index.html │ │ └── kubernetes.js.html │ ├── controllers │ │ └── index.html │ ├── base.css │ ├── sorter.js │ └── prettify.js └── clover.xml ├── .gitignore ├── .eslintignore ├── .DS_Store ├── .babelrc ├── client ├── .DS_Store ├── images │ ├── 3.png │ ├── 4.png │ ├── .DS_Store │ ├── hammer.png │ └── odin-logo.png ├── index.tsx ├── store.ts ├── components │ ├── App.tsx │ ├── PodName.tsx │ ├── Dropdown.tsx │ ├── Navbar.tsx │ ├── PopUp.tsx │ ├── DonutChart.tsx │ ├── BarChart.tsx │ ├── LiveChart.tsx │ └── LineChart.tsx ├── index.html ├── getData.ts ├── rootReducer.ts ├── containers │ ├── mongoMain.tsx │ ├── mainpage.tsx │ └── kubMain.tsx └── styles │ ├── popup.scss │ ├── colors.scss │ └── index.scss ├── dist ├── 16a2193accc50fbc12e7a7fcd55a5ddd.png ├── bb0f5a52d53086f82f7481b6b2d5807b.png ├── c5725aceae4c9e83888013d26e8c08fe.png ├── index.html └── bundle.js.LICENSE.txt ├── jest.config.js ├── help ├── exporter-values.yaml ├── mongodb.yaml └── README.md ├── types.js ├── babel.config.js ├── .eslintrc.js ├── server ├── routes │ ├── mongo.js │ ├── mongo.ts │ ├── dashboard.js │ ├── dashboard.ts │ ├── kubernetes.js │ └── kubernetes.ts ├── controllers │ ├── mongoController.js │ ├── mongoController.ts │ ├── dataObjectBuilder.ts │ ├── dashboardController.ts │ ├── kubernetesController.ts │ ├── dashboardController.js │ ├── dataObjectBuilder.js │ └── kubernetesController.js ├── server.ts └── server.js ├── __test__ ├── dashboard.test.js ├── puppeteer.test.js └── kubernetes.test.js ├── CONTRIBUTING.md ├── webpack.config.js ├── types.ts ├── README.md ├── package.json ├── CODE_OF_CONDUCT.md └── tsconfig.json /coverage/lcov.info: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /coverage/coverage-final.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/client/.DS_Store -------------------------------------------------------------------------------- /client/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/client/images/3.png -------------------------------------------------------------------------------- /client/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/client/images/4.png -------------------------------------------------------------------------------- /client/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/client/images/.DS_Store -------------------------------------------------------------------------------- /client/images/hammer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/client/images/hammer.png -------------------------------------------------------------------------------- /client/images/odin-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/client/images/odin-logo.png -------------------------------------------------------------------------------- /coverage/lcov-report/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/coverage/lcov-report/favicon.png -------------------------------------------------------------------------------- /dist/16a2193accc50fbc12e7a7fcd55a5ddd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/dist/16a2193accc50fbc12e7a7fcd55a5ddd.png -------------------------------------------------------------------------------- /dist/bb0f5a52d53086f82f7481b6b2d5807b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/dist/bb0f5a52d53086f82f7481b6b2d5807b.png -------------------------------------------------------------------------------- /dist/c5725aceae4c9e83888013d26e8c08fe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/dist/c5725aceae4c9e83888013d26e8c08fe.png -------------------------------------------------------------------------------- /coverage/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/OdinsEye/HEAD/coverage/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-puppeteer', 3 | moduleNameMapper: { 4 | '\\.(css|less)$': 'identity-obj-proxy', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /help/exporter-values.yaml: -------------------------------------------------------------------------------- 1 | mongodb: 2 | uri: "mongodb://mongodb-service:27017" 3 | 4 | 5 | serviceMonitor: 6 | enabled: true 7 | additionalLabels: 8 | release: prometheus -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | exports.end = exports.start = void 0; 4 | //start time to be set 24 hour from now 5 | exports.start = new Date(Date.now() - 86400000).toISOString(); 6 | exports.end = new Date(Date.now()).toISOString(); 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | ['@babel/preset-react', { runtime: 'automatic' }], 5 | ], 6 | overrides: [ 7 | { 8 | test: './vendor/something.umd.js', 9 | sourceType: 'script', 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /coverage/clover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | env: { 5 | browser: true, 6 | es6: true, 7 | node: true, 8 | }, 9 | plugins: ['@typescript-eslint', 'react'], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:@typescript-eslint/recommended', 13 | 'plugin:react/recommended', 14 | ], 15 | rules: { 16 | semi: [2, 'always'], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './components/App'; 4 | import { store } from './store'; 5 | import { Provider } from 'react-redux'; 6 | import { BrowserRouter as Router } from 'react-router-dom'; 7 | 8 | const container = document.getElementById('root'); 9 | 10 | // @ts-ignore 11 | const root = ReactDOM.createRoot(container); 12 | 13 | root.render( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /server/routes/mongo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var express = require('express'); 4 | var mongoController_1 = require("../controllers/mongoController"); 5 | var dataObjectBuilder_1 = require("../controllers/dataObjectBuilder"); 6 | var mongoRouter = express.Router(); 7 | mongoRouter.get('/mongoMetrics', mongoController_1["default"].mongoMetrics, dataObjectBuilder_1["default"].dataObjectBuilder, function (req, res) { 8 | return res.status(200).json(req.app.locals.data); 9 | }); 10 | exports["default"] = mongoRouter; 11 | -------------------------------------------------------------------------------- /server/routes/mongo.ts: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | import { Request, Response } from 'express'; 3 | import mongoController from '../controllers/mongoController'; 4 | import dataController from '../controllers/dataObjectBuilder'; 5 | 6 | const mongoRouter = express.Router(); 7 | 8 | mongoRouter.get( 9 | '/mongoMetrics', 10 | mongoController.mongoMetrics, 11 | dataController.dataObjectBuilder, 12 | (req: Request, res: Response) => { 13 | return res.status(200).json(req.app.locals.data); 14 | } 15 | ); 16 | 17 | 18 | export default mongoRouter; -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | ODIN'S EYE
-------------------------------------------------------------------------------- /coverage/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /help/mongodb.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: mongodb-deployment 5 | labels: 6 | app: mongodb 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: mongodb 12 | template: 13 | metadata: 14 | labels: 15 | app: mongodb 16 | spec: 17 | containers: 18 | - name: mongodb 19 | image: mongo 20 | ports: 21 | - containerPort: 27017 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: mongodb-service 27 | spec: 28 | selector: 29 | app: mongodb 30 | ports: 31 | - protocol: TCP 32 | port: 27017 33 | targetPort: 27017 34 | -------------------------------------------------------------------------------- /client/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, combineReducers } from '@reduxjs/toolkit'; 2 | import { useDispatch } from 'react-redux'; 3 | import rootReducer from './rootReducer'; 4 | import thunkMiddleware from 'redux-thunk'; 5 | 6 | export const store = configureStore({ 7 | reducer: rootReducer, 8 | middleware: (getDefaultMiddleware) => 9 | getDefaultMiddleware().concat(thunkMiddleware), 10 | devTools: true, 11 | }); 12 | 13 | //We are inferring the RootState and AppDispatch types from the store itself 14 | export type RootState = ReturnType; 15 | 16 | export type AppDispatch = typeof store.dispatch; 17 | 18 | export const useAppDispatch: () => AppDispatch = useDispatch; 19 | -------------------------------------------------------------------------------- /client/components/App.tsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | import { Routes, Route } from 'react-router-dom'; 3 | import MainPage from '../containers/mainpage'; 4 | import KubPage from '../containers/kubMain'; 5 | import MongoPage from '../containers/mongoMain'; 6 | import { useSelector } from 'react-redux'; 7 | import { State } from '../../types'; 8 | 9 | const App = () => { 10 | const namespaces = useSelector((state: State) => state.namespaces); 11 | return ( 12 | 13 | } /> 14 | } /> 15 | } /> 16 | 17 | ); 18 | }; 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | ODIN'S EYE 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /server/routes/dashboard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var express = require('express'); 4 | var dashboardController_1 = require("../controllers/dashboardController"); 5 | var dataObjectBuilder_1 = require("../controllers/dataObjectBuilder"); 6 | var dashboardRouter = express.Router(); 7 | dashboardRouter.get('/cpuUsage', dashboardController_1["default"].cpuUsageOverTotalCpu, dataObjectBuilder_1["default"].dataObjectBuilder, function (req, res) { 8 | return res.status(200).json(req.app.locals.data); 9 | }); 10 | dashboardRouter.get('/getAllMetrics', dashboardController_1["default"].getAllMetrics, dataObjectBuilder_1["default"].dataObjectBuilder, function (req, res) { 11 | return res.status(200).json(req.app.locals.data); 12 | }); 13 | exports["default"] = dashboardRouter; 14 | -------------------------------------------------------------------------------- /server/routes/dashboard.ts: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | import { Request, Response } from 'express'; 3 | import dashboardController from '../controllers/dashboardController'; 4 | import dataController from '../controllers/dataObjectBuilder'; 5 | 6 | const dashboardRouter = express.Router(); 7 | 8 | dashboardRouter.get( 9 | '/cpuUsage', 10 | dashboardController.cpuUsageOverTotalCpu, 11 | dataController.dataObjectBuilder, 12 | (req: Request, res: Response) => { 13 | return res.status(200).json(req.app.locals.data); 14 | } 15 | ); 16 | dashboardRouter.get( 17 | '/getAllMetrics', 18 | dashboardController.getAllMetrics, 19 | dataController.dataObjectBuilder, 20 | (req: Request, res: Response) => { 21 | return res.status(200).json(req.app.locals.data); 22 | } 23 | ); 24 | 25 | export default dashboardRouter; 26 | -------------------------------------------------------------------------------- /client/components/PodName.tsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | 3 | type PodNameType = { 4 | pod: string; 5 | ready: boolean; 6 | setCurrentPod: (name: string) => void; 7 | setButtonPopup: (on: boolean) => void; 8 | }; 9 | 10 | //Component to render the div containing pod name, will trigger pop up upon click 11 | const PodName = ({ 12 | pod, 13 | ready, 14 | setCurrentPod, 15 | setButtonPopup, 16 | }: PodNameType) => { 17 | let className; 18 | ready ? (className = 'pod-list') : (className = 'pod-list-bad'); 19 | return ( 20 |
21 | { 24 | setCurrentPod(pod); 25 | setButtonPopup(true); 26 | }} 27 | > 28 | {pod} 29 | 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default PodName; 36 | -------------------------------------------------------------------------------- /client/getData.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { ErrorType } from '../types'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | // import { addNamespaces } from './rootReducer'; 5 | import { createAsyncThunk } from '@reduxjs/toolkit'; 6 | // import { AllDataType } from '../types'; 7 | 8 | 9 | const addNamespaces = createAsyncThunk( 10 | 'addNamespaces', 11 | async (data, thunkApi) => { 12 | try { 13 | const response = await axios.get('/api/kubernetesMetrics/namespaceNames'); 14 | return response.data; 15 | } catch (err) { 16 | const frontErr: ErrorType = { 17 | log: 'error in getNamespaces', 18 | status: 500, 19 | message: { err: 'an error ocurred' }, 20 | }; 21 | const errorObj = Object.assign({}, frontErr, err); 22 | return thunkApi.rejectWithValue(errorObj.message); 23 | } 24 | } 25 | ); 26 | 27 | export { addNamespaces }; 28 | -------------------------------------------------------------------------------- /server/controllers/mongoController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var mongoController = { 4 | mongoMetrics: function (req, res, next) { 5 | var queryObject = { 6 | linegraph: { 7 | opcounter: 'sum(rate(mongodb_ss_opcounters[5m]))', 8 | connections: 'mongodb_ss_connections{conn_type="active"}', 9 | queues: 'sum(mongodb_ss_globalLock_currentQueue)', 10 | latency: 'rate(mongodb_ss_opLatencies_latency[5m])', 11 | uptime: 'mongodb_ss_uptime', 12 | memory: 'mongodb_sys_memory_MemAvailable_kb', 13 | processes: 'mongodb_sys_cpu_procs_running' 14 | } 15 | }; 16 | try { 17 | req.app.locals.queries = queryObject; 18 | return next(); 19 | } 20 | catch (err) { 21 | return next({ 22 | log: "Error in mongoController.mongoMetrics: ".concat(err), 23 | status: 500, 24 | message: 'Error occured while retrieving mongo metric data' 25 | }); 26 | } 27 | } 28 | }; 29 | exports["default"] = mongoController; 30 | -------------------------------------------------------------------------------- /server/controllers/mongoController.ts: -------------------------------------------------------------------------------- 1 | import { MongoController, graphDataObject, res, req, next } from '../../types'; 2 | 3 | import DataObjectBuilder from './dataObjectBuilder'; 4 | import axios from 'axios'; 5 | 6 | const mongoController: MongoController = { 7 | mongoMetrics: (req, res, next) => { 8 | const queryObject: graphDataObject = { 9 | linegraph: { 10 | opcounter: 'sum(rate(mongodb_ss_opcounters[5m]))', 11 | connections: 'mongodb_ss_connections{conn_type="active"}', 12 | queues: 'sum(mongodb_ss_globalLock_currentQueue)', 13 | latency: 'rate(mongodb_ss_opLatencies_latency[5m])', 14 | uptime: 'mongodb_ss_uptime', 15 | memory: 'mongodb_sys_memory_MemAvailable_kb', 16 | processes: 'mongodb_sys_cpu_procs_running', 17 | }, 18 | }; 19 | try { 20 | req.app.locals.queries = queryObject; 21 | return next(); 22 | } catch (err) { 23 | return next({ 24 | log: `Error in mongoController.mongoMetrics: ${err}`, 25 | status: 500, 26 | message: 'Error occured while retrieving mongo metric data', 27 | }); 28 | } 29 | }, 30 | }; 31 | 32 | export default mongoController; 33 | -------------------------------------------------------------------------------- /server/routes/kubernetes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var express = require('express'); 4 | var kubernetesController_1 = require("../controllers/kubernetesController"); 5 | var dataObjectBuilder_1 = require("../controllers/dataObjectBuilder"); 6 | var kubernetesRouter = express.Router(); 7 | kubernetesRouter.get('/namespaceNames', kubernetesController_1["default"].namespaceNames, function (req, res) { 8 | return res.status(200).json(res.locals.namespaceNames); 9 | }); 10 | kubernetesRouter.get('/podNames', kubernetesController_1["default"].podNames, function (req, res) { 11 | return res.status(200).json(res.locals.names); 12 | }); 13 | kubernetesRouter.get('/namespaceMetrics/:namespaceName', kubernetesController_1["default"].getNameSpaceMetrics, dataObjectBuilder_1["default"].dataObjectBuilder, function (req, res) { 14 | return res.status(200).json(req.app.locals.data); 15 | }); 16 | kubernetesRouter.get('/podMetrics/:podName', kubernetesController_1["default"].getPodMetrics, dataObjectBuilder_1["default"].dataObjectBuilder, function (req, res) { 17 | return res.status(200).json(req.app.locals.data); 18 | }); 19 | kubernetesRouter.get('/podsNotReadyNames/', kubernetesController_1["default"].podsNotReadyNames, function (req, res) { 20 | return res.status(200).json(res.locals.status); 21 | }); 22 | exports["default"] = kubernetesRouter; 23 | -------------------------------------------------------------------------------- /help/README.md: -------------------------------------------------------------------------------- 1 | Set Up 2 | 3 | 1. Download and install Docker: https://docs.docker.com/get-docker/ 4 | 5 | 2. Download and install minikube: https://minikube.sigs.k8s.io/docs/start/ 6 | 7 | ```sh 8 | brew install minikube 9 | ``` 10 | 11 | 3. Install Kubectl: https://kubernetes.io/docs/tasks/tools/ 12 | 13 | ```sh 14 | brew install kubectl 15 | ``` 16 | 17 | 4. Install Helm 18 | 19 | 5. Add Prometheus Community Helm Repo: https://github.com/prometheus-community/helm-charts/ 20 | 21 | ```sh 22 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 23 | ``` 24 | 25 | 6. Start minikube 26 | 27 | ```sh 28 | minikube start 29 | ``` 30 | 31 | 7. helm install prometheus - replace the release field with the desired name for your prometheus monitoring pod, note that this will affect the command used to forward the port in the next step. 32 | 33 | ```sh 34 | helm install [release] prometheus-community/kube-prometheus-stack --namespace=prometheus --create-namespace --wait 35 | ``` 36 | 37 | 8. Port forward prometheus to 9090 38 | 39 | ```sh 40 | kubectl port-forward service/[release]-kube-prometheus-prometheus -n prometheus 9090 41 | ``` 42 | 43 | 9. Apply mongodb 44 | 45 | ```sh 46 | kubectl apply -f mongodb.yaml —-namespace=prometheus 47 | ``` 48 | 49 | 10. helm install mongodb metric exporter 50 | 51 | ```sh 52 | helm install [release] prometheus-community/prometheus-mongodb-exporter --namespace=prometheus -f exporter-values.yaml 53 | ``` 54 | -------------------------------------------------------------------------------- /server/routes/kubernetes.ts: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | import { Request, Response } from 'express'; 3 | import kubernetesController from '../controllers/kubernetesController'; 4 | import dataController from '../controllers/dataObjectBuilder'; 5 | 6 | const kubernetesRouter = express.Router(); 7 | 8 | kubernetesRouter.get( 9 | '/namespaceNames', 10 | kubernetesController.namespaceNames, 11 | (req: Request, res: Response) => { 12 | return res.status(200).json(res.locals.namespaceNames); 13 | } 14 | ); 15 | kubernetesRouter.get( 16 | '/podNames', 17 | kubernetesController.podNames, 18 | (req: Request, res: Response) => { 19 | return res.status(200).json(res.locals.names); 20 | } 21 | ); 22 | kubernetesRouter.get( 23 | '/namespaceMetrics/:namespaceName', 24 | kubernetesController.getNameSpaceMetrics, 25 | dataController.dataObjectBuilder, 26 | (req: Request, res: Response) => { 27 | return res.status(200).json(req.app.locals.data); 28 | } 29 | ); 30 | kubernetesRouter.get( 31 | '/podMetrics/:podName', 32 | kubernetesController.getPodMetrics, 33 | dataController.dataObjectBuilder, 34 | (req: Request, res: Response) => { 35 | return res.status(200).json(req.app.locals.data); 36 | } 37 | ); 38 | kubernetesRouter.get( 39 | '/podsNotReadyNames/', 40 | kubernetesController.podsNotReadyNames, 41 | (req: Request, res: Response) => { 42 | return res.status(200).json(res.locals.status); 43 | } 44 | ); 45 | 46 | export default kubernetesRouter; 47 | -------------------------------------------------------------------------------- /__test__/dashboard.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import app from '../server/server'; 3 | const server = 'http://localhost:3000'; 4 | 5 | describe('Route integration', () => { 6 | describe('/', () => { 7 | describe('GET', () => { 8 | it('responds with 200 status and text/html content type', () => { 9 | return request(server) 10 | .get('/') 11 | .expect('Content-Type', 'text/html; charset=UTF-8') 12 | .expect(200); 13 | }); 14 | }); 15 | }); 16 | }); 17 | 18 | describe('dashboard route to return hi', () => { 19 | describe('/', () => { 20 | it('/dashboard', () => { 21 | return request(app) 22 | .get('/dashboard') 23 | .expect('Content-Type', 'text/html; charset=utf-8') 24 | .expect(200); 25 | }); 26 | }); 27 | }); 28 | 29 | describe('dashboard controller route integration', () => { 30 | it('/getAllMetrics', async () => { 31 | const res = await request(app).get('/api/dashboard/getAllMetrics'); 32 | expect(res.header['content-type']).toBe('application/json; charset=utf-8'); 33 | expect(res.statusCode).toBe(200); 34 | expect(typeof res).toBe('object'); 35 | }), 36 | it('/cpuUsage', async () => { 37 | const res = await request(app).get('/api/dashboard/cpuUsage'); 38 | expect(res.header['content-type']).toBe( 39 | 'application/json; charset=utf-8' 40 | ); 41 | expect(res.statusCode).toBe(200); 42 | expect(typeof res).toBe('object'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Odin's Eye 2 | 3 | Thank you for your contribution! Contributions are welcome and are greatly appreciated and every little bit helps. 4 | 5 | ## Reporting Bugs 6 | 7 | All code changes happen through Github Pull Requests and we actively welcome them. To submit your pull request, follow the steps below: 8 | 9 | ## Pull Requests 10 | 11 | 1. Fork the repo and create your featured branch. 12 | 2. If you've added code that should be tested, add tests. 13 | 3. Make sure your code lints. 14 | 4. Issue that pull request! 15 | 5. Specify what you changed in details when you are doing pull request. 16 | 17 | Note: Any contributions you make will be under the MIT Software License and your submissions are understood to be under the same that covers the project. Please reach out to the team if you have any questions. 18 | 19 | ## Issues 20 | 21 | We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 22 | 23 | ## Coding Style 24 | 25 | - 2 spaces for indentation rather than tabs 26 | - 80 character line length 27 | - Run `npm run lint` to conform to our lint rules 28 | 29 | ## License 30 | 31 | By contributing, you agree that your contributions will be licensed under KubernOcular's Mozilla Public License Version 2.0. 32 | 33 | ### References 34 | 35 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/master/CONTRIBUTING.md) -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const e = require('express'); 5 | 6 | module.exports = { 7 | mode: process.env.NODE_ENV, 8 | entry: './client/index.tsx', 9 | devServer: { 10 | host: 'localhost', 11 | port: 7070, 12 | historyApiFallback: true, 13 | proxy: { 14 | '/': { 15 | target: 'http://localhost:7070/', 16 | router: () => 'http://localhost:3000', 17 | }, 18 | '/api': { 19 | target: 'http://localhost:7070/', 20 | router: () => 'http://localhost:3000', 21 | }, 22 | }, 23 | }, 24 | output: { 25 | path: path.resolve(__dirname, 'dist'), 26 | filename: 'bundle.js', 27 | }, 28 | plugins: [ 29 | new HtmlWebpackPlugin({ 30 | template: path.resolve(__dirname, './client/index.html'), 31 | filename: 'index.html', 32 | }), 33 | ], 34 | module: { 35 | rules: [ 36 | { 37 | test: /\.jsx?/, 38 | exclude: /node_modules/, 39 | use: { 40 | loader: 'babel-loader', 41 | options: { presets: ['@babel/preset-env', '@babel/preset-react'] }, 42 | }, 43 | }, 44 | { 45 | test: /\.scss?/, 46 | exclude: /node_modules/, 47 | use: ['style-loader', 'css-loader', 'sass-loader'], 48 | }, 49 | { 50 | test: /\.tsx?$/, 51 | use: 'ts-loader', 52 | exclude: /node_modules/, 53 | }, 54 | { 55 | test: /\.(png|jpe?g|gif)$/i, 56 | use: [ 57 | { 58 | loader: 'file-loader', 59 | }, 60 | ], 61 | }, 62 | ], 63 | }, 64 | resolve: { 65 | extensions: ['.tsx', '.ts', '.js', '.jsx'], 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /server/controllers/dataObjectBuilder.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { DataController, start, end } from '../../types'; 3 | import { Request, Response, NextFunction } from 'express'; 4 | 5 | const dataController: DataController = { 6 | dataObjectBuilder: async (res: Response, req: Request, next: NextFunction) => { 7 | const objectData: { [key: string]: string | number[] } = {}; 8 | const obj = req.app.locals.queries; 9 | try { 10 | for (let key in obj) { 11 | if (key === 'linegraph') { 12 | for (let query in obj[key]) { 13 | const response = await axios.get( 14 | `http://localhost:9090/api/v1/query_range?query=${obj[key][query]}&start=${start}&end=${end}&step=5m` 15 | ); 16 | if (response.data.data.result.length === 0) { 17 | objectData[query] = []; 18 | } else { 19 | objectData[query] = [response.data.data.result[0].values]; 20 | } 21 | } 22 | } 23 | if (key === 'donutint') { 24 | for (let query in obj[key]) { 25 | const response = await axios.get( 26 | `http://localhost:9090/api/v1/query_range?query=${obj[key][query]}&start=${start}&end=${end}&step=5m` 27 | ); 28 | const data = parseInt(response.data.data.result[0].values[0][1]); 29 | objectData[query] = [data]; 30 | } 31 | } 32 | if (key === 'cpubarchart') { 33 | for (let query in obj[key]) { 34 | const response = await axios.get( 35 | `http://localhost:9090/api/v1/query_range?query=${obj[key][query]}&start=${start}&end=${end}&step=5m` 36 | ); 37 | objectData[query] = [response.data.data.result[0].values[1][1]]; 38 | } 39 | } 40 | } 41 | req.app.locals.data = objectData 42 | return next(); 43 | } catch (err) { 44 | return next({ 45 | log: `Error in dataController.dataObjectBuilder: ${err}`, 46 | status: 500, 47 | message: 'Error occured while creating data object', 48 | }) 49 | } 50 | } 51 | } 52 | export default dataController; 53 | -------------------------------------------------------------------------------- /client/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | 3 | type DropDownType = { 4 | namespaces: string[] | null; 5 | current: string; 6 | handleChange: (name: string) => void; 7 | }; 8 | 9 | const DropDown = ({ namespaces, current, handleChange }: DropDownType) => { 10 | //react hook to open/close the dropdown 11 | const [open, setOpen] = useState(false); 12 | 13 | const handleOpen = (): void => { 14 | setOpen(!open); 15 | }; 16 | 17 | //creates array of html button elements of each namespaces 18 | const nameSpaceArr: JSX.Element[] = []; 19 | if (namespaces) { 20 | namespaces.forEach((name) => { 21 | nameSpaceArr.push( 22 |
  • 23 | 32 |
  • 33 | ); 34 | }); 35 | } 36 | //function to close menu after selection click 37 | const handleMenu = (name: string) => { 38 | setOpen(false); 39 | }; 40 | 41 | const ref = useOutsideClick(() => { 42 | setOpen(false); 43 | }); 44 | 45 | return ( 46 | 52 | ); 53 | }; 54 | 55 | //helper function to close dropdown if click is outside of the dropdown 56 | const useOutsideClick = (cb: Function) => { 57 | const ref = useRef(null); 58 | useEffect(() => { 59 | const handleClick = (event: any) => { 60 | if (ref.current && !ref.current.contains(event.target)) { 61 | cb(); 62 | } 63 | }; 64 | document.addEventListener('click', handleClick); 65 | return () => { 66 | document.removeEventListener('click', handleClick); 67 | }; 68 | }, []); 69 | return ref; 70 | }; 71 | 72 | export default DropDown; 73 | -------------------------------------------------------------------------------- /client/rootReducer.ts: -------------------------------------------------------------------------------- 1 | import { createAction, createReducer } from '@reduxjs/toolkit'; 2 | import { State } from '../types'; 3 | import { addNamespaces } from './getData'; 4 | 5 | //ACTIONS 6 | const darkMode = createAction('darkMode'); 7 | const currentPage = createAction('currentPage'); 8 | const saveNamespace = createAction('saveNamespace'); 9 | 10 | const initialState: State = { 11 | dark: true, 12 | namespaces: [], 13 | data: null, 14 | currentPage: 'main', 15 | currentNamespace: '', 16 | }; 17 | 18 | //REDUCER 19 | 20 | const rootReducer = createReducer(initialState, (builder) => 21 | builder 22 | .addCase(darkMode, (state, action) => { 23 | let dark; 24 | action.payload ? (dark = false) : (dark = true); 25 | return { ...state, dark }; 26 | }) 27 | .addCase(saveNamespace, (state, action) => { 28 | let currentNamespace = action.payload; 29 | console.log(currentNamespace, 'namespacechange'); 30 | return { 31 | ...state, 32 | currentNamespace, 33 | }; 34 | }) 35 | .addCase(currentPage, (state, action) => { 36 | let previous = document.getElementById(state.currentPage); 37 | if (previous) { 38 | previous.className = 'link'; 39 | } 40 | let currentPage = action.payload; 41 | let newPage = document.getElementById(currentPage); 42 | if (newPage) { 43 | newPage.className = 'link-div current'; 44 | } 45 | return { ...state, currentPage }; 46 | }) 47 | .addCase(addNamespaces.pending, (state, action) => { 48 | console.log('Retrieving namespaces...'); 49 | return { ...state }; 50 | }) 51 | .addCase(addNamespaces.fulfilled, (state, action) => { 52 | const namespaces = action.payload; 53 | return { ...state, namespaces }; 54 | }) 55 | .addCase(addNamespaces.rejected, (state, action) => { 56 | console.log('Error, rejected'); 57 | return { ...state }; 58 | }) 59 | ); 60 | 61 | export default rootReducer; 62 | 63 | export { darkMode, currentPage, addNamespaces, saveNamespace }; 64 | -------------------------------------------------------------------------------- /__test__/puppeteer.test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | const app = `http://localhost:7070/`; 4 | 5 | describe("Odin's Eye", () => { 6 | let browser; 7 | let page; 8 | 9 | beforeAll(async () => { 10 | browser = await puppeteer.launch({ 11 | headless: true, 12 | args: ['--no-sandbox', '--disable-setuid-sandbox'], 13 | }); 14 | page = await browser.newPage(); 15 | await page.goto(app); 16 | }); 17 | 18 | afterAll(() => browser.close()); 19 | 20 | it('header should be titled "Odin\'s Eye"', async () => { 21 | await page.waitForSelector('.header'); 22 | const title = await page.$eval('.header', (el) => el.innerText); 23 | expect(title).toBe(`Odin's Eye`); 24 | }), 25 | it('expects the logo to be an image', async () => { 26 | await page.waitForSelector('#logo'); 27 | const logo = await page.$eval('#logo', (el) => el.innerHTML); 28 | expect(logo).toBe( 29 | `\"odin-eye-logo\"` 30 | ); 31 | }), 32 | describe('Kubernetes page', () => { 33 | beforeAll(async () => { 34 | await page.click('#kubernetes'); 35 | }); 36 | 37 | it('expect onclick of kubernetes link on navbar to render the kubernetes dashboard', async () => { 38 | const kubernetesContainer = await page.$eval( 39 | '#kube-list-data', 40 | (el) => el.childElementCount 41 | ); 42 | expect(kubernetesContainer).toBe(3); 43 | }), 44 | it('expect the dropdown div to contain a dropdown button element', async () => { 45 | const dropdownDiv = await page.evaluate(() => { 46 | return Array.from(document.querySelector('#dropdown').children) 47 | .length; 48 | }); 49 | expect(dropdownDiv).toBe(1); 50 | }), 51 | it('expects kubernetes page to contain pods name div', async () => { 52 | await page.waitForSelector('#pod-name-header'); 53 | const podName = await page.$eval( 54 | '#pod-name-header', 55 | (el) => el.innerText 56 | ); 57 | expect(podName).toBe('Pod Names:'); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | //request handle is a function that will be executed every time the server receives a particular request 2 | import { RequestHandler } from 'express'; 3 | import { Request, Response, NextFunction } from 'express'; 4 | 5 | export type req = Request; 6 | export type res = Response; 7 | export type next = NextFunction; 8 | 9 | //start time to be set 24 hour from now 10 | export const start = new Date(Date.now() - 86_400_000).toISOString(); 11 | export const end = new Date(Date.now()).toISOString(); 12 | 13 | //type aliases 14 | export type DashboardController = { 15 | getAllMetrics: RequestHandler; 16 | cpuUsageOverTotalCpu: RequestHandler; 17 | }; 18 | 19 | export type KubernetesController = { 20 | namespaceNames: RequestHandler; 21 | podNames: RequestHandler; 22 | getNameSpaceMetrics: RequestHandler; 23 | getPodMetrics: RequestHandler; 24 | podsNotReadyNames?: RequestHandler; 25 | }; 26 | 27 | export type MongoController = { 28 | mongoMetrics: RequestHandler; 29 | }; 30 | 31 | export type DataController = { 32 | dataObjectBuilder: any; 33 | }; 34 | 35 | export type graphDataObject = { 36 | [key: string] : {[key: string] : string} 37 | }; 38 | 39 | export type State = { 40 | dark: boolean; 41 | namespaces: string[] | null; 42 | data: null | []; 43 | currentPage: string; 44 | currentNamespace: string; 45 | }; 46 | 47 | export type ErrorType = { 48 | log: string; 49 | status: number; 50 | message: { err: string }; 51 | }; 52 | 53 | export type Data = [number, string]; 54 | 55 | export type AllDataType = { 56 | data?: number[] | number | undefined; 57 | }; 58 | 59 | export type MainDataType = { 60 | totalCpu: Data[]; 61 | totalMem: Data[]; 62 | totalTransmit: Data[]; 63 | totalReceive: Data[]; 64 | totalPods: number; 65 | notReadyPods: number; 66 | totalNamespaces: number; 67 | }; 68 | 69 | export type MongoDataType = { 70 | opcounter: Data[]; 71 | connections: Data[]; 72 | queues: Data[]; 73 | latency: Data[]; 74 | uptime: Data[]; 75 | memory: Data[]; 76 | processes: Data[]; 77 | }; 78 | 79 | export type KubDataType = { 80 | cpu: Data[]; 81 | memory: Data[]; 82 | notReady: number; 83 | ready: Data[]; 84 | reception: Data[]; 85 | restarts: Data[]; 86 | transmission: Data[]; 87 | }; 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 | 5 | Logo 6 | 7 | 8 |

    Odin's Eye

    9 | 10 |
    11 | Monitoring Tool for Kubernetes and Containerized MongoDB 12 |
    13 | 14 | 15 | 16 | ## Prerequisites 17 | 18 | 1) A Kuberenetes cluster set up with ports open 19 | 20 | 2) A Prometheus deployment - with ports properly forwarded: https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack 21 |
    a) ```helm install prometheus prometheus-community/kube-prometheus-stack``` 22 |
    b) ```kubectl port-forward service/prometheus-kube-prometheus-prometheus 9090``` 23 | 24 | 3) MongoDB metrics exported to prometheus: 25 |
    a) ```helm install exporter prometheus-community/prometheus-mongodb-exporter``` 26 | 27 | Refer to the help directory for a step-by-step walkthrough 28 | 29 | 30 | ## Installation 31 | 32 | 1. Clone this repository onto your local machine 33 | 34 | ```sh 35 | git clone https://github.com/oslabs-beta/OdinsEye.git 36 | ``` 37 | 38 | 2. Install dependencies 39 | 40 | ```sh 41 | npm install 42 | ``` 43 | 44 | 3. Start the application server 45 | 46 | ```sh 47 | npm run start 48 | ``` 49 | 50 | 4. Navigate to http://localhost:3000 51 | 52 | ## Technologies 53 | 54 | - [React](https://reactjs.org/) 55 | - [Redux](https://redux.js.org/) 56 | - [TypeScript](https://www.typescriptlang.org/) 57 | - [Chart.js](https://www.chartjs.org/) 58 | - [Kubernetes](https://kubernetes.io/) 59 | - [Docker](https://www.docker.com/) 60 | - [Node](https://nodejs.org/en/) 61 | - [Prometheus/PromQL](https://prometheus.io/) 62 | 63 | ## Authors 64 | - Peter Choi [@prismatism](https://github.com/prismatism) | [Linkedin](https://www.linkedin.com/in/peterchoi3000/) 65 | - Emily Chu [@emmychu](https://github.com/emmychu) | [Linkedin](https://www.linkedin.com/in/lin-chu-pharmd/) 66 | - Chris Hicks [@chrishicks430](https://github.com/chrishicks430) | [Linkedin](https://www.linkedin.com/in/chrishicks430/) 67 | - Wendy Zheng [@wzhengg99](https://github.com/wzhengg99) | [Linkedin](https://www.linkedin.com/in/wzheng208/) 68 | 69 | 70 | 71 | Show your support
    72 | Give a ⭐️ if this project helped you! 73 | 74 |

    (back to top)

    -------------------------------------------------------------------------------- /client/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { darkMode } from '../rootReducer'; 5 | import { State } from '../../types'; 6 | 7 | const NavBar = () => { 8 | //Dispatch action for toggling light/dark mode in global state 9 | const dispatch = useDispatch(); 10 | const dark = useSelector((state: State) => state.dark); 11 | 12 | let classInfo: string; 13 | 14 | dark ? (classInfo = 'open') : (classInfo = 'closed'); 15 | 16 | //helper function to toggle class name for theme on the root component 17 | function toggleTheme() { 18 | const main = document.getElementById('root'); 19 | //main component changes 20 | if (main) { 21 | dark ? (main.className = 'theme-dark') : (main.className = 'theme-light'); 22 | } 23 | //Used to grab the body and alter the theme for the body html element 24 | const body = Array.from(document.getElementsByTagName('body'))[0]; 25 | if (body) { 26 | dark 27 | ? body.setAttribute('data-theme', 'dark') 28 | : body.setAttribute('data-theme', 'light'); 29 | } 30 | } 31 | 32 | return ( 33 | 72 | ); 73 | }; 74 | 75 | export default NavBar; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "odinseye", 3 | "version": "1.0.0", 4 | "description": "application to monitor kubernetes statefulset pods", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production nodemon server/server.js", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": "NODE_ENV=development webpack serve --open & tsc server/server.ts & nodemon server/server.js", 10 | "test": "NODE_ENV=test jest --watch" 11 | }, 12 | "author": "Chris, Emily, Peter, Wendy", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@testing-library/react": "^13.4.0", 16 | "@testing-library/user-event": "^14.4.3", 17 | "axios": "^0.27.2", 18 | "babel-core": "^6.26.3", 19 | "babel-preset-env": "^1.7.0", 20 | "chart.js": "^3.9.1", 21 | "chartjs-adapter-luxon": "^1.3.0", 22 | "chartjs-plugin-streaming": "^2.0.0", 23 | "cookie-parser": "^1.4.6", 24 | "cors": "^2.8.5", 25 | "node": "^19.0.1", 26 | "nodemon": "^2.0.20", 27 | "prom-client": "^14.1.0", 28 | "react": "^18.2.0", 29 | "react-chartjs-2": "^4.3.1", 30 | "react-dom": "^18.2.0", 31 | "react-redux": "^8.0.5", 32 | "react-router-dom": "^6.4.3", 33 | "react-spinners": "^0.13.6", 34 | "regenerator-runtime": "^0.13.11", 35 | "sass": "^1.56.1" 36 | }, 37 | "devDependencies": { 38 | "@babel/core": "^7.20.2", 39 | "@babel/preset-env": "^7.20.2", 40 | "@babel/preset-react": "^7.18.6", 41 | "@kubernetes/client-node": "^0.17.1", 42 | "@reduxjs/toolkit": "^1.9.0", 43 | "@types/cors": "^2.8.12", 44 | "@types/express": "^4.17.14", 45 | "@types/jest": "^29.2.3", 46 | "@types/node": "^18.11.9", 47 | "@types/react": "^18.0.25", 48 | "@types/react-dom": "^18.0.8", 49 | "@typescript-eslint/eslint-plugin": "^5.45.0", 50 | "@typescript-eslint/parser": "^5.45.0", 51 | "babel-jest": "^29.3.1", 52 | "babel-loader": "^9.1.0", 53 | "css-loader": "^6.7.1", 54 | "eslint": "^8.28.0", 55 | "file-loader": "^6.2.0", 56 | "html-webpack-plugin": "^5.5.0", 57 | "identity-obj-proxy": "^3.0.0", 58 | "jest": "^29.3.1", 59 | "jest-puppeteer": "^6.1.1", 60 | "react-test-renderer": "^18.2.0", 61 | "sass-loader": "^13.1.0", 62 | "style-loader": "^3.3.1", 63 | "supertest": "^6.3.1", 64 | "ts-jest": "^29.0.3", 65 | "ts-loader": "^9.4.1", 66 | "typescript": "^4.9.3", 67 | "url-loader": "^4.1.1", 68 | "webpack": "^5.75.0", 69 | "webpack-cli": "^4.10.0", 70 | "webpack-dev-server": "^4.11.1" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /client/containers/mongoMain.tsx: -------------------------------------------------------------------------------- 1 | import NavBar from '../components/Navbar'; 2 | import axios from 'axios'; 3 | import LineChart from '../components/LineChart'; 4 | import React, { useState, useEffect } from 'react'; 5 | import { currentPage } from '../rootReducer'; 6 | import { useDispatch } from 'react-redux'; 7 | import { MongoDataType } from '../../types'; 8 | 9 | const MongoPage = () => { 10 | const dispatch = useDispatch(); 11 | 12 | useEffect(() => { 13 | dispatch(currentPage('mongo')); 14 | }, []); 15 | 16 | const initialData = { 17 | opcounter: [], 18 | connections: [], 19 | queues: [], 20 | latency: [], 21 | uptime: [], 22 | memory: [], 23 | processes: [], 24 | }; 25 | 26 | const [data, setData] = useState(initialData); 27 | 28 | // helper function to fetch mongodb data 29 | const getData = async (url: string, podsName?: boolean): Promise => { 30 | try { 31 | const response = await axios.get(url); 32 | const data = await response.data; 33 | setData(data); 34 | } catch (err) { 35 | console.log(err); 36 | } 37 | }; 38 | 39 | // fetch data on load 40 | useEffect(() => { 41 | getData('/api/mongodb/mongoMetrics'); 42 | }, []); 43 | 44 | //Creates line chart array from line chart data object 45 | const lineObject: { [key: string]: any[] } = { 46 | uptime: [data.uptime, 'Uptime', 'Current', 'Current Uptime'], 47 | currMem: [data.memory, 'Memory', 'Current', 'Current Memory'], 48 | opCounter: [data.opcounter, 'Operations', 'Current', 'Current Operations'], 49 | connect: [ 50 | data.connections, 51 | 'Connections', 52 | 'Current', 53 | 'Current Connections', 54 | ], 55 | queue: [data.queues, 'Queue', 'Current', 'Current Queue'], 56 | proc: [data.processes, 'Processes', 'Current', 'Current Processes'], 57 | latency: [data.latency, 'Latency', 'Latency', 'Current Latency'], 58 | }; 59 | 60 | const charts: JSX.Element[] = []; 61 | 62 | for (let info in lineObject) { 63 | charts.push( 64 |
    65 | 72 |
    73 | ); 74 | } 75 | 76 | return ( 77 |
    78 |
    79 |

    Odin's Eye

    80 |
    81 | 82 |
    83 |
    84 | {charts} 85 |
    86 |
    87 |
    88 | ); 89 | }; 90 | 91 | export default MongoPage; 92 | -------------------------------------------------------------------------------- /__test__/kubernetes.test.js: -------------------------------------------------------------------------------- 1 | // import kubernetesRouter from "../dist/server/routes/kubernetes" 2 | // import express from "express" 3 | // import request from 'supertest'; 4 | 5 | const request = require("supertest"); 6 | const app = require('../server/server'); 7 | 8 | 9 | describe('testing for kubernete pod metrics', function () { 10 | describe('/api/kubernetesMetrics', function(){ 11 | it('/totalRestarts', async () =>{ 12 | const res = await (request(app).get('/api/kubernetesMetrics/totalRestarts')) 13 | expect(res.header['content-type']).toBe('application/json; charset=utf-8'); 14 | expect(res.statusCode).toBe(200); 15 | expect(typeof res).toBe('object'); 16 | }), 17 | it('/namespaceNames', async () =>{ 18 | const res = await (request(app).get('/api/kubernetesMetrics/namespaceNames')) 19 | expect(res.header['content-type']).toBe('application/json; charset=utf-8'); 20 | expect(res.statusCode).toBe(200); 21 | expect(typeof res).toBe('object'); 22 | }), 23 | it('/podNames', async () =>{ 24 | const res = await (request(app).get('/api/kubernetesMetrics/podNames')) 25 | expect(res.header['content-type']).toBe('application/json; charset=utf-8'); 26 | expect(res.statusCode).toBe(200); 27 | expect(typeof res).toBe('object'); 28 | }), 29 | it('/podsNotReady', async () =>{ 30 | const res = await (request(app).get('/api/kubernetesMetrics/podsNotReady')) 31 | expect(res.header['content-type']).toBe('application/json; charset=utf-8'); 32 | expect(res.statusCode).toBe(200); 33 | expect(typeof res).toBe('object'); 34 | }), 35 | it('/namespaceMetrics/:namespaceName', async () =>{ 36 | const res = await (request(app).get('/api/kubernetesMetrics/namespaceMetrics/prometheus')) 37 | expect(res.header['content-type']).toBe('application/json; charset=utf-8'); 38 | expect(res.statusCode).toBe(200); 39 | expect(typeof res).toBe('object'); 40 | }), 41 | it('//podMetrics/:podName', async () =>{ 42 | const res = await (request(app).get('/api/kubernetesMetrics//podMetrics/mongodb-0')) 43 | expect(res.header['content-type']).toBe('application/json; charset=utf-8'); 44 | expect(res.statusCode).toBe(200); 45 | expect(typeof res).toBe('object'); 46 | }), 47 | it('/podsNotReadyNames', async () =>{ 48 | const res = await (request(app).get('/api/kubernetesMetrics/podsNotReadyNames')) 49 | expect(res.header['content-type']).toBe('application/json; charset=utf-8'); 50 | expect(res.statusCode).toBe(200); 51 | expect(typeof res).toBe('object'); 52 | }) 53 | }) 54 | }); -------------------------------------------------------------------------------- /client/components/PopUp.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import LineChart from './LineChart'; 3 | import axios from 'axios'; 4 | const styles = require('../styles/popup.scss'); 5 | import { Data } from '../../types'; 6 | 7 | type PopupType = { 8 | podName: string | undefined; 9 | trigger: boolean; 10 | setTrigger: (arg: boolean) => void; 11 | }; 12 | 13 | type PopUpDataType = { 14 | cpu: Data[]; 15 | memory: Data[]; 16 | ready: Data[]; 17 | reception: Data[]; 18 | restarts: Data[]; 19 | transmission: Data[]; 20 | }; 21 | 22 | const Popup = ({ podName, trigger, setTrigger }: PopupType) => { 23 | const initialData = { 24 | cpu: [], 25 | memory: [], 26 | ready: [], 27 | reception: [], 28 | restarts: [], 29 | transmission: [], 30 | }; 31 | 32 | const [data, setData] = useState(initialData); 33 | 34 | const getData = async (name: string): Promise => { 35 | try { 36 | const response = await axios.get( 37 | `/api/kubernetesMetrics/podMetrics/${podName}` 38 | ); 39 | const data = await response.data; 40 | setData(data); 41 | } catch (err) { 42 | console.log('Error in Popup: ', err); 43 | } 44 | }; 45 | 46 | useEffect(() => { 47 | if (podName) { 48 | getData(podName); 49 | } 50 | }, [podName]); 51 | 52 | const lineObject: { [key: string]: any[] } = { 53 | totalCpu: [data.cpu, 'Cpu Usage', 'Percent', 'Total CPU % Usage'], 54 | totalMem: [ 55 | data.memory, 56 | 'Mem Usage', 57 | 'Kilobytes', 58 | 'Total Memory Usage (kB)', 59 | ], 60 | totalRec: [ 61 | data.reception, 62 | 'Byte Usage', 63 | 'Kilobytes', 64 | 'Netword Received (kB)', 65 | ], 66 | totalTrans: [ 67 | data.transmission, 68 | 'Byte Usage', 69 | 'Kilobytes', 70 | 'Network Transmitted (kB)', 71 | ], 72 | restart: [data.restarts, 'Restarts', 'Restarts', 'Pod Restarts'], 73 | }; 74 | 75 | const charts: JSX.Element[] = []; 76 | 77 | for (let info in lineObject) { 78 | charts.push( 79 |
    80 | 87 |
    88 | ); 89 | } 90 | return trigger ? ( 91 | 102 | ) : ( 103 |
    104 | ); 105 | }; 106 | 107 | export default Popup; 108 | -------------------------------------------------------------------------------- /coverage/lcov-report/block-navigation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var jumpToCode = (function init() { 3 | // Classes of code we would like to highlight in the file view 4 | var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; 5 | 6 | // Elements to highlight in the file listing view 7 | var fileListingElements = ['td.pct.low']; 8 | 9 | // We don't want to select elements that are direct descendants of another match 10 | var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` 11 | 12 | // Selecter that finds elements on the page to which we can jump 13 | var selector = 14 | fileListingElements.join(', ') + 15 | ', ' + 16 | notSelector + 17 | missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` 18 | 19 | // The NodeList of matching elements 20 | var missingCoverageElements = document.querySelectorAll(selector); 21 | 22 | var currentIndex; 23 | 24 | function toggleClass(index) { 25 | missingCoverageElements 26 | .item(currentIndex) 27 | .classList.remove('highlighted'); 28 | missingCoverageElements.item(index).classList.add('highlighted'); 29 | } 30 | 31 | function makeCurrent(index) { 32 | toggleClass(index); 33 | currentIndex = index; 34 | missingCoverageElements.item(index).scrollIntoView({ 35 | behavior: 'smooth', 36 | block: 'center', 37 | inline: 'center' 38 | }); 39 | } 40 | 41 | function goToPrevious() { 42 | var nextIndex = 0; 43 | if (typeof currentIndex !== 'number' || currentIndex === 0) { 44 | nextIndex = missingCoverageElements.length - 1; 45 | } else if (missingCoverageElements.length > 1) { 46 | nextIndex = currentIndex - 1; 47 | } 48 | 49 | makeCurrent(nextIndex); 50 | } 51 | 52 | function goToNext() { 53 | var nextIndex = 0; 54 | 55 | if ( 56 | typeof currentIndex === 'number' && 57 | currentIndex < missingCoverageElements.length - 1 58 | ) { 59 | nextIndex = currentIndex + 1; 60 | } 61 | 62 | makeCurrent(nextIndex); 63 | } 64 | 65 | return function jump(event) { 66 | if ( 67 | document.getElementById('fileSearch') === document.activeElement && 68 | document.activeElement != null 69 | ) { 70 | // if we're currently focused on the search input, we don't want to navigate 71 | return; 72 | } 73 | 74 | switch (event.which) { 75 | case 78: // n 76 | case 74: // j 77 | goToNext(); 78 | break; 79 | case 66: // b 80 | case 75: // k 81 | case 80: // p 82 | goToPrevious(); 83 | break; 84 | } 85 | }; 86 | })(); 87 | window.addEventListener('keydown', jumpToCode); 88 | -------------------------------------------------------------------------------- /client/components/DonutChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Doughnut } from 'react-chartjs-2'; 3 | import { useSelector } from 'react-redux'; 4 | import { State } from '../../types'; 5 | 6 | import { 7 | Chart as ChartJS, 8 | ArcElement, 9 | Tooltip, 10 | Legend, 11 | ChartOptions, 12 | ChartData, 13 | } from 'chart.js'; 14 | import { totalmem } from 'os'; 15 | 16 | ChartJS.register(ArcElement, Tooltip, Legend); 17 | 18 | type DonutType = { 19 | data: number[] | number; 20 | label: string; 21 | }; 22 | 23 | //display total number active and non active pods 24 | const DonutChart = ({ data }: DonutType) => { 25 | const dark = useSelector((state: State) => state.dark); 26 | let fontColor; 27 | dark ? (fontColor = '#363946') : (fontColor = 'rgba(136, 217, 230, 0.8)'); 28 | 29 | const [chartData, setChartData] = useState([]); 30 | const [loadErr, setLoadErr] = useState(false); 31 | 32 | const initialData: ChartData<'doughnut'> = { 33 | labels: [`Pods Ready: ${chartData[0]}`, `Not Ready: ${chartData[1]}`], 34 | datasets: [ 35 | { 36 | label: 'Total Pods', 37 | data: chartData, 38 | backgroundColor: [ 39 | 'rgba(54, 133, 181, 0.6)', 40 | ' rgb(172, 128, 160, 0.6)', 41 | ], 42 | borderColor: ['rgba(54, 133, 181, 1)', ' rgb(172, 128, 160, 1.2)'], 43 | borderWidth: 1, 44 | }, 45 | ], 46 | }; 47 | 48 | //data is being passed from kubmain and mainpage 49 | useEffect(() => { 50 | if (Array.isArray(data) && data.length > 0) { 51 | setChartData(data); 52 | } 53 | if (data === undefined) { 54 | setLoadErr(true); 55 | } 56 | }, [data]); 57 | 58 | const options: ChartOptions<'doughnut'> = { 59 | animation: { 60 | easing: 'easeInQuad', 61 | duration: 1000, 62 | }, 63 | events: [], 64 | responsive: true, 65 | rotation: 270, 66 | circumference: 180, 67 | interaction: { 68 | intersect: false, 69 | }, 70 | plugins: { 71 | legend: { 72 | display: true, 73 | position: 'top', 74 | labels: { 75 | color: fontColor, 76 | }, 77 | }, 78 | title: { 79 | display: true, 80 | text: `Total Pods`, 81 | color: fontColor, 82 | }, 83 | }, 84 | }; 85 | 86 | //error handling for when server isn't connected to prometheus api 87 | if (loadErr) { 88 | return ( 89 |
    90 |
    Not Connected to Prometheus API
    91 |
    92 | ); 93 | } else { 94 | return ( 95 |
    96 |

    103 | 104 |
    105 | ); 106 | } 107 | }; 108 | 109 | export default DonutChart; 110 | -------------------------------------------------------------------------------- /server/controllers/dashboardController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DashboardController, 3 | graphDataObject, 4 | start, 5 | end, 6 | res, 7 | req, 8 | next, 9 | } from '../../types'; 10 | import DataObjectBuilder from './dataObjectBuilder'; 11 | import axios from 'axios'; 12 | const k8s = require('@kubernetes/client-node'); 13 | //prometheus client for node.js 14 | const client = require('prom-client'); 15 | 16 | const kc = new k8s.KubeConfig(); 17 | kc.loadFromDefault(); 18 | //CoreV1Api: it allows access to core k8 resources such as services 19 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 20 | //AppsV1Api: it allows access to app/v1 resources such as deployments and k8s 21 | const k8sApi1 = kc.makeApiClient(k8s.AppsV1Api); 22 | //NetworkV1Api: (ingress) - Ingress is a collection of rules that allow inbound connections to reach the endpoints defined by a backend. An Ingress can be configured to give services externally-reachable urls, load balance traffic, terminate SSL, offer name based virtual hosting etc. 23 | //https://docs.okd.io/latest/rest_api/network_apis/ingress-networking-k8s-io-v1.html 24 | const k8sApi3 = kc.makeApiClient(k8s.NetworkingV1Api); 25 | //to collect default metrics directly from prometheus client 26 | //https://github.com/siimon/prom-client 27 | client.collectDefaultMetrics(); 28 | 29 | const dashboardController: DashboardController = { 30 | getAllMetrics: async (req, res, next) => { 31 | const queryObject: graphDataObject = { 32 | linegraph: { 33 | totalCpu: 'sum(rate(container_cpu_usage_seconds_total[10m]))*100', 34 | totalMem: 'sum(container_memory_usage_bytes)', 35 | totalTransmit: 'sum(rate(node_network_transmit_bytes_total[10m]))', 36 | totalReceive: 'sum(rate(node_network_receive_bytes_total[10m]))', 37 | }, 38 | donutint: { 39 | totalPods: 'count(kube_pod_info)', 40 | notReadyPods: 'sum(kube_pod_status_ready{condition="false"})', 41 | totalNamespaces: 'count(kube_namespace_created)', 42 | }, 43 | }; 44 | try { 45 | req.app.locals.queries = queryObject; 46 | return next(); 47 | } catch (err) { 48 | return next({ 49 | log: `Error in dashboardController.getAllMetrics: ${err}`, 50 | status: 500, 51 | message: 'Error occured while retrieving dashboard all metrics data', 52 | }); 53 | } 54 | }, 55 | 56 | cpuUsageOverTotalCpu: async (req, res, next) => { 57 | const queryObject: graphDataObject = { 58 | cpubarchart: { 59 | cpu: 'sum(rate(container_cpu_usage_seconds_total[5m]))', 60 | core: 'sum(machine_cpu_cores)', 61 | percent: 62 | 'sum(rate(container_cpu_usage_seconds_total[5m]))/sum(machine_cpu_cores)*100', 63 | }, 64 | }; 65 | try { 66 | req.app.locals.queries = queryObject; 67 | return next(); 68 | } catch (err) { 69 | return next({ 70 | log: `Error in dashboardController.getTotalCpu: ${err}`, 71 | status: 500, 72 | message: 'Error occured while retrieving dashboard transmit data', 73 | }); 74 | } 75 | }, 76 | }; 77 | 78 | export default dashboardController; 79 | -------------------------------------------------------------------------------- /client/styles/popup.scss: -------------------------------------------------------------------------------- 1 | $nav: #1d1b1d; 2 | $PersianGreen: rgb(51, 153, 137); 3 | $PacificBlue: rgb(80, 178, 192); 4 | $OperaMauve: rgb(172, 128, 160); 5 | $background: rgb(31, 28, 30); 6 | $fontColor: rgba(136, 217, 230, 0.8); 7 | $font: 'Playfair Display', serif; 8 | $theme-map: null; 9 | 10 | $themes: ( 11 | dark: ( 12 | 'bg': $background, 13 | 'font': $fontColor, 14 | 'header': $PacificBlue, 15 | 'box-shadow': rgba(51, 153, 137, 0.3) 0px 1px 2px 0px, 16 | 'box-shadow2': rgba(51, 153, 137, 0.15) 0px 2px 6px 2px, 17 | 'drop-hover': rgb(151, 150, 150), 18 | 'header-back': $background, 19 | 'chart-back': transparent, 20 | 'chart-font': rgba(54, 133, 181, 1), 21 | 'nav-back': $nav, 22 | 'pod-good': $PersianGreen, 23 | 'graph-font': $PacificBlue, 24 | ), 25 | light: ( 26 | 'bg': #c9c5ba, 27 | 'font': #5d536b, 28 | 'header': #696773, 29 | 'box-shadow': #c9c5ba, 30 | 'box-shadow2': #c9c5ba, 31 | 'drop-hover': #ceede0, 32 | 'header-back': #97b1a6, 33 | 'chart-back': #e5e0d4, 34 | 'chart-font': #363946, 35 | 'nav-back': #97b1a6, 36 | 'nav-font': #272838, 37 | 'pod-good': #97b1a6, 38 | 'graph-font': rgb(46, 45, 45), 39 | ), 40 | ); 41 | 42 | @mixin themify($themes: $themes) { 43 | @each $theme, $map in $themes { 44 | .theme-#{$theme} & { 45 | $theme-map: () !global; 46 | @each $key, $submap in $map { 47 | $value: map-get(map-get($themes, $theme), '#{$key}'); 48 | $theme-map: map-merge( 49 | $theme-map, 50 | ( 51 | $key: $value, 52 | ) 53 | ) !global; 54 | } 55 | 56 | @content; 57 | $theme-map: null !global; 58 | } 59 | } 60 | } 61 | 62 | @function themed($key) { 63 | @return map-get($theme-map, $key); 64 | } 65 | 66 | #popup { 67 | position: fixed; 68 | background: rgba(6, 24, 38, 0.7); 69 | top: 0; 70 | left: 0; 71 | width: 100%; 72 | height: 100vh; 73 | display: flex; 74 | justify-content: center; 75 | align-items: center; 76 | z-index: 1; 77 | 78 | #popup-inner { 79 | position: relative; 80 | display: flex; 81 | flex-direction: column; 82 | padding: 50px; 83 | opacity: 100%; 84 | height: calc(100% - 10vh); 85 | width: 90%; 86 | overflow: scroll; 87 | align-content: center; 88 | z-index: 1; 89 | @include themify($themes) { 90 | background-color: themed('nav-back'); 91 | } 92 | 93 | h2 { 94 | position: relative; 95 | @include themify($themes) { 96 | color: themed('font'); 97 | } 98 | font-weight: bold; 99 | font-size: 50px; 100 | align-self: center; 101 | } 102 | .data-container { 103 | grid-area: main; 104 | display: flex; 105 | flex-direction: column; 106 | } 107 | .close-btn { 108 | position: fixed; 109 | padding: 5px; 110 | right: 4%; 111 | top: 50px; 112 | background: transparent; 113 | @include themify($themes) { 114 | color: themed('font'); 115 | } 116 | font-size: 20px; 117 | border-style: none; 118 | transform: translateX(-90%); 119 | &:hover { 120 | font-size: 30px; 121 | cursor: pointer; 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /dist/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * @kurkle/color v0.2.1 3 | * https://github.com/kurkle/color#readme 4 | * (c) 2022 Jukka Kurkela 5 | * Released under the MIT License 6 | */ 7 | 8 | /*! 9 | * Chart.js v3.9.1 10 | * https://www.chartjs.org 11 | * (c) 2022 Chart.js Contributors 12 | * Released under the MIT License 13 | */ 14 | 15 | /*! 16 | * chartjs-plugin-streaming v2.0.0 17 | * https://nagix.github.io/chartjs-plugin-streaming 18 | * (c) 2017-2021 Akihiko Kusanagi 19 | * Released under the MIT license 20 | */ 21 | 22 | /** 23 | * @license React 24 | * react-dom.production.min.js 25 | * 26 | * Copyright (c) Facebook, Inc. and its affiliates. 27 | * 28 | * This source code is licensed under the MIT license found in the 29 | * LICENSE file in the root directory of this source tree. 30 | */ 31 | 32 | /** 33 | * @license React 34 | * react-is.production.min.js 35 | * 36 | * Copyright (c) Facebook, Inc. and its affiliates. 37 | * 38 | * This source code is licensed under the MIT license found in the 39 | * LICENSE file in the root directory of this source tree. 40 | */ 41 | 42 | /** 43 | * @license React 44 | * react.production.min.js 45 | * 46 | * Copyright (c) Facebook, Inc. and its affiliates. 47 | * 48 | * This source code is licensed under the MIT license found in the 49 | * LICENSE file in the root directory of this source tree. 50 | */ 51 | 52 | /** 53 | * @license React 54 | * scheduler.production.min.js 55 | * 56 | * Copyright (c) Facebook, Inc. and its affiliates. 57 | * 58 | * This source code is licensed under the MIT license found in the 59 | * LICENSE file in the root directory of this source tree. 60 | */ 61 | 62 | /** 63 | * @license React 64 | * use-sync-external-store-shim.production.min.js 65 | * 66 | * Copyright (c) Facebook, Inc. and its affiliates. 67 | * 68 | * This source code is licensed under the MIT license found in the 69 | * LICENSE file in the root directory of this source tree. 70 | */ 71 | 72 | /** 73 | * @license React 74 | * use-sync-external-store-shim/with-selector.production.min.js 75 | * 76 | * Copyright (c) Facebook, Inc. and its affiliates. 77 | * 78 | * This source code is licensed under the MIT license found in the 79 | * LICENSE file in the root directory of this source tree. 80 | */ 81 | 82 | /** 83 | * @remix-run/router v1.0.3 84 | * 85 | * Copyright (c) Remix Software Inc. 86 | * 87 | * This source code is licensed under the MIT license found in the 88 | * LICENSE.md file in the root directory of this source tree. 89 | * 90 | * @license MIT 91 | */ 92 | 93 | /** 94 | * React Router DOM v6.4.3 95 | * 96 | * Copyright (c) Remix Software Inc. 97 | * 98 | * This source code is licensed under the MIT license found in the 99 | * LICENSE.md file in the root directory of this source tree. 100 | * 101 | * @license MIT 102 | */ 103 | 104 | /** 105 | * React Router v6.4.3 106 | * 107 | * Copyright (c) Remix Software Inc. 108 | * 109 | * This source code is licensed under the MIT license found in the 110 | * LICENSE.md file in the root directory of this source tree. 111 | * 112 | * @license MIT 113 | */ 114 | 115 | /** @license React v16.13.1 116 | * react-is.production.min.js 117 | * 118 | * Copyright (c) Facebook, Inc. and its affiliates. 119 | * 120 | * This source code is licensed under the MIT license found in the 121 | * LICENSE file in the root directory of this source tree. 122 | */ 123 | -------------------------------------------------------------------------------- /client/components/BarChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | BarElement, 7 | Title, 8 | Tooltip, 9 | Legend, 10 | ChartOptions, 11 | ChartData, 12 | } from 'chart.js'; 13 | import { Bar } from 'react-chartjs-2'; 14 | 15 | ChartJS.register( 16 | CategoryScale, 17 | LinearScale, 18 | BarElement, 19 | Title, 20 | Tooltip, 21 | Legend 22 | ); 23 | 24 | type BarType = { 25 | title: string; 26 | url: string; 27 | labels: string[]; 28 | }; 29 | 30 | const BarChart = ({ title, url, labels }: BarType) => { 31 | const [loadErr, setLoadErr] = useState(false); 32 | const initialData: ChartData<'bar'> = { 33 | datasets: [], 34 | }; 35 | 36 | ChartJS.defaults.datasets.bar.barThickness = 35; 37 | const [barChartData, setBarChartData] = 38 | useState>(initialData); 39 | const option: ChartOptions<'bar'> = { 40 | indexAxis: 'y', 41 | elements: { 42 | bar: { 43 | borderWidth: 2, 44 | borderSkipped: false, 45 | }, 46 | }, 47 | interaction: { 48 | intersect: false, 49 | }, 50 | responsive: false, 51 | plugins: { 52 | legend: { 53 | display: true, 54 | }, 55 | title: { 56 | display: true, 57 | text: title, 58 | color: 'rgb(53,162,235)', 59 | }, 60 | }, 61 | scales: { 62 | y: { 63 | stacked: true, 64 | grid: { 65 | display: false, 66 | drawBorder: false, 67 | drawTicks: false, 68 | }, 69 | ticks: { 70 | display: false, 71 | }, 72 | }, 73 | x: { 74 | stacked: true, 75 | grid: { 76 | display: false, 77 | drawBorder: false, 78 | drawTicks: false, 79 | }, 80 | ticks: { 81 | display: false, 82 | }, 83 | }, 84 | }, 85 | }; 86 | 87 | //main use case for bar chart is to display overall core cpu usage over total cpu usage 88 | useEffect(() => { 89 | fetch(url) 90 | .then((res) => res.json()) 91 | .then((data) => { 92 | const metrics = data; 93 | const percentage = metrics.percent.slice(0, 4) + '%'; 94 | const xAxis = [percentage]; 95 | let yAxis = [metrics.cpu, metrics.core]; 96 | const newData: ChartData<'bar'> = { 97 | labels: xAxis, 98 | datasets: [ 99 | { 100 | label: labels[0], 101 | data: yAxis, 102 | backgroundColor: 'rgb(52,162,235)', 103 | borderColor: 'rgb(52,162,235)', 104 | }, 105 | { 106 | label: labels[1], 107 | data: yAxis[1], 108 | backgroundColor: 'rgba(54, 133, 181, 1)', 109 | borderColor: 'rgba(54, 133, 181, 1)', 110 | }, 111 | ], 112 | }; 113 | setBarChartData(newData); 114 | }) 115 | .catch((err) => { 116 | console.log(err); 117 | setLoadErr(true); 118 | }); 119 | }, []); 120 | 121 | if (loadErr) { 122 | return ( 123 |
    124 |
    Not Connected to Prometheus API
    125 |
    126 | ); 127 | } else { 128 | return ( 129 |
    130 | 131 |
    132 | ); 133 | } 134 | }; 135 | 136 | export default BarChart; 137 | -------------------------------------------------------------------------------- /coverage/lcov-report/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for All files 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
    21 |
    22 |

    All files

    23 |
    24 | 25 |
    26 | Unknown% 27 | Statements 28 | 0/0 29 |
    30 | 31 | 32 |
    33 | Unknown% 34 | Branches 35 | 0/0 36 |
    37 | 38 | 39 |
    40 | Unknown% 41 | Functions 42 | 0/0 43 |
    44 | 45 | 46 |
    47 | Unknown% 48 | Lines 49 | 0/0 50 |
    51 | 52 | 53 |
    54 |

    55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

    57 | 63 |
    64 |
    65 |
    66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
    FileStatementsBranchesFunctionsLines
    83 |
    84 |
    85 |
    86 | 91 | 92 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /client/components/LiveChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import { State } from '../../types'; 4 | import { useSelector } from 'react-redux'; 5 | import 'chartjs-adapter-luxon'; 6 | import ChartStreaming from 'chartjs-plugin-streaming'; 7 | import { Filler } from 'chart.js'; 8 | 9 | import { 10 | Chart as ChartJS, 11 | CategoryScale, 12 | LinearScale, 13 | PointElement, 14 | LineElement, 15 | Title, 16 | Tooltip, 17 | Legend, 18 | ChartOptions, 19 | ChartData, 20 | } from 'chart.js'; 21 | 22 | ChartJS.register( 23 | CategoryScale, 24 | LinearScale, 25 | PointElement, 26 | LineElement, 27 | Title, 28 | Tooltip, 29 | Legend, 30 | Filler, 31 | ChartStreaming 32 | ); 33 | 34 | type LineChartDataType = { 35 | label: string; 36 | path: string; 37 | title: string; 38 | type: string; 39 | }; 40 | 41 | const LiveChart = ({ label, path, title, type }: LineChartDataType) => { 42 | const liveChart = useRef>(); 43 | const [loadErr, setLoadErr] = useState(false); 44 | let sse: EventSource; 45 | 46 | //effect to set up connection to server sent event 47 | useEffect(() => { 48 | sse = new EventSource(path); 49 | const links = Array.from(document.getElementsByClassName('link')).slice(1); 50 | links.map((link) => { 51 | link.addEventListener('click', () => { 52 | sse.close(); 53 | }); 54 | }); 55 | }, []); 56 | 57 | let fontColor = '#6c887c'; 58 | 59 | const initialData: ChartData<'line'> = { 60 | datasets: [ 61 | { 62 | data: [{ x: 0, y: 0 }], 63 | backgroundColor: '#97b1a6', 64 | borderColor: '#97b1a6', 65 | label: label, 66 | fill: true, 67 | }, 68 | ], 69 | }; 70 | 71 | const [lineChartData, setLineChartData] = useState(initialData); 72 | const [maxY, setMax] = useState(0); 73 | 74 | const options: ChartOptions<'line'> = { 75 | layout: { 76 | padding: { 77 | // top: 10, 78 | }, 79 | }, 80 | plugins: { 81 | legend: { 82 | position: 'top', 83 | labels: { 84 | color: fontColor, 85 | font: { 86 | size: 14, 87 | }, 88 | }, 89 | }, 90 | title: { 91 | display: true, 92 | text: title, 93 | color: fontColor, 94 | }, 95 | }, 96 | scales: { 97 | y: { 98 | min: 0, 99 | display: true, 100 | axis: 'y', 101 | title: { 102 | display: true, 103 | text: type, 104 | }, 105 | ticks: { 106 | color: fontColor, 107 | }, 108 | }, 109 | x: { 110 | type: 'realtime', 111 | realtime: { 112 | duration: 20000, 113 | refresh: 1000, 114 | delay: 2000, 115 | }, 116 | display: true, 117 | axis: 'x', 118 | title: { 119 | display: true, 120 | text: 'Time', 121 | }, 122 | ticks: { 123 | color: fontColor, 124 | }, 125 | }, 126 | }, 127 | }; 128 | 129 | //function to add data to the datasets 130 | useEffect(() => { 131 | if (sse) { 132 | sse.onmessage = (event) => { 133 | const data = JSON.parse(event.data); 134 | const currentTime = new Date(data[0] * 1000); 135 | let time: string = currentTime.toLocaleString('en-GB'); 136 | time = time.slice(time.indexOf(',') + 1).trim(); 137 | let value = data[1] / 1_000_000; 138 | liveChart.current?.data.datasets[0].data.push({ 139 | x: time, 140 | y: value, 141 | }); 142 | liveChart.current?.update('quiet'); 143 | }; 144 | sse.onerror = (event) => { 145 | setLoadErr(true); 146 | return sse.close(); 147 | }; 148 | } 149 | }); 150 | //Conditionally render if there is a load error 151 | if (loadErr) { 152 | return ( 153 |
    154 |
    Not Connected to Prometheus API
    155 |
    156 | ); 157 | } else { 158 | return ; 159 | } 160 | }; 161 | 162 | export default LiveChart; 163 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const cookieParser = require('cookie-parser'); 4 | import { Request, Response, NextFunction } from 'express'; 5 | import dashboardRouter from './routes/dashboard'; 6 | import kubernetesRouter from './routes/kubernetes'; 7 | import mongoRouter from './routes/mongo'; 8 | import { ErrorType } from '../types'; 9 | import axios from 'axios'; 10 | 11 | const cors = require('cors'); 12 | const app = express(); 13 | const PORT = 3000; 14 | 15 | app.use(cookieParser()); 16 | app.use(cors()); 17 | app.use(express.json()); 18 | app.use(express.urlencoded({ extended: true })); 19 | 20 | app.use((req: Request, res: Response, next: NextFunction) => { 21 | const cookie: string = req.cookies.cookieName; 22 | if (cookie == undefined) { 23 | let randomNum = Math.random().toString(); 24 | randomNum = randomNum.substring(2, randomNum.length); 25 | res.cookie('cookieName', randomNum, { maxAge: 90000, httpOnly: true }); 26 | } 27 | return next(); 28 | }); 29 | 30 | if (process.env.NODE_ENV) { 31 | app.use('/', express.static(path.join(__dirname, '../dist'))); 32 | } 33 | 34 | app.use('/kubernetes', (req: Request, res: Response) => { 35 | express.static(path.join(__dirname, '../client/index.html')); 36 | }); 37 | 38 | app.use('/mongo', (req: Request, res: Response) => { 39 | express.static(path.join(__dirname, '../client/index.html')); 40 | }); 41 | 42 | app.use('/api/dashboard', dashboardRouter); 43 | 44 | app.use('/api/kubernetesMetrics', kubernetesRouter); 45 | 46 | app.use('/api/mongodb', mongoRouter); 47 | 48 | app.get('/dashboard', (req: Request, res: Response) => { 49 | return res.status(200).send('hi'); 50 | }); 51 | 52 | //network received/transmitted 53 | 54 | app.use('/live/received', async (req: Request, res: Response) => { 55 | const headers = { 56 | 'Content-Type': 'text/event-stream', 57 | Connection: 'keep-alive', 58 | 'Cache-Control': 'no-cache', 59 | }; 60 | res.writeHead(200, headers); 61 | const timer = setInterval(async () => { 62 | const start = new Date(Date.now() - 10000).toISOString(); 63 | const end = new Date(Date.now() - 5000).toISOString(); 64 | const response = await axios.get( 65 | `http://localhost:9090/api/v1/query_range?query=sum(rate(node_network_receive_bytes_total[10m]))&start=${start}&end=${end}&step=10m` 66 | ); 67 | const data = response.data.data.result[0].values[0]; 68 | const newData = `data: ${JSON.stringify(data)}\n\n`; 69 | res.write(newData); 70 | }, 1500); 71 | res.on('close', () => { 72 | res.end(); 73 | clearInterval(timer); 74 | console.log('Connection closed'); 75 | }); 76 | }); 77 | 78 | app.use('/live/transmit', async (req: Request, res: Response) => { 79 | const headers = { 80 | 'Content-Type': 'text/event-stream', 81 | Connection: 'keep-alive', 82 | 'Cache-Control': 'no-cache', 83 | }; 84 | res.writeHead(200, headers); 85 | const timer = setInterval(async () => { 86 | const start = new Date(Date.now() - 10000).toISOString(); 87 | const end = new Date(Date.now() - 5000).toISOString(); 88 | const response = await axios.get( 89 | `http://localhost:9090/api/v1/query_range?query=sum(rate(node_network_transmit_bytes_total[10m]))&start=${start}&end=${end}&step=5m` 90 | ); 91 | const data = response.data.data.result[0].values[0]; 92 | const newData = `data: ${JSON.stringify(data)}\n\n`; 93 | res.write(newData); 94 | }, 1500); 95 | res.on('close', () => { 96 | res.end(); 97 | clearInterval(timer); 98 | console.log('Connection closed'); 99 | }); 100 | }); 101 | 102 | //redirect to page 404 when endpoint does not exist 103 | app.use('*', (req: Request, res: Response) => { 104 | return res.status(404).send('404 Page Not Found'); 105 | }); 106 | 107 | //global error handling 108 | app.use((err: unknown, req: Request, res: Response, next: NextFunction) => { 109 | const defaultErr: ErrorType = { 110 | log: 'Express error handler caught unknown middleware error', 111 | status: 500, 112 | message: { err: 'An error occurred' }, 113 | }; 114 | const errorObj = Object.assign({}, defaultErr, err); 115 | console.log(errorObj.log); 116 | return res.status(errorObj.status).json(errorObj.message); 117 | }); 118 | 119 | //start app on port 120 | app.listen(PORT, () => { 121 | console.log(`Server listening on port: ${PORT}...`); 122 | }); 123 | 124 | setTimeout; 125 | module.exports = app; 126 | -------------------------------------------------------------------------------- /client/components/LineChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import { Filler } from 'chart.js'; 4 | import { State } from '../../types'; 5 | import { useSelector } from 'react-redux'; 6 | 7 | import { 8 | Chart as ChartJS, 9 | CategoryScale, 10 | LinearScale, 11 | PointElement, 12 | LineElement, 13 | Title, 14 | Tooltip, 15 | Legend, 16 | ChartOptions, 17 | ChartData, 18 | } from 'chart.js'; 19 | 20 | ChartJS.register( 21 | CategoryScale, 22 | LinearScale, 23 | PointElement, 24 | LineElement, 25 | Title, 26 | Tooltip, 27 | Legend, 28 | Filler 29 | ); 30 | 31 | type LineChartDataType = { 32 | data: any[]; 33 | label: string; 34 | yAxis: string; 35 | title: string; 36 | }; 37 | 38 | const LineChart = ({ data, label, yAxis, title }: LineChartDataType) => { 39 | const dark = useSelector((state: State) => state.dark); 40 | let fontColor; 41 | dark ? (fontColor = '#363946') : (fontColor = 'rgba(136, 217, 230, 0.8)'); 42 | const initialData: ChartData<'line'> = { 43 | datasets: [], 44 | }; 45 | 46 | const [lineChartData, setLineChartData] = 47 | useState>(initialData); 48 | const [loadErr, setLoadErr] = useState(false); 49 | 50 | const options: ChartOptions<'line'> = { 51 | responsive: true, 52 | interaction: { 53 | intersect: false, 54 | }, 55 | layout: { 56 | padding: { 57 | right: 15, 58 | }, 59 | }, 60 | plugins: { 61 | legend: { 62 | position: 'top', 63 | labels: { 64 | color: fontColor, 65 | font: { 66 | size: 18, 67 | }, 68 | }, 69 | }, 70 | title: { 71 | display: true, 72 | text: title, 73 | color: fontColor, 74 | }, 75 | }, 76 | scales: { 77 | y: { 78 | display: true, 79 | axis: 'y', 80 | title: { 81 | display: true, 82 | text: yAxis, 83 | }, 84 | ticks: { 85 | color: fontColor, 86 | }, 87 | }, 88 | x: { 89 | display: true, 90 | axis: 'x', 91 | title: { 92 | display: true, 93 | text: 'Time', 94 | }, 95 | ticks: { 96 | color: fontColor, 97 | }, 98 | }, 99 | }, 100 | }; 101 | 102 | useEffect(() => { 103 | if (data) { 104 | const metrics = data[0]; 105 | let yAxisData: number[] = []; 106 | let xAxis = metrics; 107 | if (data.length > 0) { 108 | //convert long number metric from prometheus into a readable time stamp 109 | xAxis = metrics.map((value: [number, string]) => { 110 | const currentTime = new Date(value[0] * 1000); 111 | let time = currentTime.toLocaleString('en-GB'); 112 | time = time.slice(time.indexOf(',') + 1).trim(); 113 | return time; 114 | }); 115 | switch (yAxis) { 116 | case 'Kilobytes': 117 | yAxisData = metrics.map( 118 | (value: [number, string]) => Number(value[1]) / 1000000 119 | ); 120 | break; 121 | default: 122 | yAxisData = metrics.map((value: [number, string]) => 123 | Number(value[1]) 124 | ); 125 | } 126 | } 127 | const newData: ChartData<'line'> = { 128 | labels: xAxis, 129 | datasets: [ 130 | { 131 | label: label, 132 | data: yAxisData, 133 | backgroundColor: 'rgba(54, 133, 181, 0.8)', 134 | borderColor: 'rgba(54, 133, 181, 1)', 135 | borderWidth: 1.5, 136 | pointRadius: 1, 137 | tension: 0.4, 138 | pointBorderWidth: 1.5, 139 | pointHoverRadius: 3, 140 | fill: true, 141 | }, 142 | ], 143 | }; 144 | setLineChartData(newData); 145 | } 146 | 147 | //error handling for when no data exists 148 | if (data == undefined) { 149 | setLoadErr(true); 150 | } 151 | }, [data]); 152 | 153 | if (loadErr) { 154 | return ( 155 |
    156 |
    Not Connected to Prometheus API
    157 |
    158 | ); 159 | } else { 160 | //conditionally render either a line chart if data is present or 161 | //a separate div if no data is being sent back 162 | return ( 163 |
    164 | 165 | {data.length === 0 ? ( 166 |
    167 |

    No Data to Display

    168 |
    169 | ) : ( 170 | '' 171 | )} 172 |
    173 | ); 174 | } 175 | }; 176 | 177 | export default LineChart; 178 | -------------------------------------------------------------------------------- /coverage/lcov-report/routes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for routes 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
    21 |
    22 |

    All files routes

    23 |
    24 | 25 |
    26 | 70.58% 27 | Statements 28 | 12/17 29 |
    30 | 31 | 32 |
    33 | 71.42% 34 | Branches 35 | 5/7 36 |
    37 | 38 | 39 |
    40 | 16.66% 41 | Functions 42 | 1/6 43 |
    44 | 45 | 46 |
    47 | 70.58% 48 | Lines 49 | 12/17 50 |
    51 | 52 | 53 |
    54 |

    55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

    57 | 63 |
    64 |
    65 |
    66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
    FileStatementsBranchesFunctionsLines
    kubernetes.js 84 |
    85 |
    70.58%12/1771.42%5/716.66%1/670.58%12/17
    98 |
    99 |
    100 |
    101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /coverage/lcov-report/controllers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for controllers 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
    21 |
    22 |

    All files controllers

    23 |
    24 | 25 |
    26 | 36.17% 27 | Statements 28 | 17/47 29 |
    30 | 31 | 32 |
    33 | 38.88% 34 | Branches 35 | 7/18 36 |
    37 | 38 | 39 |
    40 | 5.55% 41 | Functions 42 | 1/18 43 |
    44 | 45 | 46 |
    47 | 40.47% 48 | Lines 49 | 17/42 50 |
    51 | 52 | 53 |
    54 |

    55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

    57 | 63 |
    64 |
    65 |
    66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
    FileStatementsBranchesFunctionsLines
    kubernetesController.js 84 |
    85 |
    36.17%17/4738.88%7/185.55%1/1840.47%17/42
    98 |
    99 |
    100 |
    101 | 106 | 107 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | peterchoi3000@gmail.com 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /client/containers/mainpage.tsx: -------------------------------------------------------------------------------- 1 | const React = require('react'); 2 | import NavBar from '../components/Navbar'; 3 | import BarChart from '../components/BarChart'; 4 | import LineChart from '../components/LineChart'; 5 | import LiveChart from '../components/LiveChart'; 6 | const styles = require('../styles/index.scss'); 7 | const styles2 = require('../styles/colors.scss'); 8 | import { useState, useEffect } from 'react'; 9 | import { useDispatch, useSelector } from 'react-redux'; 10 | import { State } from '../../types'; 11 | import { addNamespaces } from '../getData'; 12 | import { AppDispatch } from '../store'; 13 | import { currentPage } from '../rootReducer'; 14 | import { BounceLoader } from 'react-spinners'; 15 | import axios from 'axios'; 16 | import DonutChart from '../components/DonutChart'; 17 | import { MainDataType } from '../../types'; 18 | 19 | const MainPage = () => { 20 | const dispatch = useDispatch(); 21 | const [loaded, setLoaded] = useState(true); 22 | //the bounce loader will only render the first time the page loads 23 | const [firstLoad, setfirstLoad] = useState(false); 24 | const [data, setData] = useState({ 25 | totalCpu: [], 26 | totalMem: [], 27 | totalTransmit: [], 28 | totalReceive: [], 29 | totalPods: 0, 30 | notReadyPods: 0, 31 | totalNamespaces: 0, 32 | }); 33 | 34 | //helper function to grab metrics 35 | const getData = async (url: string): Promise => { 36 | try { 37 | const response = await axios.get(url); 38 | const metrics = await response.data; 39 | setData(metrics); 40 | } catch (err) { 41 | console.log('Main page: ', err); 42 | } 43 | }; 44 | 45 | //grab all metrics from our server on first load 46 | useEffect(() => { 47 | getData('/api/dashboard/getAllMetrics'); 48 | }, []); 49 | 50 | useEffect(() => { 51 | //invoking addNamespace function from root reducer 52 | dispatch(addNamespaces()); 53 | //highlight navbar main page button 54 | dispatch(currentPage('main')); 55 | setfirstLoad(true); 56 | //to wait 6s before the namespace number loads 57 | setTimeout(() => { 58 | setLoaded(false); 59 | }, 6000); 60 | }, []); 61 | 62 | const namespaces = useSelector((state: State) => state.namespaces); 63 | let nameLength; 64 | if (namespaces) { 65 | nameLength = namespaces.length; 66 | } 67 | let theme: string; 68 | 69 | //Create line charts 70 | const lineObject: { [key: string]: any[] } = { 71 | totalCpu: [data.totalCpu, 'Cpu Usage', 'Percent', 'Total CPU % Usage'], 72 | totalMem: [ 73 | data.totalMem, 74 | 'Mem Usage', 75 | 'Kilobytes', 76 | 'Total Memory Usage (kB)', 77 | ], 78 | totalRec: [ 79 | data.totalReceive, 80 | 'Byte Usage', 81 | 'Kilobytes', 82 | 'Netword Received (kB)', 83 | ], 84 | totalTrans: [ 85 | data.totalTransmit, 86 | 'Byte Usage', 87 | 'Kilobytes', 88 | 'Network Transmitted (kB)', 89 | ], 90 | }; 91 | 92 | const charts: JSX.Element[] = []; 93 | 94 | //Object used to generate line objects 95 | for (let info in lineObject) { 96 | charts.push( 97 |
    98 | 105 |
    106 | ); 107 | } 108 | 109 | return ( 110 |
    111 |
    112 |

    Odin's Eye

    113 |
    114 | 115 |
    116 |
    117 | {loaded && !firstLoad ? ( 118 | 129 | ) : ( 130 |
    131 | Total Namespaces 132 |
    {nameLength}
    133 |
    134 | )} 135 |
    136 | 141 |
    142 |
    143 |
    144 | 148 |
    149 |
    150 |
    151 |
    152 |
    153 | 160 |
    161 |
    162 | 169 |
    170 | {charts} 171 |
    172 |
    173 |
    174 | ); 175 | }; 176 | 177 | export default MainPage; 178 | -------------------------------------------------------------------------------- /server/controllers/kubernetesController.ts: -------------------------------------------------------------------------------- 1 | import { KubernetesController, start, end, req, res, next } from '../../types'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import { graphDataObject } from '../../types'; 4 | import DataObjectBuilder from './dataObjectBuilder'; 5 | import axios from 'axios'; 6 | 7 | const kubernetesController: KubernetesController = { 8 | namespaceNames: async (req: Request, res: Response, next: NextFunction) => { 9 | const namespaceQuery = 'sum+by+(namespace)+(kube_pod_info)'; 10 | try { 11 | const response = await axios.get( 12 | `http://localhost:9090/api/v1/query_range?query=${namespaceQuery}&start=${start}&end=${end}&step=5m` 13 | ); 14 | const array = await response.data.data.result; 15 | const namespaceArray: string[] = []; 16 | array.forEach((element: any) => { 17 | namespaceArray.push(element.metric.namespace); 18 | }); 19 | res.locals.namespaceNames = namespaceArray; 20 | return next(); 21 | } catch (err) { 22 | return next({ 23 | log: `Error in kubernetesController.nameSpaceNames: ${err}`, 24 | status: 500, 25 | message: 'Error occured while retrieving namespace names data', 26 | }); 27 | } 28 | }, 29 | 30 | podNames: async (req, res, next) => { 31 | const namespace = req.query.namespace; 32 | const podNameQuery = `sum by (pod)(kube_pod_info{namespace="${namespace}"})`; 33 | try { 34 | const response = await axios.get( 35 | `http://localhost:9090/api/v1/query_range?query=${podNameQuery}&start=${start}&end=${end}&step=5m` 36 | ); 37 | const array = response.data.data.result; 38 | const podNameArray: string[] = []; 39 | array.forEach((element: any) => { 40 | podNameArray.push(element.metric.pod); 41 | }); 42 | res.locals.names = podNameArray; 43 | return next(); 44 | } catch (err) { 45 | return next({ 46 | log: `Error in kubernetesController.podNames: ${err}`, 47 | status: 500, 48 | message: 'Error occured while retrieving pod names', 49 | }); 50 | } 51 | }, 52 | 53 | podsNotReadyNames: async (req, res, next) => { 54 | const { namespace, podData } = req.query; 55 | if (Array.isArray(podData)) { 56 | const promises = podData.map(async (name) => { 57 | const readyQuery = `sum(kube_pod_status_ready{condition="false",namespace="${namespace}",pod="${name}"})`; 58 | try { 59 | const response = await axios.get( 60 | `http://localhost:9090/api/v1/query_range?query=${readyQuery}&start=${start}&end=${end}&step=5m` 61 | ); 62 | if (!response) { 63 | return [undefined, name]; 64 | } 65 | const status = response.data.data.result[0].values[0][1]; 66 | if (parseInt(status) > 0) { 67 | return [status, name]; 68 | } 69 | } catch (err) { 70 | return next({ 71 | log: `Error in kubernetesController.podsNotReady: ${err}`, 72 | status: 500, 73 | message: 'Error occured while retrieving pods not ready data', 74 | }); 75 | } 76 | }); 77 | await Promise.all(promises).then((data) => { 78 | res.locals.status = data; 79 | }); 80 | } 81 | return next(); 82 | }, 83 | 84 | getNameSpaceMetrics: async (req, res, next) => { 85 | const objectData: any = {}; 86 | const { namespaceName } = req.params; 87 | const queryObject: graphDataObject = { 88 | linegraph: { 89 | restarts: `sum(changes(kube_pod_status_ready{condition="true", namespace = "${namespaceName}"}[5m]))`, 90 | ready: `sum(kube_pod_status_ready{condition="true", namespace = "${namespaceName}"})`, 91 | cpu: `sum(rate(container_cpu_usage_seconds_total{container="", namespace=~"${namespaceName}"}[10m]))`, 92 | memory: `sum(rate(container_memory_usage_bytes{container="", namespace=~"${namespaceName}"}[10m]))`, 93 | reception: `sum(rate(node_network_receive_bytes_total{namespace = "${namespaceName}"}[10m]))`, 94 | transmission: `sum(rate(node_network_transmit_bytes_total{namespace = "${namespaceName}"}[10m]))`, 95 | }, 96 | donutint: { 97 | notReady: `sum(kube_pod_status_ready{condition="false", namespace = "${namespaceName}"})`, 98 | }, 99 | }; 100 | try { 101 | req.app.locals.queries = queryObject; 102 | return next(); 103 | } catch (err) { 104 | return next({ 105 | log: `Error in kuberenetesController.getMetrics: ${err}`, 106 | status: 500, 107 | message: 'Error occured while retrieving getMetrics data', 108 | }); 109 | } 110 | }, 111 | 112 | getPodMetrics: async (req, res, next) => { 113 | const { podName } = req.params; 114 | const queryObject: graphDataObject = { 115 | linegraph: { 116 | restarts: `sum(changes(kube_pod_status_ready{condition="true", pod = "${podName}"}[5m]))`, 117 | ready: `sum(kube_pod_status_ready{condition="false", pod = "${podName}"})`, 118 | cpu: `sum(rate(container_cpu_usage_seconds_total{container="", pod=~"${podName}"}[10m]))`, 119 | memory: `sum(rate(container_memory_usage_bytes{container="", pod=~"${podName}"}[10m]))`, 120 | reception: `sum(rate(node_network_receive_bytes_total{pod = "${podName}"}[10m]))`, 121 | transmission: `sum(rate(node_network_transmit_bytes_total{pod = "${podName}"}[10m]))`, 122 | }, 123 | }; 124 | try { 125 | req.app.locals.queries = queryObject; 126 | return next(); 127 | } catch (err) { 128 | return next({ 129 | log: `Error in kuberenetesController.getPodMetrics: ${err}`, 130 | status: 500, 131 | message: 'Error occured while retrieving getMetrics data', 132 | }); 133 | } 134 | }, 135 | }; 136 | 137 | export default kubernetesController; 138 | -------------------------------------------------------------------------------- /client/styles/colors.scss: -------------------------------------------------------------------------------- 1 | $nav: #1d1b1d; 2 | $PersianGreen: rgb(51, 153, 137); 3 | $PacificBlue: rgb(80, 178, 192); 4 | $OperaMauve: rgb(172, 128, 160); 5 | $background: rgb(31, 28, 30); 6 | $fontColor: rgba(136, 217, 230, 0.8); 7 | $font: 'Playfair Display', serif; 8 | $theme-map: null; 9 | $light-bg: #c9c5ba; 10 | $light-box: #e5e0d4; 11 | 12 | $themes: ( 13 | dark: ( 14 | 'bg': $background, 15 | 'scroll-back': $fontColor, 16 | 'scroll': lighten($background, 20%), 17 | 'font': $fontColor, 18 | 'header': $PacificBlue, 19 | 'box-shadow': rgba(51, 153, 137, 0.3) 0px 1px 2px 0px, 20 | 'box-shadow2': rgba(51, 153, 137, 0.15) 0px 2px 6px 2px, 21 | 'drop-hover': rgb(151, 150, 150), 22 | 'header-back': $background, 23 | 'chart-back': transparent, 24 | 'chart-font': rgba(54, 133, 181, 1), 25 | 'nav-back': $nav, 26 | 'pod-good': $PersianGreen, 27 | 'graph-font': $PacificBlue, 28 | 'pod-bad': rgb(187, 72, 103), 29 | ), 30 | light: ( 31 | 'bg': $light-bg, 32 | 'scroll-back': $light-box, 33 | 'scroll': darken(#c9c5ba, 10%), 34 | 'font': #5d536b, 35 | 'header': #696773, 36 | 'box-shadow': $light-bg, 37 | 'box-shadow2': $light-bg, 38 | 'drop-hover': #ceede0, 39 | 'header-back': $light-box, 40 | 'chart-back': $light-box, 41 | 'chart-font': #363946, 42 | 'nav-back': #97b1a6, 43 | 'nav-font': #272838, 44 | 'pod-good': #97b1a6, 45 | 'pod-bad': rgb(146, 19, 19), 46 | 'graph-font': rgb(46, 45, 45), 47 | ), 48 | ); 49 | 50 | @mixin themify($themes: $themes) { 51 | @each $theme, $map in $themes { 52 | .theme-#{$theme} & { 53 | $theme-map: () !global; 54 | @each $key, $submap in $map { 55 | $value: map-get(map-get($themes, $theme), '#{$key}'); 56 | $theme-map: map-merge( 57 | $theme-map, 58 | ( 59 | $key: $value, 60 | ) 61 | ) !global; 62 | } 63 | 64 | @content; 65 | $theme-map: null !global; 66 | } 67 | } 68 | } 69 | 70 | @function themed($key) { 71 | @return map-get($theme-map, $key); 72 | } 73 | 74 | body[data-theme='dark'] { 75 | background: $background; 76 | } 77 | 78 | body[data-theme='light'] { 79 | background: #c9c5ba; 80 | } 81 | 82 | #main-container { 83 | @include themify($themes) { 84 | background-color: themed('bg'); 85 | } 86 | .header { 87 | @include themify($themes) { 88 | background-color: themed('header-back'); 89 | color: themed('header'); 90 | box-shadow: themed('box-shadow'), themed('box-shadow2'); 91 | } 92 | } 93 | } 94 | 95 | //navbar color themes 96 | #nav-bar { 97 | @include themify($themes) { 98 | background-color: themed('nav-back'); 99 | box-shadow: rgba(51, 153, 137, 0.3) 0px 1px 2px 0px, 100 | rgba(51, 153, 137, 0.15) 0px 2px 6px 2px; 101 | .link { 102 | color: themed('font'); 103 | &:hover { 104 | color: $OperaMauve; 105 | } 106 | } 107 | .current { 108 | background-color: lighten(themed('nav-back'), 10%); 109 | } 110 | } 111 | 112 | #eyepatch { 113 | @include themify($themes) { 114 | &:hover { 115 | background-color: themed('nav-back'); 116 | box-shadow: 0 0 20px 20px themed('nav-back'), 117 | /* inner white */ /* middle magenta */ 0 0 100px 30px #0ff; /* outer cyan */ 118 | } 119 | color: themed('font'); 120 | } 121 | } 122 | } 123 | 124 | .data-container { 125 | .line { 126 | @include themify($themes) { 127 | box-shadow: themed('box-shadow'), themed('box-shadow2'); 128 | background-color: themed('chart-back'); 129 | } 130 | } 131 | 132 | .live { 133 | @include themify($themes) { 134 | box-shadow: themed('box-shadow'), themed('box-shadow2'); 135 | background-color: themed('chart-back'); 136 | } 137 | } 138 | .bar-chart { 139 | @include themify($themes) { 140 | background-color: themed('chart-back'); 141 | } 142 | } 143 | #total-names { 144 | @include themify($themes) { 145 | color: themed('font'); 146 | } 147 | } 148 | } 149 | 150 | //Main Page list data 151 | #list-data { 152 | @include themify($themes) { 153 | background-color: themed('header-back'); 154 | color: themed('font'); 155 | box-shadow: themed('box-shadow'), themed('box-shadow2'); 156 | } 157 | #total-names { 158 | @include themify($themes) { 159 | color: themed('font'); 160 | } 161 | } 162 | 163 | #names-num { 164 | @include themify($themes) { 165 | -webkit-text-stroke: 1px themed('chart-font'); 166 | color: themed('chart-font'); 167 | } 168 | } 169 | } 170 | 171 | //Kubernetes page list data 172 | #kube-list-data { 173 | @include themify($themes) { 174 | background-color: themed('header-back'); 175 | color: themed('font'); 176 | box-shadow: themed('box-shadow') themed('box-shadow2'); 177 | } 178 | #pod-names { 179 | @include themify($themes) { 180 | .pod-list { 181 | color: themed('font'); 182 | &:hover { 183 | color: $OperaMauve; 184 | font-size: 20px; 185 | } 186 | } 187 | .pod-list-bad { 188 | color: themed('pod-bad'); 189 | &:hover { 190 | color: $OperaMauve; 191 | font-size: 20px; 192 | } 193 | } 194 | } 195 | } 196 | #dropdown { 197 | #dropdown-but { 198 | @include themify($themes) { 199 | color: themed('font'); 200 | &:hover { 201 | background-color: themed('drop-hover'); 202 | } 203 | } 204 | } 205 | .menu { 206 | @include themify($themes) { 207 | background-color: lighten(themed('header-back'), 10%); 208 | } 209 | } 210 | .menu > li { 211 | @include themify($themes) { 212 | background-color: transparent; 213 | color: themed('font'); 214 | } 215 | } 216 | 217 | .menu > li:hover { 218 | color: #1d1b1d; 219 | @include themify($themes) { 220 | background-color: themed('drop-hover'); 221 | } 222 | } 223 | } 224 | } 225 | 226 | //Scrollbar colors 227 | ::-webkit-scrollbar-track { 228 | @include themify($themes) { 229 | background: themed('scroll-back'); 230 | } 231 | } 232 | 233 | ::-webkit-scrollbar-thumb { 234 | @include themify($themes) { 235 | background: themed('scroll'); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /server/controllers/dashboardController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (g && (g = 0, op[0] && (_ = 0)), _) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | exports.__esModule = true; 39 | var k8s = require('@kubernetes/client-node'); 40 | //prometheus client for node.js 41 | var client = require('prom-client'); 42 | var kc = new k8s.KubeConfig(); 43 | kc.loadFromDefault(); 44 | //CoreV1Api: it allows access to core k8 resources such as services 45 | var k8sApi = kc.makeApiClient(k8s.CoreV1Api); 46 | //AppsV1Api: it allows access to app/v1 resources such as deployments and k8s 47 | var k8sApi1 = kc.makeApiClient(k8s.AppsV1Api); 48 | //NetworkV1Api: (ingress) - Ingress is a collection of rules that allow inbound connections to reach the endpoints defined by a backend. An Ingress can be configured to give services externally-reachable urls, load balance traffic, terminate SSL, offer name based virtual hosting etc. 49 | //https://docs.okd.io/latest/rest_api/network_apis/ingress-networking-k8s-io-v1.html 50 | var k8sApi3 = kc.makeApiClient(k8s.NetworkingV1Api); 51 | //to collect default metrics directly from prometheus client 52 | //https://github.com/siimon/prom-client 53 | client.collectDefaultMetrics(); 54 | var dashboardController = { 55 | getAllMetrics: function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { 56 | var queryObject; 57 | return __generator(this, function (_a) { 58 | queryObject = { 59 | linegraph: { 60 | totalCpu: 'sum(rate(container_cpu_usage_seconds_total[10m]))*100', 61 | totalMem: 'sum(container_memory_usage_bytes)', 62 | totalTransmit: 'sum(rate(node_network_transmit_bytes_total[10m]))', 63 | totalReceive: 'sum(rate(node_network_receive_bytes_total[10m]))' 64 | }, 65 | donutint: { 66 | totalPods: 'count(kube_pod_info)', 67 | notReadyPods: 'sum(kube_pod_status_ready{condition="false"})', 68 | totalNamespaces: 'count(kube_namespace_created)' 69 | } 70 | }; 71 | try { 72 | req.app.locals.queries = queryObject; 73 | return [2 /*return*/, next()]; 74 | } 75 | catch (err) { 76 | return [2 /*return*/, next({ 77 | log: "Error in dashboardController.getAllMetrics: ".concat(err), 78 | status: 500, 79 | message: 'Error occured while retrieving dashboard all metrics data' 80 | })]; 81 | } 82 | return [2 /*return*/]; 83 | }); 84 | }); }, 85 | cpuUsageOverTotalCpu: function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { 86 | var queryObject; 87 | return __generator(this, function (_a) { 88 | queryObject = { 89 | cpubarchart: { 90 | cpu: 'sum(rate(container_cpu_usage_seconds_total[5m]))', 91 | core: 'sum(machine_cpu_cores)', 92 | percent: 'sum(rate(container_cpu_usage_seconds_total[5m]))/sum(machine_cpu_cores)*100' 93 | } 94 | }; 95 | try { 96 | req.app.locals.queries = queryObject; 97 | return [2 /*return*/, next()]; 98 | } 99 | catch (err) { 100 | return [2 /*return*/, next({ 101 | log: "Error in dashboardController.getTotalCpu: ".concat(err), 102 | status: 500, 103 | message: 'Error occured while retrieving dashboard transmit data' 104 | })]; 105 | } 106 | return [2 /*return*/]; 107 | }); 108 | }); } 109 | }; 110 | exports["default"] = dashboardController; 111 | -------------------------------------------------------------------------------- /coverage/lcov-report/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; padding: 0; 3 | height: 100%; 4 | } 5 | body { 6 | font-family: Helvetica Neue, Helvetica, Arial; 7 | font-size: 14px; 8 | color:#333; 9 | } 10 | .small { font-size: 12px; } 11 | *, *:after, *:before { 12 | -webkit-box-sizing:border-box; 13 | -moz-box-sizing:border-box; 14 | box-sizing:border-box; 15 | } 16 | h1 { font-size: 20px; margin: 0;} 17 | h2 { font-size: 14px; } 18 | pre { 19 | font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; 20 | margin: 0; 21 | padding: 0; 22 | -moz-tab-size: 2; 23 | -o-tab-size: 2; 24 | tab-size: 2; 25 | } 26 | a { color:#0074D9; text-decoration:none; } 27 | a:hover { text-decoration:underline; } 28 | .strong { font-weight: bold; } 29 | .space-top1 { padding: 10px 0 0 0; } 30 | .pad2y { padding: 20px 0; } 31 | .pad1y { padding: 10px 0; } 32 | .pad2x { padding: 0 20px; } 33 | .pad2 { padding: 20px; } 34 | .pad1 { padding: 10px; } 35 | .space-left2 { padding-left:55px; } 36 | .space-right2 { padding-right:20px; } 37 | .center { text-align:center; } 38 | .clearfix { display:block; } 39 | .clearfix:after { 40 | content:''; 41 | display:block; 42 | height:0; 43 | clear:both; 44 | visibility:hidden; 45 | } 46 | .fl { float: left; } 47 | @media only screen and (max-width:640px) { 48 | .col3 { width:100%; max-width:100%; } 49 | .hide-mobile { display:none!important; } 50 | } 51 | 52 | .quiet { 53 | color: #7f7f7f; 54 | color: rgba(0,0,0,0.5); 55 | } 56 | .quiet a { opacity: 0.7; } 57 | 58 | .fraction { 59 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 60 | font-size: 10px; 61 | color: #555; 62 | background: #E8E8E8; 63 | padding: 4px 5px; 64 | border-radius: 3px; 65 | vertical-align: middle; 66 | } 67 | 68 | div.path a:link, div.path a:visited { color: #333; } 69 | table.coverage { 70 | border-collapse: collapse; 71 | margin: 10px 0 0 0; 72 | padding: 0; 73 | } 74 | 75 | table.coverage td { 76 | margin: 0; 77 | padding: 0; 78 | vertical-align: top; 79 | } 80 | table.coverage td.line-count { 81 | text-align: right; 82 | padding: 0 5px 0 20px; 83 | } 84 | table.coverage td.line-coverage { 85 | text-align: right; 86 | padding-right: 10px; 87 | min-width:20px; 88 | } 89 | 90 | table.coverage td span.cline-any { 91 | display: inline-block; 92 | padding: 0 5px; 93 | width: 100%; 94 | } 95 | .missing-if-branch { 96 | display: inline-block; 97 | margin-right: 5px; 98 | border-radius: 3px; 99 | position: relative; 100 | padding: 0 4px; 101 | background: #333; 102 | color: yellow; 103 | } 104 | 105 | .skip-if-branch { 106 | display: none; 107 | margin-right: 10px; 108 | position: relative; 109 | padding: 0 4px; 110 | background: #ccc; 111 | color: white; 112 | } 113 | .missing-if-branch .typ, .skip-if-branch .typ { 114 | color: inherit !important; 115 | } 116 | .coverage-summary { 117 | border-collapse: collapse; 118 | width: 100%; 119 | } 120 | .coverage-summary tr { border-bottom: 1px solid #bbb; } 121 | .keyline-all { border: 1px solid #ddd; } 122 | .coverage-summary td, .coverage-summary th { padding: 10px; } 123 | .coverage-summary tbody { border: 1px solid #bbb; } 124 | .coverage-summary td { border-right: 1px solid #bbb; } 125 | .coverage-summary td:last-child { border-right: none; } 126 | .coverage-summary th { 127 | text-align: left; 128 | font-weight: normal; 129 | white-space: nowrap; 130 | } 131 | .coverage-summary th.file { border-right: none !important; } 132 | .coverage-summary th.pct { } 133 | .coverage-summary th.pic, 134 | .coverage-summary th.abs, 135 | .coverage-summary td.pct, 136 | .coverage-summary td.abs { text-align: right; } 137 | .coverage-summary td.file { white-space: nowrap; } 138 | .coverage-summary td.pic { min-width: 120px !important; } 139 | .coverage-summary tfoot td { } 140 | 141 | .coverage-summary .sorter { 142 | height: 10px; 143 | width: 7px; 144 | display: inline-block; 145 | margin-left: 0.5em; 146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 | } 148 | .coverage-summary .sorted .sorter { 149 | background-position: 0 -20px; 150 | } 151 | .coverage-summary .sorted-desc .sorter { 152 | background-position: 0 -10px; 153 | } 154 | .status-line { height: 10px; } 155 | /* yellow */ 156 | .cbranch-no { background: yellow !important; color: #111; } 157 | /* dark red */ 158 | .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } 159 | .low .chart { border:1px solid #C21F39 } 160 | .highlighted, 161 | .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ 162 | background: #C21F39 !important; 163 | } 164 | /* medium red */ 165 | .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } 166 | /* light red */ 167 | .low, .cline-no { background:#FCE1E5 } 168 | /* light green */ 169 | .high, .cline-yes { background:rgb(230,245,208) } 170 | /* medium green */ 171 | .cstat-yes { background:rgb(161,215,106) } 172 | /* dark green */ 173 | .status-line.high, .high .cover-fill { background:rgb(77,146,33) } 174 | .high .chart { border:1px solid rgb(77,146,33) } 175 | /* dark yellow (gold) */ 176 | .status-line.medium, .medium .cover-fill { background: #f9cd0b; } 177 | .medium .chart { border:1px solid #f9cd0b; } 178 | /* light yellow */ 179 | .medium { background: #fff4c2; } 180 | 181 | .cstat-skip { background: #ddd; color: #111; } 182 | .fstat-skip { background: #ddd; color: #111 !important; } 183 | .cbranch-skip { background: #ddd !important; color: #111; } 184 | 185 | span.cline-neutral { background: #eaeaea; } 186 | 187 | .coverage-summary td.empty { 188 | opacity: .5; 189 | padding-top: 4px; 190 | padding-bottom: 4px; 191 | line-height: 1; 192 | color: #888; 193 | } 194 | 195 | .cover-fill, .cover-empty { 196 | display:inline-block; 197 | height: 12px; 198 | } 199 | .chart { 200 | line-height: 0; 201 | } 202 | .cover-empty { 203 | background: white; 204 | } 205 | .cover-full { 206 | border-right: none !important; 207 | } 208 | pre.prettyprint { 209 | border: none !important; 210 | padding: 0 !important; 211 | margin: 0 !important; 212 | } 213 | .com { color: #999 !important; } 214 | .ignore-none { color: #999; font-weight: normal; } 215 | 216 | .wrapper { 217 | min-height: 100%; 218 | height: auto !important; 219 | height: 100%; 220 | margin: 0 auto -48px; 221 | } 222 | .footer, .push { 223 | height: 48px; 224 | } 225 | -------------------------------------------------------------------------------- /client/styles/index.scss: -------------------------------------------------------------------------------- 1 | $border: #353636; 2 | $font: 'Playfair Display', serif; 3 | 4 | * { 5 | margin: 0; 6 | } 7 | 8 | #main-container { 9 | display: grid; 10 | grid-template-areas: 11 | 'head head' 12 | 'navb main' 13 | 'navb main'; 14 | grid-template-columns: 300px 1fr; 15 | margin-top: 170px; 16 | 17 | font-family: $font; 18 | .header { 19 | font-size: 30px; 20 | text-align: center; 21 | grid-area: head; 22 | max-height: 160px; 23 | padding: 25px 0 25px 0; 24 | width: 100%; 25 | position: fixed; 26 | border: 1px solid $border; 27 | z-index: 1; 28 | top: 0px; 29 | } 30 | } 31 | 32 | //navbar styling 33 | #nav-bar { 34 | min-width: 300px; 35 | align-items: center; 36 | top: 201px; 37 | grid-area: navb; 38 | display: flex; 39 | flex-direction: column; 40 | position: fixed; 41 | margin: -70px 0 0 0px; 42 | height: 100%; 43 | padding-top: 20px; 44 | border: 1px solid $border; 45 | .link-div { 46 | display: flex; 47 | width: 300px; 48 | justify-content: center; 49 | } 50 | .link { 51 | padding: 5px; 52 | align-self: center; 53 | } 54 | 55 | #eyepatch { 56 | position: fixed; 57 | height: 50px; 58 | width: 50px; 59 | border-style: none; 60 | display: flex; 61 | position: relative; 62 | margin-top: 60px; 63 | &:hover { 64 | width: 50px; 65 | height: 50px; 66 | border-radius: 50%; 67 | cursor: pointer; 68 | } 69 | bottom: 3%; 70 | } 71 | 72 | #logo { 73 | display: flex; 74 | width: 200px; 75 | height: 200px; 76 | justify-content: center; 77 | align-items: center; 78 | padding-bottom: 20px; 79 | } 80 | } 81 | 82 | .data-container { 83 | grid-area: main; 84 | display: flex; 85 | flex-direction: column; 86 | height: 100%; 87 | 88 | #mongo-h1 { 89 | font-size: 100px; 90 | top: 200px; 91 | margin-left: 50px; 92 | align-self: center; 93 | } 94 | 95 | #mongo-charts { 96 | margin-top: -50px; 97 | } 98 | 99 | .charts { 100 | display: flex; 101 | flex-direction: column; 102 | justify-content: space-around; 103 | align-items: center; 104 | margin: 0px 25px 5px 25px; 105 | } 106 | 107 | .line-graph { 108 | display: grid; 109 | grid-template-columns: repeat(auto-fill, minmax(600px, 0.9fr)); 110 | padding: 20px; 111 | padding-left: 40px; 112 | } 113 | 114 | .line { 115 | margin: 30px 20px 20px 0; 116 | border: 1px solid $border; 117 | padding: 5px; 118 | } 119 | 120 | #kube-main-dash { 121 | display: flex; 122 | flex-direction: column; 123 | } 124 | } 125 | 126 | //List data for main page 127 | #list-data { 128 | display: flex; 129 | flex-direction: row; 130 | justify-content: space-evenly; 131 | border: 1px solid $border; 132 | margin: 0px 24px 0 24px; 133 | height: 344px; 134 | 135 | #total-pods { 136 | padding-top: 15px; 137 | padding-right: 10px; 138 | align-self: center; 139 | } 140 | 141 | #total-names { 142 | display: flex; 143 | flex-direction: column; 144 | justify-content: start; 145 | align-items: center; 146 | font-size: 12.5px; 147 | font-family: Helvetica; 148 | font-weight: bolder; 149 | width: 200px; 150 | height: 200px; 151 | padding-top: 60px; 152 | } 153 | 154 | #names-num { 155 | display: flex; 156 | align-items: center; 157 | justify-content: center; 158 | font-size: 210px; 159 | } 160 | 161 | .bar-chart { 162 | display: flex; 163 | align-items: center; 164 | height: 200px; 165 | align-self: center; 166 | border: none; 167 | box-shadow: none; 168 | padding-right: 20px; 169 | } 170 | } 171 | 172 | //Kubernetes list data 173 | #kube-list-data { 174 | display: flex; 175 | flex-direction: row; 176 | justify-content: space-evenly; 177 | border: 1px solid $border; 178 | margin: 0 24px 0 24px; 179 | padding: 5px 5px 5px 5px; 180 | padding-bottom: 29px; 181 | 182 | .pod-list:hover { 183 | cursor: pointer; 184 | } 185 | 186 | #pod-names { 187 | width: 600px; 188 | max-width: 600px; 189 | border: 1px solid cl; 190 | margin-left: 60px; 191 | padding: 5px 0 0 20px; 192 | overflow-y: auto; 193 | 194 | h2 { 195 | border-bottom: 1px solid $border; 196 | font-weight: bold; 197 | padding-bottom: 5px; 198 | } 199 | div { 200 | padding: 5px; 201 | } 202 | } 203 | 204 | #kube-total-pods { 205 | margin-left: -100px; 206 | } 207 | 208 | #dropdown { 209 | padding-top: 20px; 210 | padding-bottom: 20px; 211 | padding-left: 10px; 212 | min-width: 300px; 213 | width: 400px; 214 | max-width: 400px; 215 | margin-left: 10px; 216 | 217 | #dropdown-but { 218 | font-size: 20px; 219 | font-family: $font; 220 | left: 10px; 221 | background-color: transparent; 222 | border: 1px solid $border; 223 | padding: 5px 15px; 224 | &:hover { 225 | cursor: pointer; 226 | } 227 | } 228 | .menu { 229 | position: absolute; 230 | list-style-type: none; 231 | margin: 5px 0; 232 | padding: 0; 233 | border: 1px solid grey; 234 | width: 150px; 235 | } 236 | 237 | .menu > li { 238 | margin: 0; 239 | } 240 | 241 | .menu > li > button { 242 | width: 100%; 243 | height: 100%; 244 | text-align: left; 245 | background: none; 246 | color: inherit; 247 | border: none; 248 | padding: 5px; 249 | margin: 0; 250 | font: inherit; 251 | cursor: pointer; 252 | } 253 | } 254 | } 255 | 256 | //Display for No Data 257 | .line-chart-container { 258 | display: grid; 259 | position: relative; 260 | 261 | .missing-data { 262 | display: flex; 263 | height: 100%; 264 | width: 100%; 265 | background: rgba(255, 255, 255, 0.4); 266 | justify-content: center; 267 | align-items: center; 268 | color: #1b013f; 269 | position: absolute; 270 | } 271 | } 272 | 273 | //Display for Error 274 | #error { 275 | display: flex; 276 | justify-content: center; 277 | align-items: center; 278 | margin-top: 160px; 279 | margin-bottom: 160px; 280 | } 281 | 282 | //Scrollbar styling 283 | ::-webkit-scrollbar { 284 | height: 10px; 285 | width: 10px; 286 | } 287 | 288 | ::-webkit-scrollbar-track { 289 | background: $border; 290 | } 291 | 292 | ::-webkit-scrollbar-thumb { 293 | background: #6c887c; 294 | } 295 | -------------------------------------------------------------------------------- /coverage/lcov-report/sorter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var addSorting = (function() { 3 | 'use strict'; 4 | var cols, 5 | currentSort = { 6 | index: 0, 7 | desc: false 8 | }; 9 | 10 | // returns the summary table element 11 | function getTable() { 12 | return document.querySelector('.coverage-summary'); 13 | } 14 | // returns the thead element of the summary table 15 | function getTableHeader() { 16 | return getTable().querySelector('thead tr'); 17 | } 18 | // returns the tbody element of the summary table 19 | function getTableBody() { 20 | return getTable().querySelector('tbody'); 21 | } 22 | // returns the th element for nth column 23 | function getNthColumn(n) { 24 | return getTableHeader().querySelectorAll('th')[n]; 25 | } 26 | 27 | function onFilterInput() { 28 | const searchValue = document.getElementById('fileSearch').value; 29 | const rows = document.getElementsByTagName('tbody')[0].children; 30 | for (let i = 0; i < rows.length; i++) { 31 | const row = rows[i]; 32 | if ( 33 | row.textContent 34 | .toLowerCase() 35 | .includes(searchValue.toLowerCase()) 36 | ) { 37 | row.style.display = ''; 38 | } else { 39 | row.style.display = 'none'; 40 | } 41 | } 42 | } 43 | 44 | // loads the search box 45 | function addSearchBox() { 46 | var template = document.getElementById('filterTemplate'); 47 | var templateClone = template.content.cloneNode(true); 48 | templateClone.getElementById('fileSearch').oninput = onFilterInput; 49 | template.parentElement.appendChild(templateClone); 50 | } 51 | 52 | // loads all columns 53 | function loadColumns() { 54 | var colNodes = getTableHeader().querySelectorAll('th'), 55 | colNode, 56 | cols = [], 57 | col, 58 | i; 59 | 60 | for (i = 0; i < colNodes.length; i += 1) { 61 | colNode = colNodes[i]; 62 | col = { 63 | key: colNode.getAttribute('data-col'), 64 | sortable: !colNode.getAttribute('data-nosort'), 65 | type: colNode.getAttribute('data-type') || 'string' 66 | }; 67 | cols.push(col); 68 | if (col.sortable) { 69 | col.defaultDescSort = col.type === 'number'; 70 | colNode.innerHTML = 71 | colNode.innerHTML + ''; 72 | } 73 | } 74 | return cols; 75 | } 76 | // attaches a data attribute to every tr element with an object 77 | // of data values keyed by column name 78 | function loadRowData(tableRow) { 79 | var tableCols = tableRow.querySelectorAll('td'), 80 | colNode, 81 | col, 82 | data = {}, 83 | i, 84 | val; 85 | for (i = 0; i < tableCols.length; i += 1) { 86 | colNode = tableCols[i]; 87 | col = cols[i]; 88 | val = colNode.getAttribute('data-value'); 89 | if (col.type === 'number') { 90 | val = Number(val); 91 | } 92 | data[col.key] = val; 93 | } 94 | return data; 95 | } 96 | // loads all row data 97 | function loadData() { 98 | var rows = getTableBody().querySelectorAll('tr'), 99 | i; 100 | 101 | for (i = 0; i < rows.length; i += 1) { 102 | rows[i].data = loadRowData(rows[i]); 103 | } 104 | } 105 | // sorts the table using the data for the ith column 106 | function sortByIndex(index, desc) { 107 | var key = cols[index].key, 108 | sorter = function(a, b) { 109 | a = a.data[key]; 110 | b = b.data[key]; 111 | return a < b ? -1 : a > b ? 1 : 0; 112 | }, 113 | finalSorter = sorter, 114 | tableBody = document.querySelector('.coverage-summary tbody'), 115 | rowNodes = tableBody.querySelectorAll('tr'), 116 | rows = [], 117 | i; 118 | 119 | if (desc) { 120 | finalSorter = function(a, b) { 121 | return -1 * sorter(a, b); 122 | }; 123 | } 124 | 125 | for (i = 0; i < rowNodes.length; i += 1) { 126 | rows.push(rowNodes[i]); 127 | tableBody.removeChild(rowNodes[i]); 128 | } 129 | 130 | rows.sort(finalSorter); 131 | 132 | for (i = 0; i < rows.length; i += 1) { 133 | tableBody.appendChild(rows[i]); 134 | } 135 | } 136 | // removes sort indicators for current column being sorted 137 | function removeSortIndicators() { 138 | var col = getNthColumn(currentSort.index), 139 | cls = col.className; 140 | 141 | cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); 142 | col.className = cls; 143 | } 144 | // adds sort indicators for current column being sorted 145 | function addSortIndicators() { 146 | getNthColumn(currentSort.index).className += currentSort.desc 147 | ? ' sorted-desc' 148 | : ' sorted'; 149 | } 150 | // adds event listeners for all sorter widgets 151 | function enableUI() { 152 | var i, 153 | el, 154 | ithSorter = function ithSorter(i) { 155 | var col = cols[i]; 156 | 157 | return function() { 158 | var desc = col.defaultDescSort; 159 | 160 | if (currentSort.index === i) { 161 | desc = !currentSort.desc; 162 | } 163 | sortByIndex(i, desc); 164 | removeSortIndicators(); 165 | currentSort.index = i; 166 | currentSort.desc = desc; 167 | addSortIndicators(); 168 | }; 169 | }; 170 | for (i = 0; i < cols.length; i += 1) { 171 | if (cols[i].sortable) { 172 | // add the click event handler on the th so users 173 | // dont have to click on those tiny arrows 174 | el = getNthColumn(i).querySelector('.sorter').parentElement; 175 | if (el.addEventListener) { 176 | el.addEventListener('click', ithSorter(i)); 177 | } else { 178 | el.attachEvent('onclick', ithSorter(i)); 179 | } 180 | } 181 | } 182 | } 183 | // adds sorting functionality to the UI 184 | return function() { 185 | if (!getTable()) { 186 | return; 187 | } 188 | cols = loadColumns(); 189 | loadData(); 190 | addSearchBox(); 191 | addSortIndicators(); 192 | enableUI(); 193 | }; 194 | })(); 195 | 196 | window.addEventListener('load', addSorting); 197 | -------------------------------------------------------------------------------- /client/containers/kubMain.tsx: -------------------------------------------------------------------------------- 1 | import NavBar from '../components/Navbar'; 2 | import axios from 'axios'; 3 | import React, { useState, useEffect } from 'react'; 4 | import DropDown from '../components/Dropdown'; 5 | import LineChart from '../components/LineChart'; 6 | import Popup from '../components/PopUp'; 7 | import DonutChart from '../components/DonutChart'; 8 | import { useSelector, useDispatch } from 'react-redux'; 9 | import { State } from '../../types'; 10 | import { currentPage, saveNamespace } from '../rootReducer'; 11 | import PodName from '../components/PodName'; 12 | import { KubDataType } from '../../types'; 13 | 14 | type KubType = { 15 | namespaces: string[] | null; 16 | }; 17 | 18 | const KubPage = ({ namespaces }: KubType) => { 19 | const dispatch = useDispatch(); 20 | 21 | //dark mode toggling 22 | const dark = useSelector((state: State) => state.dark); 23 | let theme: string; 24 | dark ? (theme = 'lightMode') : (theme = 'darkMode'); 25 | 26 | //state of the current namespace 27 | const currName = useSelector((state: State) => state.currentNamespace); 28 | 29 | //page represents current namespace being displayed 30 | const [page, setCurrentPage] = useState(''); 31 | const [data, setData] = useState({ 32 | cpu: [], 33 | memory: [], 34 | notReady: 0, 35 | ready: [], 36 | reception: [], 37 | restarts: [], 38 | transmission: [], 39 | }); 40 | 41 | //set pods for podsArray to spread within div 42 | const [pods, setPods] = useState([]); 43 | const [currentPod, setCurrentPod] = useState(); 44 | 45 | //button to trigger popup 46 | const [buttonPopup, setButtonPopup] = useState(false); 47 | 48 | //helper function to grab metrics 49 | const getData = async (url: string): Promise => { 50 | try { 51 | //first step grabs all data for the current namespace 52 | const response = await axios.get(url); 53 | const newData = await response.data; 54 | setData(newData); 55 | //second step grabs all pod names for current namespace and updates pod state 56 | const podResponse = await axios.get('/api/kubernetesMetrics/podNames', { 57 | params: { namespace: page }, 58 | }); 59 | const podData: string[] = await podResponse.data; 60 | setPods(podData); 61 | 62 | //thrid step checks the data that was retrieved, if num of pods NOT ready > 0 runs the following 63 | const badPods: string[] = []; 64 | if (newData.notReady > 0) { 65 | const badPodResponse = await axios.get( 66 | '/api/kubernetesMetrics/podsNotReadyNames/', 67 | { params: { namespace: page, podData: podData } } 68 | ); 69 | const badPodData = await badPodResponse.data; 70 | //data returned will be an array of arrays, ex: [] 71 | setPods(badPodData); 72 | } 73 | } catch (err) { 74 | setPods(['Error Fetching Pods']); 75 | console.log('Kubernetes Page: ', err); 76 | } 77 | }; 78 | 79 | //runs effect on first render 80 | useEffect(() => { 81 | dispatch(currentPage('kubernetes')); 82 | currName !== '' 83 | ? setCurrentPage(currName) 84 | : namespaces 85 | ? setCurrentPage(namespaces[0]) 86 | : setCurrentPage('None'); 87 | }, []); 88 | 89 | //runs effect on namespace change 90 | useEffect(() => { 91 | if (namespaces) { 92 | if (page !== 'None' && page) { 93 | getData(`/api/kubernetesMetrics/namespaceMetrics/${page}`); 94 | } 95 | } 96 | }, [page]); 97 | 98 | //Upon changing the namespace page, will save and update to current 99 | const handleChange = (newName: string) => { 100 | setCurrentPage(newName); 101 | //persists the current namespace selection when switching pages 102 | dispatch(saveNamespace(newName)); 103 | }; 104 | 105 | //Creates array of pod names 106 | const podsArray: JSX.Element[] = []; 107 | 108 | if (Array.isArray(pods) && pods.length > 0) { 109 | pods.forEach((pod: string | string[]) => { 110 | let status; 111 | let podName; 112 | 113 | Array.isArray(pod) ? (podName = pod[1]) : (podName = pod); 114 | Array.isArray(pod) && parseInt(pod[0]) > 0 115 | ? (status = false) 116 | : (status = true); 117 | 118 | podsArray.push( 119 | 126 | ); 127 | }); 128 | } 129 | 130 | let dropdown; 131 | if (namespaces) { 132 | namespaces.length > 0 133 | ? (dropdown = ( 134 | 139 | )) 140 | : (dropdown = ( 141 | 144 | )); 145 | } 146 | 147 | //Creates line chart array from line chart data object 148 | const lineObject: { [key: string]: any[] } = { 149 | totalCpu: [data.cpu, 'Cpu Usage', 'Percent', 'Total CPU % Usage'], 150 | totalMem: [ 151 | data.memory, 152 | 'Mem Usage', 153 | 'Kilobytes', 154 | 'Total Memory Usage (kB)', 155 | ], 156 | totalRec: [ 157 | data.reception, 158 | 'Byte Usage', 159 | 'Kilobytes', 160 | 'Netword Received (kB)', 161 | ], 162 | totalTrans: [ 163 | data.transmission, 164 | 'Byte Usage', 165 | 'Kilobytes', 166 | 'Network Transmitted (kB)', 167 | ], 168 | restart: [data.restarts, 'Restarts', 'Restarts', 'Pod Restarts'], 169 | }; 170 | 171 | const charts: JSX.Element[] = []; 172 | 173 | for (let info in lineObject) { 174 | charts.push( 175 |
    176 | 183 |
    184 | ); 185 | } 186 | return ( 187 |
    188 |
    189 |

    Odin's Eye

    190 |
    191 | 192 | 197 |
    198 |
    199 | {dropdown} 200 |
    201 | 209 |
    210 |
    211 |

    Pod Names:

    212 | {podsArray} 213 |
    214 |
    215 |
    {charts}
    216 |
    217 |
    218 | ); 219 | }; 220 | 221 | export default KubPage; 222 | -------------------------------------------------------------------------------- /coverage/lcov-report/routes/kubernetes.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code coverage report for routes/kubernetes.js 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 |
    21 |
    22 |

    All files / routes kubernetes.js

    23 |
    24 | 25 |
    26 | 70.58% 27 | Statements 28 | 12/17 29 |
    30 | 31 | 32 |
    33 | 71.42% 34 | Branches 35 | 5/7 36 |
    37 | 38 | 39 |
    40 | 16.66% 41 | Functions 42 | 1/6 43 |
    44 | 45 | 46 |
    47 | 70.58% 48 | Lines 49 | 12/17 50 |
    51 | 52 | 53 |
    54 |

    55 | Press n or j to go to the next uncovered block, b, p or k for the previous block. 56 |

    57 | 63 |
    64 |
    65 |
    
     66 | 
    1 67 | 2 68 | 3 69 | 4 70 | 5 71 | 6 72 | 7 73 | 8 74 | 9 75 | 10 76 | 11 77 | 12 78 | 13 79 | 14 80 | 15 81 | 16 82 | 17 83 | 18 84 | 19 85 | 20 86 | 21 87 | 22 88 | 23 89 | 24 90 | 25  91 | 1x 92 | 1x 93 |   94 | 1x 95 | 1x 96 | 1x 97 | 1x 98 | 1x 99 |   100 |   101 | 1x 102 |   103 |   104 | 1x 105 |   106 |   107 | 1x 108 |   109 |   110 | 1x 111 |   112 |   113 | 1x 114 |  
    "use strict";
    115 | var __importDefault = (this && this.__importDefault) || function (mod) {
    116 |     return (mod && mod.__esModule) ? mod : { "default": mod };
    117 | };
    118 | Object.defineProperty(exports, "__esModule", { value: true });
    119 | const express = require('express');
    120 | const kubernetesController_1 = __importDefault(require("../controllers/kubernetesController"));
    121 | const kubernetesRouter = express.Router();
    122 | kubernetesRouter.get('/totalMem', kubernetesController_1.default.totalMem, (req, res) => {
    123 |     return res.status(200).send('hi');
    124 | });
    125 | kubernetesRouter.get('/totalCpu', kubernetesController_1.default.totalCpu, (req, res) => {
    126 |     return res.status(200).json(res.locals.cpu.data);
    127 | });
    128 | kubernetesRouter.get('/totalTransmit', kubernetesController_1.default.totalTransmit, (req, res) => {
    129 |     return res.status(200).send('hi');
    130 | });
    131 | kubernetesRouter.get('/totalReceive', kubernetesController_1.default.totalReceive, (req, res) => {
    132 |     return res.status(200).send('hi');
    133 | });
    134 | kubernetesRouter.get('/totalPods', kubernetesController_1.default.totalPods, (req, res) => {
    135 |     return res.status(200).send('hi');
    136 | });
    137 | exports.default = kubernetesRouter;
    138 |  
    139 | 140 |
    141 |
    142 | 147 | 148 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /server/controllers/dataObjectBuilder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (g && (g = 0, op[0] && (_ = 0)), _) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | exports.__esModule = true; 39 | var axios_1 = require("axios"); 40 | var types_1 = require("../../types"); 41 | var dataController = { 42 | dataObjectBuilder: function (res, req, next) { return __awaiter(void 0, void 0, void 0, function () { 43 | var objectData, obj, _a, _b, _c, _i, key, _d, _e, _f, _g, query, response, _h, _j, _k, _l, query, response, data, _m, _o, _p, _q, query, response, err_1; 44 | return __generator(this, function (_r) { 45 | switch (_r.label) { 46 | case 0: 47 | objectData = {}; 48 | obj = req.app.locals.queries; 49 | _r.label = 1; 50 | case 1: 51 | _r.trys.push([1, 16, , 17]); 52 | _a = obj; 53 | _b = []; 54 | for (_c in _a) 55 | _b.push(_c); 56 | _i = 0; 57 | _r.label = 2; 58 | case 2: 59 | if (!(_i < _b.length)) return [3 /*break*/, 15]; 60 | _c = _b[_i]; 61 | if (!(_c in _a)) return [3 /*break*/, 14]; 62 | key = _c; 63 | if (!(key === 'linegraph')) return [3 /*break*/, 6]; 64 | _d = obj[key]; 65 | _e = []; 66 | for (_f in _d) 67 | _e.push(_f); 68 | _g = 0; 69 | _r.label = 3; 70 | case 3: 71 | if (!(_g < _e.length)) return [3 /*break*/, 6]; 72 | _f = _e[_g]; 73 | if (!(_f in _d)) return [3 /*break*/, 5]; 74 | query = _f; 75 | return [4 /*yield*/, axios_1["default"].get("http://localhost:9090/api/v1/query_range?query=".concat(obj[key][query], "&start=").concat(types_1.start, "&end=").concat(types_1.end, "&step=5m"))]; 76 | case 4: 77 | response = _r.sent(); 78 | if (response.data.data.result.length === 0) { 79 | objectData[query] = []; 80 | } 81 | else { 82 | objectData[query] = [response.data.data.result[0].values]; 83 | } 84 | _r.label = 5; 85 | case 5: 86 | _g++; 87 | return [3 /*break*/, 3]; 88 | case 6: 89 | if (!(key === 'donutint')) return [3 /*break*/, 10]; 90 | _h = obj[key]; 91 | _j = []; 92 | for (_k in _h) 93 | _j.push(_k); 94 | _l = 0; 95 | _r.label = 7; 96 | case 7: 97 | if (!(_l < _j.length)) return [3 /*break*/, 10]; 98 | _k = _j[_l]; 99 | if (!(_k in _h)) return [3 /*break*/, 9]; 100 | query = _k; 101 | return [4 /*yield*/, axios_1["default"].get("http://localhost:9090/api/v1/query_range?query=".concat(obj[key][query], "&start=").concat(types_1.start, "&end=").concat(types_1.end, "&step=5m"))]; 102 | case 8: 103 | response = _r.sent(); 104 | data = parseInt(response.data.data.result[0].values[0][1]); 105 | objectData[query] = [data]; 106 | _r.label = 9; 107 | case 9: 108 | _l++; 109 | return [3 /*break*/, 7]; 110 | case 10: 111 | if (!(key === 'cpubarchart')) return [3 /*break*/, 14]; 112 | _m = obj[key]; 113 | _o = []; 114 | for (_p in _m) 115 | _o.push(_p); 116 | _q = 0; 117 | _r.label = 11; 118 | case 11: 119 | if (!(_q < _o.length)) return [3 /*break*/, 14]; 120 | _p = _o[_q]; 121 | if (!(_p in _m)) return [3 /*break*/, 13]; 122 | query = _p; 123 | return [4 /*yield*/, axios_1["default"].get("http://localhost:9090/api/v1/query_range?query=".concat(obj[key][query], "&start=").concat(types_1.start, "&end=").concat(types_1.end, "&step=5m"))]; 124 | case 12: 125 | response = _r.sent(); 126 | objectData[query] = [response.data.data.result[0].values[1][1]]; 127 | _r.label = 13; 128 | case 13: 129 | _q++; 130 | return [3 /*break*/, 11]; 131 | case 14: 132 | _i++; 133 | return [3 /*break*/, 2]; 134 | case 15: 135 | req.app.locals.data = objectData; 136 | return [2 /*return*/, next()]; 137 | case 16: 138 | err_1 = _r.sent(); 139 | return [2 /*return*/, next({ 140 | log: "Error in dataController.dataObjectBuilder: ".concat(err_1), 141 | status: 500, 142 | message: 'Error occured while creating data object' 143 | })]; 144 | case 17: return [2 /*return*/]; 145 | } 146 | }); 147 | }); } 148 | }; 149 | exports["default"] = dataController; 150 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (g && (g = 0, op[0] && (_ = 0)), _) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | exports.__esModule = true; 39 | var path = require('path'); 40 | var express = require('express'); 41 | var cookieParser = require('cookie-parser'); 42 | var dashboard_1 = require("./routes/dashboard"); 43 | var kubernetes_1 = require("./routes/kubernetes"); 44 | var mongo_1 = require("./routes/mongo"); 45 | var axios_1 = require("axios"); 46 | var cors = require('cors'); 47 | var app = express(); 48 | var PORT = 3000; 49 | app.use(cookieParser()); 50 | app.use(cors()); 51 | app.use(express.json()); 52 | app.use(express.urlencoded({ extended: true })); 53 | app.use(function (req, res, next) { 54 | var cookie = req.cookies.cookieName; 55 | if (cookie == undefined) { 56 | var randomNum = Math.random().toString(); 57 | randomNum = randomNum.substring(2, randomNum.length); 58 | res.cookie('cookieName', randomNum, { maxAge: 90000, httpOnly: true }); 59 | } 60 | return next(); 61 | }); 62 | if (process.env.NODE_ENV) { 63 | app.use('/', express.static(path.join(__dirname, '../dist'))); 64 | } 65 | app.use('/kubernetes', function (req, res) { 66 | express.static(path.join(__dirname, '../client/index.html')); 67 | }); 68 | app.use('/mongo', function (req, res) { 69 | express.static(path.join(__dirname, '../client/index.html')); 70 | }); 71 | app.use('/api/dashboard', dashboard_1["default"]); 72 | app.use('/api/kubernetesMetrics', kubernetes_1["default"]); 73 | app.use('/api/mongodb', mongo_1["default"]); 74 | app.get('/dashboard', function (req, res) { 75 | return res.status(200).send('hi'); 76 | }); 77 | //network received/transmitted 78 | app.use('/live/received', function (req, res) { return __awaiter(void 0, void 0, void 0, function () { 79 | var headers, timer; 80 | return __generator(this, function (_a) { 81 | headers = { 82 | 'Content-Type': 'text/event-stream', 83 | Connection: 'keep-alive', 84 | 'Cache-Control': 'no-cache' 85 | }; 86 | res.writeHead(200, headers); 87 | timer = setInterval(function () { return __awaiter(void 0, void 0, void 0, function () { 88 | var start, end, response, data, newData; 89 | return __generator(this, function (_a) { 90 | switch (_a.label) { 91 | case 0: 92 | start = new Date(Date.now() - 10000).toISOString(); 93 | end = new Date(Date.now() - 5000).toISOString(); 94 | return [4 /*yield*/, axios_1["default"].get("http://localhost:9090/api/v1/query_range?query=sum(rate(node_network_receive_bytes_total[10m]))&start=".concat(start, "&end=").concat(end, "&step=10m"))]; 95 | case 1: 96 | response = _a.sent(); 97 | data = response.data.data.result[0].values[0]; 98 | newData = "data: ".concat(JSON.stringify(data), "\n\n"); 99 | res.write(newData); 100 | return [2 /*return*/]; 101 | } 102 | }); 103 | }); }, 1500); 104 | res.on('close', function () { 105 | res.end(); 106 | clearInterval(timer); 107 | console.log('Connection closed'); 108 | }); 109 | return [2 /*return*/]; 110 | }); 111 | }); }); 112 | app.use('/live/transmit', function (req, res) { return __awaiter(void 0, void 0, void 0, function () { 113 | var headers, timer; 114 | return __generator(this, function (_a) { 115 | headers = { 116 | 'Content-Type': 'text/event-stream', 117 | Connection: 'keep-alive', 118 | 'Cache-Control': 'no-cache' 119 | }; 120 | res.writeHead(200, headers); 121 | timer = setInterval(function () { return __awaiter(void 0, void 0, void 0, function () { 122 | var start, end, response, data, newData; 123 | return __generator(this, function (_a) { 124 | switch (_a.label) { 125 | case 0: 126 | start = new Date(Date.now() - 10000).toISOString(); 127 | end = new Date(Date.now() - 5000).toISOString(); 128 | return [4 /*yield*/, axios_1["default"].get("http://localhost:9090/api/v1/query_range?query=sum(rate(node_network_transmit_bytes_total[10m]))&start=".concat(start, "&end=").concat(end, "&step=5m"))]; 129 | case 1: 130 | response = _a.sent(); 131 | data = response.data.data.result[0].values[0]; 132 | newData = "data: ".concat(JSON.stringify(data), "\n\n"); 133 | res.write(newData); 134 | return [2 /*return*/]; 135 | } 136 | }); 137 | }); }, 1500); 138 | res.on('close', function () { 139 | res.end(); 140 | clearInterval(timer); 141 | console.log('Connection closed'); 142 | }); 143 | return [2 /*return*/]; 144 | }); 145 | }); }); 146 | //redirect to page 404 when endpoint does not exist 147 | app.use('*', function (req, res) { 148 | return res.status(404).send('404 Page Not Found'); 149 | }); 150 | //global error handling 151 | app.use(function (err, req, res, next) { 152 | var defaultErr = { 153 | log: 'Express error handler caught unknown middleware error', 154 | status: 500, 155 | message: { err: 'An error occurred' } 156 | }; 157 | var errorObj = Object.assign({}, defaultErr, err); 158 | console.log(errorObj.log); 159 | return res.status(errorObj.status).json(errorObj.message); 160 | }); 161 | //start app on port 162 | app.listen(PORT, function () { 163 | console.log("Server listening on port: ".concat(PORT, "...")); 164 | }); 165 | setTimeout; 166 | module.exports = app; 167 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | "jsx": "react" /* Specify what JSX code is generated. */, 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./dist/" /* Specify the root folder within your source files. */, 30 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./dist/" /* Specify an output folder for all emitted files. */, 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */, 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 102 | }, 103 | "include": ["client/**/*", "server/**/*", "globals.d.ts"], 104 | "exclude": ["node_modules"] 105 | } 106 | -------------------------------------------------------------------------------- /server/controllers/kubernetesController.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __generator = (this && this.__generator) || function (thisArg, body) { 12 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; 13 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; 14 | function verb(n) { return function (v) { return step([n, v]); }; } 15 | function step(op) { 16 | if (f) throw new TypeError("Generator is already executing."); 17 | while (g && (g = 0, op[0] && (_ = 0)), _) try { 18 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; 19 | if (y = 0, t) op = [op[0] & 2, t.value]; 20 | switch (op[0]) { 21 | case 0: case 1: t = op; break; 22 | case 4: _.label++; return { value: op[1], done: false }; 23 | case 5: _.label++; y = op[1]; op = [0]; continue; 24 | case 7: op = _.ops.pop(); _.trys.pop(); continue; 25 | default: 26 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } 27 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } 28 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } 29 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } 30 | if (t[2]) _.ops.pop(); 31 | _.trys.pop(); continue; 32 | } 33 | op = body.call(thisArg, _); 34 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } 35 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; 36 | } 37 | }; 38 | exports.__esModule = true; 39 | var types_1 = require("../../types"); 40 | var axios_1 = require("axios"); 41 | var kubernetesController = { 42 | namespaceNames: function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { 43 | var namespaceQuery, response, array, namespaceArray_1, err_1; 44 | return __generator(this, function (_a) { 45 | switch (_a.label) { 46 | case 0: 47 | namespaceQuery = 'sum+by+(namespace)+(kube_pod_info)'; 48 | _a.label = 1; 49 | case 1: 50 | _a.trys.push([1, 4, , 5]); 51 | return [4 /*yield*/, axios_1["default"].get("http://localhost:9090/api/v1/query_range?query=".concat(namespaceQuery, "&start=").concat(types_1.start, "&end=").concat(types_1.end, "&step=5m"))]; 52 | case 2: 53 | response = _a.sent(); 54 | return [4 /*yield*/, response.data.data.result]; 55 | case 3: 56 | array = _a.sent(); 57 | namespaceArray_1 = []; 58 | array.forEach(function (element) { 59 | namespaceArray_1.push(element.metric.namespace); 60 | }); 61 | res.locals.namespaceNames = namespaceArray_1; 62 | return [2 /*return*/, next()]; 63 | case 4: 64 | err_1 = _a.sent(); 65 | return [2 /*return*/, next({ 66 | log: "Error in kubernetesController.nameSpaceNames: ".concat(err_1), 67 | status: 500, 68 | message: 'Error occured while retrieving namespace names data' 69 | })]; 70 | case 5: return [2 /*return*/]; 71 | } 72 | }); 73 | }); }, 74 | podNames: function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { 75 | var namespace, podNameQuery, response, array, podNameArray_1, err_2; 76 | return __generator(this, function (_a) { 77 | switch (_a.label) { 78 | case 0: 79 | namespace = req.query.namespace; 80 | podNameQuery = "sum by (pod)(kube_pod_info{namespace=\"".concat(namespace, "\"})"); 81 | _a.label = 1; 82 | case 1: 83 | _a.trys.push([1, 3, , 4]); 84 | return [4 /*yield*/, axios_1["default"].get("http://localhost:9090/api/v1/query_range?query=".concat(podNameQuery, "&start=").concat(types_1.start, "&end=").concat(types_1.end, "&step=5m"))]; 85 | case 2: 86 | response = _a.sent(); 87 | array = response.data.data.result; 88 | podNameArray_1 = []; 89 | array.forEach(function (element) { 90 | podNameArray_1.push(element.metric.pod); 91 | }); 92 | res.locals.names = podNameArray_1; 93 | return [2 /*return*/, next()]; 94 | case 3: 95 | err_2 = _a.sent(); 96 | return [2 /*return*/, next({ 97 | log: "Error in kubernetesController.podNames: ".concat(err_2), 98 | status: 500, 99 | message: 'Error occured while retrieving pod names' 100 | })]; 101 | case 4: return [2 /*return*/]; 102 | } 103 | }); 104 | }); }, 105 | podsNotReadyNames: function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { 106 | var _a, namespace, podData, promises; 107 | return __generator(this, function (_b) { 108 | switch (_b.label) { 109 | case 0: 110 | _a = req.query, namespace = _a.namespace, podData = _a.podData; 111 | if (!Array.isArray(podData)) return [3 /*break*/, 2]; 112 | promises = podData.map(function (name) { return __awaiter(void 0, void 0, void 0, function () { 113 | var readyQuery, response, status_1, err_3; 114 | return __generator(this, function (_a) { 115 | switch (_a.label) { 116 | case 0: 117 | readyQuery = "sum(kube_pod_status_ready{condition=\"false\",namespace=\"".concat(namespace, "\",pod=\"").concat(name, "\"})"); 118 | _a.label = 1; 119 | case 1: 120 | _a.trys.push([1, 3, , 4]); 121 | return [4 /*yield*/, axios_1["default"].get("http://localhost:9090/api/v1/query_range?query=".concat(readyQuery, "&start=").concat(types_1.start, "&end=").concat(types_1.end, "&step=5m"))]; 122 | case 2: 123 | response = _a.sent(); 124 | if (!response) { 125 | return [2 /*return*/, [undefined, name]]; 126 | } 127 | status_1 = response.data.data.result[0].values[0][1]; 128 | if (parseInt(status_1) > 0) { 129 | return [2 /*return*/, [status_1, name]]; 130 | } 131 | return [3 /*break*/, 4]; 132 | case 3: 133 | err_3 = _a.sent(); 134 | return [2 /*return*/, next({ 135 | log: "Error in kubernetesController.podsNotReady: ".concat(err_3), 136 | status: 500, 137 | message: 'Error occured while retrieving pods not ready data' 138 | })]; 139 | case 4: return [2 /*return*/]; 140 | } 141 | }); 142 | }); }); 143 | return [4 /*yield*/, Promise.all(promises).then(function (data) { 144 | res.locals.status = data; 145 | })]; 146 | case 1: 147 | _b.sent(); 148 | _b.label = 2; 149 | case 2: return [2 /*return*/, next()]; 150 | } 151 | }); 152 | }); }, 153 | getNameSpaceMetrics: function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { 154 | var objectData, namespaceName, queryObject; 155 | return __generator(this, function (_a) { 156 | objectData = {}; 157 | namespaceName = req.params.namespaceName; 158 | queryObject = { 159 | linegraph: { 160 | restarts: "sum(changes(kube_pod_status_ready{condition=\"true\", namespace = \"".concat(namespaceName, "\"}[5m]))"), 161 | ready: "sum(kube_pod_status_ready{condition=\"true\", namespace = \"".concat(namespaceName, "\"})"), 162 | cpu: "sum(rate(container_cpu_usage_seconds_total{container=\"\", namespace=~\"".concat(namespaceName, "\"}[10m]))"), 163 | memory: "sum(rate(container_memory_usage_bytes{container=\"\", namespace=~\"".concat(namespaceName, "\"}[10m]))"), 164 | reception: "sum(rate(node_network_receive_bytes_total{namespace = \"".concat(namespaceName, "\"}[10m]))"), 165 | transmission: "sum(rate(node_network_transmit_bytes_total{namespace = \"".concat(namespaceName, "\"}[10m]))") 166 | }, 167 | donutint: { 168 | notReady: "sum(kube_pod_status_ready{condition=\"false\", namespace = \"".concat(namespaceName, "\"})") 169 | } 170 | }; 171 | try { 172 | req.app.locals.queries = queryObject; 173 | return [2 /*return*/, next()]; 174 | } 175 | catch (err) { 176 | return [2 /*return*/, next({ 177 | log: "Error in kuberenetesController.getMetrics: ".concat(err), 178 | status: 500, 179 | message: 'Error occured while retrieving getMetrics data' 180 | })]; 181 | } 182 | return [2 /*return*/]; 183 | }); 184 | }); }, 185 | getPodMetrics: function (req, res, next) { return __awaiter(void 0, void 0, void 0, function () { 186 | var podName, queryObject; 187 | return __generator(this, function (_a) { 188 | podName = req.params.podName; 189 | queryObject = { 190 | linegraph: { 191 | restarts: "sum(changes(kube_pod_status_ready{condition=\"true\", pod = \"".concat(podName, "\"}[5m]))"), 192 | ready: "sum(kube_pod_status_ready{condition=\"false\", pod = \"".concat(podName, "\"})"), 193 | cpu: "sum(rate(container_cpu_usage_seconds_total{container=\"\", pod=~\"".concat(podName, "\"}[10m]))"), 194 | memory: "sum(rate(container_memory_usage_bytes{container=\"\", pod=~\"".concat(podName, "\"}[10m]))"), 195 | reception: "sum(rate(node_network_receive_bytes_total{pod = \"".concat(podName, "\"}[10m]))"), 196 | transmission: "sum(rate(node_network_transmit_bytes_total{pod = \"".concat(podName, "\"}[10m]))") 197 | } 198 | }; 199 | try { 200 | req.app.locals.queries = queryObject; 201 | return [2 /*return*/, next()]; 202 | } 203 | catch (err) { 204 | return [2 /*return*/, next({ 205 | log: "Error in kuberenetesController.getPodMetrics: ".concat(err), 206 | status: 500, 207 | message: 'Error occured while retrieving getMetrics data' 208 | })]; 209 | } 210 | return [2 /*return*/]; 211 | }); 212 | }); } 213 | }; 214 | exports["default"] = kubernetesController; 215 | -------------------------------------------------------------------------------- /coverage/lcov-report/prettify.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); 3 | --------------------------------------------------------------------------------