├── .eslintrc.json ├── .gitignore ├── README.md ├── client ├── App.tsx ├── assets │ ├── dep-icon.png │ ├── gif2.gif │ ├── gif3.gif │ ├── images │ │ ├── KN-logo.png │ │ ├── anthony.png │ │ ├── clusterPic.png │ │ ├── docs.png │ │ ├── edit.png │ │ ├── favicon.ico │ │ ├── github.png │ │ ├── jeremiah.png │ │ ├── linkedin.png │ │ ├── mainDashBoard.png │ │ ├── michaelvan.png │ │ ├── network.png │ │ └── stephen.jpg │ ├── logo.png │ ├── name_blue.png │ ├── node-icon.png │ ├── ns-icon.png │ ├── pod-icon.png │ └── svc-icon.png ├── components │ ├── CRUD │ │ ├── CRUDMini.tsx │ │ ├── CRUDModal.tsx │ │ └── CRUDSelector.tsx │ ├── Contexts.tsx │ ├── Graphs │ │ ├── BarGraph.tsx │ │ ├── GaugeChart.tsx │ │ └── LineGraph.tsx │ ├── Logs.tsx │ ├── Mapothy.tsx │ └── helperFunctions.ts ├── containers │ ├── InvisibleNavbar │ │ ├── InvisibleNavbar.tsx │ │ └── InvisibleNavbarModal.tsx │ ├── LogsContainer.tsx │ ├── MainContainer.tsx │ ├── MainDashBoard.tsx │ ├── Navbar.tsx │ └── NetworkPerformance.tsx ├── index.html ├── index.tsx └── sass │ ├── App.scss │ ├── _CRUDModal.scss │ ├── _graph.scss │ ├── _invisNav.scss │ ├── _logs.scss │ ├── _mainContainer.scss │ ├── _map.scss │ ├── _navBar.scss │ ├── _variables.scss │ └── _vis-network.scss ├── cypress.config.ts ├── cypress ├── components │ ├── GaugeChart.cy.tsx │ ├── InvisibleNavbar.cy.tsx │ └── LineGraph.cy.tsx ├── e2e │ └── frontEndTest.cy.ts ├── fixtures │ └── example.json └── support │ ├── commands.ts │ ├── component-index.html │ ├── component.ts │ └── e2e.ts ├── jest.config.ts ├── jest └── super.test.ts ├── package-lock.json ├── package.json ├── scripts └── loadtest.js ├── server ├── controllers │ ├── clusterController.ts │ ├── crudController.ts │ ├── k6Controller.ts │ ├── lokiController.ts │ ├── mapController.ts │ └── promController.ts ├── routers │ ├── apiRouter.ts │ ├── clusterRouter.ts │ ├── crudRouter.ts │ ├── k6Router.ts │ ├── lokiRouter.ts │ ├── mapRouter.ts │ └── promRouter.ts └── server.ts ├── tsconfig.eslint.json ├── tsconfig.json ├── types ├── png.d.ts ├── react-graph-vis.d.ts └── types.ts ├── webpack.config.ts └── yamls ├── Daemonset.yaml └── values.yml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "airbnb", 8 | "standard-with-typescript", 9 | "plugin:react/recommended" 10 | ], 11 | "parserOptions": { 12 | "sourceType": "module", 13 | "project": "tsconfig.eslint.json", 14 | "tsconfigRootDir": "./" 15 | }, 16 | "plugins": [ 17 | "react" 18 | ], 19 | "rules": { 20 | "import/extensions": [ 21 | "error", 22 | "ignorePackages", 23 | { 24 | "": "never", 25 | "js": "never", 26 | "jsx": "never", 27 | "ts": "never", 28 | "tsx": "never" 29 | } 30 | ], 31 | "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }] 32 | }, 33 | "root": true, 34 | "settings": { 35 | "import/resolver": { 36 | "node": { 37 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | # npm-debug.log* 9 | # yarn-debug.log* 10 | # yarn-error.log* 11 | 12 | # output from webpack 13 | dist 14 | 15 | #dotenv 16 | .env 17 | 18 | #GKE folder 19 | gke-setup 20 | #EKS folder 21 | eks-setup 22 | 23 | #yaml files 24 | prometheus.yaml 25 | kubeconfig.yaml 26 | lokiYaml 27 | 28 | #jeremiahs garbage 29 | #CRUDSelector.tsx 30 | 31 | coverage 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | ![Logo](client/assets/images/KN-logo.png) 6 | 7 | [![JavaScript](https://img.shields.io/badge/javascript-yellow?style=for-the-badge&logo=javascript&logoColor=white)](https://www.javascript.com/) 8 | [![TypeScript](https://img.shields.io/badge/typescript-%233178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) 9 | [![NodeJS](https://img.shields.io/badge/Nodejs-%23339933?style=for-the-badge&logo=node.js&logoColor=white)](https://nodejs.org/) 10 | [![Express.js](https://img.shields.io/badge/expressjs-%23D6AC32?style=for-the-badge&logo=javascript&logoColor=white)](https://expressjs.com/) 11 | [![React](https://img.shields.io/badge/react-%234E9FF9?style=for-the-badge&logo=react&logoColor=white)](https://reactjs.org/) 12 | [![Sass](https://img.shields.io/badge/Sass-CC6699?style=for-the-badge&logo=sass&logoColor=white)](https://sass-lang.com/) 13 | [![CSS3](https://img.shields.io/badge/CSS3-1572B6?style=for-the-badge&logo=css3&logoColor=white)](https://developer.mozilla.org/en-US/docs/Web/CSS) 14 | [![HTML5](https://img.shields.io/badge/HTML5-E34F26?style=for-the-badge&logo=html5&logoColor=white)](https://developer.mozilla.org/en-US/docs/Web/HTML) 15 | [![Webpack](https://img.shields.io/badge/webpack-%236DB4F2?style=for-the-badge&logo=webpack&logoColor=white)](https://webpack.js.org/) 16 | [![Jest](https://img.shields.io/badge/jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white)](https://jestjs.io/) 17 | [![Cypress](https://img.shields.io/badge/-cypress-058a5e?style=for-the-badge&logo=cypress&logoColor=white)](https://www.cypress.io/) 18 | [![Docker](https://img.shields.io/badge/docker-%232496ED?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/) 19 | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326CE5?style=for-the-badge&logo=kubernetes&logoColor=white)](https://kubernetes.io/) 20 | [![Prometheus](https://img.shields.io/badge/prometheus-%23E6522C?style=for-the-badge&logo=prometheus&logoColor=white)](https://prometheus.io/) 21 | [![Grafana](https://img.shields.io/badge/grafana-%23F46800?style=for-the-badge&logo=grafana&logoColor=white)](https://grafana.com/) 22 | [![ChartJS](https://img.shields.io/badge/chart.js-%23FF6384?style=for-the-badge&logo=chart.js&logoColor=white)](https://www.chartjs.org/) 23 | [![Helm](https://img.shields.io/badge/helm-090E6F?style=for-the-badge&logo=helm&logoColor=white)](https://helm.sh/) 24 | [![GKE](https://img.shields.io/badge/GKE-%234285F4?style=for-the-badge&logo=googlecloud&logoColor=white)](https://cloud.google.com/kubernetes-engine) 25 | [![AKS](https://img.shields.io/badge/AKS-326CE5?style=for-the-badge&logo=microsoft-azure&logoColor=white)](https://azure.microsoft.com/en-us/services/kubernetes-service/) 26 | [![EKS](https://img.shields.io/badge/EKS-232F3E?style=for-the-badge&logo=amazon-aws&logoColor=white)](https://aws.amazon.com/eks/) 27 | 36 | 37 | 38 | --- 39 | 40 |

41 | Quick Links 42 |

43 |

44 |

45 | Website 46 | Medium 47 | 48 | LinkedIn 49 |

50 |

51 | 52 |
53 | 54 | ## KuberNautical 55 | Introducing KuberNautical, an open-source Kuberenetes developer tool designed to empower you with unparalleled insights and control over your Kubernetes clusters. Seamlessly merging the worlds of metrics analysis and streamlined cluster management, KuberNautical redefines the way you interact with your kubernetes infrastructure. 56 | 57 | [Getting Started](#set-up) 58 | ## Features 59 | ### 2D Cluster view 60 | Upon application launch, users can view a robust 2D configuration of their desired cluster. 61 | 62 | ![ClusterViewGif](https://github.com/NotHogue/GifStorage/blob/2c3d50eebdc0634be4815c5189e3a082d4beab53/gifs/ClusterView.gif?raw=true) 63 | 64 | ### Metrics Visualization 65 | Users are able to view important metrics and logs pertinent to cluster health. 66 | 67 | ![MetricsGif](https://github.com/NotHogue/GifStorage/blob/2c3d50eebdc0634be4815c5189e3a082d4beab53/gifs/Metrics.gif?raw=true) 68 | 69 | ### Cluster Logs 70 | Users are able to view logs regarding events occuring within cluster. These logs can be filtered by namespace and pod. 71 | 72 | ![LogsGif](https://github.com/NotHogue/GifStorage/blob/2c3d50eebdc0634be4815c5189e3a082d4beab53/gifs/Logs.gif?raw=true) 73 | 74 | ### Cluster Manipulation 75 | Users have the ability to make live changes to thier cluster in a variety of ways. 76 | 77 | ![NSCreateGif](https://github.com/NotHogue/GifStorage/blob/2c3d50eebdc0634be4815c5189e3a082d4beab53/gifs/NsCreate.gif?raw=true) 78 | Users can create a new namespace within the current cluster context through the "Edit Cluster" Modal. 79 | 80 | ![MakeDepGif](https://github.com/NotHogue/GifStorage/blob/2c3d50eebdc0634be4815c5189e3a082d4beab53/gifs/MakeDep.gif?raw=true) 81 | Users can create a new deployment within a given namespace using a public docker image. 82 | 83 | ![ScaleDepGif](https://github.com/NotHogue/GifStorage/blob/2c3d50eebdc0634be4815c5189e3a082d4beab53/gifs/ScaleDep.gif?raw=true) 84 | Users can scale deployments as needed to meet demand. 85 | 86 | ![ExposeDepGif](https://github.com/NotHogue/GifStorage/blob/2c3d50eebdc0634be4815c5189e3a082d4beab53/gifs/ExposeDep.gif?raw=true) 87 | Users can expose deployments within any chosen method, at the given ports. 88 | 89 | ![DeleteNsGif](https://github.com/NotHogue/GifStorage/blob/2c3d50eebdc0634be4815c5189e3a082d4beab53/gifs/DeleteNs.gif?raw=true) 90 | Users can remove a namespace and all resources inside of it. 91 | 92 | ### Load Testing 93 | Users are able to apply a load test to a deployed application of their choosing. 94 | ![LoadTestGif](https://github.com/NotHogue/GifStorage/blob/6c5fc3338eb80e9ea52707e442a7847343962b5b/gifs/LoadTest.gif?raw=true) 95 | Load Test Result 96 | ![LoadTestAfter](https://github.com/NotHogue/GifStorage/blob/fbf54ef3eaa1928acf69aab11e5af468ee745a55/gifs/LoadTestStill.png?raw=true) 97 | ## Set Up 98 | 1. Fork this repository and clone it onto your local machine: 99 | ``` 100 | git clone https://github.com/oslabs-beta/Kubernautical 101 | ``` 102 | 2. Install all package dependencies 103 | ``` 104 | npm install 105 | ``` 106 | ### Before you start checklist 107 | - [Cluster](#cluster-setup) from provider of choice 108 | - [Kubectl](https://kubernetes.io/docs/tasks/tools/) installed 109 | - [Kubectl Cheat Sheet](https://kubernetes.io/docs/reference/kubectl/cheatsheet/) 110 | - [Helm](https://helm.sh/docs/intro/install/) installed 111 | - SDK for your cluster(see specific instructions for your OS) 112 | - [GCP](https://cloud.google.com/sdk/docs/install) CLI 113 | - [Azure](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli-linux?pivots=apt) CLI 114 | - [AWS](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) CLI 115 | - [Prometheus](#prometheus) deployed 116 | - [Grafana Loki](#grafana-loki) deployed 117 | - [Grafana k6](https://k6.io/docs/get-started/installation/) installed 118 | 119 | ## Cluster Setup 120 | 121 | **Google Cluster** 122 | 1. Create a standard cluster with [Google](https://cloud.google.com/kubernetes-engine) (autopilot clusters will not work). 123 | 2. Click Connect in GCP and copy the command into your terminal or fill out the command below with pertinent information: 124 | ``` 125 | gcloud container clusters get-credentials \ 126 | --zone --project 127 | ``` 128 | **Microsoft Cluster** 129 | 1. Create a cluster with [Microsoft](https://azure.microsoft.com/en-us/products/kubernetes-service). 130 | 2. Click Connect in Azure and copy the command into your terminal or fill out the command below with pertinent information: 131 | ``` 132 | az aks get-credentials --resource-group \ 133 | --name --admin 134 | ``` 135 | **Amazon Cluster** 136 | 1. Create a cluster with [AWS](https://docs.aws.amazon.com/eks/latest/userguide/getting-started-console.htm). 137 | 2. Fill out the command below with pertinent information: 138 | ``` 139 | aws eks update-kubeconfig --region --name 140 | ``` 141 | **Minikube Cluster** 142 | 1. [Install Docker Desktop](https://www.docker.com/products/docker-desktop/). 143 | **We Reccommend you have 4g+ of free ram as Docker Desktop can overload and crash your machine.** 144 | 2. Install [Minikube](https://minikube.sigs.k8s.io/docs/start). 145 | 3. Create a cluster 146 | ``` 147 | minikube start 148 | ``` 149 | **Minikube Teardown** 150 | 1. Stop your cluster 151 | ``` 152 | minikube stop 153 | ``` 154 | 2. Delete your cluster (can also use '--all' flag to delete all) 155 | ``` 156 | minikube delete 157 | ``` 158 | ## Once your cluster is created 159 | 160 | 161 | ***Confirm that your cluster is in your kube config*** 162 | ``` 163 | kubectl config view 164 | ``` 165 | ***Confirm context is set to desired cluster*** 166 | ``` 167 | kubectl config use-context 168 | ``` 169 | ## Prometheus 170 | 1. Create a namespace for prometheus 171 | ``` 172 | kubectl create ns prometheus 173 | ``` 174 | 2. Install the helm charts 175 | ``` 176 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 177 | ``` 178 | 3. Apply the helm charts and install prometheus in the namespace 179 | ``` 180 | helm upgrade -i prometheus prometheus-community/prometheus --namespace prometheus 181 | ``` 182 | 4. For our application you need to expose the prometheus-server as a load balancer 183 | ``` 184 | kubectl expose deployment prometheus-server --port=80 --target-port=9090 --type=LoadBalancer \ 185 | --name prometheus-server-lb --n prometheus 186 | ``` 187 | 188 | **Deployment, target port, type and namespace are all mandatory as above, while port, and name can be customized if desired but name MUST start with `prometheus-server`** 189 | 190 | 191 | ## Grafana-loki 192 | 1. Use helm to add Grafana 193 | ``` 194 | helm repo add grafana https://grafana.github.io/helm-charts 195 | ``` 196 | 2. Update helm if necessary 197 | ``` 198 | helm repo update 199 | ``` 200 | 3. Check if helm charts installed 201 | ``` 202 | helm search repo loki 203 | ``` 204 | 4. Create a namespace for Loki 205 | ``` 206 | kubectl create ns loki 207 | ``` 208 | 5. Install Loki on the `loki` namespace 209 | ``` 210 | helm upgrade --install --namespace loki logging grafana/loki -f yamls/values.yml \ 211 | --set loki auth_enabled=false 212 | ``` 213 | 6. Deploy Grafana with Loki data source to enable log aggregation 214 | ``` 215 | helm upgrade --install --namespace=loki loki-grafana grafana/grafana 216 | ``` 217 | 7. Deploy Promtail to enable log scraping from whole cluster 218 | ``` 219 | helm upgrade --install promtail grafana/promtail -n loki 220 | ``` 221 | 8. Create a daemonset to ensure system-level logging 222 | ``` 223 | kubectl apply -f yamls/Daemonset.yaml 224 | ``` 225 | 9. Expose `loki-gateway` as a load balancer 226 | ``` 227 | kubectl expose deployment loki-gateway --port=80 --target-port=8080 --type=LoadBalancer \ 228 | --name loki-gateway-lb -n loki 229 | ``` 230 | 231 | ***Naming conventions for namespaces and services created are customizable, but name for `loki-gateway` service must start with `loki-gateway`.*** 232 | 233 | # Contributing 234 | Contributions play a vital role in the open-source community. Any contributions are greatly appreciated! 235 | 236 | - Fork the project. 237 | - Create and work off of your feature branch. 238 | - Create a pull request with detailed description of your changes from your feature branch to dev branch. 239 | - Inform us upon PR submission. Once the changes are reviewed and approved, we will merge your code into the main repository. 240 | 241 | # Meet the Team 242 | | Jeremiah Hogue | Anthony Vuong | Stephen Acosta | Michael Van | 243 | | ------------- | ------------- |------------- | ------------- | 244 | | Jeremiah Hogue
[](https://github.com/NotHogue) [](https://www.linkedin.com/in/jeremiah-hogue/)| Anthony Vuong
[](https://github.com/AnthonyKTVuong) [](https://www.linkedin.com/in/anthonyktvuong/) | Stephen Acosta
[](https://github.com/STAC98) [](https://www.linkedin.com/in/staclb) | Michael Van
[](https://github.com/michaelvan996) [](https://www.linkedin.com/in/michael-van-901533222/) | 245 | -------------------------------------------------------------------------------- /client/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactElement, useState } from 'react' 2 | import { type globalServiceObj, type ClusterData } from '../types/types' 3 | import './sass/App.scss' 4 | import MainContainer from './containers/MainContainer' 5 | import Navbar from './containers/Navbar' 6 | import { GlobalContext } from './components/Contexts' 7 | 8 | const serviceArr: globalServiceObj[] = [] 9 | const defaultClusterData: ClusterData = {} 10 | function App (): ReactElement { 11 | const [globalServices, setGlobalServices] = useState(serviceArr) 12 | const [globalTimer, setGlobalTimer] = useState(0) 13 | const [globalServiceTest, setGlobalServiceTest] = useState('') 14 | const [globalClusterContext, setGlobalClusterContext] = useState('') 15 | const [showEditModal, setShowEditModal] = useState(false) 16 | const [globalCrudChange, setGlobalCrudChange] = useState(false) 17 | const [ongoingCrudChange, setOngoingCrudChange] = useState(false) 18 | const [globalClusterData, setGlobalClusterData] = useState(defaultClusterData) 19 | return ( 20 | <> 21 |
22 | 42 | 43 | 44 | 45 |
46 |
@2023 Kubernautical™ | All Rights Reserved
47 | 48 | ) 49 | } 50 | 51 | export default App 52 | -------------------------------------------------------------------------------- /client/assets/dep-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/dep-icon.png -------------------------------------------------------------------------------- /client/assets/gif2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/gif2.gif -------------------------------------------------------------------------------- /client/assets/gif3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/gif3.gif -------------------------------------------------------------------------------- /client/assets/images/KN-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/KN-logo.png -------------------------------------------------------------------------------- /client/assets/images/anthony.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/anthony.png -------------------------------------------------------------------------------- /client/assets/images/clusterPic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/clusterPic.png -------------------------------------------------------------------------------- /client/assets/images/docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/docs.png -------------------------------------------------------------------------------- /client/assets/images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/edit.png -------------------------------------------------------------------------------- /client/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/favicon.ico -------------------------------------------------------------------------------- /client/assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/github.png -------------------------------------------------------------------------------- /client/assets/images/jeremiah.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/jeremiah.png -------------------------------------------------------------------------------- /client/assets/images/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/linkedin.png -------------------------------------------------------------------------------- /client/assets/images/mainDashBoard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/mainDashBoard.png -------------------------------------------------------------------------------- /client/assets/images/michaelvan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/michaelvan.png -------------------------------------------------------------------------------- /client/assets/images/network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/network.png -------------------------------------------------------------------------------- /client/assets/images/stephen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/images/stephen.jpg -------------------------------------------------------------------------------- /client/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/logo.png -------------------------------------------------------------------------------- /client/assets/name_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/name_blue.png -------------------------------------------------------------------------------- /client/assets/node-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/node-icon.png -------------------------------------------------------------------------------- /client/assets/ns-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/ns-icon.png -------------------------------------------------------------------------------- /client/assets/pod-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/pod-icon.png -------------------------------------------------------------------------------- /client/assets/svc-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kubernautical/4a0394179cf3ed464344b1fe048bb828000f8c2e/client/assets/svc-icon.png -------------------------------------------------------------------------------- /client/components/CRUD/CRUDMini.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | import React, { useState, useContext, type ReactElement } from 'react' 3 | import { v4 as uuidv4 } from 'uuid' 4 | import { type MiniProps } from '../../../types/types' 5 | import { GlobalContext } from '../Contexts' 6 | 7 | function CRUDMini ( 8 | { 9 | style, 10 | setShowModal, 11 | modalType, 12 | crudSelection, 13 | ns, 14 | service, 15 | deployment 16 | }: MiniProps 17 | ): ReactElement { 18 | const { 19 | globalClusterData, 20 | setShowEditModal, globalClusterContext, 21 | setGlobalCrudChange, globalCrudChange, 22 | setOngoingCrudChange 23 | } = useContext(GlobalContext) 24 | const [form, setForm] = useState('') 25 | const [form2, setForm2] = useState('') 26 | const [exposeType, setExposeType] = useState('') 27 | const [targetPort, setTargetPort] = useState(0) 28 | const [port, setPort] = useState(0) 29 | // dynamically set inner text for modal 30 | let name 31 | if (crudSelection === 'service') name = service 32 | if (crudSelection === 'deployment') name = deployment 33 | if (crudSelection === 'namespace') name = ns 34 | let innerText 35 | if (modalType === 'create') innerText = `Enter ${crudSelection} here` 36 | if (modalType === 'delete') innerText = name === undefined ? 'Please make a selection' : `Are you sure you want to remove ${name}?` 37 | // completely different text for some selections 38 | if (crudSelection === 'deployment' && modalType === 'edit') innerText = 'Scale Deployment' 39 | // dynamically find the obj selected for changes 40 | const obj = (globalClusterData !== undefined) ? globalClusterData[`${crudSelection}s`].find(({ name }: any) => name === (crudSelection === 'service' ? service : deployment)) : null 41 | // assign previous variable for curent scale of deployment 42 | const oldReplicas = obj?.availableReplicas ?? 0 43 | const [scale, setScale] = useState(oldReplicas) 44 | 45 | const crudFunction = async (): Promise => { 46 | try { 47 | let query = 'api/crud/' 48 | let type = '' 49 | if (modalType === 'create' && form === '') { alert('Please fill out all fields'); return } 50 | // build query string for back-end operations based on user input 51 | switch (crudSelection) { 52 | case 'namespace': 53 | query += `ns?namespace=${modalType === 'create' ? form : ns}&crud=${modalType}&context=${globalClusterContext ?? ''}` 54 | break 55 | case 'deployment': 56 | if (modalType === 'edit') { 57 | if (scale !== oldReplicas) type = 'scale' 58 | if (form2 !== '' && exposeType !== '') type = 'expose' 59 | } else { type = modalType } 60 | query += `dep?namespace=${ns}&crud=${type}&${type === 'expose' ? `name=${form2}` : `image=${form2}`} 61 | &replicas=${scale as string}&deployment=${deployment !== '' ? deployment : ''}&old=${oldReplicas as string} 62 | &context=${globalClusterContext ?? ''}&port=${port}&targetPort=${targetPort}&type=${exposeType}` 63 | break 64 | case 'service': 65 | query += `svc?namespace=${ns}&crud=${modalType}&service=${service}&context=${globalClusterContext ?? ''} 66 | &port=${port}&targetPort=${targetPort}` 67 | break 68 | default: 69 | break 70 | } 71 | const response = await fetch(query) 72 | if (setGlobalCrudChange !== undefined) { 73 | globalCrudChange === true ? setGlobalCrudChange(false) : setGlobalCrudChange(true) 74 | } 75 | if (setOngoingCrudChange !== undefined) setOngoingCrudChange(false) 76 | if (!response.ok) throw new Error() 77 | } catch (error) { 78 | console.log(error) 79 | } 80 | } 81 | 82 | return ( 83 | <> 84 |
87 |
91 |
94 | {innerText} 95 | 96 |
97 | {modalType === 'create' && ( 98 | <> 99 | { setForm(e.target.value) }} 104 | required 105 | /> 106 |
107 | {crudSelection !== 'namespace' && ( 108 | { setForm2(e.target.value) }} 114 | required 115 | /> 116 | )} 117 |
118 | 119 | )} 120 | {(modalType === 'edit' && crudSelection === 'deployment') && ( 121 | <> 122 | { setScale(Number(e.target.value)) }} 128 | /> 129 |
132 | Expose as Service 133 | 134 |
135 | { setForm2(e.target.value) }} 141 | /> 142 | 176 |
179 |
182 | Port: 183 | {' '} 184 | 185 |
186 | { setPort(Number(e.target.value)) }} 191 | /> 192 |
193 |
196 |
199 | Target Port: 200 | {' '} 201 | 202 |
203 | { setTargetPort(Number(e.target.value)) }} 208 | /> 209 |
210 | 211 | )} 212 | 224 | 232 |
233 | 234 | ) 235 | } 236 | export default CRUDMini 237 | -------------------------------------------------------------------------------- /client/components/CRUD/CRUDModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, type ReactElement } from 'react' 2 | import CrudSelector from './CRUDSelector' 3 | import CRUDMini from './CRUDMini' 4 | 5 | function CRUDModal (): ReactElement { 6 | const [ns, setNs] = useState('') 7 | const [service, setService] = useState('') 8 | const [deployment, setDeployment] = useState('') 9 | const [showModal, setShowModal] = useState(false) 10 | const [crudSelection, setCrudSelection] = useState('namespace') 11 | const [modalPos, setModalPos] = useState(0) 12 | const [modalType, setModalType] = useState('') 13 | 14 | return ( 15 |
16 | {showModal && 17 | ( 18 | 27 | )} 28 |
Cluster Editor
29 |
30 | 42 | {ns !== '' && 43 | ( 44 | 57 | )} 58 | {(crudSelection !== 'namespace' && ns !== '') && 59 | ( 60 | 73 | )} 74 |
75 | ) 76 | } 77 | export default CRUDModal 78 | -------------------------------------------------------------------------------- /client/components/CRUD/CRUDSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, type SyntheticEvent, type ReactElement } from 'react' 2 | import { v4 as uuidv4 } from 'uuid' 3 | import { GlobalContext } from '../Contexts' 4 | import { type CLusterObj, type SelectorProps } from '../../../types/types' 5 | import { capitalize } from '../helperFunctions' 6 | import edit from '../../assets/images/edit.png' 7 | 8 | function CrudSelector ( 9 | { 10 | type, 11 | state, 12 | stateSetter, 13 | showModal, 14 | setShowModal, 15 | modalPos, 16 | setModalPos, 17 | modalType, 18 | setModalType, 19 | crudSelection, 20 | setCrudSelection, 21 | ns 22 | }: SelectorProps 23 | ): ReactElement { 24 | const { globalClusterData } = useContext(GlobalContext) 25 | 26 | const openModal = (e: SyntheticEvent): void => { 27 | setModalPos(e.currentTarget.getBoundingClientRect().bottom + 5) 28 | showModal ? setShowModal(false) : setShowModal(true) 29 | } 30 | if (type === 'scope') { 31 | return ( 32 | <> 33 |
36 | Select Scope 37 | 38 |
39 |
42 | 69 |
70 | 71 | ) 72 | } 73 | return ( 74 | <> 75 |
76 | Edit 77 | {' '} 78 | {capitalize(type)} 79 | s 80 |
81 |
82 | 107 | 115 | 123 | 131 |
132 | 133 | ) 134 | } 135 | 136 | export default CrudSelector 137 | -------------------------------------------------------------------------------- /client/components/Contexts.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, type Dispatch, type SetStateAction } from 'react' 2 | import { type globalServiceObj, type ClusterData } from '../../types/types' 3 | 4 | export interface GlobalContextInterace { 5 | globalServices?: globalServiceObj[] 6 | setGlobalServices?: Dispatch> 7 | 8 | globalTimer?: number 9 | setGlobalTimer?: Dispatch> 10 | 11 | globalServiceTest?: string 12 | setGlobalServiceTest?: Dispatch> 13 | 14 | showEditModal?: boolean 15 | setShowEditModal?: Dispatch> 16 | 17 | globalClusterData?: ClusterData 18 | setGlobalClusterData?: Dispatch> 19 | 20 | globalCrudChange?: boolean 21 | setGlobalCrudChange?: Dispatch> 22 | 23 | ongoingCrudChange?: boolean 24 | setOngoingCrudChange?: Dispatch> 25 | 26 | globalClusterContext?: string 27 | setGlobalClusterContext?: Dispatch> 28 | } 29 | 30 | export const GlobalContext = createContext({}) 31 | -------------------------------------------------------------------------------- /client/components/Graphs/BarGraph.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React, { type ReactElement } from 'react' 3 | import { Bar } from 'react-chartjs-2' 4 | import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from 'chart.js' 5 | 6 | ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend) 7 | 8 | function BarGraph (): ReactElement { 9 | const labels = [1, 2, 3, 4, 5, 6, 7] // x Axis values 10 | const data = { 11 | labels, 12 | datasets: [{ 13 | label: 'My First Dataset', 14 | data: [65, 59, 80, 81, 56, 55, 40], // y Axis Values 15 | backgroundColor: [ 16 | 'rgba(255, 99, 132, 0.2)', // color inside bar 17 | 'rgba(255, 159, 64, 0.2)', 18 | 'rgba(255, 205, 86, 0.2)', 19 | 'rgba(75, 192, 192, 0.2)', 20 | 'rgba(54, 162, 235, 0.2)', 21 | 'rgba(153, 102, 255, 0.2)', 22 | 'rgba(201, 203, 207, 0.2)' 23 | ], 24 | borderColor: [ 25 | 'rgb(255, 99, 132)', // color border 26 | 'rgb(255, 159, 64)', 27 | 'rgb(255, 205, 86)', 28 | 'rgb(75, 192, 192)', 29 | 'rgb(54, 162, 235)', 30 | 'rgb(153, 102, 255)', 31 | 'rgb(201, 203, 207)' 32 | ], 33 | borderWidth: 1 34 | }] 35 | } 36 | 37 | return ( 38 | 39 | ) 40 | } 41 | 42 | export default BarGraph 43 | -------------------------------------------------------------------------------- /client/components/Graphs/GaugeChart.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React, { useState, useEffect, useContext, type ReactElement } from 'react' 3 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js' 4 | import { Doughnut } from 'react-chartjs-2' 5 | import { humanReadable } from '../helperFunctions' 6 | import type { GaugeGraphProps, globalServiceObj } from '../../../types/types' 7 | import { GlobalContext } from '../Contexts' 8 | 9 | ChartJS.register(ArcElement, Tooltip, Legend) 10 | 11 | const numberArr: number[] = [] 12 | const stringArr: string[] = [] 13 | function GaugeChart ( 14 | { 15 | type, 16 | borderColor, 17 | backgroundColor, 18 | title, 19 | graphTextColor 20 | }: GaugeGraphProps 21 | ): ReactElement { 22 | const { globalServices } = useContext(GlobalContext) 23 | const [guageData, setGuageData] = useState(numberArr) 24 | const [guageName, setGuageName] = useState(stringArr) 25 | let svcArr: globalServiceObj[] | undefined 26 | 27 | const getData = async (): Promise => { 28 | try { 29 | const localSvc = localStorage.getItem('serviceArr') 30 | if (localSvc !== null) svcArr = await JSON.parse(localSvc) 31 | else svcArr = globalServices 32 | const ep = svcArr?.find(({ name }) => name.slice(0, 17) === 'prometheus-server')?.ip ?? '' 33 | const URL = `/api/prom/${type === 'mem' ? 'mem' : 'cpu'}?type=${type}&hour=24¬Time=true&ep=${ep}` 34 | const response = await fetch(URL) 35 | const data = await response.json() 36 | if (data[0] === undefined) { setGuageData([0]); return } 37 | const dataArr: number[] = [] 38 | const dataNames: string[] = [] 39 | data.forEach((el: any) => { 40 | const key = Object.keys(el)[0] 41 | dataArr.push(el[key]) 42 | dataNames.push(humanReadable(key)) 43 | }) 44 | setGuageData(dataArr) 45 | setGuageName(dataNames) 46 | } catch (error) { 47 | console.log('Error fetching data:', error) 48 | } 49 | } 50 | useEffect(() => { 51 | void getData() 52 | }, []) 53 | 54 | const data = { 55 | labels: guageName, 56 | datasets: [{ 57 | label: 'Percentage', 58 | data: guageData, 59 | backgroundColor, 60 | borderColor, 61 | circumference: 180, 62 | rotation: 270, 63 | cutout: '60%' 64 | }] 65 | } 66 | 67 | const options = { 68 | // aspectRatioL:2, 69 | responsive: true, 70 | maintainAspectRatio: false, 71 | plugins: { 72 | legend: { 73 | labels: { 74 | color: graphTextColor 75 | }, 76 | display: true 77 | }, 78 | title: { 79 | color: graphTextColor, 80 | display: true, 81 | text: title 82 | } 83 | } 84 | } 85 | 86 | return ( 87 |
88 | 89 |
90 | ) 91 | } 92 | 93 | export default GaugeChart 94 | -------------------------------------------------------------------------------- /client/components/Graphs/LineGraph.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React, { useState, useEffect, useContext, type ReactElement } from 'react' 3 | import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, type ChartOptions, Filler } from 'chart.js' 4 | import { Line } from 'react-chartjs-2' 5 | import { v4 as uuidv4 } from 'uuid' 6 | import type { CLusterObj, LineGraphProps, globalServiceObj } from '../../../types/types' 7 | import { GlobalContext } from '../Contexts' 8 | 9 | ChartJS.register( 10 | CategoryScale, 11 | LinearScale, 12 | PointElement, 13 | LineElement, 14 | Title, 15 | Tooltip, 16 | Legend, 17 | Filler 18 | ) 19 | const defaultArr: number[] = [] 20 | const stringArr: string[] = [] 21 | 22 | function LineGraph ( 23 | { 24 | type, 25 | title, 26 | yAxisTitle, 27 | color, 28 | graphTextColor 29 | }: LineGraphProps 30 | ): ReactElement { 31 | const { globalClusterData, globalServices } = useContext(GlobalContext) 32 | const [data, setData] = useState(defaultArr) 33 | const [label, setLabel] = useState(stringArr) 34 | const [hourSelection, setHourSelection] = useState('24') 35 | const [scope, setScope] = useState('') 36 | const [scopeType, setScopeType] = useState('') 37 | 38 | const getData = async (): Promise => { 39 | const time: number[] = [] 40 | const specificData: number[] = [] 41 | const gigaBytes: number[] = [] 42 | const kiloBytes: number[] = [] 43 | let svcArr: globalServiceObj[] | undefined 44 | 45 | try { 46 | const localSvc = localStorage.getItem('serviceArr') 47 | if (localSvc !== null) svcArr = await JSON.parse(localSvc) 48 | else svcArr = globalServices 49 | const ep = svcArr?.find(({ name }) => name.slice(0, 17) === 'prometheus-server')?.ip ?? '' 50 | const response = await fetch(`/api/prom/metrics?ep=${ep}&type=${type}&hour=${hourSelection}${scope !== '' ? `&scope=${scopeType}&name=${scope}` : ''}`, { 51 | method: 'GET', 52 | headers: { 53 | 'Content-Type': 'application/json' 54 | } 55 | }) 56 | const newData = await response.json() 57 | if (newData[0] === undefined) { 58 | setData([]) 59 | return 60 | } 61 | newData[0].values.forEach((el: [number, string]) => { 62 | time.push(el[0]) 63 | specificData.push(Number(el[1])) 64 | }) 65 | // convert bytes into gigabytes if asking for mem 66 | if (type === 'mem') { 67 | specificData.forEach((el: any) => { 68 | const newEl = el / 1073741824 69 | gigaBytes.push(newEl) 70 | setData(gigaBytes) 71 | }) 72 | } else if (type === 'trans' || type === 'rec') { // convert bytes into kilobytes if asking for trans or rec 73 | specificData.forEach((el: any) => { 74 | const newEl = el / 1000 75 | kiloBytes.push(newEl) 76 | setData(kiloBytes) 77 | }) 78 | } else { 79 | setData(specificData) 80 | } 81 | // set Unix time to Hours and Minutes 82 | const timeLabels: string[] = [] 83 | time.forEach((el: any) => { 84 | const date: Date = new Date(1000 * el) // Convert seconds to milliseconds for Date Function 85 | const formattedTime = `${date.getHours()}:${date.getMinutes()}` 86 | timeLabels.push(formattedTime) 87 | setLabel(timeLabels) 88 | }) 89 | } catch (error) { 90 | console.log('Error fetching data:', error) 91 | } 92 | } 93 | 94 | // ? add needed watchers to useEffect 95 | useEffect(() => { 96 | void getData() 97 | }, [hourSelection, scope]) 98 | 99 | const dataSet = { // Data for tables 100 | labels: label, // set data for X Axis 101 | datasets: [{ 102 | label: title, 103 | data, // set data for Y Axis 104 | fill: true, 105 | // backgroundColor: color, //looks better without fill 106 | borderColor: color, 107 | pointBorderColor: color, 108 | tension: 0.5, 109 | pointBackgroundColor: 'white', 110 | pointBorderWidth: 1, 111 | pointHoverRadius: 4, 112 | pointRadius: 1, 113 | borderWidth: 1.5 114 | }] 115 | } 116 | const options: ChartOptions<'line'> = { 117 | animations: { 118 | tension: { 119 | duration: 1000, 120 | easing: 'linear', 121 | from: 0.1, 122 | to: 0, 123 | loop: true 124 | } 125 | }, 126 | plugins: { 127 | legend: { 128 | labels: { 129 | color: graphTextColor 130 | }, 131 | display: true 132 | 133 | } 134 | }, 135 | responsive: true, 136 | scales: { 137 | y: { 138 | ticks: { 139 | color: graphTextColor 140 | }, 141 | grid: { 142 | display: true, 143 | color: 'rgba(128, 128, 128, 0.1)' 144 | }, 145 | display: true, 146 | title: { 147 | display: true, 148 | text: yAxisTitle, 149 | color: graphTextColor 150 | } 151 | }, 152 | x: { 153 | ticks: { 154 | color: graphTextColor 155 | }, 156 | grid: { 157 | display: false, 158 | color: 'rgba(128, 128, 128, 0.1)' 159 | }, 160 | display: true, 161 | title: { 162 | display: true, 163 | text: `Time(${hourSelection}hrs)`, 164 | color: 'rgba(255, 255, 255, 0.702)' 165 | } 166 | } 167 | } 168 | } 169 | return ( 170 |
171 |
172 | 178 | 187 |
188 | 189 |
190 | ) 191 | } 192 | 193 | export default LineGraph 194 | -------------------------------------------------------------------------------- /client/components/Logs.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 3 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 4 | import React, { useState, useEffect, useContext, type ReactElement } from 'react' 5 | import { v4 as uuidv4 } from 'uuid' 6 | import type { LogProps, LogEntry, globalServiceObj } from '../../types/types' 7 | import { GlobalContext } from './Contexts' 8 | 9 | function Logs ({ namespace, logType, pod }: LogProps): ReactElement { 10 | const [data, setData] = useState([]) 11 | const [expandedLog, setExpandedLog] = useState(null) 12 | const { globalServices } = useContext(GlobalContext) 13 | let svcArr: globalServiceObj[] | undefined 14 | 15 | const getLogs = async (): Promise => { 16 | try { 17 | const localSvc = localStorage.getItem('serviceArr') 18 | if (localSvc !== null) svcArr = await JSON.parse(localSvc) 19 | else svcArr = globalServices 20 | const ep = svcArr?.find(({ name }) => name.slice(0, 12) === 'loki-gateway')?.ip 21 | const url = `/api/loki/logs?namespace=${namespace}&log=${logType}&pod=${pod}&ep=${ep ?? ''}` 22 | const response = await fetch(url) 23 | const newData = (await response.json()).data.result 24 | 25 | setData(newData) 26 | } catch (error) { 27 | console.log('Error fetching logs:', error) 28 | } 29 | } 30 | const handleLogClick = (log: LogEntry): void => { 31 | expandedLog === log ? setExpandedLog(null) : setExpandedLog(log) 32 | } 33 | useEffect(() => { 34 | void getLogs() 35 | }, [namespace, logType, pod]) 36 | return ( 37 |
38 | {data?.map((object: any, log) => { 39 | const { stream, values } = object 40 | const { namespace, container, job } = stream 41 | return ( 42 |
43 | {values.map((value: any) => { 44 | const i: number = value[1].indexOf('ts=') 45 | const t = value[1].substring(i + 3, i + 24) 46 | return ( 47 |
48 |
{ handleLogClick(value[0]) }}> 49 |
50 | Container: 51 | {' '} 52 | {container} 53 |
54 |
55 | Log: 56 | {' '} 57 | {value[1]} 58 |
59 |
60 | {expandedLog === value[0] && ( 61 |
62 |

63 | Time: 64 | {' '} 65 | {t} 66 |

67 |

68 | Namespace: 69 | {' '} 70 | {namespace} 71 |

72 |

73 | Container: 74 | {' '} 75 | {container} 76 |

77 |

78 | Job: 79 | {' '} 80 | {job} 81 |

82 |
83 | )} 84 |
85 | ) 86 | })} 87 |
88 | ) 89 | })} 90 |
91 | ) 92 | } 93 | 94 | export default Logs 95 | -------------------------------------------------------------------------------- /client/components/Mapothy.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable no-shadow */ 3 | import React, { useEffect, useState, useContext, type ReactElement } from 'react' 4 | import Graph from 'react-graph-vis' 5 | import { v4 as uuidv4 } from 'uuid' 6 | import { GlobalContext, type GlobalContextInterace } from './Contexts' 7 | import { type ClusterNode, type ClusterEdge, type clusterGraphData, type Props, type CLusterObj, type ContextObj, type globalServiceObj } from '../../types/types' 8 | import { makeModal, windowHelper } from './helperFunctions' 9 | import nsImg from '../assets/ns-icon.png' 10 | import podImg from '../assets/pod-icon.png' 11 | import svcImg from '../assets/svc-icon.png' 12 | import depImg from '../assets/dep-icon.png' 13 | import logoImg from '../assets/images/KN-logo.png' 14 | 15 | // ?-----------------------------------------Physics Testing---------------------------------------> 16 | const options = { 17 | layout: { 18 | // randomSeed: '0.07224874827053274:1691128352960', 19 | // randomSeed: '0.13999053405779072:1691128555260' 20 | // randomSeed: '0.26923438127640864:1691128645444' 21 | randomSeed: '0.00836184154624342:1691197042873' // current god seed 22 | // randomSeed: '0.19873095642451144:1691197919546' //current demi god seed 23 | }, 24 | edges: { 25 | color: '#00FFFF' 26 | }, 27 | interaction: { 28 | hover: true 29 | }, 30 | autoResize: true, 31 | physics: { 32 | barnesHut: { 33 | gravitationalConstant: -1000, 34 | centralGravity: 0, 35 | springLength: 150, 36 | springConstant: 0.003, 37 | damping: 0.09, 38 | avoidOverlap: 0.2 39 | } 40 | } 41 | } 42 | // ?-------------------------------------Map Component---------------------------------------------> 43 | function Mapothy ({ header }: Props): ReactElement { 44 | const { 45 | setGlobalServices, globalServices, 46 | setGlobalClusterData, globalClusterData, 47 | setGlobalCrudChange, globalCrudChange, 48 | globalClusterContext, setGlobalClusterContext 49 | } = useContext(GlobalContext) 50 | const [graph, setGraph] = useState({ 51 | nodes: [], 52 | edges: [] 53 | }) 54 | const [ns, setNs] = useState('Cluster') 55 | const [clusterContext, setClusterContext] = useState(globalClusterContext) 56 | const getData = async (): Promise => { 57 | try { 58 | // arrays used to store nodes and edges for use in react-graph-vis 59 | const nodesArr: ClusterNode[] = [] 60 | const edgesArr: ClusterEdge[] = [] 61 | const serviceArrTemp: globalServiceObj[] = [] 62 | // temporary arrray used for filtering cluster view by namespace 63 | let filteredNsArr: CLusterObj[] = [] 64 | // check to see if namespace array needs to be updated 65 | let newContext = false 66 | let data 67 | // conditionals to prevent an unnecessary fetching 68 | if (globalClusterData?.namespaces === undefined || 69 | globalCrudChange === true || 70 | globalClusterContext !== clusterContext) { 71 | // fetch made using the current context, checks used for base case and persistent context 72 | // TODO idk if this works anymore 73 | const result = await fetch(`api/map/elements?context=${clusterContext ?? ''}`) 74 | data = await result.json() 75 | // swithces flipped and state assigned 76 | // TODO all conditionals have been changed 77 | if (setGlobalCrudChange !== undefined) setGlobalCrudChange(false) 78 | if (setGlobalClusterData !== undefined) setGlobalClusterData(data) 79 | if (setGlobalClusterContext !== undefined && clusterContext !== undefined) { 80 | setGlobalClusterContext(clusterContext) 81 | } 82 | newContext = true 83 | } else { data = globalClusterData } 84 | const { pods, namespaces, deployments, services, currentContext } = data 85 | // base case for context 86 | if (globalClusterContext === '' && setGlobalClusterContext !== undefined) { 87 | setGlobalClusterContext(currentContext) 88 | } 89 | if (clusterContext === '') setClusterContext(currentContext) 90 | // center node assigned to current context, all namespaces will be connected to this node 91 | nodesArr.push({ id: '0', title: `${currentContext as string}`, size: 100, image: logoImg, shape: 'image' }) 92 | // checks to filter cluste view if a namespace has been selected 93 | if (ns === 'Cluster') filteredNsArr = namespaces 94 | else { filteredNsArr = [namespaces.find(({ name }: any) => name === ns)] } 95 | // ?----------------------------------Namespace Search-------------------------------------> 96 | // namespace array iterated over, with all other resources being attached accordingly 97 | filteredNsArr.forEach((nS: CLusterObj) => { 98 | // if (nS.name === 'loki') return 99 | const { name, uid } = nS 100 | const nsObj = { 101 | id: uid, 102 | name, 103 | title: makeModal(nS, 'Namespace'), // modals created using helper function 104 | size: 70, 105 | image: nsImg, 106 | shape: 'image' 107 | } 108 | // each node object is pushed to nodes array 109 | nodesArr.push(nsObj) 110 | // edges drawn from namespace to center (context) node 111 | edgesArr.push({ from: '0', to: uid, length: 400 }) 112 | // ?------------------------------Pod Search------------------------------------------> 113 | pods.forEach((pod: CLusterObj) => { 114 | const { namespace, uid } = pod 115 | if (namespace === nsObj.name) { 116 | const pObj = { 117 | id: uid, 118 | title: makeModal(pod, 'Pod'), 119 | size: 45, 120 | image: podImg, 121 | shape: 'image' 122 | } 123 | // same as above but edges are drawn from current node to corresponding namespace node 124 | nodesArr.push(pObj) 125 | edgesArr.push({ from: nsObj.id, to: uid, length: 200 }) 126 | } 127 | }) 128 | // ?------------------------------Services Search------------------------------------------> 129 | services.forEach((service: CLusterObj) => { 130 | const { name, namespace, uid, ingressIP, ports } = service 131 | if (namespace === nsObj.name) { 132 | const sObj = { 133 | id: uid, 134 | title: makeModal(service, 'Service'), 135 | size: 45, 136 | image: svcImg, 137 | shape: 'image' 138 | } 139 | // pertinent data regarding exposed ingress ports is extracted 140 | if (ingressIP !== undefined) serviceArrTemp.push({ name, ip: `${ingressIP as string}:${(ports != null) ? ports[0].port : ''}` }) 141 | nodesArr.push(sObj) 142 | edgesArr.push({ from: nsObj.id, to: uid, length: 300 }) 143 | } 144 | }) 145 | // ?------------------------------Deployments Search---------------------------------------> 146 | deployments.forEach((deployment: CLusterObj) => { 147 | const { namespace, uid } = deployment 148 | if (namespace === nsObj.name) { 149 | const dObj = { 150 | id: uid, 151 | title: makeModal(deployment, 'Deployment'), 152 | image: depImg, 153 | size: 45, 154 | shape: 'image' 155 | } 156 | nodesArr.push(dObj) 157 | edgesArr.push({ from: nsObj.id, to: uid, length: 150 }) 158 | } 159 | }) 160 | // ?------------------------------Ingress Search------------------------------------------> 161 | //! currently no ingresses 162 | // ingresses.forEach((ingress: ClusterObj) => { 163 | // const { name, namespace, uid } = ingress; 164 | // if (namespace === nsObj.name) { 165 | // const iObj = { 166 | // id: uid, 167 | // title: makeModal(ingress, 'Deployment'), 168 | // image: depImg, 169 | // size: 45, 170 | // shape: 'image', 171 | // } 172 | // nodesArr.push(iObj); 173 | // edgesArr.push({ from: nsObj.id, to: uid, length: 150 }); 174 | // } 175 | // }) 176 | }) 177 | if (globalServices?.length === 0 || newContext) { 178 | if (setGlobalServices !== undefined) setGlobalServices(serviceArrTemp) 179 | localStorage.setItem('serviceArr', JSON.stringify(serviceArrTemp)) 180 | } 181 | // graph created using node and edges array created above 182 | setGraph({ 183 | nodes: nodesArr, 184 | edges: edgesArr 185 | }) 186 | } catch (error) { 187 | console.log(error) 188 | } 189 | } 190 | // use effect controlling data fetch and map creation set to watch pertinent data changes 191 | useEffect(() => { 192 | void getData() 193 | }, [ns, clusterContext, globalCrudChange]) 194 | // TODO fix error so we dont have to ignore it // error is benign 195 | useEffect(() => { 196 | windowHelper() 197 | }, []) 198 | return ( 199 | <> 200 |
201 | {header} 202 |
203 |
204 | 213 | 221 |
222 |
223 | { 227 | console.log(network.getSeed()) 228 | setTimeout( 229 | () => { 230 | network.fit({ 231 | animation: { 232 | duration: 1000, 233 | easingFunction: 'easeOutQuad' 234 | } 235 | }) 236 | }, 237 | 3000 238 | ) 239 | }} 240 | /> 241 |
242 | 243 | ) 244 | } 245 | export default Mapothy 246 | -------------------------------------------------------------------------------- /client/components/helperFunctions.ts: -------------------------------------------------------------------------------- 1 | import { type CLusterObj, type nestedObj } from '../../types/types' 2 | 3 | // ~----------------------------------------Modal--------------------------------------------------> 4 | export const makeModal = (obj: CLusterObj, type: string): string => { 5 | // create necessary dom elements 6 | const subLists: HTMLElement[] = [] 7 | const div = document.createElement('div'); div.className = 'modal' 8 | const ul = document.createElement('ul') 9 | const header = document.createElement('div') 10 | header.className = 'modalHeader' 11 | // header assigned given type (Namespace, Service, etc.) 12 | header.innerText = `${type} Details` 13 | // all items in given object to be iteraeted over 14 | Object.keys(obj).forEach((key: any) => { 15 | // filtering for private data (can be turned off in private settings) 16 | if (key === 'uid' || key === 'ingressIP') return 17 | // arbitrarily nested objects dealt with using another helper function 18 | if (typeof obj[key] === 'object') subLists.push(recursiveList(obj[key], key)) 19 | else { 20 | const li = document.createElement('li') 21 | li.innerText = `${humanReadable(key)}: ${obj[key] as string}` 22 | ul.appendChild(li) 23 | } 24 | }) 25 | // elements appended in order 26 | div.appendChild(header) 27 | div.appendChild(ul) 28 | subLists.forEach((el) => { 29 | div.appendChild(el) 30 | }) 31 | // dom elements manipulated to match typing expectations for react-graph-vis tooltip 32 | return div as unknown as string 33 | } 34 | // ?recursively iterate nested objects to create sub lists 35 | const recursiveList = (obj: nestedObj, key?: any): HTMLElement => { 36 | const subList = document.createElement('ul') 37 | const smallHeader = document.createElement('div') 38 | smallHeader.className = 'modalHeader' 39 | if (key !== undefined) smallHeader.innerText = `${humanReadable(key)}` 40 | // check for specifically nested obj (ports obj) to ensure consistent prints (edge case) 41 | if (Array.isArray(obj) && obj.length === 1 && typeof obj[0] !== 'object') { 42 | const singleLi = document.createElement('li') 43 | singleLi.innerText = `${humanReadable(key)}: ${obj[0] as string}` 44 | subList.appendChild(singleLi) 45 | return subList 46 | } 47 | // header to be appended first, no header for the above case 48 | subList.appendChild(smallHeader) 49 | Object.keys(obj).forEach((k: any) => { // fix typing here 50 | const li = document.createElement('li') 51 | // check for further nested objects 52 | if (typeof obj[k] === 'object') subList.appendChild(recursiveList(obj[k])) 53 | else { 54 | // check to ensure keys are not printed for array elements 55 | Array.isArray(obj) ? li.innerText = `${obj[k] as string}` : li.innerText = `${humanReadable(k)}: ${obj[k] as string}` 56 | subList.appendChild(li) 57 | } 58 | }) 59 | return subList 60 | } 61 | // ?helper function to make keys human readable (camel case) 62 | export const humanReadable = (name: string): string => { 63 | const words = name.match(/[A-Za-z][a-z]*/g) 64 | return words?.map(capitalize).join(' ') ?? '' 65 | } 66 | export const capitalize = (word: string): string => word.charAt(0).toUpperCase() + word.substring(1) 67 | 68 | // ~---------------------------------------Window Helper-------------------------------------------> 69 | // helper function to prevent benign error message created when viewing larger clusters 70 | export const windowHelper = (): void => { 71 | window.addEventListener('error', (e) => { 72 | console.log(e.message) 73 | if (e.message === 'ResizeObserver loop completed with undelivered notifications.') { 74 | const resizeObserverErrDiv = document.getElementById( 75 | 'webpack-dev-server-client-overlay-div' 76 | ) 77 | const resizeObserverErr = document.getElementById( 78 | 'webpack-dev-server-client-overlay' 79 | ) 80 | if (resizeObserverErr != null) { 81 | resizeObserverErr.setAttribute('style', 'display: none') 82 | } 83 | if (resizeObserverErrDiv != null) { 84 | resizeObserverErrDiv.setAttribute('style', 'display: none') 85 | } 86 | } 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /client/containers/InvisibleNavbar/InvisibleNavbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, type SyntheticEvent, type ReactElement } from 'react' 2 | import InvisibleNavbarModal from './InvisibleNavbarModal' 3 | 4 | function InvisibleNavbar (): ReactElement { 5 | const [showModal, setShowModal] = useState(false) 6 | const [modalPos, setModalPos] = useState(0) 7 | 8 | const openModal = (e: SyntheticEvent): void => { 9 | setModalPos(e.currentTarget.getBoundingClientRect().bottom + 5) 10 | showModal ? setShowModal(false) : setShowModal(true) 11 | } 12 | return ( 13 | <> 14 | 15 | {showModal 16 | ? ( 17 | 21 | ) 22 | :
} 23 | 24 | 25 | ) 26 | } 27 | 28 | export default InvisibleNavbar 29 | -------------------------------------------------------------------------------- /client/containers/InvisibleNavbar/InvisibleNavbarModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, type ReactElement } from 'react' 2 | import { v4 as uuidv4 } from 'uuid' 3 | import type { InvisibleNavbarModalProps } from '../../../types/types' 4 | import { GlobalContext } from '../../components/Contexts' 5 | 6 | function InvisibleNavbarModal ( 7 | { 8 | style, 9 | setShowModal 10 | }: InvisibleNavbarModalProps 11 | ): ReactElement { 12 | const { 13 | globalServices, 14 | globalTimer, setGlobalTimer, 15 | setGlobalServiceTest 16 | } = useContext(GlobalContext) 17 | const [vU, setVu] = useState(0) 18 | const [duration, setDuration] = useState(0) 19 | const [service, setService] = useState('') 20 | 21 | const loadtest = async (): Promise => { 22 | if (vU < 0 || duration < 0) { alert('Please choose positive numbers'); return } 23 | if (vU === 0 || duration === 0) { alert('Please fill out both fields'); return } 24 | if (globalTimer !== 0) { alert('Load test is currently running, please wait'); return } 25 | try { 26 | const response = await fetch(`/api/k6/test?vus=${vU}&duration=${duration}&ip=${service}`) 27 | const data = response.json() 28 | console.log(data) 29 | setShowModal(false) 30 | const filtered = globalServices?.find((gService) => gService?.ip === service) 31 | if (setGlobalServiceTest !== undefined) setGlobalServiceTest(filtered?.name ?? '') 32 | if (setGlobalTimer !== undefined) setGlobalTimer((Date.now() + (duration * 1000))) 33 | } catch (error) { 34 | console.log('error in running load test:', error) 35 | } 36 | } 37 | return ( 38 | <> 39 |
42 |
46 |
47 | 59 |
60 | { setVu(Number(e.target.value)) }} 65 | /> 66 | { setDuration(Number(e.target.value)) }} 71 | /> 72 | 80 | 88 |
89 | 90 | ) 91 | } 92 | 93 | export default InvisibleNavbarModal 94 | -------------------------------------------------------------------------------- /client/containers/LogsContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useContext, type ReactElement } from 'react' 2 | import { v4 as uuidv4 } from 'uuid' 3 | import { GlobalContext } from '../components/Contexts' 4 | import type { CLusterObj, Props } from '../../types/types' 5 | import Logs from '../components/Logs' 6 | 7 | const types = ['error', 'info'] 8 | 9 | function LogsContainer ({ header }: Props): ReactElement { 10 | const [ns, setNs] = useState('default') 11 | const [nsArr, setNsArr] = useState([]) 12 | const [pod, setPod] = useState('') 13 | const [podArr, setPodArr] = useState([]) 14 | const [logType, setLogType] = useState('') 15 | const { globalClusterContext, globalClusterData } = useContext(GlobalContext) 16 | const getNs = async (): Promise => { 17 | try { 18 | const tempPodArr = globalClusterData?.pods 19 | ?.filter(({ namespace }: CLusterObj) => namespace === ns) 20 | .map(({ name }: CLusterObj) => name) 21 | const url = `/api/cluster/namespaces?context=${globalClusterContext ?? ''}&all=true` 22 | const response = await fetch(url) 23 | const data = (await response.json()) 24 | const arr: string[] = data.map(({ name }: CLusterObj) => name) 25 | setPodArr(tempPodArr) 26 | setNsArr(arr) 27 | setPod('') 28 | } catch (error) { 29 | console.log('Error fetching logs namespaces:', error) 30 | } 31 | } 32 | useEffect(() => { 33 | void getNs() 34 | }, [ns]) 35 | return ( 36 | <> 37 |
{header}
38 |
39 | 47 | {(ns !== '') && ( 48 | 56 | )} 57 | 65 |
66 |
67 | 68 |
69 | 70 | ) 71 | } 72 | 73 | export default LogsContainer 74 | -------------------------------------------------------------------------------- /client/containers/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactElement, useContext } from 'react' 2 | import { Route, Routes } from 'react-router-dom' 3 | import MainDashBoard from './MainDashBoard' 4 | import Mapothy from '../components/Mapothy' 5 | import { GlobalContext } from '../components/Contexts' 6 | import NetworkPerformance from './NetworkPerformance' 7 | import CRUDModal from '../components/CRUD/CRUDModal' 8 | import LogsContainer from './LogsContainer' 9 | 10 | function MainContainer (): ReactElement { 11 | const { 12 | showEditModal 13 | } = useContext(GlobalContext) 14 | 15 | return ( 16 | <> 17 |
{(showEditModal === true) && }
18 |
19 | 20 | } /> 21 | } /> 22 | } /> 23 | } /> 24 | 25 |
26 | 27 | 28 | ) 29 | } 30 | 31 | export default MainContainer 32 | -------------------------------------------------------------------------------- /client/containers/MainDashBoard.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactElement } from 'react' 2 | import LineGraph from '../components/Graphs/LineGraph' 3 | import GaugeChart from '../components/Graphs/GaugeChart' 4 | import type { Props } from '../../types/types' 5 | import InvisibleNavbar from './InvisibleNavbar/InvisibleNavbar' 6 | 7 | function MainDashBoard ({ header }: Props): ReactElement { 8 | const textColor = 'rgba(255, 255, 255, 0.702)' 9 | return ( 10 | <> 11 |
{header}
12 | 13 |
14 | 21 | 28 |
29 |
30 | 37 | 44 |
45 | 46 | 47 | ) 48 | } 49 | 50 | export default MainDashBoard 51 | -------------------------------------------------------------------------------- /client/containers/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactElement, useContext, useEffect, useState } from 'react' 2 | import { useNavigate } from 'react-router-dom' 3 | import clusterpic from '../assets/images/clusterPic.png' 4 | import mainDashBoard from '../assets/images/mainDashBoard.png' 5 | import netWork from '../assets/images/network.png' 6 | import edit from '../assets/images/edit.png' 7 | import docs from '../assets/images/docs.png' 8 | import { GlobalContext } from '../components/Contexts' 9 | import gif2 from '../assets/gif3.gif' 10 | 11 | export default function Navbar (): ReactElement { 12 | const { 13 | globalTimer, setGlobalTimer, 14 | globalServiceTest, setGlobalServiceTest, 15 | showEditModal, setShowEditModal, 16 | ongoingCrudChange 17 | } = useContext(GlobalContext) 18 | const [activeTimer, setActiveTimer] = useState(0) //! smooth brain solution 19 | const navigate = useNavigate() 20 | function MainDashBoard (): void { navigate('/dashboard') } 21 | function GoHome (): void { navigate('/') } 22 | function Network (): void { navigate('/network') } 23 | function Logs (): void { navigate('/logs') } 24 | 25 | useEffect(() => { 26 | if ((globalTimer !== undefined) && (globalTimer - Date.now()) < 0) { 27 | if (setGlobalTimer !== undefined) setGlobalTimer(0) 28 | if (setGlobalServiceTest !== undefined) setGlobalServiceTest('') 29 | } else if ((globalTimer !== undefined) && (globalTimer - Date.now()) > 0) { 30 | setTimeout(() => { 31 | setActiveTimer(Math.floor((globalTimer - Date.now()) / 1000)) 32 | }, 1000) 33 | } 34 | }, [globalTimer, activeTimer, ongoingCrudChange]) 35 | 36 | return ( 37 |
38 |
KUBERNAUTICAL
39 |
40 | 44 | 45 | 49 | 50 | 54 | 55 | 59 | 60 | 73 | 74 |
75 | {globalTimer !== 0 && globalTimer !== undefined 76 | ? `Load test time remaining: 77 | ${Math.floor((globalTimer - Date.now()) / 1000)}s, running on 78 | ${globalServiceTest as string}` 79 | : null} 80 |
81 |
82 | {ongoingCrudChange === true ? loading-gif : null} 83 |
84 | 85 |
86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /client/containers/NetworkPerformance.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactElement } from 'react' 2 | import LineGraph from '../components/Graphs/LineGraph' 3 | import type { Props } from '../../types/types' 4 | import InvisibleNavbar from './InvisibleNavbar/InvisibleNavbar' 5 | 6 | function NetworkPerformance ({ header }: Props): ReactElement { 7 | return ( 8 | <> 9 |
{header}
10 | 11 |
12 | 13 | 20 | 27 | 28 |
29 | 30 | 31 | ) 32 | } 33 | export default NetworkPerformance 34 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kubernautical 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 7 | const root = createRoot(document.getElementById('root')!); 8 | root.render( 9 | 10 | 11 | 12 | ); -------------------------------------------------------------------------------- /client/sass/App.scss: -------------------------------------------------------------------------------- 1 | //imports 2 | @import '_variables'; 3 | @import '_navBar'; 4 | @import '_mainContainer'; 5 | @import '_graph'; 6 | @import '_vis-network'; 7 | @import '_map'; 8 | @import '_invisNav'; 9 | @import '_CRUDModal'; 10 | @import '_logs'; 11 | 12 | 13 | body { 14 | background-color: $body-background; 15 | color: white; 16 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif 17 | } 18 | 19 | .App { 20 | display: flex; 21 | height: 95vh; 22 | } 23 | 24 | .hr { 25 | border: 1px solid white; 26 | width: 90%; 27 | } 28 | 29 | .footer { 30 | display: flex; 31 | flex-direction: column; 32 | align-items: flex-end; 33 | } 34 | 35 | 36 | // large screen 37 | @media screen and (max-width: 1599px) { 38 | .navBar { 39 | display: flex; 40 | flex-direction: column; 41 | align-items: center; 42 | /* height: 90vh; 43 | width: 12vw; 44 | padding: 5px; 45 | margin: 10px; 46 | color: #EEEEEE; 47 | border: 2px solid cyan; 48 | border-radius: 5px; */ 49 | font-size: 1.5rem; 50 | } 51 | 52 | .nav-title { 53 | padding-bottom: 10%; 54 | } 55 | 56 | .navButton { 57 | width: 11vw; 58 | margin: 5px; 59 | border-radius: 10px; 60 | padding: 10px 20px; 61 | font-size: 1.2rem; 62 | } 63 | 64 | .miniContainer { 65 | display: flex; 66 | flex-direction: column; 67 | justify-content: space-around; 68 | align-items: center; 69 | } 70 | } 71 | 72 | @media screen and (max-width: 1350px) { 73 | .nav-title { 74 | font-size: 1 rem; 75 | } 76 | 77 | .navButton { 78 | width: 11vw; 79 | font-size: 1.1rem; 80 | } 81 | } 82 | 83 | @media screen and (max-width: 1100px) { 84 | .App { 85 | display: flex; 86 | flex-direction: column; 87 | } 88 | 89 | .navBar { 90 | width: 98.5%; 91 | padding: 10px; 92 | display: flex; 93 | flex-direction: row; 94 | justify-content: flex-start; 95 | } 96 | 97 | .nav-title { 98 | padding-top: 10%; 99 | font-size: 0.8rem; 100 | } 101 | 102 | .navButton { 103 | width: 11vw; 104 | font-size: 1.1rem; 105 | } 106 | } -------------------------------------------------------------------------------- /client/sass/_CRUDModal.scss: -------------------------------------------------------------------------------- 1 | .editModal { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: start; 5 | align-items: start; 6 | margin: 100% -100px 5px 40px; 7 | padding: 0px 3px 3px 3px; 8 | height: 500px; 9 | width: 125%; 10 | // border: 2px solid magenta; 11 | } 12 | 13 | .centerModal { 14 | display: flex; 15 | flex-direction: column; 16 | // border: 2px solid cyan; 17 | // justify-content: center; 18 | margin: 10px 5px 5px 10px; 19 | } 20 | 21 | .crudDelete { 22 | height: 24px; 23 | width: 24px; 24 | border-radius: 8px; 25 | margin: 5px; 26 | border: none; 27 | background-color: $red-button; 28 | } 29 | 30 | .crudSelector { 31 | display: flex; 32 | justify-content: space-between; 33 | margin-bottom: 7px; 34 | height: 30px; 35 | width: 95%; 36 | } 37 | 38 | .crudHeader { 39 | color: lighten($font-color, 30%); 40 | display: flex; 41 | align-items: end; 42 | // border: 2px solid cyan; 43 | padding: 0px; 44 | margin: 5px 5px 5px 5px; 45 | width: 80%; 46 | height: 8%; 47 | animation: loadin 1s ease-in; 48 | } 49 | 50 | .hrCrud { 51 | border: 1px solid lighten($font-color, 30%); 52 | width: 90%; 53 | } 54 | 55 | .smallEdit { 56 | height: 15px; 57 | width: 15px; 58 | } 59 | 60 | .crudMini { 61 | display: flex; 62 | flex-direction: column; 63 | justify-content: end; 64 | align-items: start; 65 | width: 200px; 66 | min-height: 200px; 67 | margin: 8px; 68 | padding: 5px; 69 | border-radius: 5px; 70 | background-color: lighten($container-background, 10%); 71 | box-shadow: $container-shadow; 72 | z-index: 10; 73 | } 74 | 75 | .portsDiv { 76 | color: lighten($font-color, 30%); 77 | display: flex; 78 | align-items: center; 79 | width: 100%; 80 | } 81 | 82 | .portsEdit { 83 | width: 20%; 84 | justify-self: end; 85 | } -------------------------------------------------------------------------------- /client/sass/_graph.scss: -------------------------------------------------------------------------------- 1 | .lineGraph { 2 | width: 95%; 3 | height: 95%; 4 | margin: 8px; 5 | padding: 5px 5px 0 5px; 6 | border-radius: 5px; 7 | background-color: $container-background; 8 | box-shadow: $container-shadow; 9 | } 10 | 11 | .guageGraph { 12 | width: 95%; 13 | height: 95%; 14 | margin: 8px; 15 | padding: 5px; 16 | border-radius: 5px; 17 | background-color: $container-background; 18 | box-shadow: $container-shadow; 19 | } -------------------------------------------------------------------------------- /client/sass/_invisNav.scss: -------------------------------------------------------------------------------- 1 | .invisNavButton { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | width: 100px; 6 | height: 25px; 7 | margin: -8px 0px 5px 0px; 8 | color: $font-color; 9 | border-radius: 5px; 10 | background-color: $container-background; 11 | box-shadow: $container-shadow; 12 | border: 0 solid; 13 | outline: 1px solid; 14 | outline-color: rgba(255, 255, 255, .5); 15 | outline-offset: 0px; 16 | text-shadow: none; 17 | transition: all 1250ms cubic-bezier(0.19, 1, 0.22, 1); 18 | z-index: 9; 19 | } 20 | 21 | .invisNavButton:hover { 22 | border: 1px solid; 23 | box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.1), 0 0 20px rgba(255, 255, 255, .2); 24 | outline-color: rgba(255, 255, 255, 0); 25 | outline-offset: 15px; 26 | } 27 | 28 | .invisModal { 29 | display: flex; 30 | flex-direction: column; 31 | justify-content: center; 32 | align-items: center; 33 | width: 200px; 34 | height: 200px; 35 | margin: 8px; 36 | padding: 5px 5px 0 5px; 37 | border-radius: 5px; 38 | background-color: lighten($container-background, 10%); 39 | box-shadow: $container-shadow; 40 | z-index: 10; 41 | } 42 | 43 | .page-mask { 44 | z-index: 8; 45 | background: rgba(0, 0, 0, 0.5); 46 | position: fixed; 47 | top: 0; 48 | right: 0; 49 | bottom: 0; 50 | left: 0; 51 | } 52 | 53 | .closeInvisModal { 54 | position: absolute; 55 | height: 24px; 56 | width: 24px; 57 | top: 5px; 58 | right: 5px; 59 | border-radius: 8px; 60 | border: none; 61 | background-color: $red-button; 62 | } 63 | 64 | .closeInvisModal:hover, 65 | .InvisSubmit:hover, 66 | .crudDelete:hover { 67 | cursor: pointer; 68 | transform: translate(1px, 1px); 69 | color: white; 70 | } 71 | 72 | .InvisInput { 73 | border-radius: 6px; 74 | border: none; 75 | width: 90%; 76 | padding: 8px; 77 | margin-bottom: 4px; 78 | background-color: rgb(66, 73, 90); 79 | color: rgba(246, 247, 249, 0.8); 80 | } 81 | 82 | .InvisSubmit { 83 | width: 95%; 84 | margin-top: 5px; 85 | border-radius: 6px; 86 | padding: 9px; 87 | border: none; 88 | background-color: $red-button; 89 | } 90 | 91 | .InvisService { 92 | position: absolute; 93 | height: 24px; 94 | width: 50%; 95 | top: 5px; 96 | left: 5px; 97 | border-radius: 8px; 98 | border: none; 99 | background-color: $red-button; 100 | } -------------------------------------------------------------------------------- /client/sass/_logs.scss: -------------------------------------------------------------------------------- 1 | .log-container { 2 | margin: 10px; 3 | } 4 | 5 | .log-entry { 6 | margin: 10px; 7 | padding: 3px; 8 | // border: 1px solid #d1d0d0; 9 | background-color: #252525; 10 | color: $font-color; 11 | } 12 | 13 | .value-entry { 14 | margin-bottom: 10px; 15 | } 16 | 17 | .clickable { 18 | cursor: pointer; 19 | background-color: lighten($container-background, 10%); 20 | padding: 8px; 21 | border-radius: 4px; 22 | 23 | } 24 | 25 | .clickable:hover { 26 | background-color: lighten($container-background, 5%); 27 | } 28 | 29 | .log-details { 30 | margin-top: 5px; 31 | padding: 5px; 32 | background-color: lighten($container-background, 20%); 33 | border: 1px solid #555555; 34 | } 35 | 36 | .dropdown-container { 37 | display: flex; 38 | } -------------------------------------------------------------------------------- /client/sass/_mainContainer.scss: -------------------------------------------------------------------------------- 1 | .mainContainer { 2 | // border: 2px solid cyan; 3 | border-radius: 5px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | flex-grow: 1; 8 | padding: 5px; 9 | margin: 10px; 10 | height: 90vh; 11 | 12 | } 13 | 14 | .mainHeader { 15 | // border: 2px solid pink; 16 | color: lighten($font-color, 30%); 17 | padding: 5px; 18 | margin: 5px; 19 | width: 80%; 20 | height: 15%; 21 | border-radius: 5px; 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | font-size: 50px; 26 | animation: loadin 1s ease-in; 27 | } 28 | 29 | .miniContainerMap { 30 | // border: 2px solid cyan; 31 | border-radius: 5px; 32 | padding: 5px; 33 | width: 80%; 34 | height: 80%; 35 | display: flex; 36 | margin: 8px; 37 | border-radius: 5px; 38 | background-color: $container-background; 39 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 40 | // flex-direction: column; 41 | animation: loadin 1s ease-in; 42 | } 43 | 44 | .miniContainerGraph { 45 | // border: 2px solid cyan; 46 | border-radius: 5px; 47 | padding: 5px; 48 | width: 80%; 49 | height: 40%; 50 | display: flex; 51 | // flex-direction: column; 52 | animation: loadin 1s lienar; 53 | } 54 | 55 | .miniContainerLogs { 56 | border-radius: 5px; 57 | padding: 5px; 58 | width: 80%; 59 | height: 95%; 60 | display: flex; 61 | margin: 8px; 62 | border-radius: 5px; 63 | background-color: $container-background; 64 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 65 | overflow: auto; 66 | // flex-direction: column; 67 | // animation: loadin 1s ease-in; 68 | animation: loadin 1s lienar; 69 | } 70 | 71 | 72 | .containerButton { 73 | // width: 95%; 74 | // height: 45%; 75 | color: $font-color; 76 | margin: 5px 5px 0 5px; 77 | border-radius: 5px; 78 | border: none; 79 | background-color: $body-background; 80 | box-shadow: $container-shadow; 81 | } 82 | 83 | @keyframes loadin { 84 | from { 85 | opacity: 0; 86 | transform: translateX(-10px) 87 | } 88 | 89 | to { 90 | opacity: 1; 91 | transform: translateX(0) 92 | } 93 | } -------------------------------------------------------------------------------- /client/sass/_map.scss: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .modalHeader { 8 | font-size: large; 9 | text-decoration: underline; 10 | font-weight: bold; 11 | } 12 | 13 | li { 14 | padding: 5px; 15 | } 16 | 17 | div.vis-tooltip { 18 | position: absolute; 19 | visibility: hidden; 20 | padding: 5px; 21 | white-space: normal; 22 | font-family: verdana; 23 | font-size: 14px; 24 | color: lighten($font-color, 20%); 25 | background-color: $container-background; 26 | box-shadow: $container-shadow; 27 | margin: 5px 5px 0 5px; 28 | border-radius: 5px; 29 | border: 1px solid rgb(24, 25, 35); 30 | -moz-border-radius: 3px; 31 | -webkit-border-radius: 3px; 32 | border-radius: 3px; 33 | pointer-events: none; 34 | z-index: 5; 35 | } 36 | 37 | .mapButton { 38 | z-index: 3; 39 | background-color: lighten($container-background, 15%); 40 | } 41 | 42 | .mapButton:hover, 43 | .mapButton2:hover { 44 | cursor: pointer; 45 | transform: translate(1px, 1px); 46 | color: lighten($font-color, 20%); 47 | } 48 | 49 | .buttonWrap { 50 | // border: 2px solid cyan; 51 | width: 80%; 52 | padding: 0; 53 | margin: 0; 54 | } 55 | 56 | .buttonLeft { 57 | margin-left: -4px; 58 | } -------------------------------------------------------------------------------- /client/sass/_navBar.scss: -------------------------------------------------------------------------------- 1 | .navBar { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | height: 95vh; 6 | width: 12vw; 7 | padding: 5px; 8 | margin: 10px 5px 5px 10px; 9 | background: linear-gradient(-45deg, rgb(253, 93, 146), rgb(197, 47, 30), rgb(253, 93, 146), rgb(197, 47, 30)); 10 | background-size: 400% 400%; 11 | color: #EEEEEE; 12 | border-radius: 5px; 13 | animation: gradient 5s ease infinite; 14 | } 15 | 16 | @keyframes gradient { 17 | 0% { 18 | background-position: 0% 50%; 19 | } 20 | 21 | 50% { 22 | background-position: 100% 50%; 23 | } 24 | 25 | 100% { 26 | background-position: 0% 50%; 27 | } 28 | } 29 | 30 | // title 31 | .navBarTitle { 32 | align-self: center; 33 | font-size: 1.5rem; 34 | margin: 20px 0 20px 0; 35 | } 36 | 37 | // Button 38 | 39 | .navButton { 40 | width: 100%; 41 | height: 8%; 42 | margin: 5px; 43 | border: none; 44 | font-size: 1rem; 45 | background: transparent; 46 | display: flex; 47 | align-items: center; 48 | color: white 49 | } 50 | 51 | .navButton:hover { 52 | cursor: pointer; 53 | transform: translate(1px, 1px); 54 | color: white; 55 | } 56 | 57 | .btn-icon { 58 | margin-right: 1.2rem; 59 | height: 30px; 60 | width: 30px; 61 | margin-left: 5px; 62 | } 63 | 64 | .btn-text { 65 | text-align: left; 66 | } 67 | 68 | .loadingStatus { 69 | position: relative; 70 | top: 40%; 71 | justify-self: end; 72 | align-self: center; 73 | font-size: small; 74 | } 75 | 76 | .loadingGif { 77 | height: 50px; 78 | width: 50px; 79 | } -------------------------------------------------------------------------------- /client/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $font-color: rgba(255, 255, 255, 0.702); 2 | $container-background: rgb(38, 38, 38); 3 | $container-shadow: rgba(28, 28, 28, 0.24) 0px 3px 8px; 4 | $red-button: rgb(225, 70, 89); 5 | $body-background: rgb(25, 24, 24); -------------------------------------------------------------------------------- /client/sass/_vis-network.scss: -------------------------------------------------------------------------------- 1 | .vis .overlay { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | z-index: 10; 8 | } 9 | 10 | .vis-active { 11 | box-shadow: 0 0 10px #86d5f8; 12 | } 13 | 14 | .vis [class*="span"] { 15 | min-height: 0; 16 | width: auto; 17 | } 18 | 19 | div.vis-configuration { 20 | position: relative; 21 | display: block; 22 | float: left; 23 | font-size: 12px; 24 | } 25 | 26 | div.vis-configuration-wrapper { 27 | display: block; 28 | width: 700px; 29 | } 30 | 31 | div.vis-configuration-wrapper::after { 32 | clear: both; 33 | content: ""; 34 | display: block; 35 | } 36 | 37 | div.vis-configuration.vis-config-option-container { 38 | display: block; 39 | width: 495px; 40 | background-color: #fff; 41 | border: 2px solid #f7f8fa; 42 | border-radius: 4px; 43 | margin-top: 20px; 44 | left: 10px; 45 | padding-left: 5px; 46 | } 47 | 48 | div.vis-configuration.vis-config-button { 49 | display: block; 50 | width: 495px; 51 | height: 25px; 52 | vertical-align: middle; 53 | line-height: 25px; 54 | background-color: #f7f8fa; 55 | border: 2px solid #ceced0; 56 | border-radius: 4px; 57 | margin-top: 20px; 58 | left: 10px; 59 | padding-left: 5px; 60 | cursor: pointer; 61 | margin-bottom: 30px; 62 | } 63 | 64 | div.vis-configuration.vis-config-button.hover { 65 | background-color: #4588e6; 66 | border: 2px solid #214373; 67 | color: #fff; 68 | } 69 | 70 | div.vis-configuration.vis-config-item { 71 | display: block; 72 | float: left; 73 | width: 495px; 74 | height: 25px; 75 | vertical-align: middle; 76 | line-height: 25px; 77 | } 78 | 79 | div.vis-configuration.vis-config-item.vis-config-s2 { 80 | left: 10px; 81 | background-color: #f7f8fa; 82 | padding-left: 5px; 83 | border-radius: 3px; 84 | } 85 | 86 | div.vis-configuration.vis-config-item.vis-config-s3 { 87 | left: 20px; 88 | background-color: #e4e9f0; 89 | padding-left: 5px; 90 | border-radius: 3px; 91 | } 92 | 93 | div.vis-configuration.vis-config-item.vis-config-s4 { 94 | left: 30px; 95 | background-color: #cfd8e6; 96 | padding-left: 5px; 97 | border-radius: 3px; 98 | } 99 | 100 | div.vis-configuration.vis-config-header { 101 | font-size: 18px; 102 | font-weight: 700; 103 | } 104 | 105 | div.vis-configuration.vis-config-label { 106 | width: 120px; 107 | height: 25px; 108 | line-height: 25px; 109 | } 110 | 111 | div.vis-configuration.vis-config-label.vis-config-s3 { 112 | width: 110px; 113 | } 114 | 115 | div.vis-configuration.vis-config-label.vis-config-s4 { 116 | width: 100px; 117 | } 118 | 119 | div.vis-configuration.vis-config-colorBlock { 120 | top: 1px; 121 | width: 30px; 122 | height: 19px; 123 | border: 1px solid #444; 124 | border-radius: 2px; 125 | padding: 0; 126 | margin: 0; 127 | cursor: pointer; 128 | } 129 | 130 | input.vis-configuration.vis-config-checkbox { 131 | left: -5px; 132 | } 133 | 134 | input.vis-configuration.vis-config-rangeinput { 135 | position: relative; 136 | top: -5px; 137 | width: 60px; 138 | padding: 1px; 139 | margin: 0; 140 | pointer-events: none; 141 | } 142 | 143 | input.vis-configuration.vis-config-range { 144 | -webkit-appearance: none; 145 | border: 0 solid #fff; 146 | background-color: rgba(0, 0, 0, 0); 147 | width: 300px; 148 | height: 20px; 149 | } 150 | 151 | input.vis-configuration.vis-config-range::-webkit-slider-runnable-track { 152 | width: 300px; 153 | height: 5px; 154 | background: #dedede; 155 | background: -moz-linear-gradient(top, #dedede 0, #c8c8c8 99%); 156 | background: -webkit-gradient(linear, 157 | left top, 158 | left bottom, 159 | color-stop(0, #dedede), 160 | color-stop(99%, #c8c8c8)); 161 | background: -webkit-linear-gradient(top, #dedede 0, #c8c8c8 99%); 162 | background: -o-linear-gradient(top, #dedede 0, #c8c8c8 99%); 163 | background: -ms-linear-gradient(top, #dedede 0, #c8c8c8 99%); 164 | background: linear-gradient(to bottom, #dedede 0, #c8c8c8 99%); 165 | border: 1px solid #999; 166 | box-shadow: #aaa 0 0 3px 0; 167 | border-radius: 3px; 168 | } 169 | 170 | input.vis-configuration.vis-config-range::-webkit-slider-thumb { 171 | -webkit-appearance: none; 172 | border: 1px solid #14334b; 173 | height: 17px; 174 | width: 17px; 175 | border-radius: 50%; 176 | background: #3876c2; 177 | background: -moz-linear-gradient(top, #3876c2 0, #385380 100%); 178 | background: -webkit-gradient(linear, 179 | left top, 180 | left bottom, 181 | color-stop(0, #3876c2), 182 | color-stop(100%, #385380)); 183 | background: -webkit-linear-gradient(top, #3876c2 0, #385380 100%); 184 | background: -o-linear-gradient(top, #3876c2 0, #385380 100%); 185 | background: -ms-linear-gradient(top, #3876c2 0, #385380 100%); 186 | background: linear-gradient(to bottom, #3876c2 0, #385380 100%); 187 | box-shadow: #111927 0 0 1px 0; 188 | margin-top: -7px; 189 | } 190 | 191 | input.vis-configuration.vis-config-range:focus { 192 | outline: 0; 193 | } 194 | 195 | input.vis-configuration.vis-config-range:focus::-webkit-slider-runnable-track { 196 | background: #9d9d9d; 197 | background: -moz-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%); 198 | background: -webkit-gradient(linear, 199 | left top, 200 | left bottom, 201 | color-stop(0, #9d9d9d), 202 | color-stop(99%, #c8c8c8)); 203 | background: -webkit-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%); 204 | background: -o-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%); 205 | background: -ms-linear-gradient(top, #9d9d9d 0, #c8c8c8 99%); 206 | background: linear-gradient(to bottom, #9d9d9d 0, #c8c8c8 99%); 207 | } 208 | 209 | input.vis-configuration.vis-config-range::-moz-range-track { 210 | width: 300px; 211 | height: 10px; 212 | background: #dedede; 213 | background: -moz-linear-gradient(top, #dedede 0, #c8c8c8 99%); 214 | background: -webkit-gradient(linear, 215 | left top, 216 | left bottom, 217 | color-stop(0, #dedede), 218 | color-stop(99%, #c8c8c8)); 219 | background: -webkit-linear-gradient(top, #dedede 0, #c8c8c8 99%); 220 | background: -o-linear-gradient(top, #dedede 0, #c8c8c8 99%); 221 | background: -ms-linear-gradient(top, #dedede 0, #c8c8c8 99%); 222 | background: linear-gradient(to bottom, #dedede 0, #c8c8c8 99%); 223 | border: 1px solid #999; 224 | box-shadow: #aaa 0 0 3px 0; 225 | border-radius: 3px; 226 | } 227 | 228 | input.vis-configuration.vis-config-range::-moz-range-thumb { 229 | border: none; 230 | height: 16px; 231 | width: 16px; 232 | border-radius: 50%; 233 | background: #385380; 234 | } 235 | 236 | input.vis-configuration.vis-config-range:-moz-focusring { 237 | outline: 1px solid #fff; 238 | outline-offset: -1px; 239 | } 240 | 241 | // input.vis-configuration.vis-config-range::-ms-track { 242 | // width: 300px; 243 | // height: 5px; 244 | // background: 0 0; 245 | // border-color: transparent; 246 | // border-width: 6px 0; 247 | // color: transparent; 248 | // } 249 | 250 | input.vis-configuration.vis-config-range::-ms-fill-lower { 251 | background: #777; 252 | border-radius: 10px; 253 | } 254 | 255 | input.vis-configuration.vis-config-range::-ms-fill-upper { 256 | background: #ddd; 257 | border-radius: 10px; 258 | } 259 | 260 | input.vis-configuration.vis-config-range::-ms-thumb { 261 | border: none; 262 | height: 16px; 263 | width: 16px; 264 | border-radius: 50%; 265 | background: #385380; 266 | } 267 | 268 | input.vis-configuration.vis-config-range:focus::-ms-fill-lower { 269 | background: #888; 270 | } 271 | 272 | input.vis-configuration.vis-config-range:focus::-ms-fill-upper { 273 | background: #ccc; 274 | } 275 | 276 | .vis-configuration-popup { 277 | position: absolute; 278 | background: rgba(57, 76, 89, 0.85); 279 | border: 2px solid #f2faff; 280 | line-height: 30px; 281 | height: 30px; 282 | width: 150px; 283 | text-align: center; 284 | color: #fff; 285 | font-size: 14px; 286 | border-radius: 4px; 287 | -webkit-transition: opacity 0.3s ease-in-out; 288 | -moz-transition: opacity 0.3s ease-in-out; 289 | transition: opacity 0.3s ease-in-out; 290 | } 291 | 292 | .vis-configuration-popup:after, 293 | .vis-configuration-popup:before { 294 | left: 100%; 295 | top: 50%; 296 | border: solid transparent; 297 | content: " "; 298 | height: 0; 299 | width: 0; 300 | position: absolute; 301 | pointer-events: none; 302 | } 303 | 304 | .vis-configuration-popup:after { 305 | border-color: rgba(136, 183, 213, 0); 306 | border-left-color: rgba(57, 76, 89, 0.85); 307 | border-width: 8px; 308 | margin-top: -8px; 309 | } 310 | 311 | .vis-configuration-popup:before { 312 | border-color: rgba(194, 225, 245, 0); 313 | border-left-color: #f2faff; 314 | border-width: 12px; 315 | margin-top: -12px; 316 | } 317 | 318 | div.vis-color-picker { 319 | position: absolute; 320 | top: 0; 321 | left: 30px; 322 | margin-top: -140px; 323 | margin-left: 30px; 324 | width: 310px; 325 | height: 444px; 326 | z-index: 1; 327 | padding: 10px; 328 | border-radius: 15px; 329 | background-color: #fff; 330 | display: none; 331 | box-shadow: rgba(0, 0, 0, 0.5) 0 0 10px 0; 332 | } 333 | 334 | div.vis-color-picker div.vis-arrow { 335 | position: absolute; 336 | top: 147px; 337 | left: 5px; 338 | } 339 | 340 | div.vis-color-picker div.vis-arrow::after, 341 | div.vis-color-picker div.vis-arrow::before { 342 | right: 100%; 343 | top: 50%; 344 | border: solid transparent; 345 | content: " "; 346 | height: 0; 347 | width: 0; 348 | position: absolute; 349 | pointer-events: none; 350 | } 351 | 352 | div.vis-color-picker div.vis-arrow:after { 353 | border-color: rgba(255, 255, 255, 0); 354 | border-right-color: #fff; 355 | border-width: 30px; 356 | margin-top: -30px; 357 | } 358 | 359 | div.vis-color-picker div.vis-color { 360 | position: absolute; 361 | width: 289px; 362 | height: 289px; 363 | cursor: pointer; 364 | } 365 | 366 | div.vis-color-picker div.vis-brightness { 367 | position: absolute; 368 | top: 313px; 369 | } 370 | 371 | div.vis-color-picker div.vis-opacity { 372 | position: absolute; 373 | top: 350px; 374 | } 375 | 376 | div.vis-color-picker div.vis-selector { 377 | position: absolute; 378 | top: 137px; 379 | left: 137px; 380 | width: 15px; 381 | height: 15px; 382 | border-radius: 15px; 383 | border: 1px solid #fff; 384 | background: #4c4c4c; 385 | background: -moz-linear-gradient(top, 386 | #4c4c4c 0, 387 | #595959 12%, 388 | #666 25%, 389 | #474747 39%, 390 | #2c2c2c 50%, 391 | #000 51%, 392 | #111 60%, 393 | #2b2b2b 76%, 394 | #1c1c1c 91%, 395 | #131313 100%); 396 | background: -webkit-gradient(linear, 397 | left top, 398 | left bottom, 399 | color-stop(0, #4c4c4c), 400 | color-stop(12%, #595959), 401 | color-stop(25%, #666), 402 | color-stop(39%, #474747), 403 | color-stop(50%, #2c2c2c), 404 | color-stop(51%, #000), 405 | color-stop(60%, #111), 406 | color-stop(76%, #2b2b2b), 407 | color-stop(91%, #1c1c1c), 408 | color-stop(100%, #131313)); 409 | background: -webkit-linear-gradient(top, 410 | #4c4c4c 0, 411 | #595959 12%, 412 | #666 25%, 413 | #474747 39%, 414 | #2c2c2c 50%, 415 | #000 51%, 416 | #111 60%, 417 | #2b2b2b 76%, 418 | #1c1c1c 91%, 419 | #131313 100%); 420 | background: -o-linear-gradient(top, 421 | #4c4c4c 0, 422 | #595959 12%, 423 | #666 25%, 424 | #474747 39%, 425 | #2c2c2c 50%, 426 | #000 51%, 427 | #111 60%, 428 | #2b2b2b 76%, 429 | #1c1c1c 91%, 430 | #131313 100%); 431 | background: -ms-linear-gradient(top, 432 | #4c4c4c 0, 433 | #595959 12%, 434 | #666 25%, 435 | #474747 39%, 436 | #2c2c2c 50%, 437 | #000 51%, 438 | #111 60%, 439 | #2b2b2b 76%, 440 | #1c1c1c 91%, 441 | #131313 100%); 442 | background: linear-gradient(to bottom, 443 | #4c4c4c 0, 444 | #595959 12%, 445 | #666 25%, 446 | #474747 39%, 447 | #2c2c2c 50%, 448 | #000 51%, 449 | #111 60%, 450 | #2b2b2b 76%, 451 | #1c1c1c 91%, 452 | #131313 100%); 453 | } 454 | 455 | div.vis-color-picker div.vis-new-color { 456 | position: absolute; 457 | width: 140px; 458 | height: 20px; 459 | border: 1px solid rgba(0, 0, 0, 0.1); 460 | border-radius: 5px; 461 | top: 380px; 462 | left: 159px; 463 | text-align: right; 464 | padding-right: 2px; 465 | font-size: 10px; 466 | color: rgba(0, 0, 0, 0.4); 467 | vertical-align: middle; 468 | line-height: 20px; 469 | } 470 | 471 | div.vis-color-picker div.vis-initial-color { 472 | position: absolute; 473 | width: 140px; 474 | height: 20px; 475 | border: 1px solid rgba(0, 0, 0, 0.1); 476 | border-radius: 5px; 477 | top: 380px; 478 | left: 10px; 479 | text-align: left; 480 | padding-left: 2px; 481 | font-size: 10px; 482 | color: rgba(0, 0, 0, 0.4); 483 | vertical-align: middle; 484 | line-height: 20px; 485 | } 486 | 487 | div.vis-color-picker div.vis-label { 488 | position: absolute; 489 | width: 300px; 490 | left: 10px; 491 | } 492 | 493 | div.vis-color-picker div.vis-label.vis-brightness { 494 | top: 300px; 495 | } 496 | 497 | div.vis-color-picker div.vis-label.vis-opacity { 498 | top: 338px; 499 | } 500 | 501 | div.vis-color-picker div.vis-button { 502 | position: absolute; 503 | width: 68px; 504 | height: 25px; 505 | border-radius: 10px; 506 | vertical-align: middle; 507 | text-align: center; 508 | line-height: 25px; 509 | top: 410px; 510 | border: 2px solid #d9d9d9; 511 | background-color: #f7f7f7; 512 | cursor: pointer; 513 | } 514 | 515 | div.vis-color-picker div.vis-button.vis-cancel { 516 | left: 5px; 517 | } 518 | 519 | div.vis-color-picker div.vis-button.vis-load { 520 | left: 82px; 521 | } 522 | 523 | div.vis-color-picker div.vis-button.vis-apply { 524 | left: 159px; 525 | } 526 | 527 | div.vis-color-picker div.vis-button.vis-save { 528 | left: 236px; 529 | } 530 | 531 | div.vis-color-picker input.vis-range { 532 | width: 290px; 533 | height: 20px; 534 | } 535 | 536 | div.vis-network div.vis-manipulation { 537 | box-sizing: content-box; 538 | border-width: 0; 539 | border-bottom: 1px; 540 | border-style: solid; 541 | border-color: #d6d9d8; 542 | background: #fff; 543 | background: -moz-linear-gradient(top, 544 | #fff 0, 545 | #fcfcfc 48%, 546 | #fafafa 50%, 547 | #fcfcfc 100%); 548 | background: -webkit-gradient(linear, 549 | left top, 550 | left bottom, 551 | color-stop(0, #fff), 552 | color-stop(48%, #fcfcfc), 553 | color-stop(50%, #fafafa), 554 | color-stop(100%, #fcfcfc)); 555 | background: -webkit-linear-gradient(top, 556 | #fff 0, 557 | #fcfcfc 48%, 558 | #fafafa 50%, 559 | #fcfcfc 100%); 560 | background: -o-linear-gradient(top, 561 | #fff 0, 562 | #fcfcfc 48%, 563 | #fafafa 50%, 564 | #fcfcfc 100%); 565 | background: -ms-linear-gradient(top, 566 | #fff 0, 567 | #fcfcfc 48%, 568 | #fafafa 50%, 569 | #fcfcfc 100%); 570 | background: linear-gradient(to bottom, 571 | #fff 0, 572 | #fcfcfc 48%, 573 | #fafafa 50%, 574 | #fcfcfc 100%); 575 | padding-top: 4px; 576 | position: absolute; 577 | left: 0; 578 | top: 0; 579 | width: 100%; 580 | height: 28px; 581 | } 582 | 583 | div.vis-network div.vis-edit-mode { 584 | position: absolute; 585 | left: 0; 586 | top: 5px; 587 | height: 30px; 588 | } 589 | 590 | div.vis-network div.vis-close:hover { 591 | opacity: 0.6; 592 | } 593 | 594 | div.vis-network div.vis-edit-mode div.vis-button, 595 | div.vis-network div.vis-manipulation div.vis-button { 596 | float: left; 597 | font-family: verdana; 598 | font-size: 12px; 599 | -moz-border-radius: 15px; 600 | border-radius: 15px; 601 | display: inline-block; 602 | background-position: 0 0; 603 | background-repeat: no-repeat; 604 | height: 24px; 605 | margin-left: 10px; 606 | cursor: pointer; 607 | padding: 0 8px 0 8px; 608 | -webkit-touch-callout: none; 609 | -webkit-user-select: none; 610 | -khtml-user-select: none; 611 | -moz-user-select: none; 612 | -ms-user-select: none; 613 | user-select: none; 614 | } 615 | 616 | div.vis-network div.vis-manipulation div.vis-button:hover { 617 | box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.2); 618 | } 619 | 620 | div.vis-network div.vis-manipulation div.vis-button:active { 621 | box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.5); 622 | } 623 | 624 | /* div.vis-network div.vis-manipulation div.vis-button.vis-back { 625 | background-image: url(img/network/backIcon.png); 626 | } */ 627 | div.vis-network div.vis-manipulation div.vis-button.vis-none:hover { 628 | box-shadow: 1px 1px 8px transparent; 629 | cursor: default; 630 | } 631 | 632 | div.vis-network div.vis-manipulation div.vis-button.vis-none:active { 633 | box-shadow: 1px 1px 8px transparent; 634 | } 635 | 636 | div.vis-network div.vis-manipulation div.vis-button.vis-none { 637 | padding: 0; 638 | } 639 | 640 | div.vis-network div.vis-manipulation div.notification { 641 | margin: 2px; 642 | font-weight: 700; 643 | } 644 | 645 | div.vis-network div.vis-edit-mode div.vis-button.vis-edit.vis-edit-mode { 646 | background-color: #fcfcfc; 647 | border: 1px solid #ccc; 648 | } 649 | 650 | div.vis-network div.vis-edit-mode div.vis-label, 651 | div.vis-network div.vis-manipulation div.vis-label { 652 | margin: 0 0 0 23px; 653 | line-height: 25px; 654 | } 655 | 656 | div.vis-network div.vis-manipulation div.vis-separator-line { 657 | float: left; 658 | display: inline-block; 659 | width: 1px; 660 | height: 21px; 661 | background-color: #bdbdbd; 662 | margin: 0 7px 0 15px; 663 | } 664 | 665 | div.vis-network div.vis-navigation div.vis-button { 666 | width: 34px; 667 | height: 34px; 668 | -moz-border-radius: 17px; 669 | border-radius: 17px; 670 | position: absolute; 671 | display: inline-block; 672 | background-position: 2px 2px; 673 | background-repeat: no-repeat; 674 | cursor: pointer; 675 | -webkit-touch-callout: none; 676 | -webkit-user-select: none; 677 | -khtml-user-select: none; 678 | -moz-user-select: none; 679 | -ms-user-select: none; 680 | user-select: none; 681 | } 682 | 683 | div.vis-network div.vis-navigation div.vis-button:hover { 684 | box-shadow: 0 0 3px 3px rgba(56, 207, 21, 0.3); 685 | } 686 | 687 | div.vis-network div.vis-navigation div.vis-button:active { 688 | box-shadow: 0 0 1px 3px rgba(56, 207, 21, 0.95); 689 | } -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | }, 8 | }, 9 | 10 | component: { 11 | devServer: { 12 | framework: "react", 13 | bundler: "webpack", 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /cypress/components/GaugeChart.cy.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | // import GaugeChart from '../../client/components/Graphs/GaugeChart' 3 | 4 | // describe('', () => { 5 | // it('renders', () => { 6 | // // see: https://on.cypress.io/mounting-react 7 | // cy.mount() 8 | // }) 9 | // }) 10 | -------------------------------------------------------------------------------- /cypress/components/InvisibleNavbar.cy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import InvisibleNavbar from '../../client/containers/InvisibleNavbar/InvisibleNavbar' 3 | 4 | describe('', () => { 5 | it('renders', () => { 6 | // see: https://on.cypress.io/mounting-react 7 | cy.mount() 8 | }) 9 | }) -------------------------------------------------------------------------------- /cypress/components/LineGraph.cy.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react' 2 | // import LineGraph from '../../client/components/Graphs/LineGraph' 3 | 4 | // describe('', () => { 5 | // it('renders', () => { 6 | // // see: https://on.cypress.io/mounting-react 7 | // cy.mount() 8 | // }) 9 | // }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/frontEndTest.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import Chance from 'chance' 3 | const chance = new Chance() 4 | const fakeNumber = chance.integer({min:0,max:20}).toString() 5 | const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/ 6 | 7 | 8 | describe('ClusterView Test', () => { 9 | beforeEach(()=>{ 10 | Cypress.on('uncaught:exception', (err) => { 11 | if (resizeObserverLoopErrRe.test(err.message)) { 12 | return false 13 | } 14 | }) 15 | cy.visit('http://localhost:8000/') 16 | }) 17 | it('Selecting Namespaces should work') 18 | }) 19 | describe('Main DashBoard Monitor test', () => { 20 | beforeEach(()=>{ 21 | cy.visit('http://localhost:8000/dashboard') 22 | }) 23 | it('contain the title Cluster Health Monitor', () => { 24 | cy.contains('Cluster Health Monitor').should("be.visible") 25 | }) 26 | it('Load Testing Button works', ()=>{ 27 | cy.get('.invisNavButton').click() 28 | cy.get('input[placeholder="Number of VUs"]').type(fakeNumber) 29 | cy.get('input[placeholder="Test Duration"]').type('s') 30 | cy.contains('DDOS me').click() 31 | cy.on('window:alert', (alertText) => { 32 | expect(alertText).to.equal('Please fill out both fields'); 33 | }); 34 | }) 35 | }) 36 | 37 | describe('Network Performance Monitor test', () => { 38 | beforeEach(()=>{ 39 | cy.visit('http://localhost:8000/network') 40 | }) 41 | it('should contain the title Network Performance Monitor',()=>{ 42 | cy.contains('Network Performance Monitor').should("be.visible") 43 | }) 44 | }) 45 | 46 | describe('NavBar test', () => { 47 | beforeEach(()=>{ 48 | Cypress.on('uncaught:exception', (err) => { 49 | if (resizeObserverLoopErrRe.test(err.message)) { 50 | return false 51 | } 52 | }) 53 | cy.visit('http://localhost:8000/') 54 | }) 55 | it('Navigation to Main DashBoard works',()=>{ 56 | cy.get('#MainDashButton').click({force:true}) 57 | cy.url().should('include','dashboard') 58 | }) 59 | it('Navigation to Network Performance works',()=>{ 60 | cy.get('#NetworkButton').click({force:true}) 61 | cy.url().should('include','network') 62 | }) 63 | it('Edit Cluster should work',()=>{ 64 | cy.get('#EditClusterButton').click({force:true}) 65 | cy.contains('Select Namespace') 66 | }) 67 | }) 68 | 69 | describe('LineGraph Test',()=>{ 70 | const cpuRoute='/api/prom/metrics?type=cpu&hour=24' 71 | const NetworkTransmittedRoute='/api/prom/metrics?type=trans&hour=24' 72 | const memoryRoute='/api/prom/metrics?type=mem&hour=24' 73 | const NetworkRecievedRoute='/api/prom/metrics?type=rec&hour=24' 74 | beforeEach(()=>{ 75 | cy.visit('http://localhost:8000/dashboard') 76 | }) 77 | const testApiData = (apiRoute: string, expectedData:any) => { 78 | cy.intercept('GET', apiRoute, { 79 | statusCode: 200, 80 | body: expectedData, 81 | }).as('LineGraphData'); 82 | cy.wait('@LineGraphData').then((data:any) => { 83 | expect(data.response.statusCode).to.eq(200); 84 | }) 85 | } 86 | const time = Date.now() 87 | it('Loads Cpu Line Graph Data', () => { 88 | const expectedCpuData: any = [{ 89 | metric:{}, 90 | values:[[time,'1'],[time,'2'],[time,'3'], 91 | [time,'4'],[time,'5'],[time,'6']] 92 | }]; 93 | testApiData(cpuRoute, expectedCpuData); 94 | }) 95 | it('Loads memory Line Graph Data', () => { 96 | const expectedMemoryData: any = [{ 97 | metric:{}, 98 | values:[[time,'19224410448'],[time,'1902455540448'],[time,'1902440448'], 99 | [time,'190243410448'],[time,'1902424440448'],[time,'1904440448']] 100 | }]; 101 | testApiData(memoryRoute, expectedMemoryData); 102 | }) 103 | it('Loads memory Line Graph Data', () => { 104 | cy.visit('http://localhost:8000/network') 105 | const expectedNetworkTransmittedRoute: any = [{ 106 | metric:{}, 107 | values:[[time,'19248'],[time,'19020448'],[time,'19048'], 108 | [time,'190248'],[time,'40448'],[time,'90448']] 109 | }]; 110 | testApiData(NetworkTransmittedRoute, expectedNetworkTransmittedRoute); 111 | }) 112 | it('Loads memory Line Graph Data', () => { 113 | cy.visit('http://localhost:8000/network') 114 | const expectedNetworkRecievedRoute: any = [{ 115 | metric:{}, 116 | values:[[time,'19448'],[time,'1902448'],[time,'20448'], 117 | [time,'12448'],[time,'19048'],[time,'144448']] 118 | }]; 119 | testApiData(NetworkRecievedRoute, expectedNetworkRecievedRoute); 120 | }) 121 | it('Hour Drop Down Menu exists', ()=>{ 122 | cy.get('#hourDropDown').select('1') 123 | cy.contains('1 hour').should('have.value', '1') 124 | cy.get('#hourDropDown').select('6') 125 | cy.contains('6 hour').should('have.value', '6') 126 | cy.get('#hourDropDown').select('12') 127 | cy.contains('12 hour').should('have.value', '12') 128 | cy.get('#hourDropDown').select('24') 129 | cy.contains('24 hour').should('have.value', '24') 130 | }) 131 | it('Changing Name Spaces Menu exists') 132 | }) 133 | 134 | describe('Edit Cluster View Modal works',()=>{ 135 | it('Adding Namespace should work') 136 | it('Deleting Namespace should work') 137 | }) 138 | 139 | describe('Guage Graph Test', () => { 140 | const memoryApiRoute = '/api/prom/mem?type=mem&hour=24¬Time=true'; 141 | const cpuApiRoute = '/api/prom/cpu?type=cpu&hour=24¬Time=true'; 142 | type GuageApiData = Record; // define type as an object where string is key and number is key value 143 | 144 | beforeEach(() => { 145 | cy.visit(`http://localhost:8000/dashboard`); 146 | }); 147 | 148 | const testApiData = (apiRoute: string, expectedData: GuageApiData[]) => { 149 | cy.intercept('GET', apiRoute, { 150 | statusCode: 200, 151 | body: expectedData, 152 | }).as('fetchMock'); 153 | 154 | cy.wait('@fetchMock').then((data:any) => { 155 | expect(data.response.statusCode).to.eq(200); 156 | const responseBody = data.response.body; 157 | expect(responseBody.length).to.eq(3); 158 | expect(responseBody[0]).to.deep.equal(expectedData[0]); 159 | expect(responseBody[1]).to.deep.equal(expectedData[1]); 160 | expect(responseBody[2]).to.deep.equal(expectedData[2]); 161 | }); 162 | }; 163 | 164 | it('Loads Memory Gauge Data', () => { 165 | const expectedMemoryData: GuageApiData[] = [ 166 | { Memoryused: 10 }, 167 | { Memoryrequested: 40 }, 168 | { Memoryallocatable: 50 }, 169 | ]; 170 | 171 | testApiData(memoryApiRoute, expectedMemoryData); 172 | }); 173 | 174 | it('Loads CPU Gauge Data', () => { 175 | const expectedCpuData: GuageApiData[] = [ 176 | { Cpuused: 10 }, 177 | { Cpurequested: 40 }, 178 | { Cpuallocatable: 50 }, 179 | ]; 180 | testApiData(cpuApiRoute, expectedCpuData); 181 | }); 182 | }); 183 | 184 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/react18' 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount) 37 | 38 | // Example use: 39 | // cy.mount() -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/jest**/*.test.ts'], 5 | verbose: true, 6 | forceExit: true, 7 | clearMocks: true 8 | }; 9 | -------------------------------------------------------------------------------- /jest/super.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { expect, test, describe, beforeEach } from '@jest/globals' 3 | import request from 'supertest' 4 | import { Chance } from 'chance' 5 | import dotenv from 'dotenv' 6 | import app from '../server/server' 7 | import promController from '../server/controllers/promController' 8 | import clusterController from '../server/controllers/clusterController' 9 | import execController from '../server/controllers/crudController' 10 | import k6Controller from '../server/controllers/k6Controller' 11 | import mapController from '../server/controllers/mapController' 12 | import lokiController from '../server/controllers/lokiController' 13 | 14 | dotenv.config() 15 | 16 | let server: any 17 | beforeEach(() => { 18 | server = app.listen(1212) 19 | }) 20 | afterEach(() => { 21 | server.close() 22 | }) 23 | // Test for error handler 24 | describe('404 error handler', () => { 25 | test('should respond with a 404 status code for unknown API route', async () => { 26 | const res = await request(app).get('/non-existent-request') 27 | expect(res.statusCode).toEqual(404) 28 | }) 29 | }) 30 | 31 | // ?-----------------------------------------Prom Controller-------------------------------------> 32 | describe('Prometheus Controller', () => { 33 | const chance = new Chance() 34 | const hourNumber = chance.integer({ min: 1, max: 24 }).toString() 35 | const properties = Object.keys(promController) 36 | const getMetricsTestData = [ 37 | { type: 'cpu', hour: hourNumber }, 38 | { type: 'mem', hour: hourNumber }, 39 | { type: 'trans', hour: hourNumber }, 40 | { type: 'rec', hour: hourNumber } 41 | ] 42 | const getMemOrCpuTestData = [ 43 | { type: 'cpu' }, 44 | { type: 'mem' } 45 | ] 46 | // const testGlobalHandleError = [ 47 | // {endpoint:`/api/prom/metrics?type=sas&hour=24&ep=${process.env.PROMETHEUS_EP}`}, 48 | // {endpoint:`/api/prom/metrics`}, 49 | // ] 50 | getMetricsTestData.forEach((entry) => { 51 | test(`/GET ${entry.type} for Line Chart returns data`, async () => { 52 | const res = await request(app).get(`/api/prom/metrics?type=${entry.type}&hour=${entry.hour}&ep=${process.env.PROMETHEUS_EP ?? ''}`) 53 | expect(res.status).toEqual(200) 54 | expect(Array.isArray(res.body)).toEqual(true) 55 | // expect(res.body).toMatchSnapshot() 56 | }) 57 | }) 58 | getMemOrCpuTestData.forEach((entry) => { 59 | test(`/Get ${entry.type} for Guage Chart returns data`, async () => { 60 | const res = await request(app).get(`/api/prom/${entry.type}?type=${entry.type}&hour=24¬Time=true&ep=${process.env.PROMETHEUS_EP ?? ''}`) 61 | expect(res.status).toEqual(200) 62 | expect(typeof res.body === 'object').toEqual(true) 63 | // expect(res.body).toMatchSnapshot() 64 | }) 65 | }) 66 | // testGlobalHandleError.forEach((entry)=>{ 67 | // test('should handle global middleware errors', async()=>{ 68 | // const defaultErr = { 69 | // log: 'Error caught in global handler', 70 | // status: 400, 71 | // message:{error: 'Error getting Data'} 72 | // }; 73 | // const response = await request(app) 74 | // .get(entry.endpoint) 75 | // .expect(400) 76 | // expect(response.body).toEqual(defaultErr.message); 77 | // }) 78 | // }) 79 | test(`should have ${Object.keys(promController) as unknown as string}`, () => { 80 | properties.forEach((prop) => { expect(promController).toHaveProperty(prop) }) 81 | }) 82 | test('All Properties should be a function', () => { 83 | properties.forEach((prop) => { 84 | expect(typeof (promController as any)[prop]).toBe('function') 85 | }) 86 | }) 87 | }) 88 | // ?----------------------------------ClusterController------------------------------------------> 89 | describe('Cluster Controller', () => { 90 | const properties = Object.keys(clusterController) 91 | const ClusterEndPoint = [ 92 | { type: 'namespaces' }, 93 | { type: 'pods' }, 94 | { type: 'nodes' }, 95 | { type: 'services' }, 96 | { type: 'deployments' }, 97 | { type: 'ingresses' } 98 | ] 99 | ClusterEndPoint.forEach((endpoint) => { 100 | test(`/Get ${endpoint.type} returns data`, async () => { 101 | const res = await request(app).get(`/api/cluster/${endpoint.type}`) 102 | expect(res.status).toEqual(200) 103 | }) 104 | }) 105 | test(`should have ${Object.keys(clusterController) as unknown as string}`, () => { 106 | properties.forEach((prop) => { expect(clusterController).toHaveProperty(`${prop}`) }) 107 | }) 108 | 109 | test('All Properties should be a function', () => { 110 | properties.forEach((prop) => { 111 | expect(typeof (clusterController as any)[prop]).toBe('function') 112 | /* expect(prop.constructor.name).toBe('function'); */ 113 | }) 114 | }) 115 | }) 116 | // ?-------------------------------Exec Controller------------------------------------------------> 117 | describe('Exec Controller', () => { 118 | const properties = Object.keys(execController) 119 | // const ExecEndPoint = [ 120 | // { type: 'ns' }, 121 | // { type: 'dep' }, 122 | // ] 123 | // ExecEndPoint.forEach((endpoint) => { 124 | // test(`/Get ${endpoint.type} returns data`, async () => { 125 | // const res = await request(app).get(`/api/exec/${endpoint.type}`) 126 | // expect(res.status).toEqual(400) 127 | // }) 128 | // }) 129 | test(`should have ${Object.keys(execController) as unknown as string}`, () => { 130 | properties.forEach((prop) => { expect(execController).toHaveProperty(`${prop}`) }) 131 | }) 132 | 133 | test('All Properties should be a function', () => { 134 | properties.forEach((prop) => { 135 | expect(typeof (execController as any)[prop]).toBe('function') 136 | }) 137 | }) 138 | }) 139 | // ?---------------------------------K6 Controller------------------------------------------------> 140 | describe('k6Controller Controller', () => { 141 | const properties = Object.keys(k6Controller) 142 | test('/Get elements returns data', async () => { 143 | const res = await request(app).get('/api/k6/test') 144 | expect(res.status).toEqual(200) 145 | }) 146 | test(`should have ${Object.keys(k6Controller) as unknown as string}`, () => { 147 | properties.forEach((prop) => { expect(k6Controller).toHaveProperty(prop) }) 148 | }) 149 | }) 150 | // ?-----------------------------------------Map Controller----------------------------------------> 151 | describe('mapController Controller', () => { 152 | const properties = Object.keys(mapController) 153 | test('/Get elements returns data', async () => { 154 | const res = await request(app).get('/api/map/elements') 155 | expect(res.status).toEqual(200) 156 | }) 157 | test(`should have ${Object.keys(mapController) as unknown as string}`, () => { 158 | properties.forEach((prop) => { expect(mapController).toHaveProperty(prop) }) 159 | }) 160 | 161 | test('All Properties should be a function', () => { 162 | properties.forEach((prop) => { 163 | expect(typeof (mapController as any)[prop]).toBe('function') 164 | }) 165 | }) 166 | }) 167 | // ?-------------------------------------Loki Controller-----------------------------------------> 168 | describe('Loki Controller', () => { 169 | const properties = Object.keys(lokiController) 170 | 171 | test('/Get logs returns data', async () => { 172 | const res = await request(app).get('/api/loki/logs?namespace=gmp-system') 173 | expect(res.status).toEqual(200) 174 | }) 175 | test(`should have ${Object.keys(lokiController) as unknown as string}`, () => { 176 | properties.forEach((prop) => { expect(lokiController).toHaveProperty(prop) }) 177 | }) 178 | test('All Properties should be a function', () => { 179 | properties.forEach((prop) => { 180 | expect(typeof (lokiController as any)[prop]).toBe('function') 181 | }) 182 | }) 183 | }) 184 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubernautical", 3 | "description": "Kubernetes project", 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "scripts": { 7 | "supertest": "NODE_ENV=test concurrently \"jest --watchAll --updateSnapshot --detectOpenHandles --coverage\" \"server/server.ts\" \"kubectl --namespace=prometheus port-forward deploy/prometheus-server 9090\"", 8 | "cypress:open": "cypress open", 9 | "start": "NODE_ENV=production nodemon server/server.ts", 10 | "build": "NODE_ENV=production webpack", 11 | "dev": "NODE_ENV=development nodemon server/server.ts & NODE_ENV=development webpack serve --open --hot", 12 | "devc": "NODE_ENV=development concurrently \"webpack serve --open\" \"nodemon server/server.ts\"", 13 | "devServer": "NODE_ENV=production nodemon server/server.ts", 14 | "devkube": "NODE_ENV=development concurrently \"webpack serve --open --hot\" \"nodemon server/server.ts\"", 15 | "lint": "eslint --ext .tsx,.tx,.js --ignore-path .gitignore ." 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/oslabs-beta/Kubernautical" 20 | }, 21 | "keywords": [ 22 | "Kubernetes" 23 | ], 24 | "author": "Jeremiah Hogue", 25 | "license": "ISC", 26 | "dependencies": { 27 | "@babel/preset-typescript": "^7.22.5", 28 | "@babel/register": "^7.22.5", 29 | "@kubernetes/client-node": "^0.18.1", 30 | "@types/k6": "^0.45.1", 31 | "@types/uuid": "^9.0.2", 32 | "chart.js": "^4.3.3", 33 | "cookie-parser": "^1.4.6", 34 | "d3": "^7.8.5", 35 | "dotenv": "^16.3.1", 36 | "express": "^4.18.2", 37 | "jest": "^29.6.2", 38 | "react": "^18.2.0", 39 | "react-chartjs-2": "^5.2.0", 40 | "react-dom": "^18.2.0", 41 | "react-force-graph": "^1.43.0", 42 | "react-graph-vis": "^1.0.7", 43 | "react-router-dom": "^6.14.2", 44 | "sass": "^1.64.1", 45 | "supertest": "^6.3.3", 46 | "three": "^0.155.0", 47 | "ts-jest": "^29.1.1", 48 | "ts-loader": "^9.4.4", 49 | "ts-node": "^10.9.1", 50 | "uuid": "^9.0.0", 51 | "vis-network": "^9.1.6" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.22.9", 55 | "@babel/preset-env": "^7.22.9", 56 | "@babel/preset-react": "^7.22.5", 57 | "@testing-library/react": "^13.4.0", 58 | "@testing-library/user-event": "^14.4.3", 59 | "@types/chance": "^1.1.3", 60 | "@types/cors": "^2.8.12", 61 | "@types/express": "^4.17.14", 62 | "@types/jest": "^29.1.1", 63 | "@types/node": "^18.7.20", 64 | "@types/react": "^18.0.21", 65 | "@types/react-dom": "^18.0.6", 66 | "@types/supertest": "^2.0.12", 67 | "@types/vis": "^4.21.24", 68 | "@typescript-eslint/eslint-plugin": "^5.62.0", 69 | "@typescript-eslint/parser": "^5.40.0", 70 | "babel": "^6.23.0", 71 | "babel-loader": "^9.1.3", 72 | "chance": "^1.1.11", 73 | "concurrently": "^8.2.0", 74 | "css-loader": "^6.8.1", 75 | "cypress": "^12.17.3", 76 | "eslint": "^8.47.0", 77 | "eslint-config-airbnb": "^19.0.4", 78 | "eslint-config-airbnb-base": "^15.0.0", 79 | "eslint-config-standard-with-typescript": "^37.0.0", 80 | "eslint-plugin-import": "^2.28.0", 81 | "eslint-plugin-jsx-a11y": "^6.7.1", 82 | "eslint-plugin-n": "^16.0.1", 83 | "eslint-plugin-promise": "^6.1.1", 84 | "eslint-plugin-react": "^7.33.2", 85 | "eslint-plugin-react-hooks": "^4.6.0", 86 | "file-loader": "^6.2.0", 87 | "html-webpack-plugin": "^5.5.3", 88 | "nodemon": "^3.0.1", 89 | "sass-loader": "^13.3.2", 90 | "style-loader": "^3.3.3", 91 | "typescript": "^5.1.6", 92 | "url-loader": "^4.1.1", 93 | "webpack": "^5.88.1", 94 | "webpack-cli": "^5.1.4", 95 | "webpack-dev-server": "^4.15.1", 96 | "webpack-manifest-plugin": "^5.0.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /scripts/loadtest.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { sleep } from 'k6'; 3 | 4 | export default function () { 5 | //pull ip dynamically 6 | //? IN TERMINAL : export CLUSTER_ENDPOINT= 'your endpoint here' 7 | const response = http.get(`${__ENV.INGRESS_EP}`); 8 | if (response.status === 200) { 9 | console.log('Request successful!'); 10 | } else { 11 | console.log(`Request failed with status code: ${response.status}`); 12 | } 13 | sleep(1); 14 | } 15 | -------------------------------------------------------------------------------- /server/controllers/clusterController.ts: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import * as k8s from '@kubernetes/client-node' 3 | import type { Request, Response, NextFunction, RequestHandler } from 'express' 4 | import { type ClusterController, type ClientObj, type container } from '../../types/types' 5 | 6 | const clusterController: ClusterController = { 7 | // setContext called before every route to obtain the correct cluster information 8 | setContext: (async (req: Request, res: Response, next: NextFunction): Promise => { 9 | // TODO fix typing 10 | const context = req.query.context as string 11 | console.log('context:', context) 12 | try { 13 | // pull kube config from secret dir 14 | const KUBE_FILE_PATH = `${os.homedir()}/.kube/config` 15 | // create kubernetes client using kube config 16 | const kc = new k8s.KubeConfig() 17 | kc.loadFromFile(KUBE_FILE_PATH) 18 | // expose all necessary apis for further use in middleware using given context 19 | if (context !== undefined && context !== '') kc.setCurrentContext(context) 20 | res.locals.currentContext = kc.getCurrentContext() 21 | res.locals.contexts = kc.getContexts() 22 | res.locals.k8sApi = kc.makeApiClient(k8s.CoreV1Api) // used for nodes, pods, namespaces 23 | res.locals.k8sApi2 = kc.makeApiClient(k8s.AppsV1Api) // used for deployments and services 24 | res.locals.k8sApi3 = kc.makeApiClient(k8s.NetworkingV1Api) // used for ingress 25 | next() 26 | } catch (error) { 27 | next(error) 28 | } 29 | }) as RequestHandler, 30 | getAllPods: (async (req: Request, res: Response, next: NextFunction): Promise => { 31 | const { k8sApi } = res.locals 32 | try { 33 | const result = await k8sApi.listPodForAllNamespaces() 34 | const pods = result.body.items.map((pod: ClientObj) => { 35 | const { name, namespace, uid } = pod.metadata 36 | const { nodeName, serviceAccount, subdomain } = pod.spec 37 | const { containerStatuses, phase } = pod.status 38 | const containerNames = containerStatuses?.map((container: container) => (container.name)) 39 | return { 40 | name, 41 | namespace, 42 | uid, 43 | containerNames, 44 | nodeName, 45 | serviceAccount, 46 | subdomain, 47 | phase 48 | } 49 | }) 50 | res.locals.pods = pods 51 | next() 52 | } catch (error) { 53 | next({ 54 | log: `Error in clusterController.getAllPods${error as string}`, 55 | status: 400, 56 | message: { error: 'Error getting data' } 57 | }) 58 | } 59 | }) as RequestHandler, 60 | getAllNodes: (async (req: Request, res: Response, next: NextFunction): Promise => { 61 | const { k8sApi } = res.locals 62 | try { 63 | const result = await k8sApi.listNode() 64 | const nodes = result.body.items.map((node: ClientObj) => { 65 | const { name, uid, labels } = node.metadata 66 | const { allocatable, capacity, conditions, nodeInfo } = node.status 67 | return { 68 | name, 69 | labels, 70 | uid, 71 | allocatable, 72 | capacity, 73 | conditions, 74 | nodeInfo 75 | } 76 | }) 77 | res.locals.nodes = nodes 78 | next() 79 | } catch (error) { 80 | next({ 81 | log: `Error in clusterController.getAllNodes${error as string}`, 82 | status: 400, 83 | message: { error: 'Error getting data' } 84 | }) 85 | } 86 | }) as RequestHandler, 87 | getAllNamespaces: (async (req: Request, res: Response, next: NextFunction): Promise => { 88 | const { k8sApi } = res.locals 89 | const { all } = req.query 90 | try { 91 | const result = await k8sApi.listNamespace() 92 | const filtered = all === 'true' 93 | ? result.body.items 94 | : result.body.items.filter((namespace: ClientObj) => { 95 | const name = namespace.metadata?.name 96 | return !!(name?.slice(0, 4) !== 'kube' && name?.slice(0, 7) !== 'default' && name?.slice(0, 10) !== 'gmp-public') 97 | // && name?.slice(0, 4) !== 'loki' 98 | }) 99 | const namespaces = filtered.map((namespace: ClientObj) => { 100 | const { metadata } = namespace 101 | return { 102 | name: metadata?.name, 103 | uid: metadata?.uid 104 | } 105 | }) 106 | res.locals.namespaces = namespaces 107 | next() 108 | } catch (error) { 109 | next({ 110 | log: `Error in clusterController.getAllNameSpaces${error as string}`, 111 | status: 400, 112 | message: { error: 'Error getting data' } 113 | }) 114 | } 115 | }) as RequestHandler, 116 | getAllServices: (async (req: Request, res: Response, next: NextFunction): Promise => { 117 | const { k8sApi } = res.locals 118 | try { 119 | const result = await k8sApi.listServiceForAllNamespaces() 120 | const services = result.body.items 121 | .map((service: ClientObj) => { 122 | const { name, uid, namespace } = service.metadata 123 | const { ipFamilies, ports, type } = service.spec 124 | const ips = service.status?.loadBalancer?.ingress 125 | let ingressIP 126 | if (ips !== undefined) ingressIP = ips[0]?.ip ?? ips[0]?.hostname 127 | // TODO pull more data as needed here 128 | return { 129 | name, 130 | uid, 131 | namespace, 132 | ipFamilies, 133 | ports, 134 | ingressIP, 135 | type 136 | } 137 | }) 138 | res.locals.services = services 139 | next() 140 | } catch (error) { 141 | next({ 142 | log: `Error in clusterController.getAllServices${error as string}`, 143 | status: 400, 144 | message: { error: 'Error getting data' } 145 | }) 146 | } 147 | }) as RequestHandler, 148 | getAllDeployments: (async (req: Request, res: Response, next: NextFunction): Promise => { 149 | const { k8sApi2 } = res.locals 150 | try { 151 | const result = await k8sApi2.listDeploymentForAllNamespaces() 152 | const deployments = result.body.items 153 | .map((deployment: ClientObj) => { 154 | const { name, uid, namespace } = deployment.metadata 155 | const { strategy } = deployment.spec 156 | const { type } = strategy 157 | const { availableReplicas } = deployment.status 158 | // TODO pull more data as needed here 159 | return { 160 | name, 161 | uid, 162 | namespace, 163 | type, 164 | availableReplicas 165 | } 166 | }) 167 | res.locals.deployments = deployments 168 | next() 169 | } catch (error) { 170 | next({ 171 | log: `Error in clusterController.getAllDeployments${error as string}`, 172 | status: 400, 173 | message: { error: 'Error getting data' } 174 | }) 175 | } 176 | }) as RequestHandler, 177 | //! We dont have any ingresses yet 178 | getAllIngresses: (async (req: Request, res: Response, next: NextFunction): Promise => { 179 | const { k8sApi3 } = res.locals 180 | try { 181 | const result = await k8sApi3.listIngressForAllNamespaces() 182 | res.locals.ingresses = result 183 | next() 184 | } catch (error) { 185 | next({ 186 | log: `Error in clusterController.getAllIngresses${error as string}`, 187 | status: 400, 188 | message: { error: 'Error getting data' } 189 | }) 190 | } 191 | }) as RequestHandler 192 | } 193 | 194 | export default clusterController 195 | -------------------------------------------------------------------------------- /server/controllers/crudController.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import type { Request, Response, NextFunction, RequestHandler } from 'express' 3 | import { type CrudController } from '../../types/types' 4 | 5 | const crudController: CrudController = { 6 | namespace: (async (req: Request, res: Response, next: NextFunction): Promise => { 7 | const { namespace, crud, context } = req.query 8 | try { 9 | const command = `kubectl config use-context ${context as string}\ 10 | && kubectl ${crud as string} namespace ${namespace as string}` 11 | exec(command, (err, stdout, stderr) => { 12 | if (err != null) { 13 | console.log('Error executing command:', err) 14 | throw new Error() 15 | } 16 | console.log('stdout:', stdout) 17 | next() 18 | }) 19 | } catch (error) { 20 | next({ 21 | log: `Error in crudController.namespace${error as string}`, 22 | status: 400, 23 | message: { error: 'Error getting data' } 24 | }) 25 | } 26 | }) as RequestHandler, 27 | deployment: (async (req: Request, res: Response, next: NextFunction): Promise => { 28 | const { 29 | namespace, 30 | crud, 31 | image, 32 | deployment, 33 | replicas, 34 | type, 35 | port, 36 | targetPort, 37 | old, 38 | context, 39 | name 40 | } = req.query 41 | try { 42 | let action = '' 43 | switch (crud) { 44 | case 'create': 45 | action = `--image=${image as string}` 46 | break 47 | case 'scale': 48 | action = `--replicas=${replicas as string}` 49 | break 50 | case 'expose': 51 | action = `--port=${port as string} --target-port=${targetPort as string}\ 52 | --type=${type as string} --name=${name as string}` 53 | break 54 | default: 55 | break 56 | } 57 | // TODO handle errors/edge cases from front end 58 | const command = `kubectl config use-context ${context as string} \ 59 | && kubectl ${crud as string} deployment ${deployment as string}\ 60 | ${action !== undefined ? `${action}` : ''} -n ${namespace as string}` 61 | console.log('command:', command) 62 | exec(command, (err, stdout, stderr) => { 63 | if (err != null) { 64 | console.log('Error executing command:', err) 65 | throw new Error() 66 | } 67 | console.log('stdout:', stdout) 68 | let timeOut = 2000 69 | if (old !== undefined && replicas !== undefined) { 70 | timeOut = old < replicas ? 5000 : 45000 71 | } 72 | return setTimeout(() => { 73 | next() 74 | }, timeOut) // 45 works 75 | }) 76 | } catch (error) { 77 | next({ 78 | log: `Error in crudController.deployment${error as string}`, 79 | status: 400, 80 | message: { error: 'Error getting data' } 81 | }) 82 | } 83 | }) as RequestHandler, 84 | service: (async (req: Request, res: Response, next: NextFunction): Promise => { 85 | const { namespace, crud, service, context } = req.query 86 | // type port and targetPort 87 | try { 88 | const action = '' 89 | switch (crud) { 90 | case 'create': 91 | // action = `--image=${image}`; 92 | break 93 | case 'scale': 94 | // action = `--replicas=${replicas}`; 95 | break 96 | case 'expose': 97 | // action = `--port=${port} --target-port=${targetPort} --type=${type}`; 98 | break 99 | default: 100 | break 101 | } 102 | // TODO handle errors/edge cases from front end 103 | const command = `kubectl config use-context ${context as string}\ 104 | && kubectl ${crud as string} svc ${service as string}\ 105 | ${action !== undefined ? `${action}` : ''} -n ${namespace as string}` 106 | console.log('command:', command) 107 | exec(command, (err, stdout, stderr) => { 108 | if (err != null) { 109 | console.log('Error executing command:', err) 110 | throw new Error() 111 | } 112 | console.log('stdout:', stdout) 113 | return setTimeout(() => { next() }, 1000) 114 | }) 115 | } catch (error) { 116 | next({ 117 | log: `Error in crudController.deployment${error as string}`, 118 | status: 400, 119 | message: { error: 'Error getting data' } 120 | }) 121 | } 122 | }) as RequestHandler 123 | } 124 | 125 | export default crudController 126 | -------------------------------------------------------------------------------- /server/controllers/k6Controller.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | import type { Request, Response, NextFunction, RequestHandler } from 'express' 3 | import { type K6Controller } from '../../types/types' 4 | 5 | const k6Controller: K6Controller = { 6 | testing: (async (req: Request, res: Response, next: NextFunction): Promise => { 7 | const { vus, duration, ip } = req.query 8 | try { 9 | let command = `export INGRESS_EP=http://${ip} && k6 run` 10 | if (vus !== undefined && duration !== undefined) { 11 | command += ` --vus ${vus as string} --duration ${duration as string}s scripts/loadtest.js` 12 | } else throw new Error() 13 | console.log(command) 14 | exec(command, (err) => { 15 | if (err !== null) { 16 | throw new Error() 17 | } 18 | }) 19 | next() 20 | } catch (error) { 21 | next({ 22 | log: `Error in k6Controller.testing ${error as string}`, 23 | status: 400, 24 | message: { error: 'Error getting data' } 25 | }) 26 | } 27 | }) as RequestHandler 28 | } 29 | 30 | export default k6Controller 31 | -------------------------------------------------------------------------------- /server/controllers/lokiController.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction, RequestHandler } from 'express' 2 | import { type LokiController } from '../../types/types' 3 | 4 | // require('dotenv').config() 5 | 6 | // const GATE = process.env.LOKI_GATE; 7 | const lokiController: LokiController = { 8 | testing: (async (req: Request, res: Response, next: NextFunction): Promise => { 9 | // {cluster=~".+"} |= "level=error" 10 | try { 11 | const { namespace, log, pod, ep } = req.query 12 | // limit, start, and end 13 | // lokiEndpoint using exposed gateway IP via load balancer 14 | const lokiEndpoint = `http://${ep as string}/loki/api/v1/query_range?query=` 15 | let logQuery = `{namespace="${namespace as string}"${pod !== undefined && pod !== '' ? `, pod="${pod as string}"` : ''}}` 16 | 17 | // havent tested start and end yet 18 | // if (start && end) logQuery += `{time >= ${start} and time <= ${end}}`; 19 | 20 | // query by type, error or info 21 | if (log !== undefined && log !== '') logQuery += ` |= "level=${log as string}"` 22 | 23 | // if (limit) logQuery += `&limit=${limit}`; 24 | const query = lokiEndpoint + logQuery 25 | const response = await fetch(query) 26 | const data = await response.json() 27 | 28 | res.locals.data = data 29 | 30 | next() 31 | } catch (error) { 32 | next({ 33 | log: `Error happened at lokiController.logs${error as string}`, 34 | status: 400, 35 | message: { error: 'Error getting data' } 36 | }) 37 | } 38 | }) as RequestHandler 39 | } 40 | 41 | export default lokiController 42 | -------------------------------------------------------------------------------- /server/controllers/mapController.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction, RequestHandler } from 'express' 2 | import { type MapController } from '../../types/types' 3 | 4 | const mapController: MapController = { 5 | 6 | getElements: (async (req: Request, res: Response, next: NextFunction): Promise => { 7 | try { 8 | const { pods, namespaces, deployments, services, contexts, currentContext } = res.locals 9 | res.locals.elements = { 10 | pods, 11 | namespaces, 12 | deployments, 13 | services, 14 | contexts, 15 | currentContext 16 | } 17 | next() 18 | } catch (error) { 19 | next({ 20 | log: `Error in mapController.getElements${error as string}`, 21 | status: 400, 22 | message: { error: 'Error getting data' } 23 | }) 24 | } 25 | }) as RequestHandler 26 | } 27 | 28 | export default mapController 29 | -------------------------------------------------------------------------------- /server/controllers/promController.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response, NextFunction, RequestHandler } from 'express' 2 | import type { prometheusController } from '../../types/types' 3 | 4 | // start and end total time period, step is how often to grab data point 5 | // [time in minutes] averages to smoothe the graph 6 | // sum 7 | // summation 8 | // rate 9 | // per second usage 10 | // 100 does (changed for 2.82 (940*3/1000) cores) 11 | // number of cores used * 100/2.82 (cores used * 35.46) 12 | // how to find available cores dynamically ---- Steves next task 13 | 14 | const promController: prometheusController = { 15 | // TODO refactor controllers 16 | 17 | getMetrics: (async (req: Request, res: Response, next: NextFunction): Promise => { 18 | const { type, hour, scope, name, notTime, ep } = req.query // pods--->scope, podname---->name 19 | console.log(ep) 20 | // ? default start, stop, step, query string 21 | let start = new Date(Date.now() - 1440 * 60000).toISOString() // 24 hours 22 | const end = new Date(Date.now()).toISOString() 23 | let step = 10 24 | let query = `http://${ep as string}/api/v1/${notTime !== undefined ? 'query' : 'query_range'}?query=` 25 | 26 | // api/prom/metrics?type=cpu&hour=24&name=gmp-system 27 | // ^hour is required 28 | const userCores = 100 / res.locals.available 29 | start = new Date(Date.now() - Number(hour) * 3600000).toISOString() 30 | step = Math.ceil((step / (24 / Number(hour)))) 31 | 32 | //! <----------------------------QUERIES (NOW MODULARIZED)-------------------------------------> 33 | if (type === 'cpu') { 34 | query += `sum(rate(container_cpu_usage_seconds_total{container!="", 35 | ${scope !== undefined ? `${scope as string}="${name as string}"` : ''}}[10m])) 36 | ${notTime === undefined ? `*${userCores}` : ''}` 37 | } 38 | if (type === 'mem') { 39 | query += `sum(container_memory_usage_bytes{container!="", 40 | ${scope !== undefined ? `${scope as string}="${name as string}"` : ''}})` 41 | } 42 | if (type === 'trans') { 43 | query += `sum(rate(container_network_transmit_bytes_total 44 | ${scope !== undefined ? `{${scope as string}="${name as string}"}` : ''}[10m]))` 45 | } 46 | if (type === 'rec') { 47 | query += `sum(rate(container_network_receive_bytes_total 48 | ${scope !== undefined ? `{${scope as string}="${name as string}"}` : ''}[10m]))` 49 | } 50 | if (notTime === undefined) query += `&start=${start}&end=${end}&step=${step}m` 51 | try { 52 | const response = await fetch(query) 53 | const result = await response.json() 54 | if (notTime !== undefined && type === 'cpu') { 55 | const usedCpu = result.data.result[0].value[1] 56 | res.locals.usedCpu = usedCpu 57 | } else if (notTime !== undefined && type === 'mem') { 58 | const usedMem = result.data.result[0].value[1] 59 | res.locals.usedMem = usedMem 60 | } else res.locals.data = result.data.result 61 | next() 62 | } catch (error) { 63 | next({ 64 | log: `Error in promController.getMetrics${error as string}`, 65 | status: 400, 66 | message: { error: 'Error getting Data' } 67 | }) 68 | } 69 | }) as RequestHandler, 70 | getCores: (async (req: Request, res: Response, next: NextFunction): Promise => { 71 | const { ep } = req.query 72 | try { 73 | const response = await fetch(`http://${ep as string}/api/v1/query?query=sum(kube_node_status_allocatable{resource="cpu"})`) 74 | const result = await response.json() 75 | const availableCores = result.data.result[0].value[1] 76 | res.locals.available = availableCores 77 | next() 78 | } catch (error) { 79 | next({ 80 | log: `Error in promController.getCores${error as string}`, 81 | status: 400, 82 | message: { error: 'Error getting Data' } 83 | }) 84 | } 85 | }) as RequestHandler, 86 | getCpu: (async (req: Request, res: Response, next: NextFunction): Promise => { 87 | const { ep } = req.query 88 | const requestedQuery = `http://${ep as string}/api/v1/query?query=sum(kube_pod_container_resource_requests{resource="cpu"})` 89 | try { 90 | const { usedCpu, available } = res.locals 91 | const response = await fetch(requestedQuery) 92 | const requested = Number((await response.json()).data.result[0].value[1]) 93 | const remaining = available - (requested + Number(usedCpu)) 94 | const cpuArr = [usedCpu, requested, remaining] 95 | const cpuPercents = cpuArr.map((value) => (value / available) * 100) 96 | res.locals.cpuPercents = [ 97 | { usedCpu: cpuPercents[0] }, 98 | { requestedCpu: cpuPercents[1] }, 99 | { availableCpu: cpuPercents[2] } 100 | ] 101 | next() 102 | } catch (error) { 103 | next({ 104 | log: `Error in promController.getCpu${error as string}`, 105 | status: 400, 106 | message: { error: 'Error getting Data' } 107 | }) 108 | } 109 | }) as RequestHandler, 110 | getMem: (async (req: Request, res: Response, next: NextFunction): Promise => { 111 | const { ep } = req.query 112 | const query = `http://${ep as string}/api/v1/query?query=` 113 | const totalMemory = `${query}sum(node_memory_MemTotal_bytes)` 114 | const reqMemory = `${query}sum(kube_pod_container_resource_requests{resource="memory"})` 115 | 116 | try { 117 | const { usedMem } = res.locals 118 | const response = await fetch(totalMemory) 119 | const totalMem = Number((await response.json()).data.result[0].value[1]) 120 | const response2 = await fetch(reqMemory) 121 | const reqMem = Number((await response2.json()).data.result[0].value[1]) 122 | const remaining = totalMem - (reqMem + Number(usedMem)) 123 | const memArr = [usedMem, reqMem, remaining] 124 | const memoryPercents = memArr.map((value) => (value / totalMem) * 100) 125 | res.locals.memoryPercents = [ 126 | { usedMemory: memoryPercents[0] }, 127 | { requestedMemory: memoryPercents[1] }, 128 | { availableMemory: memoryPercents[2] } 129 | ] 130 | next() 131 | } catch (error) { 132 | next({ 133 | log: `Error in promController.getMem${error as string}`, 134 | status: 400, 135 | message: { error: 'Error getting Data' } 136 | }) 137 | } 138 | }) as RequestHandler 139 | } 140 | 141 | export default promController 142 | -------------------------------------------------------------------------------- /server/routers/apiRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import promRouter from './promRouter' 3 | import clusterRouter from './clusterRouter' 4 | import mapRouter from './mapRouter' 5 | import k6Router from './k6Router' 6 | import crudRouter from './crudRouter' 7 | import lokiRouter from './lokiRouter' 8 | 9 | const router = express.Router() 10 | 11 | // routes for various functional parts of the application 12 | router.use('/prom', promRouter) 13 | router.use('/cluster', clusterRouter) 14 | router.use('/map', mapRouter) 15 | router.use('/k6', k6Router) 16 | router.use('/crud', crudRouter) 17 | router.use('/loki', lokiRouter) 18 | 19 | export default router 20 | -------------------------------------------------------------------------------- /server/routers/clusterRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { Request, Response, NextFunction } from 'express' 3 | import clusterController from '../controllers/clusterController' 4 | 5 | const router = express.Router() 6 | 7 | // routes for retrieving imorantant cluster information 8 | // setContext called before every route to obatin the correct cluster information 9 | router.get( 10 | '/namespaces', 11 | clusterController.setContext, 12 | 13 | clusterController.getAllNamespaces, 14 | 15 | (req: Request, res: Response, next: NextFunction) => { 16 | res.status(200).json(res.locals.namespaces) 17 | } 18 | ) 19 | router.get( 20 | '/pods', 21 | clusterController.setContext, 22 | 23 | clusterController.getAllPods, 24 | 25 | (req: Request, res: Response, next: NextFunction) => { 26 | res.status(200).json(res.locals.pods) 27 | } 28 | ) 29 | router.get( 30 | '/nodes', 31 | clusterController.setContext, 32 | 33 | clusterController.getAllNodes, 34 | 35 | (req: Request, res: Response, next: NextFunction) => { 36 | res.status(200).json(res.locals.nodes) 37 | } 38 | ) 39 | router.get( 40 | '/services', 41 | clusterController.setContext, 42 | 43 | clusterController.getAllServices, 44 | 45 | (req: Request, res: Response, next: NextFunction) => { 46 | res.status(200).json(res.locals.services) 47 | } 48 | ) 49 | router.get( 50 | '/deployments', 51 | clusterController.setContext, 52 | 53 | clusterController.getAllDeployments, 54 | 55 | (req: Request, res: Response, next: NextFunction) => { 56 | res.status(200).json(res.locals.deployments) 57 | } 58 | ) 59 | 60 | //! ------------------------------These Routes are for testing (for now)---------------------------> 61 | router.get( 62 | '/ingresses', 63 | clusterController.setContext, 64 | 65 | clusterController.getAllIngresses, 66 | 67 | (req: Request, res: Response, next: NextFunction) => { 68 | res.status(200).json(res.locals.ingresses) 69 | } 70 | ) 71 | 72 | export default router 73 | -------------------------------------------------------------------------------- /server/routers/crudRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { Request, Response, NextFunction } from 'express' 3 | import crudController from '../controllers/crudController' 4 | 5 | const router = express.Router() 6 | 7 | router.get( 8 | '/ns', 9 | crudController.namespace, 10 | 11 | (req: Request, res: Response, next: NextFunction) => { 12 | res.status(200).json({ message: 'success' }) 13 | } 14 | ) 15 | router.get( 16 | '/dep', 17 | crudController.deployment, 18 | 19 | (req: Request, res: Response, next: NextFunction) => { 20 | res.status(200).json({ message: 'success' }) 21 | } 22 | ) 23 | router.get( 24 | '/svc', 25 | crudController.service, 26 | 27 | (req: Request, res: Response, next: NextFunction) => { 28 | res.status(200).json({ message: 'success' }) 29 | } 30 | ) 31 | 32 | export default router 33 | -------------------------------------------------------------------------------- /server/routers/k6Router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { Request, Response, NextFunction } from 'express' 3 | import k6Controller from '../controllers/k6Controller' 4 | 5 | const router = express.Router() 6 | 7 | router.get( 8 | '/test', 9 | k6Controller.testing, 10 | 11 | (req: Request, res: Response, next: NextFunction) => { 12 | res.status(200).json({ message: 'Load test triggered succeessfully' }) 13 | } 14 | ) 15 | 16 | export default router 17 | -------------------------------------------------------------------------------- /server/routers/lokiRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { Request, Response, NextFunction } from 'express' 3 | import lokiController from '../controllers/lokiController' 4 | 5 | const router = express.Router() 6 | 7 | router.get( 8 | '/logs', 9 | lokiController.testing, 10 | 11 | (req: Request, res: Response, next: NextFunction) => { 12 | res.status(200).json(res.locals.data) 13 | } 14 | ) 15 | 16 | export default router 17 | -------------------------------------------------------------------------------- /server/routers/mapRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { Request, Response, NextFunction } from 'express' 3 | import mapController from '../controllers/mapController' 4 | import clusterController from '../controllers/clusterController' 5 | 6 | const router = express.Router() 7 | 8 | router.get( 9 | '/elements', 10 | clusterController.setContext, 11 | clusterController.getAllPods, 12 | // clusterController.getAllNodes, 13 | clusterController.getAllNamespaces, 14 | clusterController.getAllDeployments, 15 | clusterController.getAllServices, 16 | mapController.getElements, 17 | (req: Request, res: Response, next: NextFunction) => { 18 | res.status(200).json(res.locals.elements) // fix to send elements back 19 | 20 | // res.status(200).json(res.locals.result); 21 | } 22 | ) 23 | 24 | export default router 25 | -------------------------------------------------------------------------------- /server/routers/promRouter.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { Request, Response, NextFunction } from 'express' 3 | import promController from '../controllers/promController' 4 | 5 | const router = express.Router() 6 | 7 | router.get( 8 | '/metrics', 9 | promController.getCores, 10 | promController.getMetrics, 11 | (req: Request, res: Response, next: NextFunction) => { 12 | res.status(200).json(res.locals.data) 13 | } 14 | ) 15 | 16 | router.get( 17 | '/mem', 18 | promController.getMetrics, 19 | promController.getMem, 20 | (req: Request, res: Response, next: NextFunction) => { 21 | res.status(200).json(res.locals.memoryPercents) 22 | } 23 | ) 24 | 25 | router.get( 26 | '/cpu', 27 | promController.getCores, 28 | promController.getMetrics, 29 | promController.getCpu, 30 | (req: Request, res: Response, next: NextFunction) => { 31 | res.status(200).json(res.locals.cpuPercents) 32 | } 33 | ) 34 | 35 | export default router 36 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import type { Request, Response, NextFunction } from 'express' 3 | import type { ServerError } from '../types/types' 4 | import apiRouter from './routers/apiRouter' 5 | 6 | const PORT = 3000 7 | 8 | const app = express() 9 | app.use(express.json()) 10 | 11 | // general endpoint for routes 12 | app.use('/api', apiRouter) 13 | 14 | // error handler for bad routes/requests to backend 15 | app.use((req, res) => { 16 | res.sendStatus(404) 17 | }) 18 | 19 | // global error handler for all middleware and routes 20 | app.use((err: ServerError, req: Request, res: Response, next: NextFunction) => { 21 | const defaultErr = { 22 | log: 'Error caught in global handler', 23 | status: 500, 24 | message: { err: 'An error occurred' } 25 | } 26 | const errorObj = { ...defaultErr, ...err } 27 | console.log(errorObj.log) 28 | console.log(err) 29 | return res.status(errorObj.status).json(errorObj.message) 30 | }) 31 | 32 | if (process.env.NODE_ENV !== 'test') { 33 | app.listen(PORT, () => { 34 | console.log(`App listening on port ${PORT}`) 35 | }) 36 | } 37 | export default app 38 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "coverage"], 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 14 | "jsx": "react", /* Specify what JSX code is generated. */ 15 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 24 | /* Modules */ 25 | "module": "commonjs", /* Specify what module code is generated. */ 26 | // "rootDir": "./", /* Specify the root folder within your source files. */ 27 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 32 | "types": ["node", "jest"], /* Specify type package names to be included without being referenced in a source file. */ 33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 35 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 36 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 37 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 38 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 39 | // "resolveJsonModule": true, /* Enable importing .json files. */ 40 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 41 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 42 | /* JavaScript Support */ 43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 53 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 54 | "removeComments": true, /* Disable emitting comments. */ 55 | // "noEmit": true, /* Disable emitting files from a compilation. */ 56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | /* Interop Constraints */ 71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 72 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 77 | /* Type Checking */ 78 | "strict": true, /* Enable all strict type-checking options. */ 79 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 80 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 81 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 82 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 83 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 84 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 85 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 86 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 87 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 88 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 89 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 90 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 91 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 92 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 93 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 94 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 95 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 96 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | }, 101 | // "include": ["server/**/*", "client/**/*"], 102 | "esclude": ["node_modules"] 103 | } -------------------------------------------------------------------------------- /types/png.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" 2 | declare module "*.gif" -------------------------------------------------------------------------------- /types/react-graph-vis.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-graph-vis" { 2 | import { Network, NetworkEvents, Options, Node, Edge, DataSet } from "vis"; 3 | export { Network, NetworkEvents, Options, Node, Edge, DataSet } from "vis"; 4 | import { Component } from "react"; 5 | 6 | export interface graphEvents { 7 | [event: NetworkEvents]: (params?: any) => void; 8 | } 9 | //Doesn't appear that this module supports passing in a vis.DataSet directly. Once it does graph can just use the Data object from vis. 10 | export interface graphData { 11 | nodes: Node[]; 12 | edges: Edge[]; 13 | } 14 | 15 | export interface NetworkGraphProps { 16 | graph: graphData; 17 | options?: Options; 18 | events?: graphEvents; 19 | getNetwork?: (network: Network) => void; 20 | identifier?: string; 21 | style?: React.CSSProperties; 22 | getNodes?: (nodes: DataSet) => void; 23 | getEdges?: (edges: DataSet) => void; 24 | } 25 | 26 | export interface NetworkGraphState { 27 | identifier: string; 28 | } 29 | 30 | export default class NetworkGraph extends Component< 31 | NetworkGraphProps, 32 | NetworkGraphState 33 | > { 34 | render(); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /types/types.ts: -------------------------------------------------------------------------------- 1 | // import type { V1Container, V1ContainerImage, V1PodIP } from '@kubernetes/client-node'; 2 | import { type IncomingMessage } from 'http' 3 | import type { RequestHandler } from 'express' 4 | import { type Dispatch, type SetStateAction } from 'react' 5 | 6 | export interface ServerError { 7 | err: '400' 8 | } 9 | 10 | export interface ClusterController { 11 | getAllPods: RequestHandler 12 | getAllNodes: RequestHandler 13 | getAllNamespaces: RequestHandler 14 | getAllServices: RequestHandler 15 | getAllDeployments: RequestHandler 16 | getAllIngresses: RequestHandler 17 | // getAllContexts: RequestHandler 18 | // setContext: RequestHandler 19 | // getAllPodLogs: RequestHandler 20 | setContext: RequestHandler 21 | } 22 | 23 | export interface prometheusController { 24 | getMetrics: RequestHandler 25 | getCores: RequestHandler 26 | getMem: RequestHandler 27 | getCpu: RequestHandler 28 | } 29 | 30 | export interface K6Controller { 31 | testing: RequestHandler 32 | } 33 | 34 | export interface LokiController { 35 | testing: RequestHandler 36 | } 37 | 38 | export interface CrudController { 39 | namespace: RequestHandler 40 | deployment: RequestHandler 41 | service: RequestHandler 42 | } 43 | 44 | export interface MapController { 45 | getElements: RequestHandler 46 | } 47 | export interface ClusterNode { 48 | // kind: string; 49 | id: string 50 | title?: string 51 | label?: string | undefined 52 | // font: { 53 | // color: string; 54 | // size?: number; 55 | // }; 56 | // labels?: any; 57 | // matchLabels?: any; 58 | size?: number 59 | image?: any 60 | shape?: string 61 | } 62 | export interface ClusterEdge { 63 | from: string 64 | to: string 65 | length?: number 66 | } 67 | export interface clusterGraphData { 68 | nodes: ClusterNode[] 69 | edges: ClusterEdge[] 70 | } 71 | export interface Props { 72 | type?: string 73 | // graphType?: string 74 | // title?: string 75 | header?: string 76 | // yAxisTitle?: string 77 | // color?: string | string[] 78 | // graphTextColor?: string 79 | // backgroundColor?: string | string[] 80 | // borderColor?: string | string[] 81 | hour?: string 82 | style?: number 83 | clusterData?: ClusterData 84 | namespace?: string 85 | ep?: string 86 | // logType?: string 87 | // pod?: string 88 | } 89 | export interface InvisibleNavbarModalProps { 90 | style: number 91 | setShowModal: Dispatch> 92 | } 93 | export interface GaugeGraphProps { 94 | type: string 95 | title: string 96 | graphTextColor: string 97 | backgroundColor: string | string[] 98 | borderColor: string | string[] 99 | } 100 | export interface LineGraphProps { 101 | type: string 102 | title: string 103 | yAxisTitle: string 104 | color: string | string[] 105 | graphTextColor: string 106 | } 107 | export interface LogProps { 108 | namespace: string 109 | logType: string 110 | pod: string 111 | } 112 | export interface MiniProps { 113 | style: number 114 | modalType: string 115 | setShowModal: Dispatch> 116 | crudSelection: string 117 | ns: string 118 | service: string 119 | deployment: string 120 | } 121 | export interface SelectorProps { 122 | type: string 123 | state: any 124 | stateSetter: Dispatch> 125 | modalPos: number 126 | setModalPos: Dispatch> 127 | modalType: string 128 | setModalType: Dispatch> 129 | showModal: boolean 130 | setShowModal: Dispatch> 131 | crudSelection?: string 132 | setCrudSelection: Dispatch> 133 | ns?: string 134 | } 135 | export interface CLusterObj { 136 | // [key: string]: number | string | CLusterObj | undefined 137 | [key: string]: any 138 | name: string 139 | namespace?: string 140 | uid: string 141 | containerNames?: string[] // array of strings (for now) 142 | nodeName?: string 143 | serviceAccount?: string 144 | subdomain?: string 145 | phase?: string 146 | ipFamilies?: string[] 147 | ports?: portObj[] // array of obj (portObj) 148 | type?: string 149 | // strategy?: strategyObj //object with type as string 150 | availableReplicas?: number 151 | // conditions?: string 152 | } 153 | export interface ClusterData { 154 | [key: string]: any 155 | pods?: CLusterObj 156 | namespaces?: CLusterObj 157 | deployments?: CLusterObj 158 | services?: CLusterObj 159 | contexts?: ContextObj 160 | } 161 | export interface ContextObj { 162 | [key: string]: any 163 | cluster: string 164 | name?: string 165 | user?: string 166 | namespace?: string 167 | } 168 | export interface portObj { 169 | [key: string]: any 170 | name: string 171 | port: number 172 | protocol: string 173 | targetPort: number 174 | } 175 | export type nestedObj = Record 176 | export interface Pod { 177 | name: string 178 | namespace: string 179 | uid: string 180 | // labels: any; 181 | containerNames: string[] 182 | nodeName: string 183 | serviceAccount: string 184 | phase: string 185 | subdomain: string 186 | } 187 | export interface log { 188 | response: IncomingMessage 189 | body: string 190 | } 191 | export interface globalServiceObj { 192 | name: string 193 | ip: string 194 | ports?: portObj[] 195 | } 196 | export interface ClientObj { 197 | metadata: CLusterObj 198 | spec: CLusterObj 199 | status: CLusterObj 200 | } 201 | export interface container { 202 | name: string 203 | } 204 | 205 | export interface LogEntry { 206 | timestamp: string 207 | message: string 208 | namespace: string 209 | container: string 210 | pod: string 211 | job: string 212 | // values?: [string, string][]; 213 | } 214 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './client/index.tsx', 6 | mode: process.env.NODE_ENV, 7 | resolve: { 8 | extensions: ['.jsx', '.js', '.tsx', '.ts'], 9 | }, 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /.(js|jsx)$/, 18 | exclude: /node_modules/, 19 | use: { 20 | loader: 'babel-loader', 21 | options: { 22 | presets: ['@babel/preset-env', '@babel/preset-react'], 23 | }, 24 | }, 25 | }, 26 | { 27 | test: /\.(ts|tsx)$/, 28 | exclude: /node_modules/, 29 | use: ['ts-loader'], 30 | }, 31 | { 32 | test: /.(css|scss)$/, 33 | exclude: [/node_modules/], 34 | use: ['style-loader', 'css-loader', 'sass-loader'], 35 | }, 36 | { 37 | test: /\.(png|jpe?g|gif)$/i, 38 | use: [ 39 | { 40 | loader: 'file-loader', 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | plugins: [ 47 | //removes all files under output.path and all unused assets after every successful rebuild 48 | // new CleanWebpackPlugin(), 49 | // we want our bundle.js file to be loaded into an HTMl file 50 | new HtmlWebpackPlugin({ 51 | //where to inject the bundle.js file 52 | template: './client/index.html', 53 | }), 54 | // generates a manifest.json file 55 | // new WebpackManifestPlugin({ 56 | // fileName: 'manifest.json', 57 | // }), 58 | ], 59 | devServer: { 60 | host: 'localhost', 61 | port: 8000, 62 | hot: false, 63 | liveReload: false, 64 | historyApiFallback: true, 65 | static: { 66 | // what the public sees so they dont know the path 67 | publicPath: '/build', 68 | directory: path.join(__dirname, './build'), 69 | }, 70 | proxy: { 71 | //TODO: change to route that you need 72 | '/api': { 73 | target: 'http://localhost:3000', 74 | secure: false, 75 | }, 76 | }, 77 | }, 78 | }; 79 | -------------------------------------------------------------------------------- /yamls/Daemonset.yaml: -------------------------------------------------------------------------------- 1 | --- # Daemonset.yaml 2 | apiVersion: apps/v1 3 | kind: DaemonSet 4 | metadata: 5 | name: promtail-daemonset 6 | spec: 7 | selector: 8 | matchLabels: 9 | name: promtail 10 | template: 11 | metadata: 12 | labels: 13 | name: promtail 14 | spec: 15 | serviceAccount: promtail-serviceaccount 16 | containers: 17 | - name: promtail-container 18 | image: grafana/promtail 19 | args: 20 | - -config.file=/etc/promtail/promtail.yaml 21 | env: 22 | - name: 'HOSTNAME' # needed when using kubernetes_sd_configs 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: 'spec.nodeName' 26 | volumeMounts: 27 | - name: logs 28 | mountPath: /var/log 29 | - name: promtail-config 30 | mountPath: /etc/promtail 31 | - mountPath: /var/lib/docker/containers 32 | name: varlibdockercontainers 33 | readOnly: true 34 | volumes: 35 | - name: logs 36 | hostPath: 37 | path: /var/log 38 | - name: varlibdockercontainers 39 | hostPath: 40 | path: /var/lib/docker/containers 41 | - name: promtail-config 42 | configMap: 43 | name: promtail-config 44 | --- # configmap.yaml 45 | apiVersion: v1 46 | kind: ConfigMap 47 | metadata: 48 | name: promtail-config 49 | data: 50 | promtail.yaml: | 51 | server: 52 | http_listen_port: 9080 53 | grpc_listen_port: 0 54 | 55 | clients: 56 | - url: https://loki-gateway.loki.svc.cluster.local/loki/api/v1/push 57 | 58 | positions: 59 | filename: /tmp/positions.yaml 60 | target_config: 61 | sync_period: 10s 62 | scrape_configs: 63 | - job_name: pod-logs 64 | kubernetes_sd_configs: 65 | - role: pod 66 | pipeline_stages: 67 | - docker: {} 68 | relabel_configs: 69 | - source_labels: 70 | - __meta_kubernetes_pod_node_name 71 | target_label: __host__ 72 | - action: labelmap 73 | regex: __meta_kubernetes_pod_label_(.+) 74 | - action: replace 75 | replacement: $1 76 | separator: / 77 | source_labels: 78 | - __meta_kubernetes_namespace 79 | - __meta_kubernetes_pod_name 80 | target_label: job 81 | - action: replace 82 | source_labels: 83 | - __meta_kubernetes_namespace 84 | target_label: namespace 85 | - action: replace 86 | source_labels: 87 | - __meta_kubernetes_pod_name 88 | target_label: pod 89 | - action: replace 90 | source_labels: 91 | - __meta_kubernetes_pod_container_name 92 | target_label: container 93 | - replacement: /var/log/pods/*$1/*.log 94 | separator: / 95 | source_labels: 96 | - __meta_kubernetes_pod_uid 97 | - __meta_kubernetes_pod_container_name 98 | target_label: __path__ 99 | 100 | --- # Clusterrole.yaml 101 | apiVersion: rbac.authorization.k8s.io/v1 102 | kind: ClusterRole 103 | metadata: 104 | name: promtail-clusterrole 105 | rules: 106 | - apiGroups: [""] 107 | resources: 108 | - nodes 109 | - services 110 | - pods 111 | verbs: 112 | - get 113 | - watch 114 | - list 115 | 116 | --- # ServiceAccount.yaml 117 | apiVersion: v1 118 | kind: ServiceAccount 119 | metadata: 120 | name: promtail-serviceaccount 121 | 122 | --- # Rolebinding.yaml 123 | apiVersion: rbac.authorization.k8s.io/v1 124 | kind: ClusterRoleBinding 125 | metadata: 126 | name: promtail-clusterrolebinding 127 | subjects: 128 | - kind: ServiceAccount 129 | name: promtail-serviceaccount 130 | namespace: default 131 | roleRef: 132 | kind: ClusterRole 133 | name: promtail-clusterrole 134 | apiGroup: rbac.authorization.k8s.io 135 | -------------------------------------------------------------------------------- /yamls/values.yml: -------------------------------------------------------------------------------- 1 | minio: 2 | enabled: true --------------------------------------------------------------------------------