├── .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 |
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 | Node Name
24 | CPU
25 |
26 |
27 | {nodeList}
28 |
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 |
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 | Service Name
32 | Type
33 | Namespace
34 | Port
35 | Cluster IP
36 |
37 |
38 | {table}
39 |
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 | selectCluster(e.target.value)}
43 | >
44 | {cluster}
45 |
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 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Features
53 |
54 |
55 | Contribute
56 |
57 |
58 | Team
59 |
60 | {navOption}
61 |
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 |
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 | Pod Name
93 | Namespace
94 | Status
95 | Pod IP
96 | Created At
97 |
98 |
99 | {table}
100 |
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 | Pod Name
99 | Namespace
100 | Status
101 | Pod IP
102 | Time
103 |
104 |
105 | {table}
106 |
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 | {/*
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 | Choose...
139 | us-east-1
140 | us-east-2
141 | us-west-2
142 | ca-central-1
143 | ap-east-1
144 | ap-south-1
145 | ap-northeast-1
146 | ap-northeast-2
147 | ap-southeast-1
148 | ap-southeast-2
149 | cn-north-1
150 | cn-northwest-1
151 | eu-central-1
152 | eu-west-1
153 | eu-west-2
154 | eu-west-3
155 | eu-north-1
156 | me-south-1
157 | sa-east-1
158 |
159 |
160 |
161 |
162 | Sign In with AWS
163 |
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 |
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 |
201 | Sign Up
202 |
203 |
204 |
205 | {/* user sign up w/o aws */}
206 | Already have an account?
207 |
208 |
210 | setLogin({ ...login, email: e.target.value })}
215 | />
216 |
217 |
218 | setLogin({ ...login, password: e.target.value })}
223 | />
224 |
225 |
226 |
227 | Sign In
228 |
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}]}
--------------------------------------------------------------------------------