├── .gitignore ├── README.md ├── client └── src │ ├── .DS_Store │ ├── App.jsx │ ├── assets │ ├── .DS_Store │ ├── images │ │ ├── .DS_Store │ │ ├── cluster_overview.jpeg │ │ ├── logo.png │ │ ├── node-data-backend.jpeg │ │ └── node-graph-flow-chart.jpeg │ └── stylesheets │ │ ├── FlowChart.css │ │ └── styles.css │ ├── components │ ├── FlowChart.js │ ├── Main.jsx │ ├── Metrics.jsx │ ├── MetricsTable.jsx │ ├── NavBar.jsx │ ├── NodeInfo.jsx │ ├── Protected.js │ ├── SideBar.jsx │ ├── Signin.jsx │ ├── Throughput.jsx │ └── WelcomePage.jsx │ ├── context │ └── AuthContent.js │ ├── firebase.js │ └── index.tsx ├── package-lock.json ├── package.json ├── server ├── controllers │ ├── metricsController.ts │ └── throughputController.ts ├── routes │ └── metricsRouter.ts └── server.ts ├── template.html ├── tests └── space.spec.js ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /client/public/ 3 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 |

7 | Monitor your Kubernetes cluster's resource usage and vulnerabilities across different levels 🖥️ 8 |

9 | 10 |

11 | 12 |

13 |

14 | 15 |

16 |

17 | 18 |

19 | 20 | 21 | ## Introduction 22 | 23 | `Kube Est8(beta)` is an open source application being developed to offer developers a Kubernetes monitoring solution that places a focus on bottleneck detection and security vulnerabilities. Core features include metric visualization through Grafana graphs, using time-series data collected by Prometheus from your cluster, node graph flow chart for interactive rendering of the cluster as well as ability to create and delete nodes, health metrics related to each node of the cluster, security vulnerability scan, and bottleneck detection, especially those resulting from AI or ML workloads. 24 | 25 | 26 | ## Setup 27 | 28 | Clone repository, install dependencies, and run: 29 | 30 | ``` 31 | git clone https://github.com/oslabs-beta/kube-est8.git 32 | cd kube-est8 33 | npm i 34 | npm start 35 | ``` 36 | 37 | 38 | ## Spinning up Kubernetes 39 | 40 | The Kubernetes command-line tool, [kubectl](https://kubernetes.io/releases/download/), allows you to run commands against Kubernetes clusters. 41 | 42 | Kube-Est8 will connect to the cluster configured in your [`/kubeconfig`](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/) file. 43 | 44 | 45 | 46 | ## Setting up Metrics Server 47 | 48 | [Kubernetes Metrics Server](https://github.com/kubernetes-sigs/metrics-server) collects resource metrics from Kubelets and exposes them in Kubernetes apiserver. 49 | 50 | 51 | ## Setting up Prometheus 52 | 53 | ### Prometheus 54 | [Prometheus](https://prometheus.io/) is an open-source monitoring tool. It offers built-in monitoring capabilities for Kubernetes clusters, and it excels at collecting and analyzing metrics to trigger alerts. 55 | 56 | ### Image 57 | The latest Prometheus version can be found on [DockerHub](https://hub.docker.com/r/prom/prometheus/tags). 58 | 59 | ### Namespace 60 | We can create a new namespace for our Prometheus deployment objects: `kubectl create namespace monitoring`. 61 | 62 | ### RBAC Policy 63 | We need to create an RBAC policy with read access to required API groups and bind the policy to the monitoring namespace because Prometheus uses Kubernetes APIs to read metrics. 64 | Create the role using your configured clusterRole file: `kubectl create -f clusterRole.yaml` 65 | 66 | ### Config Map 67 | A config map needs to be created to externalize Prometheus configurations so we don't have to rebuild the Prometheus image when adding or removing configurations. Create a `config-map.yaml` file, configure it appropriately, then create the config map using `kubectl create -f config-map.yaml`. 68 | 69 | ### Prometheus deployment 70 | Next, create and configure your `prometheus-deployment.yaml`. It can be created with `kubectl create -f prometheus-deployment.yaml`. You can check the deployment using `kubectl get deployments --namespace=monitoring`. 71 | 72 | ### Accessing Prometheus Dashboard 73 | Can be accessed by exposing Prometheus as a service by creating and configuring your `prometheus-service.yaml` file using a nodeport type, then using `kubectl create -f prometheus-service.yaml --namespace=monitoring` to create service. 74 | 75 | ## Setting up Grafana 76 | [Grafana](https://grafana.com/docs/grafana/latest/setup-grafana/installation/mac/) is one of the best open source visualization tools. It can be easily integrated with Prometheus for visualizing all the target metrics. 77 | 78 | After starting your Grafana server: 79 | 1. Log in to Grafana Dashboard 80 | 2. Set Prometheus server as source 81 | 3. Set up dashboard panels 82 | 83 | After that, your panels will be available for use. 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /client/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kube-est8/363a5108e3341ca64fe92ba87bd44bf470d949fd/client/src/.DS_Store -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter as Router, Route, Routes, Switch } from "react-router-dom"; 3 | import { AuthContextProvider } from "./context/AuthContent.js"; 4 | import NavBar from "./components/NavBar.jsx"; 5 | import Main from "./components/Main.jsx"; 6 | import Signin from './components/Signin.jsx'; 7 | import WelcomePage from './components/WelcomePage.jsx' 8 | import Protected from './components/Protected.js' 9 | 10 | import Flow from "./components/FlowChart.js"; 11 | import "./assets/stylesheets/styles.css"; 12 | 13 | const App = () => { 14 | return ( 15 |
16 | 17 | 18 | {/* */} 19 | 20 | 21 | } /> 22 |
} /> 23 | }/> 24 | 25 | {/* */} 26 | 27 |
28 | ); 29 | }; 30 | 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /client/src/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kube-est8/363a5108e3341ca64fe92ba87bd44bf470d949fd/client/src/assets/.DS_Store -------------------------------------------------------------------------------- /client/src/assets/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kube-est8/363a5108e3341ca64fe92ba87bd44bf470d949fd/client/src/assets/images/.DS_Store -------------------------------------------------------------------------------- /client/src/assets/images/cluster_overview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kube-est8/363a5108e3341ca64fe92ba87bd44bf470d949fd/client/src/assets/images/cluster_overview.jpeg -------------------------------------------------------------------------------- /client/src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kube-est8/363a5108e3341ca64fe92ba87bd44bf470d949fd/client/src/assets/images/logo.png -------------------------------------------------------------------------------- /client/src/assets/images/node-data-backend.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kube-est8/363a5108e3341ca64fe92ba87bd44bf470d949fd/client/src/assets/images/node-data-backend.jpeg -------------------------------------------------------------------------------- /client/src/assets/images/node-graph-flow-chart.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kube-est8/363a5108e3341ca64fe92ba87bd44bf470d949fd/client/src/assets/images/node-graph-flow-chart.jpeg -------------------------------------------------------------------------------- /client/src/assets/stylesheets/FlowChart.css: -------------------------------------------------------------------------------- 1 | .flow-container { 2 | width: 100; 3 | height: 500px; 4 | background-color: rgb(62, 9, 32); 5 | } 6 | .custom-background { 7 | z-index: -1; /* Lower the z-index */ 8 | opacity: 0.5; /* Reduce the opacity */ 9 | 10 | } 11 | .custom-controls { 12 | z-index: 20; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /client/src/assets/stylesheets/styles.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Sixtyfour&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap"); 3 | 4 | 5 | html { 6 | height: 100vh; 7 | width: 100vw; 8 | background-color: var(--background-stone-900); 9 | color: var(--white); 10 | --white: #f0f0f0; 11 | --dark-gray: #18181b; 12 | --background-blue: #363349; 13 | --background-gray: #232328; 14 | --background-stone: #0c0a09; 15 | --background-stone-900: #1c1917; 16 | --background-stone-800: #292524; 17 | --background-stone-600: #57534E; 18 | } 19 | 20 | body { 21 | margin: 0; 22 | height: 100vh; 23 | overflow-x: hidden; 24 | } 25 | 26 | .logoBox { 27 | display: flex; 28 | align-items: center; 29 | padding-left: 5vw; 30 | gap: 10vw; 31 | > * { 32 | padding: 20px; 33 | } 34 | } 35 | 36 | .logo { 37 | height: 15vmin; 38 | border-radius: 50%; 39 | border: 3px solid var(--dark-gray); 40 | padding: 0; 41 | transition: transform 0.2s; 42 | box-shadow: 0 0 10px 2px rgba(255, 255, 255, 0.6); 43 | margin-top: 50px; 44 | } 45 | 46 | .logo:hover { 47 | transform: scale(1.1); 48 | cursor: pointer; 49 | } 50 | 51 | .logo:active { 52 | box-shadow: 0 0 10px 2px rgba(255, 255, 255, 0.9); 53 | } 54 | 55 | .mainTitle { 56 | font-family: "Montserrat", "Copperplate"; 57 | text-align: center; 58 | } 59 | 60 | /* NAVBAR SIGN IN BUTTON */ 61 | 62 | .signInLink { 63 | height: 25%; 64 | display: flex; 65 | align-items: center; 66 | text-decoration: none; 67 | color: white; 68 | font-weight: bold; 69 | font-size: 0.7em; 70 | padding: 0.5rem 1rem; 71 | border-radius: 5px; /* Rounded corners */ 72 | background-color: #3d8ce0; /* Sign In button background color */ 73 | transition: background-color 0.3s ease; /* Smooth transition */ 74 | 75 | /* Additional styles for a modern look */ 76 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Shadow effect */ 77 | cursor: pointer; 78 | } 79 | 80 | .welcome-container { 81 | display: flex; 82 | justify-content: center; 83 | } 84 | 85 | a{ 86 | text-decoration: none; 87 | } 88 | 89 | .go-main { 90 | margin-top: 10vmin; 91 | } 92 | 93 | .go-main>a { 94 | height: 25%; 95 | display: flex; 96 | align-items: center; 97 | text-decoration: none; 98 | color: white; 99 | font-weight: bold; 100 | font-size: 1.5em; 101 | padding: 2rem 4rem; 102 | border-radius: 5px; /* Rounded corners */ 103 | background-color: #3d8ce0; /* Sign In button background color */ 104 | transition: background-color 0.3s ease; /* Smooth transition */ 105 | 106 | /* Additional styles for a modern look */ 107 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Shadow effect */ 108 | cursor: pointer; 109 | 110 | /* margin: 1vmin 5vmin; */ 111 | } 112 | 113 | .go-main>a:hover { 114 | background-color: #469dfa; 115 | } 116 | 117 | 118 | .signin-block { 119 | display: flex; 120 | flex-direction: column; 121 | align-items: center; 122 | padding-top: 10vmin; 123 | } 124 | 125 | .signin-container { 126 | text-align: center; 127 | border-radius: 20px; 128 | border: 1px solid var(--background-stone-800); 129 | background-color: var(--background-stone); 130 | box-shadow: 5px 7px 10px 5px rgba(0, 0, 0, 0.2); 131 | } 132 | 133 | .google-signin { 134 | margin: 5vmin 5vmin; 135 | } 136 | 137 | .signInLink:hover { 138 | background-color: #0056b3; /* Darker color on hover */ 139 | } 140 | 141 | .root { 142 | height: 100vh; 143 | width: 100vw; 144 | 145 | display: flex; 146 | flex-direction: column; 147 | border: none; 148 | /* grid-template-rows: 1fr 5fr; */ 149 | } 150 | 151 | .NavBar { 152 | background-color: var(--background-stone); 153 | border: 1px solid var(--background-stone-800); 154 | box-shadow: 0 7px 10px 5px rgba(0, 0, 0, 0.2); 155 | display: flex; 156 | align-items: center; 157 | justify-content: space-between; 158 | padding: 0 5vmin; 159 | } 160 | 161 | .logout-button { 162 | height: 25%; 163 | display: flex; 164 | align-items: center; 165 | text-decoration: none; 166 | color: var(--background-stone-600); 167 | font-weight: bold; 168 | font-size: 0.7em; 169 | padding: 0.5rem 1rem; 170 | border-radius: 5px; /* Rounded corners */ 171 | border: 1px solid var(--background-stone-800); 172 | background-color: var(--background-stone-900); /* Sign In button background color */ 173 | transition: background-color 0.3s ease; /* Smooth transition */ 174 | 175 | /* Additional styles for a modern look */ 176 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Shadow effect */ 177 | cursor: pointer; 178 | } 179 | 180 | 181 | 182 | .logout-button:hover { 183 | background-color: var(--background-stone-800); 184 | } 185 | 186 | .welcome-page { 187 | text-align: center; 188 | } 189 | 190 | .Sidebar { 191 | grid-column-start: 0; 192 | grid-column-end: 1; 193 | width: 20vw; 194 | 195 | background-color: var(--background-stone); 196 | display: flex; 197 | flex-direction: column; 198 | align-items: center; 199 | border-right: 2px solid var(--background-stone-800); 200 | } 201 | 202 | button { 203 | cursor: pointer; 204 | background-color: #3f3f46; 205 | color: #d4d4d4; 206 | } 207 | 208 | .sidebar-buttons { 209 | width: 100%; 210 | padding-top: 80px; 211 | display: flex; 212 | flex-direction: column; 213 | align-items: center; 214 | } 215 | 216 | .sidebar-buttons > * { 217 | text-align: center; 218 | width: 100%; 219 | font-size: min(2vw, 2vh); 220 | padding: 20px 0; 221 | font-family: "Montserrat"; 222 | /* border-top: 1px solid gray; */ 223 | border-bottom: 1px solid gray; 224 | cursor: pointer; 225 | } 226 | 227 | .sidebar-buttons > *:first-child { 228 | border-top: 1px solid gray; 229 | } 230 | 231 | .sidebar-buttons > *:hover { 232 | color: white; 233 | box-shadow: 0 0 2px 2px rgba(255, 255, 255, 0.423); 234 | } 235 | 236 | .Main { 237 | flex: 1; 238 | height: 100%; 239 | display: grid; 240 | grid-template-columns: minmax(0, 1fr); 241 | } 242 | 243 | .Metrics { 244 | display: flex; 245 | flex-direction: column; 246 | grid-column-start: 1; 247 | grid-column-end: -1; 248 | } 249 | 250 | .MetricsTable { 251 | /* margin: calc(5vh + 1px); */ 252 | display: grid; 253 | justify-items: center; 254 | } 255 | 256 | .MetricsTable>* { 257 | width: 100%; 258 | } 259 | 260 | .grafana1 { 261 | display: grid; 262 | grid-template-columns: 1fr 1fr; 263 | justify-items: center; 264 | } 265 | 266 | .grafana1>* { 267 | width: 100%; 268 | } 269 | 270 | .grafanaGraph { 271 | } 272 | 273 | /* iframe { 274 | width: 100%; 275 | height: 300px; 276 | } */ 277 | 278 | .graph { 279 | width: 80%; 280 | border: 2px solid black; 281 | margin: 20px; 282 | } 283 | 284 | .no-info { 285 | display: flex; 286 | justify-content: center; 287 | } 288 | 289 | .no-info-text { 290 | font-family: "Montserrat"; 291 | font-size: min(5vh, 5vw); 292 | text-align: center; 293 | padding: 10vmin 20vmin; 294 | } 295 | 296 | .text-span { 297 | color: #455eb5; 298 | } 299 | 300 | .dataTitle { 301 | font-family: "Montserrat", Helvetica, Arial, sans-serif; 302 | color: var(--white); 303 | } 304 | 305 | /* NODE INFO STYLING */ 306 | 307 | .nodes-table { 308 | border-collapse: collapse; 309 | margin: 25px 0; 310 | font-size: 0.9em; 311 | font-family: sans-serif; 312 | min-width: 400px; 313 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); 314 | border-radius: 5px 5px 0 0; 315 | overflow: hidden; 316 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); 317 | } 318 | 319 | .nodes-table thead tr { 320 | background-color: #021e18; 321 | color: #ffffff; 322 | text-align: left; 323 | font-weight: bold; 324 | } 325 | 326 | .nodes-table th, 327 | .nodes-table td { 328 | padding: 12px 15px; 329 | } 330 | 331 | .nodes-table tbody tr { 332 | border-bottom: 1px solid #dddddddd; 333 | } 334 | 335 | .nodes-table tbody tr:nth-of-type(even) { 336 | background-color: rgb(2, 44, 42); 337 | } 338 | 339 | .nodes-table tbody tr:last-of-type { 340 | border-bottom: 2px solid #011e18; 341 | } 342 | 343 | .nodes-table tbody tr:active-row { 344 | font-weight: bold; 345 | color: #009879; 346 | } 347 | 348 | .style { 349 | background: red; 350 | width: '100%'; 351 | height: 300; 352 | } 353 | -------------------------------------------------------------------------------- /client/src/components/FlowChart.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Fragment } from "react"; 3 | import { useState, useCallback } from 'react'; 4 | import ReactFlow, { applyEdgeChanges, applyNodeChanges, Controls, Background, MiniMap} from 'react-flow-renderer'; 5 | 6 | import '../assets/stylesheets/FlowChart.css' 7 | 8 | const initialEdges = [{ id: 'e1-2', source: '1', target: '2'}]; 9 | 10 | const initialNodes = [ 11 | { 12 | id: '1', 13 | data: {label: 'Node 1'}, 14 | position: { x: 0, y: 0 }, 15 | type: 'input', 16 | }, 17 | { 18 | id: '2', 19 | data: {label: 'Node 2'}, 20 | position: {x: 100, y: 100}, 21 | 22 | }, 23 | ]; 24 | const CustomNode = ({ id, data }) => { 25 | return ( 26 |
27 | {data.label} 28 |
29 | ); 30 | }; 31 | 32 | function Flow() { 33 | const [nodes, setNodes] = useState(initialNodes); 34 | const [name, setName] = useState("") 35 | const [edges, setEdges] = useState(initialEdges); 36 | 37 | const addNode = () => { 38 | setNodes (e => e.concat({ 39 | id: (e.length+1).toString(), 40 | data:{label: `${name}`}, 41 | position: {x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight} 42 | })) 43 | } 44 | 45 | return ( 46 |
47 | 48 | 51 | 52 | { 54 | if( n.type === 'input') return 'blue'; 55 | 56 | return '#FFCC00' 57 | }} 58 | /> 59 | 60 | 61 |
62 | setName(e.target.value)} 64 | name="title"/> 65 |
69 | 70 |
71 | 72 | )}; 73 | 74 | export default Flow; 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /client/src/components/Main.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {userAuth} from '../context/AuthContent.js'; 3 | import SideBar from './SideBar.jsx' 4 | import Metrics from './Metrics.jsx' 5 | import Flow from './FlowChart.js'; 6 | 7 | const Main = (props) => { 8 | 9 | const [display, setDisplay] = useState(''); 10 | 11 | 12 | // FUNCTION TO CHANGE STATE FOR RENDERING CLUSTER INFO IN METRICS TABLE 13 | const clusterInfoRender = (e) => { 14 | setDisplay('clusterInfo'); 15 | return; 16 | } 17 | 18 | // FUNCTION TO CHANGE STATE FOR RENDERING POD INFO 19 | const nodeInfoRender = (e) => { 20 | setDisplay('nodeInfo'); 21 | return; 22 | } 23 | 24 | // FUNCTION TO CHANGE STATE FOR RENDERING 25 | const flowChartRender = (e) => { 26 | setDisplay('flowChart'); 27 | return; 28 | } 29 | 30 | const homeRender = () => { 31 | setDisplay(''); 32 | return; 33 | } 34 | 35 | const throughputRender = (e) => { 36 | setDisplay('throughput'); 37 | return; 38 | } 39 | 40 | 41 | return ( 42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 | ) 52 | } 53 | 54 | export default Main; -------------------------------------------------------------------------------- /client/src/components/Metrics.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MetricsTable from './MetricsTable.jsx' 3 | 4 | const Metrics = (props) => { 5 | 6 | return ( 7 | 8 |
9 | 10 |
11 | 12 | ) 13 | } 14 | 15 | export default Metrics; -------------------------------------------------------------------------------- /client/src/components/MetricsTable.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NodeInfo from './NodeInfo.jsx' 3 | import Flow from './FlowChart.js'; 4 | import Throughput from './Throughput.jsx' 5 | import { UserAuth } from '../context/AuthContent.js'; 6 | 7 | const MetricsTable = (props) => { 8 | 9 | /*-------------------CONDITIONALLY RENDER CLUSTER INFO BASED ON STATE-----------------*/ 10 | if (props.display === 'clusterInfo') { 11 | return ( 12 | 13 |
14 | {/*

Cluster Overview

*/} 15 | 16 |
17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | /*-------------------CONDITIONALLY RENDER NODE INFO BASED ON STATE-----------------*/ 30 | if (props.display === 'nodeInfo') { 31 | return ( 32 | 33 | ) 34 | } 35 | 36 | if (props.display === 'flowChart'){ 37 | return ( 38 | 39 | ) 40 | } 41 | 42 | if (props.display === 'throughput') { 43 | return ( 44 | 45 | ) 46 | } 47 | 48 | const { user } = UserAuth(); 49 | 50 | /*-----------------------IF USER JUST ARRIVED / NO DATA LOADED------------------------*/ 51 | return ( 52 |
53 |
54 | Welcome to your Kube Est8, {user?.displayName}. 55 |

56 | Please use the sidebar to render desired cluster info. 57 |
58 |
59 | ) 60 | } 61 | 62 | export default MetricsTable; -------------------------------------------------------------------------------- /client/src/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import mainLogo from '../assets/images/logo.png'; 3 | import { Link } from 'react-router-dom'; 4 | import '../assets/stylesheets/styles.css' 5 | import {UserAuth} from '../context/AuthContent' 6 | 7 | 8 | const NavBar = () => { 9 | const {user, logOut} = UserAuth(); 10 | 11 | const handleSignOut = async () => { 12 | try { 13 | await logOut(); 14 | } catch (error) { 15 | console.log(error) 16 | } 17 | } 18 | 19 | return ( 20 | 21 |
22 | {/*
*/} 23 | {/* */} 24 |

Kube Est8

25 | {/*
*/} 26 | 27 | {/* if user is logged in, display logout button, otherwise display sign in button */} 28 | {user?.displayName ? :
Get Started
} 29 |
30 | 31 | ) 32 | } 33 | 34 | export default NavBar; -------------------------------------------------------------------------------- /client/src/components/NodeInfo.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | const NodeInfo = (props) => { 4 | 5 | // All this peace of code, fetches the data to the server, iterates over the data 6 | //and then is tranfer so is render into the Main display. 7 | const [nodesList, setNodesList] = useState([]); 8 | 9 | useEffect(() => { 10 | nodes(); 11 | }, []); 12 | 13 | const nodes = async () => { 14 | const response = await fetch('/metrics'); 15 | 16 | setNodesList(await response.json()) 17 | }; 18 | 19 | return ( 20 |
21 | {nodesList.map((data) => { 22 | return ( 23 |
24 |

{`Name: ${data.name}`}

25 |

{`UID: ${data.uid}`}

26 |

{`Day created: ${data.creationTimestamp}`}

27 |

{`CPU capacity: ${data.capacity.cpuCapacity}`}

28 |

{`Memory capacity: ${data.capacity.memoryCapacity}`}

29 |

{`Pod capacity: ${data.capacity.podsCapacity}`}

30 |

{`Last heartbeat: ${data.presureStatus.lastHeartbeatTime}`}

31 |

{`Kubelet status: ${data.presureStatus.status}`}

32 |

{`Memory pressure: ${data.presureStatus.memoryPressure}`}

33 |

{`Disk pressure: ${data.presureStatus.diskPressure}`}

34 |

{`'PID pressure': ${data.presureStatus.PIDPressure}`}

35 |

{`Total images: ${data.totalImages}`}

36 |

----------------------------------------------------------------

37 | 38 |
39 | ) 40 | }) 41 | 42 | } 43 |
44 | 45 | ); 46 | 47 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 48 | 49 | // The code below will render in the screen the chart that Erika created 50 | 51 | // return ( 52 | //
53 | //

Your Nodes

54 | // 55 | // 56 | // 57 | // 58 | // 59 | // 60 | // 61 | // 62 | // 63 | // 64 | // 65 | // 66 | // 67 | // 68 | // 69 | // 70 | // 71 | // 72 | // 73 | // 74 | // 75 | // 76 | // {/* */} 77 | // 78 | // 79 | // 80 | // 81 | // 82 | // 83 | // 84 | // {/* */} 85 | // 86 | // 87 | // 88 | // 89 | // 90 | // 91 | // 92 | // 93 | // 94 | // 95 | //
Node (will insert the data pulled from backend)Name: CPU/Memory: Role: Message Throughput: Number of Deployments: Number of Pods:
1 Erika's Node56%The role of the node The Message Throughput2 Deployments! 5 pods!
2Chunky Bean's Node56%The role of the node The Message Throughput3 Deployments! 7 pods!
3Peep's Node70% CPUThe role of the node The Message Throughput1 Deployment! 2 pods!
96 | //
97 | // ); 98 | 99 | 100 | }; 101 | 102 | export default NodeInfo; 103 | -------------------------------------------------------------------------------- /client/src/components/Protected.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate } from 'react-router-dom'; 3 | import { UserAuth } from '../context/AuthContent'; 4 | 5 | const Protected = ({children}) => { 6 | const {user} = UserAuth() 7 | if (!user) { 8 | return () 9 | } 10 | 11 | return children; 12 | } 13 | 14 | export default Protected; -------------------------------------------------------------------------------- /client/src/components/SideBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | // import ReactDOM from 'react-dom'; 3 | import mainLogo from '../assets/images/logo.png'; 4 | 5 | 6 | const SideBar = (props) => { 7 | 8 | 9 | 10 | return ( 11 | 12 |
13 | 14 | 15 | 16 | 17 | 23 | 24 |
25 | ) 26 | }; 27 | 28 | export default SideBar; -------------------------------------------------------------------------------- /client/src/components/Signin.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from 'react'; 2 | import { GoogleButton } from 'react-google-button'; 3 | import {UserAuth} from '../context/AuthContent'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | const Signin = () => { 7 | 8 | const {googleSignIn, user} = UserAuth(); 9 | 10 | const navigate = useNavigate(); 11 | 12 | const handleGoogleSignIn = async () => { 13 | try { 14 | await googleSignIn(); 15 | } catch(error) { 16 | console.log(error); 17 | } 18 | }; 19 | 20 | useEffect(() => { 21 | //if user is signed in, navigate to main page 22 | if(user !== null) { 23 | navigate('/main'); 24 | } 25 | }, [user]) 26 | 27 | return ( 28 |
29 |
30 |

Sign In

31 | 32 |
33 |
34 | ) 35 | } 36 | 37 | export default Signin; -------------------------------------------------------------------------------- /client/src/components/Throughput.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Throughput = (props) => { 4 | 5 | 6 | 7 | return ( 8 |
9 | 10 | Throughput here 11 | {/* 12 | REQUEST HERE TO BACKEND SERVER 13 | WHERE KUBERNETES API WILL BE 14 | INTERACTED WITH. RENDER ADDITIONAL DISPLAYS. 15 | 16 | 17 | */} 18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 | ) 29 | } 30 | 31 | export default Throughput; -------------------------------------------------------------------------------- /client/src/components/WelcomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {UserAuth} from '../context/AuthContent' 3 | import { Link } from 'react-router-dom'; 4 | 5 | 6 | 7 | const WelcomePage = () => { 8 | 9 | const {user, logOut} = UserAuth(); 10 | 11 | return ( 12 |
13 | {user?.displayName ? Proceed to my estate : 14 | 15 |
16 |

Welcome!

17 |

If your cluster has been configured,

please proceed to your Kubernetes estate.

18 |
19 | } 20 | 21 |
22 | ) 23 | } 24 | 25 | export default WelcomePage; -------------------------------------------------------------------------------- /client/src/context/AuthContent.js: -------------------------------------------------------------------------------- 1 | import { useContext, createContext, useEffect } from "react"; 2 | import { useState } from 'react'; 3 | import React from 'react'; 4 | //import authentication from firebase library 5 | import { auth } from '../firebase'; 6 | //all of the features needed from firebase 7 | import { 8 | GoogleAuthProvider, 9 | signInWithPopup, 10 | signInWithRedirect, 11 | signOut, 12 | onAuthStateChanged, 13 | } from "firebase/auth"; 14 | 15 | const AuthContext = createContext(); 16 | 17 | export const AuthContextProvider = ({ children }) => { 18 | //useState Functionality 19 | const [user, setUser] = useState({}); 20 | 21 | //declare function for googleSignIn 22 | const googleSignIn = () => { 23 | //create GoogleAuthProvider (this is built in method with firebase) 24 | const provider = new GoogleAuthProvider(); 25 | provider.setCustomParameters({ 26 | prompt: 'select_account' 27 | }) 28 | //take auth and provider with pop up 29 | signInWithPopup(auth, provider) 30 | .then((result) => { 31 | // handle successful authentication 32 | console.log('Authentication successful', result.user); 33 | }) 34 | .catch((error) => { 35 | // handle authentication error 36 | console.log('Authentication error', error); 37 | }); 38 | // signInWithRedirect(auth, provider); 39 | } 40 | 41 | //function for logOut (pass this down to return statement) 42 | const logOut = () => { 43 | signOut(auth) 44 | } 45 | 46 | //checking for onAuthStateChanged - imported from firebase 47 | useEffect(() =>{ 48 | const unsubscribe = onAuthStateChanged(auth, (currentUser)=> { 49 | setUser(currentUser) 50 | // console.log('User', currentUser) 51 | }) 52 | return () => { 53 | unsubscribe(); 54 | } 55 | }, []) 56 | 57 | 58 | return ( 59 | // pass down user 60 | 61 | {children} 62 | 63 | ); 64 | }; 65 | 66 | export const UserAuth = () => { 67 | return useContext(AuthContext); 68 | }; 69 | -------------------------------------------------------------------------------- /client/src/firebase.js: -------------------------------------------------------------------------------- 1 | // Import the functions you need from the SDKs you need 2 | import { initializeApp } from "firebase/app"; 3 | import { getAnalytics } from "firebase/analytics"; 4 | import { getAuth } from "firebase/auth"; 5 | // TODO: Add SDKs for Firebase products that you want to use 6 | // https://firebase.google.com/docs/web/setup#available-libraries 7 | 8 | // Your web app's Firebase configuration 9 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional 10 | 11 | //firebase config that returns firebase app instance of our connected firebase backend 12 | const firebaseConfig = { 13 | apiKey: "AIzaSyAz2atMQHm87mfUpSf9rcIVgoi5uMRXvA0", 14 | authDomain: "kubeest8.firebaseapp.com", 15 | projectId: "kubeest8", 16 | storageBucket: "kubeest8.appspot.com", 17 | messagingSenderId: "324917939409", 18 | appId: "1:324917939409:web:254b4b9366c062b80dc9f0", 19 | measurementId: "G-1FEJ0Z7V14" 20 | }; 21 | 22 | // Initialize Firebase 23 | const app = initializeApp(firebaseConfig); 24 | const analytics = getAnalytics(app); 25 | export const auth = getAuth(app); -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { createRoot } from "react-dom/client"; 4 | import "./assets/stylesheets/styles.css"; 5 | import { BrowserRouter } from "react-router-dom";import Flow from './components/FlowChart.js'; 6 | 7 | 8 | import App from "./App.jsx"; 9 | 10 | //changed root variable source to createRoot 11 | const root = createRoot(document.getElementById('root') as HTMLElement) ; 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kube-est8", 3 | "version": "1.0.0", 4 | "description": "Kube Est8 is a Kubernetes Visualization tool that assists with detecting bottlenecks and security vulnerabilities.", 5 | "main": "./client/src/index.js", 6 | "scripts": { 7 | "start": "nodemon server/server.ts", 8 | "test": "jest", 9 | "test:watch": "npm run test -- --watch", 10 | "build": "webpack", 11 | "dev": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open --hot --progress --color\" \"nodemon ./server/server.ts\"" 12 | }, 13 | "author": "Brandon Hernandez, Erika Wester, Ines Chavez, Patrick Post", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@babel/preset-env": "^7.23.8", 17 | "@kubernetes/client-node": "^0.20.0", 18 | "axios": "^1.6.7", 19 | "cors": "^2.8.5", 20 | "css-loader": "^6.10.0", 21 | "express": "^4.18.2", 22 | "file-loader": "^6.2.0", 23 | "firebase": "^10.8.0", 24 | "nodemon": "^3.1.1", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-flow": "^1.0.3", 28 | "react-flow-renderer": "^10.3.17", 29 | "react-google-button": "^0.7.2", 30 | "react-router-dom": "^6.22.0", 31 | "reactflow": "^11.10.3" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.23.7", 35 | "@babel/plugin-syntax-jsx": "^7.23.3", 36 | "@babel/preset-react": "^7.23.3", 37 | "@babel/preset-typescript": "^7.23.3", 38 | "@types/cors": "^2.8.17", 39 | "@types/dotenv": "^8.2.0", 40 | "@types/express": "^4.17.21", 41 | "@types/jest": "^29.5.12", 42 | "@types/node": "^20.11.20", 43 | "@types/react": "^18.2.58", 44 | "@types/react-dom": "^18.2.19", 45 | "babel-loader": "^9.1.3", 46 | "concurrently": "^8.2.2", 47 | "cross-env": "^7.0.3", 48 | "css-loader": "^6.10.0", 49 | "html-webpack-plugin": "^5.6.0", 50 | "jest": "^29.7.0", 51 | "style-loader": "^3.3.4", 52 | "supertest": "^6.3.4", 53 | "ts-node": "^10.9.2", 54 | "typescript": "^5.3.3", 55 | "webpack": "^5.90.0", 56 | "webpack-cli": "^5.1.4", 57 | "webpack-dev-server": "^4.15.1" 58 | }, 59 | "proxy": "http://localhost:3333" 60 | } 61 | -------------------------------------------------------------------------------- /server/controllers/metricsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | 4 | //import Kubernetes API client package 5 | const k8s = require('@kubernetes/client-node'); 6 | 7 | 8 | interface MetricsController { 9 | getNodeList: (req: Request, res: Response, next: NextFunction) => Promise; 10 | } 11 | 12 | //making new kube config instance 13 | const kc = new k8s.KubeConfig(); 14 | 15 | //loading the info from kube config file on your local device 16 | //adding the info to the variable kc 17 | //loadFromDefault is native built in method of kubenetes API client package 18 | kc.loadFromDefault(); 19 | 20 | //creating API Client. What will be used to communicate with the control plane of kubernetes cluster 21 | //makeApiClient is native built in method of kubenetes API client package 22 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 23 | 24 | //Initial middleware object 25 | // const metricsController = {}; 26 | 27 | 28 | const metricsController: MetricsController = { 29 | // Now you can safely assign getNodeList to it 30 | getNodeList: async (req, res, next) => { 31 | 32 | try { 33 | //.listNode is an API client method (native built in method) 34 | const data = await k8sApi.listNode(); 35 | 36 | // *** TODO *** Conver the memory in to Gigabytes 37 | // const memoryInGb = (8029624 * 1024) / (1024 ** 3).toFixed(2) 38 | 39 | res.locals.nodesList = [ 40 | 41 | { 42 | name: data.response.body.items[0].metadata.name, 43 | uid: data.response.body.items[0].metadata.uid, 44 | creationTimestamp: data.response.body.items[0].metadata.creationTimestamp, 45 | capacity: { 46 | cpuCapacity: data.response.body.items[0].status.capacity.cpu, 47 | memoryCapacity: data.response.body.items[0].status.capacity.memory, 48 | podsCapacity: data.response.body.items[0].status.capacity.pods 49 | }, 50 | presureStatus: { 51 | lastHeartbeatTime: data.response.body.items[0].status.conditions[0].lastHeartbeatTime, 52 | memoryPressure: data.response.body.items[0].status.conditions[0].message, 53 | diskPressure: data.response.body.items[0].status.conditions[1].message, 54 | PIDPressure: data.response.body.items[0].status.conditions[2].message, 55 | status: data.response.body.items[0].status.conditions[3].message 56 | }, 57 | totalImages: data.response.body.items[0].status.images.length 58 | }, 59 | 60 | { 61 | name: data.response.body.items[1].metadata.name, 62 | uid: data.response.body.items[1].metadata.uid, 63 | creationTimestamp: data.response.body.items[1].metadata.creationTimestamp, 64 | capacity: { 65 | cpuCapacity: data.response.body.items[1].status.capacity.cpu, 66 | memoryCapacity: data.response.body.items[1].status.capacity.memory, 67 | podsCapacity: data.response.body.items[1].status.capacity.pods 68 | }, 69 | presureStatus: { 70 | lastHeartbeatTime: data.response.body.items[1].status.conditions[0].lastHeartbeatTime, 71 | memoryPressure: data.response.body.items[1].status.conditions[0].message, 72 | diskPressure: data.response.body.items[1].status.conditions[1].message, 73 | PIDPressure: data.response.body.items[1].status.conditions[2].message, 74 | status: data.response.body.items[1].status.conditions[3].message 75 | }, 76 | totalImages: data.response.body.items[1].status.images.length 77 | }, 78 | 79 | { 80 | name: data.response.body.items[2].metadata.name, 81 | uid: data.response.body.items[2].metadata.uid, 82 | creationTimestamp: data.response.body.items[2].metadata.creationTimestamp, 83 | capacity: { 84 | cpuCapacity: data.response.body.items[2].status.capacity.cpu, 85 | memoryCapacity: data.response.body.items[2].status.capacity.memory, 86 | podsCapacity: data.response.body.items[2].status.capacity.pods 87 | }, 88 | presureStatus: { 89 | lastHeartbeatTime: data.response.body.items[2].status.conditions[0].lastHeartbeatTime, 90 | memoryPressure: data.response.body.items[2].status.conditions[0].message, 91 | diskPressure: data.response.body.items[2].status.conditions[1].message, 92 | PIDPressure: data.response.body.items[2].status.conditions[2].message, 93 | status: data.response.body.items[2].status.conditions[3].message 94 | }, 95 | totalImages: data.response.body.items[2].status.images.length 96 | }, 97 | 98 | { 99 | name: data.response.body.items[3].metadata.name, 100 | uid: data.response.body.items[3].metadata.uid, 101 | creationTimestamp: data.response.body.items[3].metadata.creationTimestamp, 102 | capacity: { 103 | cpuCapacity: data.response.body.items[3].status.capacity.cpu, 104 | memoryCapacity: data.response.body.items[3].status.capacity.memory, 105 | podsCapacity: data.response.body.items[3].status.capacity.pods 106 | }, 107 | presureStatus: { 108 | lastHeartbeatTime: data.response.body.items[3].status.conditions[0].lastHeartbeatTime, 109 | memoryPressure: data.response.body.items[3].status.conditions[0].message, 110 | diskPressure: data.response.body.items[3].status.conditions[1].message, 111 | PIDPressure: data.response.body.items[3].status.conditions[2].message, 112 | status: data.response.body.items[3].status.conditions[3].message 113 | }, 114 | totalImages: data.response.body.items[3].status.images.length 115 | } 116 | 117 | ]; 118 | 119 | return next(); 120 | 121 | } catch (err) { 122 | console.error(err); 123 | }; 124 | } 125 | }; 126 | 127 | export default metricsController; -------------------------------------------------------------------------------- /server/controllers/throughputController.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler, Request, Response, NextFunction } from 'express'; 2 | 3 | const k8s = require('@kubernetes/client-node'); 4 | const kc = new k8s.KubeConfig(); 5 | kc.loadFromDefault(); 6 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 7 | 8 | interface ThroughputController { 9 | getThroughput: (req: Request, res: Response, next: NextFunction) => Promise; 10 | } 11 | 12 | 13 | 14 | const throughputController: ThroughputController = { 15 | 16 | 17 | getThroughput: async (req, res, next) => { 18 | 19 | try { 20 | 21 | const data = await k8sApi.listNode(); 22 | console.log(data); 23 | 24 | 25 | 26 | 27 | 28 | return next(); 29 | } 30 | catch(err) { 31 | console.error(err); 32 | } 33 | 34 | } 35 | 36 | } 37 | 38 | 39 | 40 | 41 | export default throughputController; -------------------------------------------------------------------------------- /server/routes/metricsRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import metricsController from '../controllers/metricsController'; // Adjust this path if necessary 3 | import throughputController from '../controllers/throughputController'; 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/', metricsController.getNodeList, (req: Request, res: Response) => { 8 | return res.status(200).send(res.locals.nodesList); 9 | }); 10 | 11 | router.get('/throughput', throughputController.getThroughput, (req: Request, res: Response) => { 12 | return res.status(200).send(res.locals.throughputData); 13 | }) 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Express, Request, Response, NextFunction } from "express"; 2 | import dotenv from "dotenv"; 3 | import path from "path"; 4 | import cors from 'cors'; 5 | import metricsRouter from './routes/metricsRouter'; // Assuming you'll also convert this to TS and use export default 6 | 7 | dotenv.config(); 8 | 9 | const app: Express = express(); 10 | const port: string | number = process.env.PORT || 3333; 11 | 12 | // HANDLE PARSING OF REQUEST BODY 13 | app.use(express.json()); 14 | app.use(express.urlencoded({ extended: true })); 15 | app.use(cors()); 16 | 17 | // SERVE STATIC FILES 18 | app.use(express.static(path.resolve(__dirname, '../client/public'))); 19 | 20 | // METRICS ROUTER 21 | app.use('/metrics', metricsRouter); 22 | 23 | // CATCH-ALL ROUTE HANDLER 24 | app.use('/*', (req: Request, res: Response) => { 25 | return res.status(404).send('Page not found - 404'); 26 | }); 27 | 28 | // Global error handler 29 | app.use((err: any, req: Request, res: Response, next: NextFunction) => { 30 | const defaultErr = { 31 | log: 'Express error handler caught unknown middleware error', 32 | status: 500, 33 | message: { err: 'An error occurred' } 34 | }; 35 | const errorObj = { ...defaultErr, ...err }; 36 | console.log(errorObj.log); 37 | return res.status(errorObj.status).json(errorObj.message); 38 | }); 39 | 40 | app.listen(3333, () => { 41 | console.log(`[server]: Server is running at http://localhost:${port}`); 42 | }); 43 | 44 | export default app; 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kube Est8 7 | 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /tests/space.spec.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../server/server"); 3 | 4 | describe("Is the content Object & Is content status 200", () => { 5 | it("server /destinations endpoints", async () => { 6 | const response = await request(app).get("/metrics"); 7 | expect(response.statusCode).toBe(200); 8 | expect(typeof response).toBe('object'); 9 | }); 10 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", // Compile to ES5 4 | "lib": ["dom", "dom.iterable", "esnext"], // Include DOM and ESNext lib for frontend code 5 | "allowJs": true, // Allow JavaScript files to be compiled 6 | "skipLibCheck": true, // Skip type checking of all declaration files (*.d.ts) 7 | "esModuleInterop": true, // Enables emit interoperability between CommonJS and ES Modules 8 | "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export 9 | "strict": true, // Enable all strict type-checking options 10 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file 11 | "module": "commonjs", // Use commonjs module resolution 12 | "moduleResolution": "node", // Use Node.js module resolution strategy 13 | "resolveJsonModule": true, // Allow importing .json files 14 | "isolatedModules": true, // Ensure each file can be safely transpiled without relying on other imports 15 | "noEmit": true, // Do not emit outputs (useful when using Babel for transpilation) 16 | "jsx": "react-jsx" // Support JSX in .tsx files 17 | }, 18 | "include": [ 19 | "client/src/**/*", // Include your frontend source files 20 | "server/**/*" // Include your backend source files 21 | ], 22 | "exclude": [ 23 | "node_modules", // Exclude the node_modules directory 24 | "**/*.spec.ts" // Exclude test files 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV, 6 | entry: "./client/src/index.tsx", 7 | output: { 8 | path: path.resolve(__dirname, "./client/public"), 9 | filename: "bundle.js", 10 | }, 11 | devServer: { 12 | port: "8090", 13 | static: ["./client/public"], 14 | open: true, 15 | hot: true, 16 | liveReload: true, 17 | historyApiFallback: true, 18 | proxy: { 19 | "/": "http://localhost:3333", 20 | }, 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(ts|tsx|js|jsx)$/, 26 | exclude: /node_modules/, 27 | use: { 28 | loader: "babel-loader", 29 | options: { 30 | presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], // Already correctly included 31 | }, 32 | }, 33 | }, 34 | { 35 | test: /.(css|scss)$/, 36 | exclude: /node_modules/, 37 | use: ["style-loader", "css-loader"], 38 | }, 39 | { 40 | test: /\.(png|jpe?g|gif)$/i, 41 | use: [ 42 | { 43 | loader: "file-loader", 44 | }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | plugins: [ 50 | new HtmlWebpackPlugin({ 51 | template: "./template.html", 52 | }), 53 | ], 54 | }; 55 | --------------------------------------------------------------------------------