├── .dockerignore ├── jest.config.js ├── .gitignore ├── client ├── assets │ ├── evan.jpg │ ├── icon.png │ ├── nick.png │ ├── awsLogo.png │ ├── kritika.png │ ├── landing.png │ ├── whiteLogo.png │ ├── clara_photo.jpg │ ├── elie_photo.jpg │ ├── ThermaKubeLogo.png │ ├── vis_screenshot.png │ ├── alerts_screenshot.png │ ├── cluster_screenshot.png │ ├── cloud-loader.json │ └── doneLoading.json ├── index.js ├── components │ ├── home │ │ ├── Contribute.jsx │ │ ├── Hero.jsx │ │ ├── Features.jsx │ │ └── Team.jsx │ ├── cluster │ │ ├── Nodes.jsx │ │ ├── Services.jsx │ │ └── Pods.jsx │ ├── visualizer │ │ ├── useResizeObserver.jsx │ │ └── RadialTree.jsx │ ├── Home.jsx │ ├── Eks.jsx │ ├── Dashboard.jsx │ ├── Loader.jsx │ └── Login.jsx ├── containers │ ├── Visualizer_Container.jsx │ ├── Cluster_Container.jsx │ ├── Alerts_Container.jsx │ └── Main_Container.jsx └── App.jsx ├── .babelrc ├── Dockerfile ├── jest-mongodb-config.js ├── server ├── models │ └── userModel.js ├── kubeconfig.js ├── query │ ├── NodeQuery.js │ ├── ServiceQuery.js │ └── PodQuery.js ├── routes │ ├── login.js │ ├── aws.js │ └── api.js ├── controllers │ ├── NodeController.js │ ├── ServiceController.js │ ├── UserController.js │ ├── PodController.js │ ├── cookieController.js │ ├── AlertsController.js │ └── AwsController.js └── server.js ├── index.html ├── LICENSE ├── db └── thermakube_postgres_create.sql ├── webpack.config.js ├── readme.md ├── package.json ├── __test__ └── db.test.js └── stylesheets └── styles.scss /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@shelf/jest-mongodb', 3 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .DS_Store 4 | .env 5 | server/secret.js -------------------------------------------------------------------------------- /client/assets/evan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/evan.jpg -------------------------------------------------------------------------------- /client/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/icon.png -------------------------------------------------------------------------------- /client/assets/nick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/nick.png -------------------------------------------------------------------------------- /client/assets/awsLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/awsLogo.png -------------------------------------------------------------------------------- /client/assets/kritika.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/kritika.png -------------------------------------------------------------------------------- /client/assets/landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/landing.png -------------------------------------------------------------------------------- /client/assets/whiteLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/whiteLogo.png -------------------------------------------------------------------------------- /client/assets/clara_photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/clara_photo.jpg -------------------------------------------------------------------------------- /client/assets/elie_photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/elie_photo.jpg -------------------------------------------------------------------------------- /client/assets/ThermaKubeLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/ThermaKubeLogo.png -------------------------------------------------------------------------------- /client/assets/vis_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/vis_screenshot.png -------------------------------------------------------------------------------- /client/assets/alerts_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/alerts_screenshot.png -------------------------------------------------------------------------------- /client/assets/cluster_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ThermaKube/HEAD/client/assets/cluster_screenshot.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.14 2 | WORKDIR /usr/src/app 3 | COPY . /usr/src/app 4 | RUN npm install 5 | RUN npm run build 6 | EXPOSE 3000 7 | CMD node ./server/server.js -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './App.jsx'; 4 | 5 | //import 'bootstrap/dist/css/bootstrap.min.css'; 6 | import '../stylesheets/styles.scss'; 7 | 8 | render(, document.getElementById('container')); 9 | -------------------------------------------------------------------------------- /jest-mongodb-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mongodbMemoryServerOptions: { 3 | instance: { 4 | dbName: 'jest' 5 | }, 6 | binary: { 7 | version: '4.0.2', // Version of MongoDB 8 | skipMD5: true 9 | }, 10 | autoStart: false 11 | } 12 | }; -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const pool = new Pool({ 4 | connectionString: process.env.ELEPHANT_URL, 5 | }); 6 | 7 | module.exports = { 8 | query: (text, params, callback) => { 9 | // console.log('exectued query', text); 10 | return pool.query(text, params, callback); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ThermaKube 6 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /server/kubeconfig.js: -------------------------------------------------------------------------------- 1 | // API from the Kubernetes JavaScrip Client Library 2 | const k8Api = require('@kubernetes/client-node'); 3 | 4 | // Create an empty config file 5 | const kubeConfig = new k8Api.KubeConfig(); 6 | 7 | // Load the default config file, whichever kubectl is connected to 8 | kubeConfig.loadFromDefault(); 9 | 10 | // Use the existing config to connect to cluster 11 | const kube = kubeConfig.makeApiClient(k8Api.CoreV1Api); 12 | 13 | 14 | module.exports = { kube }; 15 | -------------------------------------------------------------------------------- /client/components/home/Contribute.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Github } from '@icons-pack/react-simple-icons'; 3 | 4 | 5 | const Contribute = () => { 6 | return ( 7 |
8 |

ThermaKube is an Open-Source Project

9 |
Feel free to contribute!
10 | 11 | 12 | 13 |
14 | ) 15 | } 16 | 17 | export default Contribute; -------------------------------------------------------------------------------- /client/containers/Visualizer_Container.jsx: -------------------------------------------------------------------------------- 1 | //traffic view of kubernetes clusters/individual pods 2 | import React, { useEffect, useState } from 'react'; 3 | import RadialTree from '../components/visualizer/RadialTree.jsx'; 4 | 5 | const Visualizer = (props) => { 6 | // console.log(props, 'props from vis'); 7 | let data = [props.data[1]]; 8 | 9 | return ( 10 |
11 |

Traffic Visualizer

12 | 13 |
14 | ); 15 | }; 16 | 17 | export default Visualizer; 18 | -------------------------------------------------------------------------------- /client/containers/Cluster_Container.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Pods from '../components/cluster/Pods.jsx'; 4 | import Nodes from '../components/cluster/Nodes.jsx'; 5 | import Services from '../components/cluster/Services.jsx'; 6 | 7 | const Cluster_Container = (props) => { 8 | const { data } = props; 9 | return ( 10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Cluster_Container; 21 | -------------------------------------------------------------------------------- /server/query/NodeQuery.js: -------------------------------------------------------------------------------- 1 | function NodeQuery(data) { 2 | this.name = []; 3 | this.cpu = []; 4 | 5 | // loop through body.items length 6 | for (let i = 0; i < data.body.items.length; i++) { 7 | (this.name[i] = data.body.items[i].metadata.name), 8 | (this.cpu[i] = data.body.items[i].status.allocatable.cpu); 9 | } 10 | } 11 | function AwsNodeQuery(data) { 12 | this.name = []; 13 | this.cpu = []; 14 | 15 | // loop through body.items length 16 | for (let i = 0; i < data.items.length; i++) { 17 | (this.name[i] = data.items[i].metadata.name), 18 | (this.cpu[i] = data.items[i].status.allocatable.cpu); 19 | } 20 | } 21 | 22 | module.exports = { NodeQuery, AwsNodeQuery }; 23 | -------------------------------------------------------------------------------- /server/routes/login.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const loginRouter = express.Router(); 4 | 5 | const UserController = require('../controllers/UserController'); 6 | const CookieController = require('../controllers/CookieController'); 7 | 8 | loginRouter.post( 9 | '/signup', 10 | UserController.addUser, 11 | CookieController.setNewSSID, 12 | CookieController.setJwtToken, 13 | (req, res) => { 14 | res.status(200).json(res.locals.token); 15 | } 16 | ); 17 | 18 | loginRouter.post( 19 | '/verify', 20 | UserController.verifyUser, 21 | CookieController.setSSID, 22 | CookieController.setJwtToken, 23 | (req, res) => { 24 | res.status(200).json(res.locals.token); 25 | } 26 | ); 27 | 28 | module.exports = loginRouter; 29 | -------------------------------------------------------------------------------- /server/routes/aws.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const awsRouter = express.Router(); 4 | 5 | const AwsController = require('../controllers/AwsController'); 6 | 7 | awsRouter.post('/clusters', AwsController.cluster, (req, res) => { 8 | res.status(200).json(res.locals.clusters); 9 | }); 10 | 11 | awsRouter.post( 12 | '/select', 13 | AwsController.selectCluster, 14 | AwsController.authToken, 15 | AwsController.getPods, 16 | AwsController.getNodes, 17 | AwsController.getServices, 18 | AwsController.getPodUsage, 19 | (req, res) => { 20 | res.status(200).json({ 21 | pods: res.locals.awsPods, 22 | nodes: res.locals.awsNodes, 23 | services: res.locals.awsServices, 24 | podUsage: res.locals.awsPodUsage, 25 | }); 26 | } 27 | ); 28 | 29 | module.exports = awsRouter; 30 | -------------------------------------------------------------------------------- /client/components/cluster/Nodes.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Table } from 'react-bootstrap'; 3 | import axios from 'axios'; 4 | 5 | const Nodes = ({ data }) => { 6 | const nodeList = data[0].children.map((data, index) => { 7 | return ( 8 | 9 | 10 | {data.name} 11 | {data.cpu} 12 | 13 | 14 | ); 15 | }); 16 | 17 | return ( 18 |
19 |

Nodes List

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {nodeList} 28 |
Node NameCPU
29 |
30 | ); 31 | }; 32 | 33 | export default Nodes; 34 | -------------------------------------------------------------------------------- /client/components/visualizer/useResizeObserver.jsx: -------------------------------------------------------------------------------- 1 | //custom react hook for dynamically-sized, responsive diagram 2 | //returns changed width and height of element 3 | import React, { useState, useEffect } from 'react'; 4 | //for support in other browsers, i.e. Edge and Safari 5 | import ResizeObserver from 'resize-observer-polyfill'; 6 | 7 | const useResizeObserver = (ref) => { 8 | const [dimensions, setDimensions] = useState(null); 9 | 10 | useEffect(() => { 11 | const observeTarget = ref.current; 12 | const resizeObserver = new ResizeObserver(entries => { 13 | entries.forEach(entry => { 14 | setDimensions(entry.contentRect); 15 | }); 16 | }); 17 | resizeObserver.observe(observeTarget); 18 | return () => { 19 | resizeObserver.unobserve(observeTarget); 20 | }; 21 | }, [ref]); 22 | return dimensions; 23 | } 24 | 25 | export default useResizeObserver; -------------------------------------------------------------------------------- /server/controllers/NodeController.js: -------------------------------------------------------------------------------- 1 | const { kube } = require('../kubeconfig'); 2 | const { NodeQuery } = require('../query/NodeQuery'); 3 | 4 | const NodeController = {}; 5 | 6 | NodeController.getNodes = (req, res, next) => { 7 | // console.log('test from inside NodeController'); 8 | //get data from kube api 9 | kube 10 | .listNode('default') 11 | .then((data) => { 12 | const result = new NodeQuery(data); 13 | const nodeArray = []; 14 | for (let i = 0; i < result.name.length; i++) { 15 | let obj = { 16 | name: result.name[i], 17 | cpu: result.cpu[i], 18 | }; 19 | nodeArray.push(obj); 20 | } 21 | // console.log('nodeArr', nodeArray); 22 | res.locals.nodes = nodeArray; 23 | return next(); 24 | }) 25 | .catch((err) => { 26 | console.log('err in node k8 api', err); 27 | }); 28 | }; 29 | 30 | module.exports = NodeController; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/components/home/Hero.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FadeIn from 'react-fade-in'; 3 | import icon from '../../assets/icon.png'; 4 | import GitHubButton from 'react-github-btn'; 5 | 6 | const Hero = () => { 7 | return ( 8 |
9 |
10 | 11 |

12 | Power your metrics and monitoring with a modern open-source solution 13 | for
any Kubernetes cluster 14 |

15 |
16 | Logo 17 |
18 |

ThermaKube

19 |
20 | 25 | Follow ThermaKube 26 | 27 |
28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Hero; 35 | -------------------------------------------------------------------------------- /server/query/ServiceQuery.js: -------------------------------------------------------------------------------- 1 | function ServiceQuery(data) { 2 | this.name = []; 3 | this.type = []; 4 | this.namespace = []; 5 | this.port = []; 6 | this.clusterIP = []; 7 | 8 | // loop through body.items length 9 | for (let i = 0; i < data.body.items.length; i++) { 10 | (this.name[i] = data.body.items[i].metadata.name), 11 | (this.type[i] = data.body.items[i].spec.type), 12 | (this.namespace[i] = data.body.items[i].metadata.namespace), 13 | (this.port[i] = data.body.items[i].spec.ports[0].port), 14 | (this.clusterIP[i] = data.body.items[i].spec.clusterIP); 15 | } 16 | } 17 | 18 | function AwsServiceQuery(data) { 19 | this.name = []; 20 | this.type = []; 21 | this.namespace = []; 22 | this.port = []; 23 | this.clusterIP = []; 24 | 25 | // loop through body.items length 26 | for (let i = 0; i < data.items.length; i++) { 27 | (this.name[i] = data.items[i].metadata.name), 28 | (this.type[i] = data.items[i].spec.type), 29 | (this.namespace[i] = data.items[i].metadata.namespace), 30 | (this.port[i] = data.items[i].spec.ports[0].port), 31 | (this.clusterIP[i] = data.items[i].spec.clusterIP); 32 | } 33 | } 34 | module.exports = { ServiceQuery, AwsServiceQuery }; 35 | -------------------------------------------------------------------------------- /client/components/cluster/Services.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Table } from 'react-bootstrap'; 3 | import axios from 'axios'; 4 | 5 | const Services = ({ data }) => { 6 | const [table, setTable] = useState([]); 7 | 8 | useEffect(() => { 9 | const serviceList = data.map((data, index) => { 10 | return ( 11 | 12 | 13 | {data.name} 14 | {data.type} 15 | {data.namespace} 16 | {data.port} 17 | {data.clusterIP} 18 | 19 | 20 | ); 21 | }); 22 | setTable(serviceList); 23 | }, []); 24 | 25 | return ( 26 |
27 |

Services List

28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {table} 39 |
Service NameTypeNamespacePortCluster IP
40 |
41 | ); 42 | }; 43 | 44 | export default Services; 45 | -------------------------------------------------------------------------------- /server/controllers/ServiceController.js: -------------------------------------------------------------------------------- 1 | const { kube } = require('../kubeconfig'); 2 | const { ServiceQuery } = require('../query/ServiceQuery'); 3 | 4 | const ServiceController = {}; 5 | 6 | ServiceController.getServices = (req, res, next) => { 7 | // console.log('test from inside ServiceController'); 8 | //get data from kube api 9 | kube 10 | .listNamespacedService('default') 11 | .then((data) => { 12 | const result = new ServiceQuery(data); 13 | const serviceArray = []; 14 | for (let i = 0; i < result.name.length; i++) { 15 | let obj = { 16 | name: result.name[i], 17 | type: result.type[i], 18 | namespace: result.namespace[i], 19 | port: result.port[i], 20 | clusterIP: result.clusterIP[i], 21 | }; 22 | serviceArray.push(obj); 23 | } 24 | res.locals.service = serviceArray; 25 | 26 | // //used to check the data coming back from query 27 | // console.log('from inside service controller') 28 | // res.locals.service = data.body.items[0].spec.clusterIP; 29 | // console.log(res.locals.service); 30 | return next(); 31 | }) 32 | .catch((err) => { 33 | console.log('err in service k8 api', err); 34 | }); 35 | }; 36 | 37 | module.exports = ServiceController; 38 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const app = express(); 4 | const PORT = 3000; 5 | const cors = require('cors'); 6 | const cookieParser = require('cookie-parser'); 7 | 8 | require('dotenv').config(); 9 | 10 | // require routers 11 | const awsRouter = require('./routes/aws'); 12 | const apiRouter = require('./routes/api'); 13 | const loginRouter = require('./routes/login'); 14 | 15 | app.use(cors()); 16 | app.use(express.json()); 17 | app.use(cookieParser()); 18 | app.use(express.static(path.join(__dirname, '../dist/'))); 19 | 20 | // route handlers 21 | app.use('/aws', awsRouter); 22 | app.use('/api', apiRouter); 23 | app.use('/login', loginRouter); 24 | 25 | // serve html 26 | app.use('/', (req, res) => { 27 | res.status(200).sendFile(path.resolve(__dirname, '../dist/index.html')); 28 | }); 29 | // catch all 30 | app.use('/', (req, res) => { 31 | res.sendStatus(404); 32 | }); 33 | // global error handler 34 | app.use((err, req, res, next) => { 35 | const defaultErr = { 36 | log: 'Express error handler caught unknown middleware error', 37 | status: 400, 38 | message: { err: 'error occurred' }, 39 | }; 40 | const errorObj = Object.assign({}, defaultErr, err); 41 | console.log(errorObj.log); 42 | return res.status(errorObj.status).json(errorObj.message); 43 | }); 44 | 45 | app.listen(PORT, () => { 46 | console.log(`Listening to Port ${PORT}...`); 47 | }); 48 | -------------------------------------------------------------------------------- /client/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | 4 | import Hero from './home/Hero.jsx'; 5 | import Features from './home/Features.jsx'; 6 | import Contribute from './home/Contribute.jsx'; 7 | import Team from './home/Team.jsx'; 8 | 9 | const Home = props => { 10 | const [awsData, setAwsData] = useState({ 11 | pods: [], 12 | nodes: [], 13 | services: [], 14 | }); 15 | useEffect(() => { 16 | if (props.location.state) { 17 | const awsInfo = props.location.state.data; 18 | // console.log('awsInfo', awsInfo); 19 | if (awsInfo) { 20 | setAwsData({ 21 | ...awsData, 22 | pods: awsInfo.pods, 23 | nodes: awsInfo.nodes, 24 | services: awsInfo.services, 25 | }); 26 | } else { 27 | // console.log('none'); 28 | } 29 | } 30 | }, []); 31 | return ( 32 | <> 33 | {/* {console.log('awsData', awsData)} */} 34 | {awsData !== 0 ? ( 35 | 41 | ) : null} 42 | 43 |
44 | 45 | 46 | 47 | 48 |
49 |
50 | 51 | ); 52 | }; 53 | 54 | export default Home; 55 | -------------------------------------------------------------------------------- /db/thermakube_postgres_create.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "users" ( 2 | "id" serial NOT NULL UNIQUE, 3 | "email" varchar(255) NOT NULL UNIQUE, 4 | "password" varchar(255) NOT NULL, 5 | CONSTRAINT "users_pk" PRIMARY KEY ("id") 6 | ) WITH ( 7 | OIDS=FALSE 8 | ); 9 | 10 | 11 | 12 | CREATE TABLE "aws_users" ( 13 | "id" serial NOT NULL UNIQUE, 14 | "access_key_id" varchar(255) NOT NULL UNIQUE, 15 | "secret_access_key" varchar(255) NOT NULL, 16 | CONSTRAINT "aws_users_pk" PRIMARY KEY ("id") 17 | ) WITH ( 18 | OIDS=FALSE 19 | ); 20 | 21 | 22 | 23 | CREATE TABLE "alerts" ( 24 | "id" serial NOT NULL UNIQUE, 25 | "user_id" integer NOT NULL, 26 | "pod_name" varchar(255) NOT NULL, 27 | "namespace" varchar(255) NOT NULL, 28 | "status" varchar(255) NOT NULL, 29 | "pod_ip" varchar(255), 30 | "timestamp" varchar(255) NOT NULL, 31 | CONSTRAINT "alerts_pk" PRIMARY KEY ("id") 32 | ) WITH ( 33 | OIDS=FALSE 34 | ); 35 | 36 | 37 | 38 | CREATE TABLE "aws_alerts" ( 39 | "id" serial NOT NULL UNIQUE, 40 | "user_id" integer NOT NULL, 41 | "pod_name" varchar(255) NOT NULL, 42 | "namespace" varchar(255) NOT NULL, 43 | "status" varchar(255) NOT NULL, 44 | "pod_ip" varchar(255), 45 | "timstamp" TIMESTAMP(255) NOT NULL, 46 | CONSTRAINT "aws_alerts_pk" PRIMARY KEY ("id") 47 | ) WITH ( 48 | OIDS=FALSE 49 | ); 50 | 51 | 52 | 53 | 54 | 55 | ALTER TABLE "alerts" ADD CONSTRAINT "alerts_fk0" FOREIGN KEY ("user_id") REFERENCES "users"("id"); 56 | 57 | ALTER TABLE "aws_alerts" ADD CONSTRAINT "aws_alerts_fk0" FOREIGN KEY ("user_id") REFERENCES "aws_users"("id"); 58 | -------------------------------------------------------------------------------- /server/query/PodQuery.js: -------------------------------------------------------------------------------- 1 | // constructor function for first pod 2 | function PodQuery(data) { 3 | // assign all variables as empty array 4 | this.name = []; 5 | this.namespace = []; 6 | this.status = []; 7 | this.podIP = []; 8 | this.createdAt = []; 9 | this.nodeName = []; 10 | this.labels = []; 11 | 12 | // loop through body.items length 13 | for (let i = 0; i < data.body.items.length; i++) { 14 | (this.name[i] = data.body.items[i].metadata.name), 15 | (this.namespace[i] = data.body.items[i].metadata.namespace), 16 | (this.status[i] = data.body.items[i].status.phase), 17 | (this.podIP[i] = data.body.items[i].status.podIP), 18 | (this.createdAt[i] = data.body.items[i].status.startTime), 19 | (this.nodeName[i] = data.body.items[i].spec.nodeName), 20 | (this.labels[i] = data.body.items[i].metadata.labels.run); 21 | } 22 | } 23 | 24 | function AwsPodQuery(data) { 25 | this.name = []; 26 | this.namespace = []; 27 | this.status = []; 28 | this.podIP = []; 29 | this.createdAt = []; 30 | this.nodeName = []; 31 | 32 | for (let i = 0; i < data.items.length; i++) { 33 | (this.name[i] = data.items[i].metadata.name), 34 | (this.namespace[i] = data.items[i].metadata.namespace), 35 | (this.status[i] = data.items[i].status.phase), 36 | (this.podIP[i] = data.items[i].status.podIP), 37 | (this.createdAt[i] = data.items[i].metadata.creationTimestamp), 38 | (this.nodeName[i] = data.items[i].spec.nodeName); 39 | } 40 | } 41 | 42 | module.exports = { PodQuery, AwsPodQuery }; 43 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const apiRouter = express.Router(); 4 | 5 | const PodController = require('../controllers/PodController'); 6 | const NodeController = require('../controllers/NodeController'); 7 | const ServiceController = require('../controllers/ServiceController'); 8 | const CookieController = require('../controllers/CookieController'); 9 | const AlertsController = require('../controllers/AlertsController'); 10 | 11 | // fetch pods from K8 Api 12 | apiRouter.get( 13 | '/pods', 14 | PodController.getPods, 15 | PodController.getPodUsage, 16 | (req, res) => { 17 | // console.log('res.local.usage', res.locals.usage); 18 | // res.status(200).json(res.locals.pod); 19 | res.status(200).json({ pod: res.locals.pod, usage: res.locals.usage }); 20 | } 21 | ); 22 | 23 | // fetch nodes from K8 Api 24 | apiRouter.get('/nodes', NodeController.getNodes, (req, res) => { 25 | res.status(200).json(res.locals.nodes); 26 | }); 27 | 28 | // fetch services from K8 Api 29 | apiRouter.get('/services', ServiceController.getServices, (req, res) => { 30 | res.status(200).json(res.locals.service); 31 | }); 32 | 33 | // fetch alerts from db 34 | apiRouter.get( 35 | '/alerts', 36 | CookieController.verifyToken, 37 | AlertsController.getAlerts, 38 | (req, res) => { 39 | res.status(200).json(res.locals.alerts); 40 | } 41 | ); 42 | 43 | // add a new alert to db 44 | apiRouter.post( 45 | '/alerts', 46 | CookieController.verifyToken, 47 | AlertsController.addAlerts, 48 | (req, res) => { 49 | res.status(200).json(res.locals.alert); 50 | } 51 | ); 52 | 53 | module.exports = apiRouter; 54 | -------------------------------------------------------------------------------- /server/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/userModel'); 2 | 3 | const UserController = {}; 4 | 5 | // controller for adding new users to db 6 | UserController.addUser = (req, res, next) => { 7 | // req body will come with email, and password 8 | const { newEmail, newPassword } = req.body.signup; 9 | // create add user query 10 | const addUser = { 11 | text: ` 12 | INSERT INTO users 13 | (email, password) 14 | VALUES 15 | ($1, $2) 16 | RETURNING * 17 | `, 18 | values: [newEmail, newPassword], 19 | }; 20 | db.query(addUser) 21 | .then((user) => { 22 | res.locals.signup = user.rows[0]; 23 | return next(); 24 | }) 25 | .catch((err) => { 26 | console.log('err in user sign up controller', err); 27 | }); 28 | }; 29 | 30 | //controller to verify user login 31 | UserController.verifyUser = (req, res, next) => { 32 | const { email, password } = req.body.login; 33 | const userQuery = { 34 | text: ` 35 | SELECT * FROM users 36 | WHERE email = $1 37 | AND password = $2 38 | `, 39 | values: [email, password], 40 | }; 41 | db.query(userQuery) 42 | .then((user) => { 43 | if (user.rows[0]) { 44 | console.log('verified'); 45 | res.locals.user = user.rows[0]; 46 | return next(); 47 | } else { 48 | console.log('unverified or user does not exist'); 49 | return res.status(401).json({ message: 'unverified' }); 50 | } 51 | }) 52 | .catch((err) => { 53 | console.log('error in verify user controller', err); 54 | }); 55 | }; 56 | 57 | module.exports = UserController; 58 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV, 6 | entry: './client/index.js', 7 | output: { 8 | path: path.resolve(__dirname, 'dist/'), 9 | publicPath: '/', 10 | filename: 'main.js', 11 | }, 12 | devServer: { 13 | // Required for Docker to work with dev server 14 | host: '0.0.0.0', 15 | //host: localhost, 16 | port: 8080, 17 | // match the output path 18 | contentBase: path.resolve(__dirname, 'dist'), 19 | //enable HMR on the devServer 20 | hot: true, 21 | //match the output 'publicPath' 22 | publicPath: '/', 23 | proxy: [ 24 | { 25 | context: ['/aws/', '/api/', '/login/'], 26 | target: 'http://localhost:3000/', 27 | secure: false, 28 | }, 29 | ], 30 | // hot: true, 31 | historyApiFallback: true, 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.(js|jsx)$/, 37 | exclude: /node_modules/, 38 | use: { 39 | loader: 'babel-loader', 40 | }, 41 | }, 42 | { 43 | test: /\.html$/, 44 | use: [ 45 | { 46 | loader: 'html-loader', 47 | }, 48 | ], 49 | }, 50 | { 51 | test: /\.s[ac]ss$/i, 52 | use: ['style-loader', 'css-loader', 'sass-loader'], 53 | }, 54 | { 55 | test: /\.(jpg|png)$/, 56 | use: { 57 | loader: 'url-loader', 58 | }, 59 | }, 60 | ], 61 | }, 62 | plugins: [ 63 | new HtmlWebPackPlugin({ 64 | template: './index.html', 65 | }), 66 | ], 67 | resolve: { 68 | extensions: ['.js', '.jsx'], 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /client/components/Eks.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import axios from 'axios'; 4 | 5 | const Eks = props => { 6 | const [auth, setAuth] = useState(false); 7 | const [myCluster, setMyCluster] = useState({ 8 | pods: [], 9 | nodes: [], 10 | services: [], 11 | podUsage: [], 12 | }); 13 | const data = props.location.state.data; 14 | const credentials = props.location.state.credentials; 15 | 16 | // function for selecting cluster 17 | const selectCluster = async cluster => { 18 | const selected = await axios.post('/aws/select', { 19 | credentials, 20 | cluster, 21 | }); 22 | const awsCluster = selected.data; 23 | if (awsCluster) { 24 | setMyCluster({ 25 | ...myCluster, 26 | pods: awsCluster.pods, 27 | nodes: awsCluster.nodes, 28 | services: awsCluster.services, 29 | podUsage: awsCluster.podUsage, 30 | }); 31 | setAuth(true); 32 | } else { 33 | // console.log('none'); 34 | } 35 | }; 36 | 37 | const namesList = data.map(cluster => { 38 | return ( 39 | 46 | ); 47 | }); 48 | // once cluster is selected, pass down data from aws api 49 | return ( 50 | <> 51 | {/* {console.log('myCluster', myCluster)} */} 52 | {auth ? ( 53 | 59 | ) : null} 60 |
61 |
Choose Cluster
62 | {namesList} 63 |
64 | 65 | ); 66 | }; 67 | 68 | export default Eks; 69 | -------------------------------------------------------------------------------- /client/components/home/Features.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Fade from 'react-reveal/Fade'; 3 | // screenshots 4 | import cluster_ss from '../../assets/cluster_screenshot.png'; 5 | import vis_ss from '../../assets/vis_screenshot.png'; 6 | 7 | const Features = () => { 8 | return ( 9 |
10 |

Visualize Your Kubernetes Clusters

11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 | 19 |
20 |

Monitor and Visualize your Kubernetes Clusters

21 |

22 | ThermaKube monitors the health and performance of Kubernetes 23 | clusters with support for Amazon's Elastic Kubernetes Service 24 | (EKS) deployments. It tracks real-time data and renders 25 | visualization of clusters, and alerts you when pods within the 26 | cluster crash - all features you can utilize with a click of a 27 | button, without having to download or configure anything! 28 |

29 |

Why Use Us

30 |

31 | We provide: 32 |

  • a simplified way to display your Kubernetes cluster data
  • 33 |
  • 34 | a visualization tool to display relationships within the 35 | cluster, as well as usage data on pods 36 |
  • 37 |
    38 | Monitor your pods with a click of a button! 39 |

    40 |
    41 |
    42 |
    43 |
    44 | ); 45 | }; 46 | 47 | export default Features; 48 | -------------------------------------------------------------------------------- /server/controllers/PodController.js: -------------------------------------------------------------------------------- 1 | const { kube } = require('../kubeconfig'); 2 | const { PodQuery } = require('../query/PodQuery'); 3 | const cmd = require('node-cmd'); 4 | 5 | const PodController = {}; 6 | 7 | // middleware to get pods upon loading the home page 8 | PodController.getPods = (req, res, next) => { 9 | // grabbing data from kube api 10 | kube 11 | .listNamespacedPod('default') 12 | .then((data) => { 13 | // create new object with retrieved data - result will now containe pod name, namespace, status, ip address, and createdAt 14 | const result = new PodQuery(data); 15 | const podArray = []; 16 | for (let i = 0; i < result.name.length; i++) { 17 | let obj = { 18 | name: result.name[i], 19 | namespace: result.namespace[i], 20 | status: result.status[i], 21 | podIP: result.podIP[i], 22 | createdAt: result.createdAt[i].toString(), 23 | nodeName: result.nodeName[i], 24 | labels: result.labels[i], 25 | }; 26 | podArray.push(obj); 27 | } 28 | // store in res.locals 29 | res.locals.pod = podArray; 30 | // console.log('podArr', podArray); 31 | return next(); 32 | }) 33 | .catch((err) => { 34 | console.log('err in pod k8 api', err); 35 | }); 36 | }; 37 | 38 | // middleware to get pod usage info 39 | PodController.getPodUsage = (req, res, next) => { 40 | //cmd library to access CLI 41 | //using kubectl top pod 42 | cmd.get('kubectl top pod', function (err, data, stderr) { 43 | if (err) return next(err); 44 | 45 | //split by enter 46 | const lines = data.split('\n'); 47 | 48 | const result = []; 49 | for (let i = 1; i < lines.length - 1; i++) { 50 | //use regex, split string by any number of whitespaces 51 | const words = lines[i].match(/\S+/g); 52 | const podUse = { name: words[0], cpu: words[1], memory: words[2] }; 53 | result.push(podUse); 54 | } 55 | res.locals.usage = result; 56 | return next(); 57 | }); 58 | }; 59 | 60 | module.exports = PodController; 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ThermaKube 2 | 3 | ThermaKube is an open-source Kubernetes web application that monitors the health and performance of Kubernetes clusters with support for AWS EKS deployments. It tracks real-time data, renders visualization of clusters, and has alerts for when pods within the clusters crash. 4 | 5 | ## Getting Started 6 | **Prerequisites:** In order to properly use ThermaKube, have your Kubernetes cluster deployed and kubectl configured. 7 | To check the location and credentials that kubectl knows about, use the following command: 8 | ``` 9 | kubectl config view 10 | ``` 11 | Alternatively, you can refer to the offical K8s documents for more [information](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-api/). 12 | 13 | You can either use your Kubernetes configured on your desktop, or you can login and use Kubernetes cluster deployed on AWS. 14 | 15 | ## How to Use 16 | ThermaKube is a client-side application available via the browser at https://thermakube.com. You can navigate to different sections or pages to look at cluster information in different ways. 17 | 18 | **Cluster Information** 19 | 20 | * Displays data on pods, nodes and services within a single cluster in a easy-to-view, compact table format. 21 | 22 | **Visualizer** 23 | 24 | * Renders a radial tree of a cluster that shows relationships between pods, nodes and services with conditional coloring to mirror how kube-proxy directs incoming requests through services. 25 | 26 | **Alerts** 27 | 28 | * Displays the name, time, and current status of pods that have crashed. 29 | 30 | ## Contributing 31 | We love feedback and are always looking to improve! For any major changes, please open an issue and discuss what you would like to change. Pull requests are welcome. 32 | 33 | ## Authors 34 | 35 | * Clara Kim - [@clarakm](https://github.com/clarakm) 36 | * Elie Baik - [@semtemp](https://github.com/semtemp) 37 | * Evan Amoranto - [@eamoranto](https://github.com/eamoranto) 38 | * Kritika Sah - [@hellokritty](https://github.com/hellokritty) 39 | * Nick Primuth - [@nickprimuth](https://github.com/nickprimuth) 40 | 41 | ## License 42 | Distributed under the MIT License. See `LICENSE` for more information. 43 | -------------------------------------------------------------------------------- /client/components/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Nav, NavDropdown } from 'react-bootstrap'; 3 | 4 | import white from '../assets/whiteLogo.png'; 5 | import Cookies from 'js-cookie'; 6 | 7 | const Dashboard = ({ isLoggedIn, removeAuth }) => { 8 | const [navOption, setNavOption] = useState([]); 9 | // remove cookie w/ token when users sign out and update auth value in local storage 10 | const signOut = () => { 11 | Cookies.remove('token'); 12 | removeAuth(); 13 | }; 14 | useEffect(() => { 15 | if (isLoggedIn == 'true') { 16 | //if logged in, show cluster option in dashboard 17 | setNavOption( 18 | <> 19 | 20 | Cluster 21 | Visualizer 22 | Alerts 23 | 24 | 25 | Sign Out 26 | 27 | 28 | ); 29 | } else { 30 | setNavOption( 31 | //login option shows only if not logged in - for now, since no logout in backend 32 | 33 | Login 34 | 35 | ); 36 | } 37 | }, [isLoggedIn]); 38 | 39 | return ( 40 |
    41 | 62 |
    63 | ); 64 | }; 65 | 66 | export default Dashboard; 67 | -------------------------------------------------------------------------------- /server/controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/userModel'); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | const CookieController = {}; 5 | 6 | CookieController.setNewSSID = (req, res, next) => { 7 | const { newEmail } = req.body.signup; 8 | // store user id in a cookie 9 | const findUser = { 10 | text: ` 11 | SELECT id FROM users 12 | WHERE email = $1 13 | `, 14 | values: [newEmail], 15 | }; 16 | db.query(findUser) 17 | .then((id) => { 18 | console.log(id.rows[0]); 19 | res.locals.userId = id.rows[0].id; 20 | return next(); 21 | }) 22 | .catch((err) => { 23 | console.log('err in cookie middleware', err); 24 | }); 25 | }; 26 | 27 | CookieController.setSSID = (req, res, next) => { 28 | const { email } = req.body.login; 29 | // store user id in a cookie 30 | const findUser = { 31 | text: ` 32 | SELECT id FROM users 33 | WHERE email = $1 34 | `, 35 | values: [email], 36 | }; 37 | db.query(findUser) 38 | .then((id) => { 39 | console.log(id.rows[0]); 40 | res.locals.userId = id.rows[0].id; 41 | return next(); 42 | }) 43 | .catch((err) => { 44 | console.log('err in cookie middleware', err); 45 | }); 46 | }; 47 | 48 | CookieController.setJwtToken = (req, res, next) => { 49 | try { 50 | // create jwt 51 | const payload = { userId: res.locals.userId }; 52 | // add jwt as cookie 53 | const token = jwt.sign(payload, process.env.JWTSECRET); 54 | console.log('token', token); 55 | res.locals.token = token; 56 | res.cookie('jwt_token', token, { httpOnly: true }); 57 | return next(); 58 | } catch (err) { 59 | console.log('err in jwt token middleware', err); 60 | } 61 | }; 62 | 63 | CookieController.verifyToken = (req, res, next) => { 64 | try { 65 | console.log('header', req.headers.authorization); 66 | const token = req.headers.authorization.slice(6); 67 | const payload = jwt.verify(token, process.env.JWTSECRET); 68 | res.locals.userId = payload.userId; 69 | console.log(res.locals.userId); 70 | return next(); 71 | } catch (err) { 72 | console.log('error in verify token middleware', err); 73 | } 74 | }; 75 | 76 | module.exports = CookieController; 77 | -------------------------------------------------------------------------------- /client/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Lottie from 'react-lottie'; 3 | import * as animationData from '../assets/cloud-loader.json'; 4 | import * as animationDataDone from '../assets/doneLoading.json'; 5 | 6 | const Loader = ({ doneFetching, setStillLoading, path }) => { 7 | //state for render animation logic based on if fetching in parent is done 8 | const [loadingNotFinished, setLoadingNotFinished] = useState(true); 9 | // settings for two Lottie animations 10 | let pathName = path.substring(1, path.length); 11 | // console.log(pathName, 'pathname from loader'); 12 | const defaultOptionsLoading = { 13 | loop: true, 14 | autoplay: true, 15 | animationData: animationData.default, 16 | rendererSettings: { 17 | preserveAspectRatio: 'xMidYMid slice', 18 | }, 19 | }; 20 | const defaultOptionsDone = { 21 | loop: true, 22 | autoplay: true, 23 | animationData: animationDataDone.default, 24 | rendererSettings: { 25 | preserveAspectRatio: 'xMidYMid slice', 26 | }, 27 | }; 28 | const container = { 29 | display: 'flex', 30 | justifyContent: 'center', 31 | alignItems: 'baseline', 32 | }; 33 | useEffect(() => { 34 | let doneFetchingTimer; 35 | let stillLoadingTimer; 36 | //when done fetching, set 3.3 second timeout to then throw green checkmark and setStillloading in parent to false 37 | if (doneFetching) { 38 | doneFetchingTimer = setTimeout(() => { 39 | //set state 40 | setLoadingNotFinished(false); 41 | // console.log('doneFetchingTimer from loader'); 42 | stillLoadingTimer = setTimeout(() => { 43 | setStillLoading(false); 44 | // console.log('stillLoadingTimer from loader'); 45 | }, 700); 46 | }, 3300); 47 | } 48 | return () => clearTimeout(doneFetchingTimer, stillLoadingTimer); 49 | }, [doneFetching]); 50 | return ( 51 |
    52 |

    Loading {pathName} data...

    53 | {loadingNotFinished ? ( 54 |
    55 | 56 |
    57 | ) : ( 58 |
    59 | 60 |
    61 | )} 62 |
    63 | ); 64 | }; 65 | 66 | export default Loader; 67 | -------------------------------------------------------------------------------- /client/components/home/Team.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Github, Linkedin } from '@icons-pack/react-simple-icons'; 3 | import Flip from 'react-reveal/Flip'; 4 | //profile pics 5 | import kritika from '../../assets/kritika.png'; 6 | import nick from '../../assets/nick.png'; 7 | import clara from '../../assets/clara_photo.jpg'; 8 | import elie from '../../assets/elie_photo.jpg'; 9 | import evan from '../../assets/evan.jpg'; 10 | 11 | const Team = () => { 12 | const members = [ 13 | { 14 | name: 'Evan Amoranto', 15 | github: 'https://github.com/eamoranto', 16 | linkedin: 'https://www.linkedin.com/in/eamoranto', 17 | img: evan, 18 | }, 19 | { 20 | name: 'Elie Baik', 21 | github: 'https://github.com/semtemp', 22 | linkedin: 'https://www.linkedin.com/in/sae-min-baik/', 23 | img: elie, 24 | }, 25 | { 26 | name: 'Clara Kim', 27 | github: 'https://github.com/clarakm', 28 | linkedin: 'https://www.linkedin.com/in/clarayhkim/', 29 | img: clara, 30 | }, 31 | { 32 | name: 'Nick Primuth', 33 | github: 'https://github.com/NickPrimuth', 34 | linkedin: 'https://www.linkedin.com/in/nick-primuth/', 35 | img: nick, 36 | }, 37 | { 38 | name: 'Kritika Sah', 39 | github: 'https://github.com/hellokritty', 40 | linkedin: 'https://www.linkedin.com/in/kritikasah/', 41 | img: kritika, 42 | }, 43 | ]; 44 | 45 | const teamArr = members.map((mem, index) => { 46 | return ( 47 | 48 | 49 |
    50 | 51 |
    {mem.name}
    52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
    61 |
    62 |
    63 | ); 64 | }); 65 | 66 | return ( 67 |
    68 |

    ThermaKube Team

    69 |
    {teamArr}
    70 |
    71 | ); 72 | }; 73 | 74 | export default Team; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-react-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --mode production", 8 | "start": "webpack-dev-server --open --mode development", 9 | "dev": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open\" \"nodemon server/server.js\"" 10 | }, 11 | "proxy": "http://localhost:3000", 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@babel/core": "^7.8.7", 17 | "@babel/plugin-transform-runtime": "^7.8.3", 18 | "@babel/preset-env": "^7.8.7", 19 | "@babel/preset-react": "^7.8.3", 20 | "@icons-pack/react-simple-icons": "^2.6.0", 21 | "axios": "^0.19.2", 22 | "babel-loader": "^8.0.6", 23 | "css-loader": "^3.4.2", 24 | "cssloader": "^1.1.1", 25 | "html-loader": "^0.5.5", 26 | "html-webpack-plugin": "^3.2.0", 27 | "node-cmd": "^3.0.0", 28 | "node-sass": "^4.13.1", 29 | "nodemon": "^2.0.2", 30 | "react-hot-loader": "^4.12.19", 31 | "resize-observer-polyfill": "^1.5.1", 32 | "sass-loader": "^8.0.2", 33 | "style-loader": "^1.1.3", 34 | "url-loader": "^4.0.0", 35 | "webpack": "^4.42.0", 36 | "webpack-cli": "^3.3.11", 37 | "webpack-dev-server": "^3.10.3" 38 | }, 39 | "dependencies": { 40 | "@babel/runtime": "^7.8.7", 41 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 42 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 43 | "@fortawesome/react-fontawesome": "^0.1.9", 44 | "@kubernetes/client-node": "^0.11.1", 45 | "aws4": "^1.9.1", 46 | "bootstrap": "^4.4.1", 47 | "concurrently": "^5.1.0", 48 | "cookie-parser": "^1.4.5", 49 | "cors": "^2.8.5", 50 | "cross-env": "^7.0.2", 51 | "d3": "^5.15.0", 52 | "dotenv": "^8.2.0", 53 | "express": "^4.17.1", 54 | "js-base64": "^2.5.2", 55 | "js-cookie": "^2.2.1", 56 | "jsonwebtoken": "^8.5.1", 57 | "mongodb": "^3.5.5", 58 | "mongoose": "^5.9.6", 59 | "pg": "^8.0.2", 60 | "react": "^16.13.0", 61 | "react-bootstrap": "^1.0.0-beta.17", 62 | "react-dom": "^16.13.0", 63 | "react-fade-in": "^0.1.8", 64 | "react-github-btn": "^1.1.1", 65 | "react-lottie": "^1.2.3", 66 | "react-reveal": "^1.2.2", 67 | "react-router-dom": "^5.1.2", 68 | "reactstrap": "^8.4.1", 69 | "request": "^2.88.2" 70 | }, 71 | "engines": { 72 | "node": "12.14" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/controllers/AlertsController.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/userModel'); 2 | 3 | const AlertsController = {}; 4 | 5 | AlertsController.getAlerts = (req, res, next) => { 6 | const userId = res.locals.userId; 7 | console.log('userId', userId); 8 | const alertsQuery = { 9 | text: ` 10 | SELECT * FROM alerts 11 | WHERE user_id = ${userId} 12 | `, 13 | }; 14 | db.query(alertsQuery) 15 | .then((alerts) => { 16 | if (alerts.rows[0]) { 17 | console.log('alerts exist'); 18 | const data = alerts.rows; 19 | // console.log('data', data); 20 | const alertArray = []; 21 | for (let i = 0; i < data.length; i++) { 22 | let obj = { 23 | name: data[i].pod_name, 24 | namespace: data[i].namespace, 25 | status: data[i].status, 26 | podIP: data[i].pod_ip, 27 | time: data[i].timestamp, 28 | }; 29 | alertArray.push(obj); 30 | } 31 | 32 | res.locals.alerts = alertArray; 33 | // console.log('locals', res.locals.alerts); 34 | return next(); 35 | } else { 36 | console.log('no alerts'); 37 | res.locals.alerts = false; 38 | return next(); 39 | } 40 | }) 41 | .catch((err) => { 42 | console.log('err in get alerts middleware', err); 43 | }); 44 | }; 45 | 46 | AlertsController.addAlerts = (req, res, next) => { 47 | console.log('in add alerts'); 48 | console.log('req.body', req.body); 49 | const alertInfo = { 50 | name: req.body.name, 51 | namespace: req.body.namespace, 52 | status: req.body.status, 53 | podIP: req.body.podIP, 54 | time: req.body.time, 55 | }; 56 | const { name, namespace, status, podIP, time } = alertInfo; 57 | const userId = res.locals.userId; 58 | console.log('userId', userId); 59 | const addAlerts = { 60 | text: ` 61 | INSERT INTO alerts 62 | (user_id, pod_name, namespace, status, pod_ip, timestamp) 63 | VALUES 64 | ($1, $2, $3, $4, $5, $6) 65 | RETURNING * 66 | `, 67 | values: [userId, name, namespace, status, podIP, time], 68 | }; 69 | db.query(addAlerts) 70 | .then((alert) => { 71 | res.locals.alert = alert.rows[0]; 72 | return next(); 73 | }) 74 | .catch((err) => { 75 | console.log('err in add alert controller', err); 76 | }); 77 | }; 78 | 79 | module.exports = AlertsController; 80 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | Redirect, 7 | } from 'react-router-dom'; 8 | import Cookies from 'js-cookie'; 9 | 10 | import Dashboard from './components/Dashboard.jsx'; 11 | import Main_Container from './containers/Main_Container'; 12 | import Home from './components/Home.jsx'; 13 | import Login from './components/Login.jsx'; 14 | import Eks from './components/Eks.jsx'; 15 | import Cluster_Container from './containers/Cluster_Container.jsx'; 16 | 17 | const App = () => { 18 | // set an initial auth value into local storage in order to persist auth data 19 | const initialCheck = () => window.localStorage.getItem('auth') || null; 20 | const [mainCont, setMainCont] = useState(); 21 | const [isLoggedIn, setIsLoggedIn] = useState(initialCheck); 22 | 23 | // set local storage auth value to true - values in local storage are strings 24 | const isAuthed = () => { 25 | setIsLoggedIn('true'); 26 | }; 27 | // remove auth from local storage 28 | const removeAuth = () => { 29 | setIsLoggedIn('false'); 30 | }; 31 | 32 | useEffect(() => { 33 | // set isLoggedIn in local storage 34 | window.localStorage.setItem('auth', isLoggedIn); 35 | // values in local storage are strings 36 | if (isLoggedIn == 'true') { 37 | //if token exists, render cluster page paths 38 | setMainCont( 39 | ( 42 | 43 | )} 44 | /> 45 | ); 46 | } else { 47 | //else login path 48 | setMainCont( 49 | } 53 | /> 54 | ); 55 | } 56 | }, [isLoggedIn]); 57 | 58 | //get current pathname for each 59 | const path = window.location.pathname; 60 | return ( 61 | 62 | 67 |
    68 | 69 | 70 | {/* */} 71 | 72 | {mainCont} 73 | 74 |
    75 |
    76 | ); 77 | }; 78 | 79 | export default App; 80 | -------------------------------------------------------------------------------- /__test__/db.test.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { MongoMemoryServer } from 'mongodb-memory-server'; 3 | 4 | let mongoServer; 5 | 6 | const Alert = mongoose.model('Alert', new mongoose.Schema({ name: {type: String, required: true}, 7 | namespace: String, 8 | status: String, 9 | podIP: String, 10 | time: {type: String, required: true},})); 11 | 12 | const alertData = { name: 'Test String', namespace: 'Kube Namespace', status: 'running', podIP: '0123456', time: 'current time' }; 13 | 14 | const badAlertData = { name: 'Test String', namespace: 'Kube Namespace', status: 'running', podIP: '0123456', time: 'current time' ,nickkname: 'MyKube'}; 15 | 16 | const alertMissingField = {name: 'Test String'} 17 | 18 | 19 | beforeAll(async () => { 20 | mongoServer = new MongoMemoryServer(); 21 | const mongoUri = await mongoServer.getUri(); 22 | await mongoose.connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true}, (err) => { 23 | if (err) console.error(err); 24 | }); 25 | }); 26 | 27 | afterAll(async () => { 28 | await mongoose.disconnect(); 29 | await mongoServer.stop(); 30 | }); 31 | 32 | describe('Pod Alert DB Tests', () => { 33 | 34 | it('Create & save alert successfully', async () => { 35 | const validAlert = await new Alert(alertData); 36 | const savedAlert = await validAlert.save(); 37 | expect(savedAlert._id).toBeDefined(); 38 | expect(savedAlert.name).toBe(alertData.name); 39 | expect(savedAlert.namespace).toBe(alertData.namespace); 40 | expect(savedAlert.status).toBe(alertData.status); 41 | expect(savedAlert.podIP).toBe(alertData.podIP); 42 | expect(savedAlert.time).toBe(alertData.time); 43 | }); 44 | 45 | 46 | it('Insert alert successfully, however if input has a field not defined in schema should be undefined', async () => { 47 | const alertWithInvalidField = await new Alert(badAlertData); 48 | const savedAlertWithInvalidField = await alertWithInvalidField.save(); 49 | expect(savedAlertWithInvalidField.name).toBeDefined(); 50 | expect(savedAlertWithInvalidField.namespace).toBeDefined(); 51 | expect(savedAlertWithInvalidField.status).toBeDefined(); 52 | expect(savedAlertWithInvalidField.podIP).toBeDefined(); 53 | expect(savedAlertWithInvalidField.time).toBeDefined(); 54 | expect(savedAlertWithInvalidField.nickkname).toBeUndefined(); 55 | }); 56 | 57 | it('create alert without required field should failed', async () => { 58 | const alertWithoutRequiredField = await new Alert(alertMissingField); 59 | let err; 60 | try { 61 | const savedAlertWithoutRequiredField = await alertWithoutRequiredField.save(); 62 | error = savedAlertWithoutRequiredField; 63 | } catch (error) { 64 | err = error 65 | } 66 | expect(err).toBeInstanceOf(mongoose.Error.ValidationError) 67 | expect(err.errors.time).toBeDefined(); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /client/components/cluster/Pods.jsx: -------------------------------------------------------------------------------- 1 | // display details and data about each Pod 2 | import React, { useState, useEffect, useRef } from 'react'; 3 | import { Table } from 'react-bootstrap'; 4 | import axios from 'axios'; 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | import { 7 | faCheckCircle, 8 | faMinusCircle, 9 | } from '@fortawesome/free-solid-svg-icons'; 10 | 11 | import Cookies from 'js-cookie'; 12 | 13 | const Pods = ({ data }) => { 14 | // console.log('pods data', data); 15 | // console.log('props', props); 16 | // using hooks to set state 17 | const [table, setTable] = useState([]); //pod data in table 18 | let children = []; 19 | data[0].children.map(child => children.push(...child.children)); 20 | // console.log('children', children); 21 | useEffect(() => { 22 | const podList = children.map((p, i) => { 23 | // check status - if "Running" then render green check circle 24 | if (p.status === 'Running') { 25 | return ( 26 | 27 | 28 | {p.name} 29 | {p.namespace} 30 | 31 | 32 |    33 | {p.status} 34 | 35 | {p.podIP} 36 | {p.createdAt} 37 | 38 | 39 | ); 40 | } else { 41 | // if not "Running", invoke the addAlert func to add to database and render red circle 42 | addAlert(p); 43 | return ( 44 | 45 | 46 | {p.name} 47 | {p.namespace} 48 | 49 | 50 |    51 | {p.status} 52 | 53 | {p.podIP} 54 | {p.createdAt} 55 | 56 | 57 | ); 58 | } 59 | }); 60 | setTable(podList); 61 | // use Effect will trigger every time data is changed 62 | }, data); 63 | 64 | // function that adds a new Alert - gets called in ^useEffect when pod status is not "Running" 65 | const addAlert = async p => { 66 | const token = Cookies.get('token'); 67 | const header = { 68 | headers: { 69 | Authorization: 'Bearer' + token, 70 | }, 71 | }; 72 | // console.log('header', header); 73 | const postAlert = await axios.post( 74 | '/api/alerts', 75 | { 76 | name: p.name, 77 | namespace: p.namespace, 78 | status: p.status, 79 | podIP: p.podIP, 80 | time: Date(Date.now()).toString(), 81 | }, 82 | header 83 | ); 84 | }; 85 | 86 | return ( 87 |
    88 |

    Pods List

    89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | {table} 100 |
    Pod NameNamespaceStatusPod IPCreated At
    101 |
    102 | ); 103 | }; 104 | 105 | export default Pods; 106 | -------------------------------------------------------------------------------- /client/containers/Alerts_Container.jsx: -------------------------------------------------------------------------------- 1 | //crash logs, errors, CPU/memory usage exceeded 2 | /* 3 | The Status field should be "Running" - any other status will indicate issues with the environment. 4 | - fetch podlist and check statuses, if not running, create a log 5 | */ 6 | import React, { useState, useEffect } from 'react'; 7 | import axios from 'axios'; 8 | import { Table } from 'react-bootstrap'; 9 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 10 | import { faMinusCircle } from '@fortawesome/free-solid-svg-icons'; 11 | 12 | import Cookies from 'js-cookie'; 13 | 14 | const Alerts = () => { 15 | let [alerts, setAlerts] = useState([]); 16 | const [table, setTable] = useState([]); //alert data in table 17 | let [subHeader, setSubHeader] = useState(''); 18 | // useEffect = Hook version of componentDidMount 19 | useEffect(() => { 20 | // console.log('cookies', Cookies.get('token')); 21 | const token = Cookies.get('token'); 22 | const header = { 23 | headers: { 24 | Authorization: 'Bearer' + token, 25 | }, 26 | }; 27 | const fetchPods = async () => { 28 | // axios request to server side 29 | const result = await axios.get('/api/alerts', header); 30 | if (result.data) { 31 | alerts = []; //empty the alerts before updating with fetched data 32 | setAlerts(alerts.push(result.data)); 33 | } else { 34 | alerts = [[]]; 35 | } 36 | // console.log('alerts', alerts); 37 | let alertList; 38 | if (!alerts[0][0]) { 39 | setSubHeader('(No alerts currently detected)'); 40 | alertList = []; 41 | alertList.push( 42 | 43 | 44 | N/A 45 | N/A 46 | N/A 47 | N/A 48 | N/A 49 | 50 | 51 | ); 52 | 53 | // console.log(alertList); 54 | } else { 55 | // console.log('alerts found!'); 56 | alertList = alerts[0].map((p, i) => { 57 | return ( 58 | 59 | 60 | {p.name} 61 | {p.namespace} 62 | 63 | 64 |   {p.status} 65 | 66 | {p.podIP} 67 | {p.time} 68 | 69 | 70 | ); 71 | }); 72 | } 73 | setTable(alertList); 74 | }; 75 | 76 | //update every 5 seconds 77 | const fetchOnLoad = () => { 78 | if (!alerts[0]) { 79 | // console.log('First fetch called'); 80 | fetchPods(); 81 | } 82 | // setInterval(() => { 83 | // console.log('setInterval called'); 84 | // fetchPods(); 85 | // }, 5000); 86 | }; 87 | fetchOnLoad(); 88 | }, []); 89 | 90 | return ( 91 |
    92 | {table[0] && ( 93 |
    94 |

    Alerts {subHeader}

    95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | {table} 106 |
    Pod NameNamespaceStatusPod IPTime
    107 |
    108 | )} 109 |
    110 | ); 111 | }; 112 | 113 | export default Alerts; 114 | -------------------------------------------------------------------------------- /client/assets/cloud-loader.json: -------------------------------------------------------------------------------- 1 | {"v":"5.1.20","fr":25,"ip":0,"op":76,"w":200,"h":200,"nm":"cloud1_100","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 2","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[100,100,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[24,24,100],"ix":6}},"ao":0,"ip":0,"op":77,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 3","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[60.388,26.242],[106.239,0],[20.007,-95.39],[1.503,0],[0,-94.174],[-94.175,0],[-2.509,0.11],[0,0],[0,0],[0,94.174]],"o":[[-14.306,-102.297],[-101.337,0],[-1.493,-0.039],[-94.175,0],[0,94.174],[2.537,0],[0,0],[0,0],[94.175,0],[0,-70.043]],"v":[[243.353,-72.971],[34.974,-254],[-170.966,-86.977],[-175.459,-87.036],[-345.978,83.482],[-175.459,254],[-167.893,253.824],[-167.893,254],[175.46,254],[345.978,83.482]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[-1.763]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_-1p763_0p167_0p167"],"t":18,"s":[0],"e":[2]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.108]},"n":["0p833_0p833_0p167_0p108"],"t":39,"s":[2],"e":[90]},{"t":75}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.565]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p565_0p167_0p167"],"t":18,"s":[0],"e":[10.8]},{"i":{"x":[0.836],"y":[0.846]},"o":{"x":[0.17],"y":[0.104]},"n":["0p836_0p846_0p17_0p104"],"t":39,"s":[10.8],"e":[90]},{"t":75}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.949019610882,0.823529422283,0.301960796118,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 2","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":77,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[60.388,26.242],[106.239,0],[20.007,-95.39],[1.503,0],[0,-94.174],[-94.175,0],[-2.509,0.11],[0,0],[0,0],[0,94.174]],"o":[[-14.306,-102.297],[-101.337,0],[-1.493,-0.039],[-94.175,0],[0,94.174],[2.537,0],[0,0],[0,0],[94.175,0],[0,-70.043]],"v":[[243.353,-72.971],[34.974,-254],[-170.966,-86.977],[-175.459,-87.036],[-345.978,83.482],[-175.459,254],[-167.893,253.824],[-167.893,254],[175.46,254],[345.978,83.482]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.442]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p442_0p167_0p167"],"t":6,"s":[0],"e":[14]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.101]},"n":["0p833_0p833_0p167_0p101"],"t":39,"s":[14],"e":[98]},{"t":75}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.657]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p657_0p167_0p167"],"t":6,"s":[0],"e":[22.8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.113]},"n":["0p833_0p833_0p167_0p113"],"t":39,"s":[22.8],"e":[98]},{"t":75}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.090196080506,0.239215686917,0.415686279535,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 2","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":77,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[60.388,26.242],[106.239,0],[20.007,-95.39],[1.503,0],[0,-94.174],[-94.175,0],[-2.509,0.11],[0,0],[0,0],[0,94.174]],"o":[[-14.306,-102.297],[-101.337,0],[-1.493,-0.039],[-94.175,0],[0,94.174],[2.537,0],[0,0],[0,0],[94.175,0],[0,-70.043]],"v":[[243.353,-72.971],[34.974,-254],[-170.966,-86.977],[-175.459,-87.036],[-345.978,83.482],[-175.459,254],[-167.893,253.824],[-167.893,254],[175.46,254],[345.978,83.482]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.657]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p657_0p167_0p167"],"t":0,"s":[0],"e":[25.8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.104]},"n":["0p833_0p833_0p167_0p104"],"t":39,"s":[25.8],"e":[98]},{"t":72}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.886]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p886_0p167_0p167"],"t":0,"s":[0],"e":[77.8]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.371]},"n":["0p833_0p833_0p167_0p371"],"t":39,"s":[77.8],"e":[98]},{"t":72}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.952941179276,0.533333361149,0.529411792755,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 2","np":4,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":77,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /client/containers/Main_Container.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | 4 | import Loader from '../components/Loader.jsx'; 5 | import Visualizer_Container from './Visualizer_Container.jsx'; 6 | import Alerts_Container from './Alerts_Container.jsx'; 7 | import Cluster_Container from './Cluster_Container.jsx'; 8 | 9 | const Main_Container = props => { 10 | // console.log('props', props.path); 11 | const { path } = props; 12 | // AWS auth under construction 13 | // let awsApi; - 14 | // if (props.history.location.state) { 15 | // awsApi = props.history.location.state.data; 16 | // console.log('awsAPI', awsApi); 17 | // } 18 | 19 | //data to pass to children | pod, node, and service will fetched and put into data 20 | //stillLoading and donFetching at booleans to check check if loading is finalized and throw appropriate loader 21 | let [data, setData] = useState([]); 22 | let [pod, setPod] = useState([]); 23 | let [podUsage, setPodUsage] = useState([]); 24 | let [node, setNode] = useState([]); 25 | let [service, setService] = useState([]); 26 | let [stillLoading, setStillLoading] = useState(true); 27 | let [doneFetching, setdoneFetching] = useState(false); 28 | 29 | //function to parse pod usage info 30 | function getPodUsage(name) { 31 | for (let i = 0; i < podUsage.length; i++) { 32 | //if pod name matches, include usage information 33 | if (name == podUsage[i].name) { 34 | return { cpu: podUsage[i].cpu, memory: podUsage[i].memory }; 35 | } 36 | } 37 | } 38 | //function to parse info back from /getPods 39 | function getPods(parent) { 40 | const podArr = []; 41 | for (let i = 0; i < pod.length; i++) { 42 | //check node name passed thru parameter against pod's nodeName 43 | if (parent == pod[i].nodeName) { 44 | const podObj = {}; 45 | podObj.name = pod[i].name; 46 | podObj.namespace = pod[i].namespace; 47 | podObj.status = pod[i].status; 48 | podObj.podIP = pod[i].podIP; 49 | podObj.createdAt = pod[i].createdAt; 50 | podObj.parent = pod[i].nodeName; 51 | // podObj.labels = pod[i].labels; 52 | podObj.usage = getPodUsage(pod[i].name); //object with cpu and memory properties 53 | podArr.push(podObj); 54 | } 55 | } 56 | return podArr; 57 | } 58 | //function to parse info back from /getNods and push pods from getPods function 59 | function getNodes() { 60 | const nodeArr = []; 61 | for (let i = 0; i < node.length; i++) { 62 | const nodeObj = {}; 63 | nodeObj.name = node[i].name; 64 | nodeObj.cpu = node[i].cpu; 65 | //pods/children related to the node 66 | nodeObj.children = getPods(node[i].name); 67 | nodeArr.push(nodeObj); 68 | } 69 | return nodeArr; 70 | } 71 | //function to parse info back from /getServices and place child nodes on relavant obj 72 | function getServices() { 73 | const serviceArr = []; 74 | for (let i = 0; i < service.length; i++) { 75 | const serviceObj = {}; 76 | //copy all info from services into serviceObj 77 | serviceObj.name = service[i].name; 78 | serviceObj.type = service[i].type; 79 | serviceObj.namespace = service[i].namespace; 80 | serviceObj.port = service[i].port; 81 | serviceObj.clusterIP = service[i].clusterIP; 82 | serviceObj.children = getNodes(); 83 | /////////length to compare data 84 | serviceObj.length = service.length + node.length + pod.length; 85 | 86 | serviceArr.push(serviceObj); 87 | } 88 | return serviceArr; 89 | } 90 | // 91 | let setInt; 92 | useEffect(() => { 93 | // fetch service, node, pod info 94 | const fetchInfo = async () => { 95 | let serviceRes; 96 | let nodeRes; 97 | let podRes; 98 | let podUsageRes; 99 | 100 | // if (awsApi) { 101 | // serviceRes = awsApi.services; 102 | // nodeRes = awsApi.nodes; 103 | // podRes = awsApi.pods; //data on pods 104 | // podUsageRes = awsApi.podUsage; //data on pod usage 105 | // } else { 106 | try { 107 | const serviceReq = axios.get('/api/services'); 108 | const nodeReq = axios.get('/api/nodes'); 109 | const podReq = axios.get('/api/pods'); 110 | 111 | const res = await axios.all([serviceReq, nodeReq, podReq]); 112 | 113 | //set returned data as constants - identify based on their index 114 | serviceRes = res[0].data; 115 | nodeRes = res[1].data; 116 | podRes = res[2].data.pod; //data on pods 117 | podUsageRes = res[2].data.usage; //data on pod usage 118 | // } 119 | service = []; 120 | node = []; 121 | pod = []; 122 | podUsage = []; 123 | 124 | setService(service.push(...serviceRes)); 125 | setNode(node.push(...nodeRes)); 126 | setPod(pod.push(...podRes)); 127 | setPodUsage(podUsage.push(...podUsageRes)); 128 | 129 | setData(getServices()); //set data 130 | //data has been fetched and Loader component will through new animation 131 | setdoneFetching(true); 132 | } catch (err) { 133 | // console.log('error', err); 134 | } 135 | }; 136 | // fetching data call for initial load and every 3 seconds 137 | (function fetchOnLoad() { 138 | if (!data[0]) { 139 | // console.log('First fetch called'); 140 | fetchInfo(); 141 | // console.log('made it through'); 142 | } 143 | 144 | setInt = setInterval(() => { 145 | // console.log('setInterval called'); 146 | fetchInfo(); 147 | // console.log('data', data); 148 | // console.log('pod', pod); 149 | }, 3000); 150 | })(); 151 | 152 | //clear setInterval when component is removed from dom 153 | return () => clearInterval(setInt); 154 | }, [data, path]); 155 | 156 | return ( 157 |
    158 |
    159 | {stillLoading ? ( 160 | 165 | ) : path === '/visualizer' ? ( 166 | 167 | ) : path === '/alerts' ? ( 168 | 169 | ) : ( 170 | 171 | )} 172 |
    173 |
    174 | ); 175 | }; 176 | 177 | export default Main_Container; 178 | -------------------------------------------------------------------------------- /server/controllers/AwsController.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const aws4 = require('aws4'); 3 | const { Base64 } = require('js-base64'); 4 | const request = require('request'); 5 | const { AwsPodQuery } = require('../query/PodQuery'); 6 | const { AwsNodeQuery } = require('../query/NodeQuery'); 7 | const { AwsServiceQuery } = require('../query/ServiceQuery'); 8 | const cmd = require('node-cmd'); 9 | 10 | const AwsController = {}; 11 | 12 | AwsController.authToken = async (req, res, next) => { 13 | console.log('in auth token'); 14 | const credentials = { 15 | accessKeyId: req.body.credentials.accessKeyId, 16 | secretAccessKey: req.body.credentials.secretAccessKey, 17 | }; 18 | const region = req.body.credentials.region; 19 | const name = req.body.cluster; 20 | const options = { 21 | host: 'sts.amazonaws.com', 22 | service: 'sts', 23 | path: '/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Expires=60', 24 | headers: { 25 | 'x-k8s-aws-id': name, 26 | }, 27 | signQuery: true, 28 | }; 29 | const query = aws4.sign(options, credentials); 30 | const signedUrl = `https://${query.host}${query.path}`; 31 | const encoded = Base64.encodeURI(signedUrl); 32 | const token = encoded.replace(/=+$/, ''); 33 | const authToken = `k8s-aws-v1.${token}`; 34 | res.locals.awsInfo = { 35 | credentials: credentials, 36 | name: name, 37 | region: region, 38 | token: authToken, 39 | }; 40 | return next(); 41 | }; 42 | 43 | // fetch all AWS clusters associated to user 44 | AwsController.cluster = async (req, res, next) => { 45 | console.log('in aws cluster middleware'); 46 | // access credentials and region should be in the request 47 | const credentials = { 48 | accessKeyId: req.body.access.accessKeyId, 49 | secretAccessKey: req.body.access.secretAccessKey, 50 | }; 51 | const region = req.body.access.region; 52 | res.locals.credentials = { 53 | accessKeyId: req.body.access.accessKeyId, 54 | secretAccessKey: req.body.access.secretAccessKey, 55 | region: req.body.access.region, 56 | }; 57 | // create an options query 58 | const options = { 59 | host: `eks.${region}.amazonaws.com`, 60 | path: '/clusters', 61 | }; 62 | // create query with custom aws signature 63 | const query = aws4.sign(options, credentials); 64 | try { 65 | const fetchCluster = await axios( 66 | `https://eks.${region}.amazonaws.com/clusters`, 67 | query 68 | ); 69 | res.locals.clusters = fetchCluster.data.clusters; 70 | return next(); 71 | } catch (err) { 72 | return 'error in aws middleware'; 73 | } 74 | }; 75 | 76 | // Select AWS cluster 77 | AwsController.selectCluster = async (req, res, next) => { 78 | console.log('in aws select middleware'); 79 | const credentials = { 80 | accessKeyId: req.body.credentials.accessKeyId, 81 | secretAccessKey: req.body.credentials.secretAccessKey, 82 | }; 83 | const region = req.body.credentials.region; 84 | const name = req.body.cluster; 85 | // create an options query 86 | const options = { 87 | host: `eks.${region}.amazonaws.com`, 88 | path: `/clusters/${name}`, 89 | }; 90 | // create query with custom aws signature 91 | const query = aws4.sign(options, credentials); 92 | try { 93 | const fetchCluster = await axios( 94 | `https://eks.${region}.amazonaws.com/clusters/${name}`, 95 | { headers: query.headers } 96 | ); 97 | res.locals.select = fetchCluster.data; 98 | res.locals.url = fetchCluster.data.cluster.endpoint; 99 | return next(); 100 | } catch (err) { 101 | return 'error in aws middleware'; 102 | } 103 | }; 104 | 105 | // fetch AWS pods 106 | AwsController.getPods = async (req, res, next) => { 107 | console.log('in aws get pods'); 108 | const options = { 109 | uri: `${res.locals.url}/api/v1/pods`, 110 | rejectUnauthorized: false, 111 | headers: { 112 | Authorization: `Bearer ${res.locals.awsInfo.token}`, 113 | }, 114 | }; 115 | let podInfo; 116 | let awsPodArray = []; 117 | let awsPods; 118 | function callback(error, response, body) { 119 | if (error) { 120 | return 'error in aws pod request'; 121 | } else { 122 | podInfo = JSON.parse(body); 123 | awsPods = new AwsPodQuery(podInfo); 124 | for (let i = 0; i < awsPods.name.length; i++) { 125 | let obj = { 126 | name: awsPods.name[i], 127 | namespace: awsPods.namespace[i], 128 | status: awsPods.status[i], 129 | podIP: awsPods.podIP[i], 130 | createdAt: awsPods.createdAt[i].toString(), 131 | nodeName: awsPods.nodeName[i], 132 | }; 133 | awsPodArray.push(obj); 134 | } 135 | res.locals.awsPods = awsPodArray; 136 | return next(); 137 | } 138 | } 139 | await request(options, callback); 140 | }; 141 | 142 | AwsController.getNodes = async (req, res, next) => { 143 | console.log('in aws get nodes'); 144 | const options = { 145 | uri: `${res.locals.url}/api/v1/nodes`, 146 | rejectUnauthorized: false, 147 | headers: { 148 | Authorization: `Bearer ${res.locals.awsInfo.token}`, 149 | }, 150 | }; 151 | let nodeInfo; 152 | let awsNodes; 153 | let awsNodeArray = []; 154 | function callback(error, response, body) { 155 | if (error) { 156 | return 'error in aws node request'; 157 | } else { 158 | nodeInfo = JSON.parse(body); 159 | awsNodes = new AwsNodeQuery(nodeInfo); 160 | for (let i = 0; i < awsNodes.name.length; i++) { 161 | let obj = { 162 | name: awsNodes.name[i], 163 | cpu: awsNodes.cpu[i], 164 | }; 165 | awsNodeArray.push(obj); 166 | } 167 | res.locals.awsNodes = awsNodeArray; 168 | return next(); 169 | } 170 | } 171 | await request(options, callback); 172 | }; 173 | 174 | AwsController.getServices = async (req, res, next) => { 175 | console.log('in aws get services'); 176 | const options = { 177 | uri: `${res.locals.url}/api/v1/services`, 178 | rejectUnauthorized: false, 179 | headers: { 180 | Authorization: `Bearer ${res.locals.awsInfo.token}`, 181 | }, 182 | }; 183 | let serviceInfo; 184 | let awsServices; 185 | let awsServiceArray = []; 186 | function callback(error, response, body) { 187 | if (error) { 188 | return 'error in aws service request'; 189 | } else { 190 | serviceInfo = JSON.parse(body); 191 | awsServices = new AwsServiceQuery(serviceInfo); 192 | for (let i = 0; i < awsServices.name.length; i++) { 193 | let obj = { 194 | name: awsServices.name[i], 195 | type: awsServices.type[i], 196 | namespace: awsServices.namespace[i], 197 | port: awsServices.port[i], 198 | clusterIP: awsServices.clusterIP[i], 199 | }; 200 | awsServiceArray.push(obj); 201 | } 202 | res.locals.awsServices = awsServiceArray; 203 | return next(); 204 | } 205 | } 206 | await request(options, callback); 207 | }; 208 | 209 | AwsController.getPodUsage = (req, res, next) => { 210 | //cmd library to access CLI 211 | //using kubectl top pod 212 | cmd.get('kubectl top pod', function (err, data, stderr) { 213 | if (err) return next(err); 214 | 215 | //split by enter 216 | const lines = data.split('\n'); 217 | 218 | const result = []; 219 | for (let i = 1; i < lines.length - 1; i++) { 220 | //use regex, split string by any number of whitespaces 221 | const words = lines[i].match(/\S+/g); 222 | const podUse = { name: words[0], cpu: words[1], memory: words[2] }; 223 | result.push(podUse); 224 | } 225 | res.locals.awsPodUsage = result; 226 | return next(); 227 | }); 228 | }; 229 | 230 | module.exports = AwsController; 231 | -------------------------------------------------------------------------------- /client/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import { Form, Button } from 'react-bootstrap'; 4 | import awsLogo from '../assets/awsLogo.png'; 5 | import axios from 'axios'; 6 | import Cookies from 'js-cookie'; 7 | 8 | /** 9 | * 10 | * LOGIN WITH AWS AUTH UNDER CONSTRUCTION 11 | * 12 | */ 13 | const Login = ({ isAuthed }) => { 14 | //hooks for AWS sign in 15 | const [access, setAccess] = useState({ 16 | accessKeyId: '', 17 | secretAccessKey: '', 18 | region: '', 19 | }); 20 | const [auth, setAuth] = useState(false); 21 | const [verify, setVerify] = useState(false); 22 | const [clusters, setClusters] = useState([]); 23 | 24 | //hooks for user sign up 25 | const [signup, setSignup] = useState({ 26 | newEmail: '', 27 | newPassword: '', 28 | }); 29 | 30 | //hooks for user login 31 | const [login, setLogin] = useState({ 32 | email: '', 33 | password: '', 34 | }); 35 | //function to authenticate credentials 36 | const handleAwsSignin = async event => { 37 | event.preventDefault(); 38 | // console.log('accessInfo', access); 39 | // make a request to the aws api with credentials. if data is returned then redirect. 40 | const accessData = await axios.post('/aws/clusters', { 41 | access, 42 | }); 43 | // console.log('aD', accessData); 44 | if (accessData) { 45 | setClusters(accessData.data); 46 | setAuth(true); 47 | } else { 48 | // console.log('none'); 49 | } 50 | }; 51 | //function to sign up new users 52 | const handleSignup = async event => { 53 | event.preventDefault(); 54 | const signupSuccess = await axios.post('/login/signup', { 55 | signup, 56 | }); 57 | // console.log('signup success', signupSuccess); 58 | if (signupSuccess) { 59 | Cookies.set('token', signupSuccess.data); 60 | setVerify(true); 61 | isAuthed(); 62 | } 63 | }; 64 | //function to login/verify existing users 65 | const handleLogin = async event => { 66 | event.preventDefault(); 67 | const loginSuccess = await axios.post('/login/verify', { 68 | login, 69 | }); 70 | // console.log('login success', loginSuccess); 71 | if (loginSuccess.data) { 72 | Cookies.set('token', loginSuccess.data); 73 | setVerify(true); 74 | isAuthed(); 75 | } else { 76 | // console.log('user not verified'); 77 | } 78 | }; 79 | 80 | const { accessKeyId, secretAccessKey, region } = access; 81 | const { newEmail, newPassword } = signup; 82 | const { email, password } = login; 83 | 84 | return ( 85 | <> 86 | {/* if authenticated, direct user to cluster page */} 87 | {auth ? ( 88 | 94 | ) : null} 95 | {verify ? ( 96 | 101 | ) : null} 102 |
    103 |
    104 | 105 |

    Sign in with AWS coming soon!

    106 | {/*
    107 | 108 | Access Key ID 109 | 114 | setAccess({ ...access, accessKeyId: e.target.value }) 115 | } 116 | /> 117 | 118 | 119 | Secret Access Key 120 | 125 | setAccess({ ...access, secretAccessKey: e.target.value }) 126 | } 127 | /> 128 | 129 | 130 | Region Code 131 | 135 | setAccess({ ...access, region: e.target.value }) 136 | } 137 | > 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
    161 | 164 |
    165 |
    */} 166 |
    167 | 168 | {/* user sign up w/o aws */} 169 | 170 |
    171 |
    172 | Sign up to monitor your
    173 | Kubernetes cluster. 174 |
    175 |
    176 | 177 | Email 178 | 182 | setSignup({ ...signup, newEmail: e.target.value }) 183 | } 184 | /> 185 | 186 | We'll never share your email with anyone else. 187 | 188 | 189 | 190 | Password 191 | 195 | setSignup({ ...signup, newPassword: e.target.value }) 196 | } 197 | /> 198 | 199 |
    200 | 203 |
    204 | 205 | {/* user sign up w/o aws */} 206 |
    Already have an account?
    207 | 208 |
    209 | 210 | setLogin({ ...login, email: e.target.value })} 215 | /> 216 | 217 | 218 | setLogin({ ...login, password: e.target.value })} 223 | /> 224 | 225 |
    226 | 229 |
    230 |
    231 |
    232 | 233 | ); 234 | }; 235 | 236 | export default Login; 237 | -------------------------------------------------------------------------------- /stylesheets/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Lato&family=Open+Sans&family=Quicksand&family=Teko&display=swap'); 2 | 3 | //colors 4 | $light: #c2dde6; 5 | $morningsky: #cae4db; 6 | $skyblue: #f9ffff; 7 | $mist: #7a9d96; 8 | $navy: #001e3f; 9 | // $bg-img: url('../client/assets/polygon-background.jpg'); 10 | $bg-img: url('../client/assets/landing.png'); 11 | 12 | //fonts 13 | $quicksand: 'Quicksand', sans-serif; 14 | $teko: 'Teko', sans-serif; 15 | $openSans: 'Open Sans', sans-serif; 16 | $lato: 'Lato', sans-serif; 17 | 18 | html, 19 | body { 20 | width: 100%; 21 | height: 100%; 22 | } 23 | 24 | //Container for whole page 25 | #container { 26 | display: flex; 27 | flex-direction: column; 28 | position: absolute; 29 | top: 0; 30 | bottom: 0; 31 | width: 100%; 32 | height: 100%; 33 | font-family: $quicksand; 34 | } 35 | 36 | //for Landing page 37 | .appContainer { 38 | display: flex; 39 | flex-direction: row; 40 | justify-content: center; 41 | padding-top: 4em; 42 | align-items: stretch; 43 | height: 100%; 44 | } 45 | 46 | //for My Cluster page 47 | .appCont { 48 | display: flex; 49 | flex-direction: row; 50 | width: 100%; 51 | height: 100%; 52 | } 53 | 54 | ////////////// 55 | //Home page 56 | ////////////// 57 | .homeContainer { 58 | width: 100%; 59 | overflow: auto; 60 | display: flex; 61 | flex-direction: column; 62 | align-items: stretch; 63 | scroll-behavior: smooth; 64 | 65 | h2 { 66 | //headers for each containers in home 67 | font-family: $teko; 68 | font-size: 50px; 69 | } 70 | } 71 | 72 | #navbarHome { 73 | background-color: $navy; 74 | padding: 0.5em; 75 | opacity: 0.95; 76 | position: fixed; 77 | width: 100%; 78 | z-index: 10; //appear over all other components 79 | -webkit-box-shadow: 0 9px 6px -6px #999; 80 | -moz-box-shadow: 0 9px 6px -6px #999; 81 | box-shadow: 0 9px 6px -6px #999; 82 | .ml-auto, 83 | .dashStarted { 84 | color: $skyblue; 85 | } 86 | .ml-auto:hover { 87 | color: #427eff; 88 | } 89 | .logoHome { 90 | width: 40px; 91 | height: 30px; 92 | } 93 | .featuresScroll { 94 | color: white; 95 | padding: 0.5em 1.5em; 96 | } 97 | .contributeScroll { 98 | color: white; 99 | padding: 0.5em 1.5em; 100 | } 101 | .teamScroll { 102 | color: white; 103 | padding: 0.5em 1.5em; 104 | } 105 | .navLink { 106 | color: white; 107 | } 108 | .navLink:hover { 109 | color: #427eff; 110 | } 111 | } 112 | 113 | // .heroContainer, .featuresContainer, 114 | 115 | .heroContainer { 116 | padding: 5em 0 5em 0; 117 | background-image: $bg-img; 118 | background-repeat: no-repeat; 119 | background-size: 100%; 120 | display: flex; 121 | justify-content: center; 122 | align-items: center; 123 | .heroComp { 124 | display: flex; 125 | flex-direction: column; 126 | width: 1800px; 127 | justify-content: center; 128 | align-items: center; 129 | .heroQuote { 130 | text-align: center; 131 | margin-bottom: 50px; 132 | } 133 | } 134 | 135 | .col1 { 136 | // background-color: $skyblue; 137 | // opacity: 0.9; 138 | height: 70%; 139 | width: 100%; 140 | display: flex; 141 | justify-content: center; 142 | align-items: center; 143 | 144 | .logoMain { 145 | //logo img 146 | height: 400px; 147 | } 148 | } 149 | .logoName { 150 | font-size: 100px; 151 | font-family: $teko; 152 | letter-spacing: 2px; 153 | text-align: center; 154 | } 155 | .col2 { 156 | background-color: $navy; 157 | height: 30%; 158 | width: 100%; 159 | } 160 | .github-buttons { 161 | display: flex; 162 | justify-content: center; 163 | } 164 | } 165 | 166 | .featuresContainer, 167 | .contributeContainer, 168 | .teamContainer { 169 | padding: 8em 0 8em 0; 170 | width: 100%; 171 | display: flex; 172 | flex-direction: column; 173 | align-items: center; 174 | 175 | a { 176 | //github and linkedin icons 177 | padding: 0.4em; 178 | 179 | &:hover { 180 | opacity: 0.5; 181 | } 182 | } 183 | } 184 | 185 | .featuresContainer { 186 | .featureInfo { 187 | display: flex; 188 | align-items: center; 189 | 190 | section { 191 | width: 30em; 192 | margin-top: 3em; 193 | } 194 | img { 195 | width: 450px; 196 | margin-top: 1em; 197 | margin-bottom: 5em; 198 | } 199 | .featureText { 200 | margin-bottom: 8em; 201 | } 202 | } 203 | } 204 | 205 | .contributeContainer { 206 | padding-bottom: 10em; 207 | // background-color: #dff4ff; 208 | 209 | h5, 210 | a { 211 | padding-top: 1em; 212 | } 213 | background-color: #dff4ff; 214 | } 215 | 216 | .teamContainer { 217 | justify-content: space-evenly; 218 | background-color: $navy; 219 | width: 100%; 220 | color: white; 221 | 222 | .profileGrp { 223 | display: flex; 224 | margin-top: 2em; 225 | 226 | .member { 227 | display: flex; 228 | flex-direction: column; 229 | align-items: center; 230 | margin: 0 2em 0 2em; 231 | 232 | .teamImg { 233 | width: 150px; 234 | border-radius: 50%; 235 | margin-bottom: 1em; 236 | } 237 | } 238 | } 239 | } 240 | 241 | ////////////// 242 | //Cluster page 243 | ////////////// 244 | //Containers for different pages - Login, Cluster, Alerts, Traffic 245 | .loginContainer, 246 | .mainContainer, 247 | .alertsContainer, 248 | .visContainer { 249 | width: 100%; 250 | background-color: $skyblue; 251 | padding-top: 1.2em; 252 | // font-family: $quicksand; 253 | overflow: auto; 254 | } 255 | 256 | //Cluster + Alerts table properties 257 | .dashHome, 258 | .dashCluster, 259 | .dashTraffic, 260 | .dashAlerts { 261 | color: white; 262 | font-size: large; 263 | // font-family: $quicksand; 264 | } 265 | 266 | .homeTitle, 267 | .podsTitle, 268 | .alertsTitle, 269 | .nodeTitle, 270 | .serviceTitle { 271 | padding-bottom: 0.5em; 272 | text-align: left; 273 | } 274 | 275 | .podContainer, 276 | .nodeContainer, 277 | .serviceContainer, 278 | .alertsContainer, 279 | .visContainer { 280 | padding-left: 4em; 281 | padding-right: 4em; 282 | } 283 | 284 | //LOGIN PAGE 285 | .loginPage { 286 | display: flex; 287 | flex-direction: row; 288 | align-self: center; 289 | align-items: center; 290 | justify-content: center; 291 | margin-top: 50px; 292 | .loginContainer { 293 | align-self: center; 294 | text-align: center; 295 | justify-content: center; 296 | align-items: center; 297 | width: 500px; 298 | height: 700px; 299 | border: 1px solid #a4bfce; 300 | border-radius: 4px; 301 | margin: 20px; 302 | .loginForm { 303 | margin: 20px; 304 | margin-top: 0; 305 | padding: 20px; 306 | display: flex; 307 | flex-direction: column; 308 | .inputName { 309 | align-self: flex-start; 310 | padding-left: 10px; 311 | } 312 | } 313 | .awsInput:focus { 314 | outline: none; 315 | } 316 | .awsLogo { 317 | width: 250px; 318 | height: 80px; 319 | margin-top: 40px; 320 | } 321 | .inputAccess { 322 | text-align: left; 323 | } 324 | .signupTitle { 325 | padding-top: 20px; 326 | padding-left: 50px; 327 | padding-right: 50px; 328 | font-weight: 600; 329 | color: #5b5b5b; 330 | } 331 | .comingSoon { 332 | margin-top: 150px; 333 | } 334 | } 335 | } 336 | 337 | // SELECT CLUSTER PAGE 338 | .selectClusterContainer { 339 | align-self: center; 340 | text-align: center; 341 | justify-content: center; 342 | align-items: center; 343 | background-color: $skyblue; 344 | border: 1px solid #a4bfce; 345 | border-radius: 4px; 346 | margin-top: 50px; 347 | padding: 50px; 348 | .chooseTitle { 349 | font-weight: 800; 350 | } 351 | .clusterButton { 352 | margin: 10px; 353 | padding: 10px; 354 | border-radius: 4px; 355 | } 356 | .clusterButton:hover { 357 | color: white; 358 | background-color: #318dfd; 359 | } 360 | .clusterButton:focus { 361 | outline: none; 362 | } 363 | } 364 | 365 | //visualizer tree 366 | .visContainer { 367 | display: flex; 368 | flex-direction: column; 369 | } 370 | 371 | .svgWrapper { 372 | padding: 4em 4em 8em 4em; 373 | align-self: center; 374 | 375 | .radialTreeSvg { 376 | overflow: visible; 377 | padding: 12rem 0 5rem 15rem; 378 | } 379 | 380 | .link { 381 | fill: none; 382 | stroke-width: 2.5px; 383 | } 384 | 385 | .node, 386 | .label { 387 | font-family: $quicksand; 388 | font-size: 14px; 389 | font-weight: bold; 390 | } 391 | 392 | circle { 393 | stroke-width: 3px; 394 | } 395 | } 396 | 397 | .tooltip { 398 | position: absolute; 399 | padding: 0.5em; 400 | width: 12em; 401 | font-family: $quicksand; 402 | font-size: 12px; 403 | background-color: rgba(204, 204, 255, 0.8); 404 | border: 0px; 405 | border-radius: 8px; 406 | pointer-events: none; 407 | } 408 | -------------------------------------------------------------------------------- /client/components/visualizer/RadialTree.jsx: -------------------------------------------------------------------------------- 1 | //renders radial tree visualization of cluster using d3 in Visualizer.js 2 | import React, { useRef, useEffect } from 'react'; 3 | import { select, hierarchy, tree, linkRadial, event } from 'd3'; 4 | import useResizeObserver from './useResizeObserver.jsx'; 5 | 6 | //function to compare data array length for rendering tree animation 7 | function compareData(length) { 8 | const ref = useRef(); 9 | 10 | useEffect(() => { 11 | ref.current = length; 12 | }); 13 | //render animation first time 14 | if (ref.current === undefined) return true; 15 | //compare data 16 | if (ref.current === length) return false; 17 | return true; 18 | } 19 | 20 | //div to show values on node hover/mouseover 21 | const div = select('body') 22 | .append('div') 23 | .attr('class', 'tooltip') 24 | .style('opacity', 0); 25 | 26 | const RadialTree = ({ data }) => { 27 | const svgRef = useRef(); 28 | const wrapperRef = useRef(); 29 | const dimensions = useResizeObserver(wrapperRef); 30 | 31 | // we save data to see if it changed 32 | const reanimate = compareData(data[0].length); 33 | 34 | // will be called initially and on every data change 35 | useEffect(() => { 36 | // console.log('data in radTree', data); 37 | if (data[0] !== undefined) { 38 | // IF VALID DATA WAS PASSED 39 | const svg = select(svgRef.current); 40 | 41 | // use dimensions from useResizeObserver, 42 | // but use getBoundingClientRect on initial render 43 | // (dimensions are null for the first render) 44 | const { height } = 45 | dimensions || wrapperRef.current.getBoundingClientRect(); 46 | 47 | // transform hierarchical data 48 | //changing width dynamically distorts the graph 49 | const root = hierarchy(data[0]); 50 | const treeLayout = tree().size([2 * Math.PI, height / 1.5]); 51 | 52 | // radial tree link 53 | const radialLink = linkRadial() 54 | .angle(function (d) { 55 | return d.x; 56 | }) 57 | .radius(function (d) { 58 | return d.y; 59 | }); 60 | 61 | // enrich hierarchical data with coordinates 62 | treeLayout(root); 63 | 64 | // console.log('root', root) 65 | // console.log('rt descendants', root.descendants()); 66 | // console.log('rt links', root.links()); 67 | 68 | // links 69 | const enteringAndUpdatingLinks = svg 70 | .selectAll('.link') 71 | .data(root.links()) 72 | .join('path') 73 | .attr('class', 'link') 74 | .attr('d', radialLink) 75 | .attr('stroke-dasharray', function () { 76 | const length = this.getTotalLength(); 77 | return `${length} ${length}`; 78 | }) 79 | .attr('stroke', '#bfbfbf') 80 | .attr('opacity', 1); 81 | 82 | // nodes 83 | const node = svg 84 | .selectAll('.node') 85 | .data(root.descendants()) 86 | .join('circle') //append circles to nodes 87 | .attr('class', 'node') 88 | .attr('opacity', 0) 89 | .attr( 90 | //angle to radian 91 | 'transform', 92 | (d) => ` 93 | rotate(${(d.x * 180) / Math.PI - 90}) 94 | translate(${d.y},0) 95 | ` 96 | ) 97 | .attr('r', 10) 98 | .attr('fill', function (node) { 99 | //color based on depth 100 | if (node.depth == 0) return '#f8b58c'; //services - salmon 101 | if (node.depth == 1) return '#0788ff'; //nodes - blue 102 | if (node.depth == 2) return '#ccccff'; //pods - grey 103 | }) 104 | .attr('stroke', function(d) { //color change based on traffic 105 | let color = '#bfbfbf'; //base color = gray 106 | if (d.depth === 2) { //for pods 107 | if (d.data.usage !== undefined) { 108 | //change usage data from string to number 109 | let cpuUse = parseInt(d.data.usage.cpu.slice(0, -1)); 110 | let memUse = parseInt(d.data.usage.memory.slice(0, -2)); 111 | 112 | //update prev usage info if nonexistant 113 | // if (!prevCpu[d.data.name]) { prevCpu[d.data.name] = cpuUse; console.log('added to obj')} 114 | // else { 115 | // console.log('name, cpu and prev', d.data.name, cpuUse, prevCpu[d.data.name]) 116 | // //usage increased => return red color 117 | // if (prevCpu[d.data.name] < cpuUse) color = '#ee2c2c'; 118 | // //usage decreased => return green color 119 | // else if (prevCpu[d.data.name] > cpuUse) color = '#03e0a0'; 120 | // //update 121 | // prevCpu[d.data.name] = cpuUse; 122 | // } 123 | 124 | //if CPU usage increased, return red color 125 | if (cpuUse > 0) color = '#ee2c2c'; 126 | } 127 | 128 | } 129 | return color; 130 | }); 131 | 132 | //add mouseover event to nodes 133 | node 134 | .on('mouseover', function (d) { 135 | select(this).transition().duration('50').attr('opacity', '.65'); 136 | 137 | //div appear on hover 138 | div.transition().duration(50).style('opacity', 1); 139 | 140 | let toolInfo = ''; //info to appear on hover 141 | if (d.depth === 0) { 142 | toolInfo = 143 | 'name: ' + 144 | d.data.name + 145 | '
    ' + 146 | 'type: ' + 147 | d.data.type + 148 | '
    ' + 149 | 'namespace: ' + 150 | d.data.namespace + 151 | '
    ' + 152 | 'port: ' + 153 | d.data.port + 154 | '
    ' + 155 | 'clusterIP: ' + 156 | d.data.clusterIP; 157 | } else if (d.depth === 1) { 158 | toolInfo = 'name: ' + d.data.name; 159 | } else if (d.depth === 2) { 160 | toolInfo = 161 | 'name: ' + 162 | d.data.name + 163 | '
    ' + 164 | 'namespace: ' + 165 | d.data.namespace + 166 | '
    ' + 167 | 'status: ' + 168 | d.data.status + 169 | '
    ' + 170 | 'CPU usage: ' + 171 | d.data.usage.cpu + 172 | '
    ' + 173 | 'memory usage: ' + 174 | d.data.usage.memory + 175 | '
    ' + 176 | 'podIP: ' + 177 | d.data.podIP + 178 | '
    ' + 179 | 'created: ' + 180 | d.data.createdAt; 181 | } 182 | 183 | div 184 | .html(toolInfo) //append info to div 185 | .style('left', event.pageX + 15 + 'px') //mouse position 186 | .style('top', event.pageY - 20 + 'px'); 187 | }) 188 | .on('mouseout', function (d) { 189 | select(this).transition().duration('50').attr('opacity', '1'); 190 | 191 | //div disappears with mouseout 192 | div.transition().duration(50).style('opacity', 0); 193 | }); 194 | 195 | //labels 196 | const label = svg 197 | .selectAll('.label') 198 | .data(root.descendants()) 199 | .join('text') 200 | .attr('class', 'label') 201 | .attr('opacity', 0) 202 | .attr('y', -15) 203 | .attr('x', -5) 204 | .attr( 205 | //angle to radian, find position THEN rotate texts to be horizontal 206 | 'transform', 207 | (d) => 208 | `rotate(${(d.x * 180) / Math.PI - 90}) 209 | translate(${d.y},0)` + 210 | `rotate(${(Math.PI / 2 - d.x) * (180 / Math.PI)})` 211 | ) 212 | .attr('text-anchor', 'middle') 213 | .attr('font-size', 12) 214 | .text(function (node) { 215 | if (node.depth === 0) return 'service: ' + node.data.name; 216 | if (node.depth === 1) return 'node'; 217 | if (node.depth === 2) return 'pod'; 218 | }); 219 | 220 | // console.log('reanimate', reanimate); 221 | // animation 222 | if (reanimate) { 223 | //do not re-render animation if data is not updated 224 | // link animation 225 | enteringAndUpdatingLinks 226 | .attr('stroke-dashoffset', function () { 227 | return this.getTotalLength(); 228 | }) 229 | .transition() 230 | .duration(500) 231 | .delay((link) => link.source.depth * 500) 232 | .attr('stroke-dashoffset', 0); 233 | 234 | //node animation 235 | node 236 | .transition() 237 | .duration(500) 238 | .delay((node) => node.depth * 300) 239 | .attr('opacity', 1); 240 | 241 | //label animation 242 | label 243 | .transition() 244 | .duration(500) 245 | .delay((node) => node.depth * 300) 246 | .attr('opacity', 1); 247 | } else { 248 | //else just change visibility to 1 249 | enteringAndUpdatingLinks.attr('opacity', 1); 250 | node.attr('opacity', 1); 251 | label.attr('opacity', 1); 252 | } 253 | } 254 | }, [data, dimensions, reanimate]); 255 | 256 | return ( 257 |
    258 | 259 |
    260 | ); 261 | }; 262 | 263 | export default RadialTree; 264 | -------------------------------------------------------------------------------- /client/assets/doneLoading.json: -------------------------------------------------------------------------------- 1 | {"v":"4.6.3","fr":24,"ip":0,"op":21,"w":320,"h":320,"nm":"checklist","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 13","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":300},"p":{"a":0,"k":[160,159.5,0]},"a":{"a":0,"k":[0,-34,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":17}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.7921569,0.4470588,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":6,"s":[-8.142,-92.147],"e":[-7.675,-162.544],"to":[0.07779947668314,-11.7327470779419],"ti":[-0.07779947668314,11.7327470779419]},{"t":17}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[83.981,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":6,"s":[20.367],"e":[6.367]},{"t":17}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":21}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.9070925,0.5119235,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[16.585,-99.759],"e":[28.521,-187.495],"to":[1.9892578125,-14.6227216720581],"ti":[-1.9892578125,14.6227216720581]},{"t":21}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[97.419,116],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":6,"s":[14.733],"e":[8.733]},{"t":21}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":6,"op":22,"st":-21,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 12","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":250},"p":{"a":0,"k":[160,159.5,0]},"a":{"a":0,"k":[0,-34,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":17}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.7921569,0.4470588,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":6,"s":[-8.142,-92.147],"e":[-7.675,-162.544],"to":[0.07779947668314,-11.7327470779419],"ti":[-0.07779947668314,11.7327470779419]},{"t":17}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[83.981,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":6,"s":[20.367],"e":[6.367]},{"t":17}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":21}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.9070925,0.5119235,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[16.585,-99.759],"e":[28.521,-187.495],"to":[1.9892578125,-14.6227216720581],"ti":[-1.9892578125,14.6227216720581]},{"t":21}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[97.419,116],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":6,"s":[14.733],"e":[8.733]},{"t":21}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":6,"op":22,"st":-21,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 11","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":200},"p":{"a":0,"k":[160,159.5,0]},"a":{"a":0,"k":[0,-34,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":17}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.7921569,0.4470588,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":6,"s":[-8.142,-92.147],"e":[-7.675,-162.544],"to":[0.07779947668314,-11.7327470779419],"ti":[-0.07779947668314,11.7327470779419]},{"t":17}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[83.981,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":6,"s":[20.367],"e":[6.367]},{"t":17}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":21}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.9070925,0.5119235,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[16.585,-99.759],"e":[28.521,-187.495],"to":[1.9892578125,-14.6227216720581],"ti":[-1.9892578125,14.6227216720581]},{"t":21}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[97.419,116],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":6,"s":[14.733],"e":[8.733]},{"t":21}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":6,"op":22,"st":-21,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 10","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":150},"p":{"a":0,"k":[160,159.5,0]},"a":{"a":0,"k":[0,-34,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":17}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.7921569,0.4470588,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":6,"s":[-8.142,-92.147],"e":[-7.675,-162.544],"to":[0.07779947668314,-11.7327470779419],"ti":[-0.07779947668314,11.7327470779419]},{"t":17}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[83.981,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":6,"s":[20.367],"e":[6.367]},{"t":17}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":21}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.9070925,0.5119235,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[16.585,-99.759],"e":[28.521,-187.495],"to":[1.9892578125,-14.6227216720581],"ti":[-1.9892578125,14.6227216720581]},{"t":21}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[97.419,116],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":6,"s":[14.733],"e":[8.733]},{"t":21}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":6,"op":22,"st":-21,"bm":0,"sr":1},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 9","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":100},"p":{"a":0,"k":[160,159.5,0]},"a":{"a":0,"k":[0,-34,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":17}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.7921569,0.4470588,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":6,"s":[-8.142,-92.147],"e":[-7.675,-162.544],"to":[0.07779947668314,-11.7327470779419],"ti":[-0.07779947668314,11.7327470779419]},{"t":17}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[83.981,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":6,"s":[20.367],"e":[6.367]},{"t":17}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":21}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.9070925,0.5119235,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[16.585,-99.759],"e":[28.521,-187.495],"to":[1.9892578125,-14.6227216720581],"ti":[-1.9892578125,14.6227216720581]},{"t":21}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[97.419,116],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":6,"s":[14.733],"e":[8.733]},{"t":21}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":6,"op":22,"st":-21,"bm":0,"sr":1},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 8","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":50},"p":{"a":0,"k":[160,159.5,0]},"a":{"a":0,"k":[0,-34,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":17}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.7921569,0.4470588,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":6,"s":[-8.142,-92.147],"e":[-7.675,-162.544],"to":[0.07779947668314,-11.7327470779419],"ti":[-0.07779947668314,11.7327470779419]},{"t":17}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[83.981,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":6,"s":[20.367],"e":[6.367]},{"t":17}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":21}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.9070925,0.5119235,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[16.585,-99.759],"e":[28.521,-187.495],"to":[1.9892578125,-14.6227216720581],"ti":[-1.9892578125,14.6227216720581]},{"t":21}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[97.419,116],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":6,"s":[14.733],"e":[8.733]},{"t":21}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":6,"op":22,"st":-21,"bm":0,"sr":1},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 7","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[160,159.5,0]},"a":{"a":0,"k":[0,-34,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p667_1_0p167_0p167","0p667_1_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":17}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.7921569,0.4470588,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":6,"s":[-8.142,-92.147],"e":[-7.675,-162.544],"to":[0.07779947668314,-11.7327470779419],"ti":[-0.07779947668314,11.7327470779419]},{"t":17}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[83.981,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"n":["0p667_1_0p167_0p167"],"t":6,"s":[20.367],"e":[6.367]},{"t":17}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0p833_0p833_0p167_0p167","0p833_0p833_0p167_0p167"],"t":6,"s":[15.021,15.021],"e":[0,0]},{"t":21}]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[0.0823529,0.6784314,0.3843137,1],"e":[0,0.9070925,0.5119235,1]},{"t":17}]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":6,"s":[16.585,-99.759],"e":[28.521,-187.495],"to":[1.9892578125,-14.6227216720581],"ti":[-1.9892578125,14.6227216720581]},{"t":21}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[97.419,116],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":6,"s":[14.733],"e":[8.733]},{"t":21}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":6,"op":22,"st":-21,"bm":0,"sr":1},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 5","parent":11,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":-44},"p":{"a":0,"k":[0.378,-0.641,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[7.39,7.39,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[-274.219,-254.097]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-17,-16],[-17,10.5],[41,10.5]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":8},"lc":2,"lj":1,"ml":5,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"}],"ip":7,"op":22,"st":-21,"bm":0,"sr":1},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 6","ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":4,"s":[50],"e":[0]},{"t":14}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[160,160,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":4,"s":[100,100,100],"e":[1085,1085,100]},{"t":14}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[19.779,19.779]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0,0.7921569,0.4470588,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-0.068,0.036],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":4,"op":22,"st":-23,"bm":0,"sr":1},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 4","ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":6,"s":[30],"e":[100]},{"t":9}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[160.312,161.188,0]},"a":{"a":0,"k":[0.812,-0.562,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":6,"s":[100,100,100],"e":[1087,1087,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":11,"s":[1087,1087,100],"e":[866,866,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p833_0p833_0p333_0","0p833_0p833_0p333_0","0p833_0p833_0p333_0p333"],"t":13,"s":[866,866,100],"e":[878,878,100]},{"t":16}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10.068,10.068]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0,0.7921569,0.4470588,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0.784,-0.716],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":6,"op":22,"st":-19,"bm":0,"sr":1},{"ddd":0,"ind":12,"ty":4,"nm":"Shape Layer 3","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[161,160,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":3,"s":[100,100,100],"e":[224,224,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,0.667]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0.333]},"n":["0p667_1_0p333_0","0p667_1_0p333_0","0p667_0p667_0p333_0p333"],"t":4,"s":[224,224,100],"e":[476,476,100]},{"t":8}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[6.009,6.009]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":4,"s":[0.0558609,0.688557,0.3778246,1],"e":[0.1089485,0.6693168,0.3941063,1]},{"t":8}]},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":4,"s":[0],"e":[100]},{"t":5}]},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":4,"s":[3],"e":[0]},{"t":8}]},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0,0.7921569,0.4470588,1]},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":3,"s":[100],"e":[99]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":4,"s":[99],"e":[0]},{"t":5}]},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-0.338,0.065],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[649.112,649.112],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 2","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":3,"op":22,"st":-21,"bm":0,"sr":1},{"ddd":0,"ind":13,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[160.142,159.987,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[377.603,377.603,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[22.315,22.315]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[0.8352941,0.8352941,0.8352941,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-0.038,0.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":-21,"op":22,"st":-21,"bm":0,"sr":1}]} --------------------------------------------------------------------------------