├── .DS_Store ├── .gitignore ├── README.md ├── __tests__ ├── db.js ├── supertest.js └── test.js ├── client ├── index.tsx └── src │ ├── components │ ├── App.tsx │ ├── ClusterEditor.tsx │ ├── ClustersView.tsx │ ├── Dashboard.tsx │ ├── Home.tsx │ ├── LoginPage.tsx │ ├── Logo.tsx │ ├── MainPage.tsx │ ├── NavBar.tsx │ ├── Panel.tsx │ └── dashboard │ │ ├── ApiServer.tsx │ │ ├── KubePrometheus.tsx │ │ ├── KubeStateMetrics.tsx │ │ └── NodeExporter.tsx │ ├── index.d.ts │ └── stylesheets │ ├── ClusterEditor.scss │ ├── ClustersView.scss │ ├── Dashboard.scss │ ├── Home.scss │ ├── LoginPage.scss │ ├── MainPage.scss │ └── NavBar.scss ├── dashboardJson ├── apiServer.json ├── kubePrometheus.json ├── kubeStateMetrics.json └── nodeExporter.json ├── dist ├── 4604ba421e4dc08f9e948022c6934730.gif ├── bundle.js ├── bundle.js.LICENSE.txt ├── e8db7fd442129943dd99.gif └── index.html ├── electron └── main.tsx ├── index.html ├── metrics.ts ├── package-lock.json ├── package.json ├── public ├── addingClusters.gif ├── apiServer.gif ├── argo-logo-full.png ├── argo-logo-large.gif ├── argo-logo.gif ├── argo-logo.png ├── google-icon-matte.png ├── google-icon.png ├── google-light.svg ├── key.svg ├── kubeStateMetrics.gif ├── landingPage.gif ├── lock.svg ├── nodeExporter.gif ├── person.svg └── shield-lock.svg ├── server ├── .DS_Store ├── config │ ├── db.ts │ └── passport.ts ├── controllers │ ├── clusterController.ts │ ├── cookieController.ts │ ├── dashboardController.ts │ ├── sessionController.ts │ └── userController.ts ├── models │ ├── clusterModel.ts │ ├── sessionModel.ts │ ├── test_models │ │ ├── cluster_test.ts │ │ ├── session_test.ts │ │ └── user_test.ts │ └── userModel.ts ├── routers │ ├── authRouter.ts │ ├── clusterRouter.ts │ ├── profileRouter.ts │ └── router.ts └── server.ts ├── tsconfig.json ├── types.ts └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Argometrics/9ebb88c4ea3fb32095aa4033a5c9553873e4a7f2/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | # Technologies 4 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 5 | ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white) 6 | ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 7 | ![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) 8 | ![OAUTH](https://img.shields.io/badge/OAUTH2.0-black?style=for-the-badge&logo=JSON%20web%20tokens) 9 | ![Electron.js](https://img.shields.io/badge/Electron-191970?style=for-the-badge&logo=Electron&logoColor=white) 10 | ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) 11 | ![kubernetes](https://img.shields.io/badge/Kubernetes-100000?style=for-the-badge&logo=Kubernetes&logoColor=white&labelColor=000000&color=black) 12 | ![Prometheus](https://img.shields.io/badge/Prometheus-E6522C?style=for-the-badge&logo=Prometheus&logoColor=white) 13 | ![Grafana](https://img.shields.io/badge/grafana-%23F46800.svg?style=for-the-badge&logo=grafana&logoColor=white) 14 | ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) 15 | ![Webpack](https://img.shields.io/badge/webpack-%238DD6F9.svg?style=for-the-badge&logo=webpack&logoColor=black) 16 | ![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white) 17 | ![K6](https://img.shields.io/badge/-K6-white?style=for-the-badge&logo=k6) 18 | ![Keda](https://img.shields.io/badge/-KEDA-darkblue?style=for-the-badge&logo=lightning&logoColor=white) 19 | ![Bcrypt](https://img.shields.io/badge/BCRYPT-grey?style=for-the-badge&logo=letsencrypt) 20 | ![Javascript](https://img.shields.io/badge/javascript-yellow?style=for-the-badge&logo=javascript) 21 | ![reactdnd](https://img.shields.io/badge/REACT%20DND-blue?style=for-the-badge&logo=react&logocolor=red) 22 | ![passport](https://img.shields.io/badge/PASSPORT-black?style=for-the-badge&logo=passport) 23 | ![node](https://img.shields.io/badge/nodejs-forestgreen?style=for-the-badge&logo=nodedotjs&logoColor=black) 24 | # Argometrics 25 | Argometrics is an open source product that allows users to visualize the health of their local Kubernetes clusters. With Prometheus and Grafana scraping and displaying metrics from our cluster, our application visualizes key metrics such as pod and container health, prometheus health, and performance and usage from the cluster in real time. Argometrics allows users to change between clusters at the click of a button, making it easy to monitor all clusters in one location. 26 | 27 | ![landingPage](./public/landingPage.gif) 28 | ![addingClusters](./public/addingClusters.gif) 29 | ![apiServer](./public/apiServer.gif) 30 | ![kubeStateMetrics](./public/kubeStateMetrics.gif) 31 | ![nodeExporter](./public/nodeExporter.gif) 32 | 33 | # Prerequisites 34 | - Our application works with local kubernetes clusters. Be sure to have a local cluster configured with some type of Kuberenetes implementation (Docker Desktop recommended) 35 | 36 | - Install helm 37 | - You can install on brew using `brew install helm`. 38 | 39 | # Set-Up 40 | - `helm install prometheus prometheus-community/kube-prometheus-stack`. 41 | - This command deploys Prometheus and Grafana on your local cluster. 42 | 43 | - `kubectl patch ds prometheus-prometheus-node-exporter --type "json" -p '[{"op": "remove", "path" : "/spec/template/spec/containers/0/volumeMounts/2/mountPropagation"}]'` 44 | - This command is for users running on Docker Desktop. 45 | 46 | # Porting Forward 47 | - `kubectl port-forward -n default {prometheus podname} {port}` 48 | - ex: `kubectl port-forward -n default prometheus-prometheus-kube-prometheus-prometheus-0 9090` 49 | - `kubectl port-forward -n default {grafana podname} {port}` 50 | - ex: `kubectl port-forward -n default prometheus-grafana-85978cf69c-29dw9 3000` 51 | 52 | # How To Change Grafana Settings Via Grafana Config 53 | - `kubectl get deployment` 54 | - Find the deployment associated with Prometheus and Grafana 55 | 56 | - `kubectl edit configmap {deployment}` 57 | - Opens up vi file of Prometheus/Grafana configmap 58 | - add this code under grafana.ini 59 | ``` 60 | [security] 61 | allow_embedding: true 62 | [auth.anonymous] 63 | enabled: true 64 | ``` 65 | * HELPFUL VI COMMANDS 66 | - `i` -> To edit the file ( you will see 'INSERT' at bottom ) 67 | - `ESC` -> Escape edit mode back to command mode 68 | - Common commands in command mode 69 | - `:wq` -> Write quit ( when you make an update ) 70 | - `:q!` -> Force quit without saving changes 71 | - Restart docker desktop or whatever virtualization software is being used 72 | - Forward your ports again and the changes to grafana.ini should be reflected in settings tab 73 | 74 | # Adding Dashboards 75 | - Port forward your grafana pod and open up the dashboards page 76 | - There should be an option to import dashboards on the righthand side. 77 | - Inside of the dashboardJson folder in our application, we have precofigured graphs. Simply copy paste into the import. 78 | 79 | # Useful Helm AND K8 Commands 80 | - `helm list` 81 | - `helm repo list` 82 | - `kubectl --namespace default get pods -l "release=prometheus"` 83 | - `kubectl get secret --namespace {namespace} {podname} -o jsonpath="{.data.admin-password}" | base64 --decode ; echo` 84 | 85 | # Launching the app 86 | - To launch Argometrics 87 | - Create a .env file in the root directory with the following variables 88 | - `ATLAS_URI = {your MongoDB URI}` 89 | - `GOOGLE_CLIENT_ID = {Google Client ID}` 90 | - `GOOGLE_CLIENT_SECRET = {Google Client Secret}` 91 | - `npm install` 92 | - `npm run dev` 93 | - `npm electron-start` 94 | 95 | # Contributions 96 | We are always looking for improvement and are open to feedback. If you had a feature suggestion, please fork and clone this repo and make a pull request with your new branch. 97 | 98 | - Fork our repo 99 | - Clone it to your local machine 100 | - `git checkout -b newFeatureBranch` in terminal to enter a new branch 101 | - Add and commit your changes once the modifications have been made 102 | - `git push origin newFeatureBranch` 103 | - Make a pull request from the newFeatureBranch 104 | 105 | # Potential Features for Iteration 106 | - Built in CLI 107 | - KEDA integration with our application 108 | - Cloud cluster compatibility 109 | - Setting up ingress to stabilize the cluster connection 110 | 111 | We originally planned to deploy our application with KEDA and give the user the ability to choose metrics to scale by. Our command line interface would allow the user to add loads to their cluster and the user could test how their cluster health performed under different environments and different scaled objects. Additionally, port-forwarding is currently being used to make our cluster available to our application. We did not know at the time but this causes many instability issues. Moving forward, either making our application compatible with cloud clusters and/or using an ingress instead of porting forward to connect the cluster with our application are things to consider when iterating. 112 | 113 | # Authors 114 | 115 | - Ryan Sun - [Github](https://github.com/ryansun222) | [LinkedIn](https://www.linkedin.com/in/ryansun792/) 116 | - Taylor Ball - [Github](https://github.com/tb1121) | [LinkedIn](https://www.linkedin.com/in/taylorball5/) 117 | - Joey Schwartz - [Github](https://github.com/joeyschwartz) | [LinkedIn](linkedin.com/in/joey-schwartz-7605621a7) 118 | - Alex Yam - [Github](https://github.com/alexyam0) | [LinkedIn](https://www.linkedin.com/in/alex-yam/) 119 | - Jake Crawford - [Github](https://github.com/jake-up-0517) | [LinkedIn](https://www.linkedin.com/in/jakecrawford512/) 120 | 121 | 122 | # Technologies Icon Reference 123 | Technology icons by shields.io 124 | -------------------------------------------------------------------------------- /__tests__/db.js: -------------------------------------------------------------------------------- 1 | //tests for database 2 | const Cluster_test = require('../server/models/test_models/cluster_test'); 3 | const Session_test = require('../server/models/test_models/session_test'); 4 | const User_test = require('../server/models/test_models/user_test'); 5 | 6 | 7 | describe('Cluster model unit tests', () => { 8 | 9 | beforeAll((done) =>{ 10 | Cluster_test.deleteMany({}); 11 | done(); 12 | }); 13 | 14 | describe('sync', () => { 15 | it('adds valid clusters to Cluster model', () =>{ 16 | const newCluster = { 17 | userId: '111', 18 | clusterName: 'cluster name', 19 | url: 'clusterurl' 20 | } 21 | const result = Cluster_test.create(newCluster); 22 | expect(result).not.toBeInstanceOf(Error); 23 | }) 24 | }); 25 | 26 | it('does not add invalid clusters to Cluster model', () =>{ 27 | const newCluster = { 28 | userId: 2, 29 | clusterName: null, 30 | url: 22, 31 | } 32 | const result = Cluster_test.create(newCluster); 33 | expect(result).toBeInstanceOf(Error); 34 | }); 35 | 36 | }) 37 | 38 | describe('Session model unit tests', () => { 39 | 40 | beforeAll((done) =>{ 41 | Session_test.deleteMany({}); 42 | done(); 43 | }); 44 | 45 | describe('sync', () => { 46 | it('adds valid session to Session model', () =>{ 47 | const newSession = { 48 | cookieId: 2 49 | } 50 | const result = Session_test.create(newSession); 51 | expect(result).not.toBeInstanceOf(Error); 52 | }) 53 | }); 54 | 55 | it('does not allow duplicate cookieIds', () =>{ 56 | const newSession = { 57 | cookieId: 2 58 | } 59 | const addOne = Session_test.create(newSession); 60 | const result = Session_test.create(newSession); 61 | expect(result).toBeInstanceOf(Error); 62 | }); 63 | }) 64 | 65 | 66 | describe('User model unit tests', () => { 67 | 68 | beforeAll((done) =>{ 69 | User_test.deleteMany({}); 70 | done(); 71 | }); 72 | 73 | describe('sync', () => { 74 | it('adds user to User model', () =>{ 75 | const newUser = { 76 | username: 'testingUser', 77 | password: 'pass' 78 | }; 79 | const result = Session_test.create(newUser); 80 | expect(result).not.toBeInstanceOf(Error); 81 | }) 82 | it('hashes user password', () =>{ 83 | const newUser = { 84 | username: 'testingUser', 85 | password: 'pass' 86 | }; 87 | const create = Session_test.create(newUser); 88 | const hashed = Session_test.findOne({username: 'testingUser'}); 89 | expect(hashed).not.toEqual(newUser.password); 90 | }); 91 | }); 92 | }) 93 | 94 | -------------------------------------------------------------------------------- /__tests__/supertest.js: -------------------------------------------------------------------------------- 1 | // tests our express routes 2 | const request = require('supertest'); 3 | const server = 'http://localhost:6000'; 4 | 5 | describe("route integration", () => { 6 | describe('/', () => { 7 | describe('GET', () => { 8 | it('responds with 200 status and text/html content type', () => { 9 | return request(server) 10 | .get('/') 11 | .expect('Content-Type', /text\/html/) 12 | .expect(200); 13 | }); 14 | }); 15 | }) 16 | 17 | describe('/api/auth/login', () => { 18 | describe('POST', () => { 19 | it('responds with 200 status and json content type', () => { 20 | return request(server) 21 | .post('/api/auth/login') 22 | .set('Authorization', 'Basic ' + Buffer.from('auth:auth').toString('base64')) 23 | .expect('Content-Type', /application\/json/) 24 | .expect(200) 25 | }) 26 | }) 27 | }) 28 | describe('/api/auth/register', () => { 29 | describe('GET', () => { 30 | it('responds with 200 status and json content type', () => { 31 | return request(server) 32 | .get('/api/auth/register') 33 | .set('Accept', 'application/json') 34 | .send(JSON.stringify({ 35 | username: 'test', 36 | password: 'test' 37 | })) 38 | .expect('Content-Type', /json/) 39 | .expect(200) 40 | }) 41 | }) 42 | }) 43 | 44 | describe('/api/cluster/get', () => { 45 | describe('GET', () => { 46 | it('responds with 200 status and json content type', () => { 47 | return request(server) 48 | .get('/api/cluster/get') 49 | .expect('Content-Type', /application\/json/) 50 | .expect(200) 51 | }) 52 | }) 53 | }) 54 | 55 | describe('/api/cluster/add', () => { 56 | describe('POST', () => { 57 | it('responds with 200 status and json content type', () => { 58 | return request(server) 59 | .get('/api/cluster/post') 60 | .expect('Content-Type', /application\/json/) 61 | .expect(200) 62 | }) 63 | }) 64 | }) 65 | 66 | }) -------------------------------------------------------------------------------- /__tests__/test.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { sleep, run} from 'k6'; 3 | 4 | export default function () { 5 | http.get('http://localhost:9090/'); 6 | sleep(1); 7 | } 8 | 9 | // run({ vus: 10, duration: '30s' }); 10 | // k6 run --vus 10 --duration 30s __tests__/test.js 11 | // -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {createRoot} from 'react-dom/client' 3 | import App from './src/components/App'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | const root = createRoot(document.getElementById('root') as HTMLInputElement); 6 | 7 | root.render( 8 | 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /client/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, createContext, useEffect, useContext} from 'react'; 2 | import { Route, Routes, useNavigate} from "react-router-dom"; 3 | import LoginPage from './LoginPage'; 4 | import MainPage from './MainPage'; 5 | import { DndProvider } from 'react-dnd'; 6 | import { HTML5Backend } from 'react-dnd-html5-backend'; 7 | import Logo from './Logo' 8 | 9 | //needs logic to check if current session and redirect to main page if session is active 10 | 11 | const App = () => { 12 | 13 | const navigate = useNavigate(); 14 | const [userId, setUserId] = useState(''); 15 | 16 | return ( 17 | 18 | 19 | } /> 20 | {/* } caseSensitive={true} /> */} 21 | } caseSensitive={true}/> 22 | 23 | 24 | ) 25 | } 26 | 27 | export default App; 28 | 29 | -------------------------------------------------------------------------------- /client/src/components/ClusterEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Cluster} from '../../../types' 3 | import '../stylesheets/ClusterEditor.scss' 4 | 5 | interface ClusterEditorProps { 6 | setShowClusterEditor: React.Dispatch> 7 | cluster: Array 8 | } 9 | function ClusterEditor({ setShowClusterEditor , cluster}: ClusterEditorProps) { 10 | const [clusterEditorUrl, setClusterEditorUrl] = useState(''); 11 | const [clusterEditorName, setClusterEditorName] = useState(''); 12 | 13 | const createCluster = async () => { 14 | //add url and name to db 15 | try { 16 | if (clusterEditorUrl && clusterEditorName){ 17 | const clusterObj = {name: clusterEditorName, url: clusterEditorUrl}; 18 | const response = await fetch('/api/cluster/add',{ 19 | method: 'POST', 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | }, 23 | body: JSON.stringify(clusterObj) 24 | }); 25 | const result = await response.json() 26 | } 27 | } 28 | catch(err){ 29 | console.log(err, 'cluster post req unsuccessful'); 30 | } 31 | } 32 | 33 | const addCluster = () => { 34 | createCluster(); 35 | setShowClusterEditor(false); 36 | } 37 | 38 | 39 | // 40 | return ( 41 |
42 |

Cluster Editor

43 |
44 |
Add Cluster
45 | setClusterEditorName(e.target.value)}> 46 | setClusterEditorUrl(e.target.value)}> 47 |
48 | 49 | 50 |
51 |
52 | 53 |
54 | ) 55 | } 56 | 57 | export default ClusterEditor; 58 | 59 | // cluster obj { 60 | // dashboards: Object 61 | // apiServer: {dashboardUIDKey: "", grafanaLinkDText: ""} 62 | // kubePrometheus: {dashboardUIDKey: "", grafanaLinkDText: ""} 63 | // kubeStateMetric: {dashboardUIDKey: "", grafanaLinkDText: ""} 64 | // nodeExporter: {dashboardUIDKey: "", grafanaLinkDText: ""} 65 | // url: "" 66 | // userId: "" 67 | // } -------------------------------------------------------------------------------- /client/src/components/ClustersView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect, ReactHTMLElement } from 'react' 2 | import { Cluster } from '../../../types' 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import '../stylesheets/ClustersView.scss' 5 | import { useDrag, useDrop } from 'react-dnd' 6 | 7 | interface ClusterProps { 8 | userId: string 9 | cluster: Array 10 | currCluster: Cluster 11 | setCurrCluster: React.Dispatch> 12 | showClusterEditor: boolean 13 | // pass in handleClusterClick too 14 | } 15 | 16 | function ClusterView({ userId, cluster, currCluster, setCurrCluster , showClusterEditor}: ClusterProps) { 17 | const [ buttons, setButtons ] = useState([]) 18 | const [ reCluster, setReCluster ] = useState(cluster) 19 | 20 | const [ { canDrop, isOver }, dropRef ] = useDrop({ 21 | accept: 'button', 22 | drop: (item: any, monitor) => { 23 | const { clusterContent, index } = item 24 | const hoverIndex = buttons.findIndex((btn) => btn.props.index === monitor.getItem().index) 25 | handleDrop(index, hoverIndex) 26 | 27 | }, 28 | collect: (monitor) => ({ 29 | isOver: monitor.isOver(), 30 | canDrop: monitor.canDrop() 31 | }) 32 | }) 33 | 34 | const handleDrop = (index: number, hoverIndex: any) => { 35 | const draggedButton = buttons[index] 36 | const newButtons = [...buttons] 37 | newButtons.splice(index, 1) 38 | newButtons.splice(hoverIndex.index, 0, draggedButton) 39 | setButtons(newButtons) 40 | } 41 | 42 | // useEffect to set buttons whatever clusters the user already has 43 | useEffect(() => { 44 | // if cluster is defined 45 | if (Array.isArray(cluster)) { 46 | const button: React.ReactElement[] = cluster.map((clusterContent: any, idx: number) => { 47 | return ; 48 | }) 49 | setButtons(button) 50 | } 51 | }, [cluster, reCluster]) 52 | 53 | 54 | 55 | 56 | 57 | return( 58 | <> 59 |
60 |
61 |

Cluster Name

62 |
63 | {currCluster.clusterName} 64 |
65 |
66 |
67 | {buttons} 68 |
69 |
70 | 71 | 72 | ) 73 | } 74 | 75 | export default ClusterView; 76 | 77 | // button component 78 | interface ButtonProps { 79 | clusterContent: Cluster 80 | buttons: Array 81 | setButtons: (state: []) => void 82 | index: number 83 | setCurrCluster: React.Dispatch> 84 | handleDrop: (index: number, item: any) => void; 85 | } 86 | 87 | function CreateButton({clusterContent, buttons, setButtons, index, setCurrCluster, handleDrop}: ButtonProps) { 88 | 89 | const [ { isDragging }, dragRef ] = useDrag({ 90 | type: 'button', 91 | item: { clusterContent, index }, 92 | // end: (item, monitor) => { 93 | // const dropResult = monitor.getDropResult() 94 | // // console.log('item in drag end', item) 95 | // // console.log('dropResult', dropResult) 96 | // }, 97 | collect: (monitor) => ({ 98 | isDragging: monitor.isDragging() 99 | }) 100 | }) 101 | 102 | const [{ isOver, canDrop }, dropRef] = useDrop({ 103 | accept: 'button', 104 | drop: (item) => handleDrop(index, item), 105 | collect: (monitor) => ({ 106 | isOver: monitor.isOver(), 107 | canDrop: monitor.canDrop(), 108 | }), 109 | }); 110 | 111 | 112 | return ( 113 | <> 114 |
115 | 116 | {isDragging} 117 |
118 | 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /client/src/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import '../stylesheets/Dashboard.scss' 3 | import {Cluster} from '../../../types' 4 | import Panel from './Panel' 5 | import allMetrics, { grafanaIFrameGenerator } from '../../../metrics' 6 | import ApiServer from './dashboard/ApiServer' 7 | import KubePrometheus from './dashboard/KubePrometheus' 8 | import KubeStateMetrics from './dashboard/KubeStateMetrics' 9 | import NodeExporter from './dashboard/NodeExporter' 10 | 11 | interface DashboardProps { 12 | userId: string; 13 | cluster: Array; 14 | currCluster: Cluster; 15 | toggleDashboard: string; 16 | setToggleDashboard: React.Dispatch>; 17 | } 18 | const Dashboard = ({ userId, cluster, currCluster, toggleDashboard, setToggleDashboard }: DashboardProps) => { 19 | const [ apiDashboard, setApiDashboard ] = useState([]) 20 | const [ kubePromDashboard, setKubePromDashboard ] = useState([]) 21 | const [ kubeStateDashboard, setKubeStateDashboard ] = useState([]) 22 | const [ nodeDashboard, setNodeDashboard ] = useState([]) 23 | 24 | 25 | const updateDashboard = (item: number, hoverIndex: number, searchBy: string) => { 26 | // find panel in dashboard state and update position 27 | switch (searchBy) { 28 | case 'apiServer': { 29 | const moveItem = apiDashboard[item] 30 | if (moveItem) { 31 | setApiDashboard((prevState: any) => { 32 | const copy = [...prevState] 33 | copy.splice(item, 1) 34 | copy.splice(hoverIndex, 0, moveItem) 35 | return copy 36 | }) 37 | } 38 | break 39 | } 40 | case 'kubePrometheus': { 41 | const moveItem = kubePromDashboard[item] 42 | if (moveItem) { 43 | setKubePromDashboard((prevState: any) => { 44 | const copy = [...prevState] 45 | copy.splice(item, 1) 46 | copy.splice(hoverIndex, 0, moveItem) 47 | return copy 48 | }) 49 | } 50 | break 51 | } 52 | case 'kubeStateMetrics': { 53 | const moveItem = kubeStateDashboard[item] 54 | if (moveItem) { 55 | setKubeStateDashboard((prevState: any) => { 56 | const copy = [...prevState] 57 | copy.splice(item, 1) 58 | copy.splice(hoverIndex, 0, moveItem) 59 | return copy 60 | }) 61 | } 62 | break 63 | } 64 | case 'nodeExporter': { 65 | const moveItem = nodeDashboard[item] 66 | if (moveItem) { 67 | setNodeDashboard((prevState: any) => { 68 | const copy = [...prevState] 69 | copy.splice(item, 1) 70 | copy.splice(hoverIndex, 0, moveItem) 71 | return copy 72 | }) 73 | } 74 | break 75 | } 76 | default: 77 | console.log('Dashboard does not exist') 78 | } 79 | } 80 | 81 | return( 82 | <> 83 |
84 |

{toggleDashboard === "dash"? "": toggleDashboard} Dashboard

85 | { 86 | toggleDashboard === 'apiServer' ? : 87 | toggleDashboard === 'kubePrometheus' ? : 88 | toggleDashboard === 'kubeStateMetrics' ? : 89 | toggleDashboard === 'nodeExporter' ? : 90 | toggleDashboard === 'keda' ? : 91 | null 92 | } 93 |
94 | 95 | ) 96 | } 97 | 98 | export default Dashboard; -------------------------------------------------------------------------------- /client/src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSpring, animated } from 'react-spring'; 3 | import ClusterEditor from './ClusterEditor'; 4 | import '../stylesheets/Home.scss' 5 | import { Cluster } from '../../../types'; 6 | 7 | interface HomeProps { 8 | userId: string; 9 | cluster: Array 10 | setCluster: React.Dispatch>>; 11 | showClusterEditor: boolean; 12 | setShowClusterEditor: React.Dispatch>; 13 | } 14 | const Home = ({ userId, cluster, setCluster, showClusterEditor, setShowClusterEditor}: HomeProps) => { 15 | 16 | // animated button that on click, opens the cluster editor 17 | const buttonAnimation = useSpring({ 18 | transform: 'translateY(0)', // Starting position 19 | from: { transform: 'translateY(3000px)' }, // Initial position 20 | }); 21 | 22 | return ( 23 | <> 24 |
25 |

Add Cluster

26 |
27 |
28 |
29 |
30 | {!showClusterEditor && setShowClusterEditor(true) } 31 | id="cluster-btn" 32 | className="fa-thin fa-plus fa-fade fa-xl" 33 | style={buttonAnimation} 34 | >+} 35 | {showClusterEditor && } 36 |
37 | 38 | ); 39 | }; 40 | 41 | export default Home; -------------------------------------------------------------------------------- /client/src/components/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import '../stylesheets/LoginPage.scss' 4 | 5 | interface LoginPageProps { 6 | userId: string; 7 | setUserId: React.Dispatch>; 8 | } 9 | 10 | const LoginPage = ({ userId, setUserId,}: LoginPageProps) =>{ 11 | const [username, setUsername] = useState(''); 12 | const [password, setPassword] = useState(''); 13 | const [clickedSignUp, setSignUp] = useState(false); 14 | const navigate = useNavigate(); 15 | const handleLoginClick = () => { 16 | //receives back user info object and sets user info to that object 17 | //[{googleId: null, password: "$2a$10$suXtDx/4k/VbkC5ScBzg0eDwwWl83iWX.xbU0QwkpPyR8HRW3TKIS", username: "auth", __v: 0, _id: "646277116c71f57f0d34b74f"}] 18 | 19 | const userObj = {username: username, password: password}; 20 | // make a fetch request to our backend using input username and password 21 | fetch('api/auth/login', { 22 | method: 'POST', 23 | headers: { 24 | 'Content-Type': 'application/json' 25 | }, 26 | body: JSON.stringify(userObj) 27 | }) 28 | .then((res)=> res.json()) 29 | .then((data)=>{ 30 | setUserId(data._id); 31 | navigate('/mainPage'); 32 | }) 33 | .catch((err)=>{ 34 | console.log(err) 35 | }) 36 | } 37 | 38 | const handleSignupClick = () =>{ 39 | const newUserObj = {username: username, password: password}; 40 | console.log('clicked!', newUserObj) 41 | fetch('api/auth/register', { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/json' 45 | }, 46 | body: JSON.stringify(newUserObj) 47 | }) 48 | .then((res)=> res.json()) 49 | .then((data)=>{ 50 | setUserId(data._id); 51 | navigate('/mainPage'); 52 | }) 53 | .catch((err)=>{ 54 | console.log(err) 55 | }) 56 | } 57 | let logInOrSignUp; 58 | if(!clickedSignUp){ 59 | logInOrSignUp = [
60 |
61 | Welcome! 62 |
63 | 64 | setUsername(e.target.value)} 69 | /> 70 |
71 |
72 | 73 | setPassword(e.target.value)} 78 | /> 79 |
80 | 81 | 82 |
83 | 89 | 90 |
] 91 | } 92 | else { 93 | logInOrSignUp = [ 94 |
95 | setUsername(e.target.value)} 100 | /> 101 | setPassword(e.target.value)} 106 | /> 107 | 108 |
109 | ] 110 | } 111 | 112 | // useEffect(()=>{ 113 | // //make fetch req to front end, look for session cookie, 114 | // //if cookie matches active session, redirect to mainpage 115 | // fetch('api/auth/checkSession') 116 | // .then((data)=> data.json() ) 117 | // .then((data)=>{ 118 | // console.log("uedata", data) 119 | // if (data){ 120 | // navigate('/mainPage') 121 | // } 122 | // }) 123 | // },[]) 124 | 125 | return( 126 |
127 | {/*
Argometrics
*/} 128 | {logInOrSignUp} 129 | {/* Log in with Google */} 130 |
131 | 132 | ) 133 | } 134 | 135 | export default LoginPage -------------------------------------------------------------------------------- /client/src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import '../stylesheets/LoginPage.scss' 3 | import LoginPage from './LoginPage'; 4 | 5 | interface MainPageProps { 6 | userId: string; 7 | setUserId: React.Dispatch> 8 | } 9 | 10 | const Logo = ({userId, setUserId}: MainPageProps) => { 11 | const [ isLogin, setLogin ] = useState(false) 12 | 13 | useEffect(() => { 14 | if (!isLogin) { 15 | setTimeout(() => setLogin(true), 5500) 16 | } 17 | }, []) 18 | 19 | return ( 20 | <> 21 | 22 | {!isLogin &&
23 | Logo 28 |
} 29 | {isLogin && } 30 | 31 | 32 | ) 33 | } 34 | 35 | export default Logo -------------------------------------------------------------------------------- /client/src/components/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useContext, useEffect, MouseEvent} from 'react'; 2 | import Dashboard from './Dashboard'; 3 | import NavBar from './NavBar'; 4 | import ClustersView from './ClustersView' 5 | import { useSpring, animated } from 'react-spring'; 6 | import { config } from '@react-spring/web'; 7 | import { v4 as uuidv4 } from 'uuid'; 8 | import Home from './Home'; 9 | import '../stylesheets/MainPage.scss'; 10 | import { Cluster, DashboardUIds } from '../../../types'; 11 | import { useNavigate } from 'react-router-dom'; 12 | interface MainPageProps { 13 | userId: string; 14 | setUserId: React.Dispatch> 15 | } 16 | 17 | const MainPage = ({userId, setUserId}: MainPageProps) => { 18 | 19 | // default values so type script knows the type of what its taking in 20 | const defaultDashboard: DashboardUIds = { 21 | apiServer: { 22 | dashboardUIDKey: '', 23 | grafanaLinkDText: '' 24 | }, 25 | kubeStateMetrics: { 26 | dashboardUIDKey: '', 27 | grafanaLinkDText: '' 28 | }, 29 | kubePrometheus: { 30 | dashboardUIDKey:'', 31 | grafanaLinkDText: '' 32 | }, 33 | nodeExporter: { 34 | dashboardUIDKey: '', 35 | grafanaLinkDText: '' 36 | }, 37 | keda: { 38 | dashboardUIDKey: '', 39 | grafanaLinkDText: '', 40 | } 41 | } 42 | const defaultCluster: Cluster = { 43 | userId: userId, 44 | clusterName: '', 45 | url: '', 46 | dashboards: defaultDashboard 47 | }; 48 | 49 | // hook to set the cluster you are working on 50 | // clicking on a different cluster should call setCluster 51 | const [cluster, setCluster] = useState>([]); 52 | const [currCluster, setCurrCluster] = useState(defaultCluster); 53 | const [clusterFetched, setClusterFetched] = useState(false) 54 | const [toggleDashboard, setToggleDashboard] = useState('home'); 55 | const [showClusterEditor, setShowClusterEditor] = useState(false) 56 | 57 | const navigate = useNavigate(); 58 | 59 | const handleLogoutClick = () => { 60 | fetch('api/auth/logout',{ 61 | method: 'POST', 62 | headers: { 63 | 'Content-Type': 'application/json' 64 | }, 65 | body: JSON.stringify({userId}) , 66 | }) 67 | .then((res)=> res.json()) 68 | .then((res) =>{ 69 | if (res === 'success'){ 70 | navigate('/'); 71 | } 72 | }) 73 | .catch((err)=>{ 74 | console.log(err); 75 | }) 76 | } 77 | 78 | 79 | // fetch to the backend to get clusters 80 | useEffect( () => { 81 | async function fetchCluster(userId: string) { 82 | const response = await fetch('/api/cluster/get'); 83 | const cluster = await response.json(); 84 | setCluster(cluster); 85 | // setClusterFetched(true) 86 | } 87 | fetchCluster(userId); 88 | }, []) 89 | 90 | // determines what to render based off what button was clicked 91 | let mainComponent = ; 92 | if (toggleDashboard === 'home'){ 93 | mainComponent = ; 94 | } else { 95 | mainComponent = ; 96 | } 97 | 98 | return ( 99 |
100 | 101 | 102 |
103 | {mainComponent} 104 | 105 |
106 | 107 | 108 | 109 |
110 | ) 111 | } 112 | 113 | export default MainPage; 114 | -------------------------------------------------------------------------------- /client/src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSpring, animated } from 'react-spring'; 3 | import { config } from '@react-spring/web'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | import '../stylesheets/NavBar.scss' 6 | interface NavBarProps { 7 | className: string; 8 | toggleDashboard: string; 9 | setToggleDashboard: React.Dispatch> 10 | 11 | } 12 | 13 | const NavBar = ({toggleDashboard, setToggleDashboard}: NavBarProps)=>{ 14 | 15 | const buttonAnimation = useSpring({ 16 | transform: 'translateY(0)', // Starting position 17 | from: { transform: 'translateY(300px)' }, // Initial position 18 | config: config.molasses, 19 | }); 20 | 21 | 22 | let dropDown; 23 | const dashOptions = ['dash', 'apiServer', 'kubeStateMetrics', 'prometheusExporter', 'kubePrometheus', 'nodeExporter', 'keda']; 24 | if (dashOptions.includes(toggleDashboard)){ 25 | dropDown = [ 26 | <> 27 | 28 | 29 | 30 | 31 | 32 | 33 | ] 34 | } 35 | return ( 36 | <> 37 |
38 | {setToggleDashboard('home')}}>Home 39 | {setToggleDashboard('dash')}}>Dashboard 40 | {dropDown} 41 |
42 | 43 | ) 44 | } 45 | 46 | export default NavBar; -------------------------------------------------------------------------------- /client/src/components/Panel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { useDrag, useDrop } from 'react-dnd'; 3 | 4 | interface PanelProps { 5 | grafanaPanelUrl: string; 6 | i: number; 7 | } 8 | function Panel({ grafanaPanelUrl, i }: PanelProps) { 9 | 10 | const [ { isDragging }, dragRef ] = useDrag({ 11 | type: 'index', 12 | item: { i, grafanaPanelUrl }, 13 | end: (item, monitor) => { 14 | const dropResult = monitor.getDropResult() 15 | }, 16 | collect: (monitor) => ({ 17 | isDragging: monitor.isDragging() 18 | }) 19 | }) 20 | 21 | const opacity = isDragging ? 0.3 : 1 22 | 23 | 24 | return ( 25 | <> 26 |
27 | 55 | 56 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const Dotenv = require('dotenv-webpack') 4 | 5 | module.exports = { 6 | entry: path.join(__dirname, "./client/index.tsx"), 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.(js|jsx|tsx|ts)$/, 15 | exclude: /node_modules/, 16 | use: { 17 | loader: 'babel-loader', 18 | options: { 19 | presets: [ 20 | '@babel/preset-env', 21 | '@babel/preset-react', 22 | '@babel/preset-typescript' 23 | ] 24 | } 25 | } 26 | }, 27 | { 28 | test: /\.(ts|tsx)$/, 29 | exclude: /node_modules/, 30 | use: ['ts-loader'], 31 | }, 32 | { 33 | test: /\.(gif|svg|mov|png|jpg|jpeg)$/i, 34 | type: 'asset/resource', 35 | loader: 'file-loader', 36 | // generator: { 37 | // filename: './client/assets/[name].[ext]', 38 | // }, 39 | // use: { 40 | // loader: 'file-loader', 41 | // options: { 42 | // outputPath: 'assets/', 43 | // name: './client/assets/[name].[ext]' 44 | // } 45 | // }, 46 | exclude: /node_modules/, 47 | }, 48 | { 49 | test: /\.s?css/, 50 | use: [ 51 | 'style-loader', 52 | 'css-loader', 53 | 'sass-loader' 54 | ], 55 | exclude: /node_modules/, 56 | }, 57 | ] 58 | }, 59 | plugins:[ 60 | new HtmlWebpackPlugin({ 61 | template: path.resolve(__dirname, "index.html") 62 | }), 63 | new Dotenv() 64 | ], 65 | devServer: { 66 | host: 'localhost', 67 | port: 8888, 68 | static: path.join(__dirname, 'public'), 69 | // devMiddleware: { 70 | // publicPath: './dist' 71 | // }, 72 | // static: { 73 | // directory: path.resolve(__dirname, 'dist'), 74 | // publicPath: '/', 75 | // }, 76 | hot: true, 77 | historyApiFallback: true, 78 | headers: { 'Access-Control-Allow-Origin': '*' }, 79 | proxy: { 80 | '/api/**': { 81 | target: 'http://localhost:6000', 82 | // changeOrigin: true, 83 | } 84 | } 85 | }, 86 | resolve: { 87 | extensions: ['.*', '.ts', '.tsx', '.js', '.jsx', '.json', '.gif', '.svg', 'png'], 88 | fallback: { 89 | fs: false, 90 | }, 91 | } 92 | 93 | } --------------------------------------------------------------------------------