├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── __mocks__ ├── fileMock.js └── styleMock.js ├── assets ├── KuberSee-1.png └── KuberSee-t.png ├── babel.config.js ├── client ├── Components │ ├── App │ │ └── App.tsx │ ├── Dashboard │ │ ├── Dashboard.test.jsx │ │ ├── Dashboard.tsx │ │ └── LogsDashboard.tsx │ ├── Dropdown │ │ ├── DropdownButton.tsx │ │ └── DropdownPods.tsx │ ├── LineGraph │ │ └── LineGraph.tsx │ ├── LogTable │ │ └── LogTable.tsx │ ├── Logout │ │ └── Logout.tsx │ └── NavBar │ │ └── NavBar.tsx ├── Pages │ ├── Home │ │ └── HomePage.tsx │ ├── Login │ │ └── LoginPage.tsx │ ├── NotFound │ │ ├── NotFound.test.tsx │ │ └── NotFound.tsx │ └── Signup │ │ └── SignupPage.tsx ├── index.js ├── styling │ └── styles.css └── types.ts ├── electron └── main.ts ├── fileTransformer.js ├── index.d.ts ├── index.html ├── jest.config.js ├── kubectl ├── package.json ├── server ├── controllers │ ├── sessionController.js │ ├── socketController.js │ └── userController.js ├── model │ ├── logsModel.js │ ├── sessionModel.js │ └── userModels.js ├── routes │ ├── authRoute.js │ └── socketRoutes.js └── server.js ├── styles.css ├── tailwind.config.js ├── target └── npmlist.json ├── tsconfig.json └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'plugin:react/recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | parser: '@typescript-eslint/parser', 12 | overrides: [ 13 | { 14 | env: { 15 | node: true, 16 | }, 17 | files: [ 18 | '.eslintrc.{js,cjs}', 19 | ], 20 | parserOptions: { 21 | sourceType: 'script', 22 | }, 23 | }, 24 | ], 25 | parserOptions: { 26 | ecmaVersion: 'latest', 27 | sourceType: 'module', 28 | }, 29 | plugins: [ 30 | 'react', 31 | '@typescript-eslint' 32 | ], 33 | rules: { 34 | }, 35 | root: true, 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .DS_Store 4 | package-lock.json 5 | .vscode 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | .pnpm-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | *.lcov 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (https://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # Snowpack dependency directory (https://snowpack.dev/) 52 | web_modules/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Optional stylelint cache 64 | .stylelintcache 65 | 66 | # Microbundle cache 67 | .rpt2_cache/ 68 | .rts2_cache_cjs/ 69 | .rts2_cache_es/ 70 | .rts2_cache_umd/ 71 | 72 | # Optional REPL history 73 | .node_repl_history 74 | 75 | # Output of 'npm pack' 76 | *.tgz 77 | 78 | # Yarn Integrity file 79 | .yarn-integrity 80 | 81 | # dotenv environment variable files 82 | .env 83 | .env.development.local 84 | .env.test.local 85 | .env.production.local 86 | .env.local 87 | 88 | # parcel-bundler cache (https://parceljs.org/) 89 | .cache 90 | .parcel-cache 91 | 92 | # Next.js build output 93 | .next 94 | out 95 | 96 | # Nuxt.js build / generate output 97 | .nuxt 98 | dist 99 | 100 | # Gatsby files 101 | .cache/ 102 | # Comment in the public line in if your project uses Gatsby and not Next.js 103 | # https://nextjs.org/blog/next-9-1#public-directory-support 104 | # public 105 | 106 | # vuepress build output 107 | .vuepress/dist 108 | 109 | # vuepress v2.x temp and cache directory 110 | .temp 111 | .cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | #env 139 | .env 140 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.17.1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KuberSee 2 | 3 |
4 |
5 | Logo 6 |
7 |
8 | 9 | 10 | ## About KuberSee 11 | KuberSee is an open source product that provides developers an easier way to visualize their Kubernetes clusters in real-time. Through our application, developers can obtain log data on their pods and view information on their pods, nodes, and namespaces. 12 | 13 | For more information visit our [website](http://www.kubersee.com/) and our [medium article](https://medium.com/@kubersee/visualize-your-kubernetes-clusters-and-log-data-in-real-time-d58eb47409e0). 14 | 15 | 16 | ## Tech Stacks 17 | 18 |
19 | 20 | [![Typescript][TS.js]][TS-url] [![JavaScript][JavaScript]][JavaScript-url] [![React][React.js]][React-url] [![React Router][React Router]][React-Router-url] [![Node.js][Node.js]][Node-url] [![Kubernetes][Kubernetes]][Kubernetes-url] [![Jest][Jest]][Jest-url] [![Tailwind][Tailwind]][Tailwind-url] [![DaisyUI][DaisyUI]][DaisyUI-url] [![MUI][MUI]][MUI-url] [![SocketIO][SocketIO]][SocketIO-url] [![Electron.js][Electron.js]][Electron-url] [![AWS][AWS]][AWS-url] 21 |
22 | 23 | https://github.com/oslabs-beta/KuberSee/assets/124442983/b3f1c2df-390a-4980-bfa0-658924404a3d 24 | 25 | ## Table of Contents 26 | 27 |
    28 |
  1. 29 | About KuberSee
  2. 30 |
  3. Download the Application
  4. 31 |
  5. Configuration of Cloud Kubernetes Cluster
  6. 32 |
  7. How to Test Through Minikube
  8. 33 |
  9. Authors
  10. 34 |
35 | 36 | ## Download the Application 37 | - Download the application from www.kubersee.com (for Mac users only). 38 | - Drag the zip file to your Application folder. 39 | - Open the zip file. 40 | - Once installed, control-click the app icon and choose "Open" from the shortcut menu. Then hit “cancel” and control-click the app icon a second time and choose “open”. You will receive a prompt to “open” the application, allowing you to override Apple's default preferences. 41 | - From here, if your cluster is already configured, you can sign up for an account and begin using the application to visualize your Kubernetes cluster. 42 | 43 | ## Configuration of Cloud Kubernetes Cluster 44 | - ### Configure with GCloud: 45 | - Enter command line: `gcloud init` This command will guide you through the authentication process and ask you to log in with your Google account using a web browser. 46 | - Follow the prompts to select your Google Cloud project, authenticate your account, and set your default region and zone. 47 | - Set Up Kubernetes Configuration: If you are using Google Kubernetes Engine (GKE) to create your Kubernetes clusters, you can use the following command to set up kubectl to use the credentials from your Google Cloud project: `gcloud container clusters get-credentials CLUSTER_NAME --zone ZONE --project PROJECT_ID` Replace CLUSTER_NAME with the name of your GKE cluster, ZONE with the GCP zone where your cluster is located, and PROJECT_ID with your Google Cloud project ID. 48 | - Verify Credentials: To verify that kubectl is using the correct credentials, you can use the following command to get information about your Kubernetes cluster:`kubectl cluster-info` This command will show you the API server address of your cluster, indicating that you are successfully using the credentials obtained from your Google Cloud project. 49 | - After following these steps, you should have the necessary credentials to interact with your Google Kubernetes Engine cluster using kubectl. If you need to switch between different Google Cloud projects or clusters, you can use the gcloud config set project PROJECT_ID and kubectl config use-context CONTEXT_NAME commands to switch between configurations. 50 | 51 | - ### Configure with Amazon EKS (Elastic Kubernetes Service) 52 | - Install Dependencies: Ensure you have the following dependencies installed on your machine: 53 | - AWS CLI: Install the AWS Command Line Interface as explained in the previous response. 54 | - kubectl: Install kubectl to interact with Kubernetes clusters. You can find installation instructions here: https://kubernetes.io/docs/tasks/tools/install-kubectl/ 55 | - Configure AWS CLI: If you haven't already, configure the AWS CLI with your AWS access keys using the aws configure command. This step is necessary to access the Amazon EKS cluster. 56 | - Create or Access an Amazon EKS Cluster: Using the AWS Management Console or the AWS CLI, create an Amazon EKS cluster or access an existing one. Note down the cluster name, region, and AWS account ID associated with the cluster. 57 | - Install AWS IAM Authenticator: Amazon EKS requires the AWS IAM Authenticator to authenticate with your EKS cluster using IAM roles. You can find installation instructions here: https://docs.aws.amazon.com/eks/latest/userguide/install-aws-iam-authenticator.html 58 | - Configure kubectl for Amazon EKS: Run the following command to update the Kubernetes configuration and associate kubectl with your Amazon EKS cluster:`aws eks update-kubeconfig --name CLUSTER_NAME --region REGION` Replace CLUSTER_NAME with the name of your Amazon EKS cluster and REGION with the AWS region where your cluster is located. This command retrieves the necessary authentication information and updates the Kubernetes configuration file (~/.kube/config). 59 | - Verify Configuration: You can verify that kubectl is correctly configured to access your Amazon EKS cluster by running a simple command: `kubectl get nodes` This command should return the list of nodes in your Amazon EKS cluster. Now, your Kubernetes configuration is set up, and you can use kubectl to interact with your Amazon EKS cluster and manage Kubernetes resources. 60 | - Please note that managing Amazon EKS clusters may involve additional steps, such as creating and configuring worker nodes, setting up security groups, and managing IAM roles. For more detailed information on managing Amazon EKS, refer to the official AWS documentation: https://aws.amazon.com/eks/ 61 | 62 | ## How to test through Minikube: 63 | Note: If Kubernetes Cluster configuration setup with .kube DISREGARD minikube 64 | - Download Docker Desktop 65 | - Install Docker Desktop on your device and enable the Kubernetes extension: https://www.docker.com/products/docker-desktop/ 66 | - Install Minikube 67 | - Follow the installation guide for your device and install the appropriate minikube for your operating system. https://minikube.sigs.k8s.io/docs/start/ 68 | - On your terminal, start your minikube: `minikube start` 69 | - Enable metrics: `minikube addons enable metrics-server` 70 | - Create a pod: `kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080` 71 | - When you are done with the session, stop your minikube: `minikube stop` 72 |
73 | 74 | ______________________________________________________ 75 | ## Authors 76 | 77 | | Developed By | Github | LinkedIn | 78 | | :----------: | :---------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------: | 79 | | Joey Cheng | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/joey-cheng-codes/) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/joey-cheng-works/) | 80 | | Elinor Weissberg | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/inorle) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/elinorweissberg/) | 81 | | Jordan Lopez | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/jordanlope) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/jordan-lopez-28538019a/) | 82 | | Alexis Contreras | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/alexis-contre) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/alexis-contre/) | 83 | | Tianqi Zhao | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/TianqiZhao416) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/tianqi-zhao416/) | 84 | 85 | 86 |
87 | 88 | 89 | [React.js]: https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB 90 | [React-url]: https://reactjs.org/ 91 | [TS.js]: https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white 92 | [TS-url]: https://www.typescriptlang.org/ 93 | [D3.js]: https://img.shields.io/badge/d3.js-F9A03C?style=for-the-badge&logo=d3.js&logoColor=white 94 | [D3-url]: https://d3js.org/ 95 | [React Router]: https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white 96 | [React-Router-url]: https://reactrouter.com/en/main 97 | [JavaScript]: https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E 98 | [JavaScript-url]: https://www.javascript.com/ 99 | [Node.js]: https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white 100 | [Node-url]: https://nodejs.org/ 101 | [Kubernetes]: https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white 102 | [Kubernetes-url]: https://kubernetes.io/ 103 | [Jest]: https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white 104 | [Jest-url]: https://jestjs.io/ 105 | [AWS]: https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white 106 | [AWS-url]: https://aws.amazon.com/ 107 | [DaisyUI]: https://img.shields.io/badge/daisyui-5A0EF8?style=for-the-badge&logo=daisyui&logoColor=white 108 | [DaisyUI-url]: https://daisyui.com/ 109 | [Tailwind]: https://img.shields.io/badge/Tailwind-%231DA1F2.svg?style=for-the-badge&logo=tailwind-css&logoColor=white 110 | [Tailwind-url]: https://tailwindcss.com/ 111 | [MUI]: https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white 112 | [MUI-url]: https://mui.com/ 113 | [SocketIO]: https://img.shields.io/badge/Socket.io-black?style=for-the-badge&logo=socket.io&badgeColor=010101 114 | [SocketIO-url]: https://socket.io/ 115 | [Electron.js]: https://img.shields.io/badge/Electron-191970?style=for-the-badge&logo=Electron&logoColor=white 116 | [Electron-url]: https://www.electronjs.org/ 117 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /assets/KuberSee-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KuberSee/abe326df25f43a056287c4e07ec7c43df98cac1d/assets/KuberSee-1.png -------------------------------------------------------------------------------- /assets/KuberSee-t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KuberSee/abe326df25f43a056287c4e07ec7c43df98cac1d/assets/KuberSee-t.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | '@babel/preset-react' 6 | ], 7 | }; -------------------------------------------------------------------------------- /client/Components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | import NotFound from '../../Pages/NotFound/NotFound'; 4 | import HomePage from '../../Pages/Home/HomePage'; 5 | import LoginPage from '../../Pages/Login/LoginPage'; 6 | import SignupPage from '../../Pages/Signup/SignupPage'; 7 | import NavBar from '../NavBar/NavBar'; 8 | import { io, Socket } from 'socket.io-client'; 9 | 10 | const socket: Socket = io('http://localhost:3000'); 11 | 12 | const App = () => { 13 | useEffect(() => { 14 | socket.emit('stats', null); 15 | }, []); 16 | 17 | return ( 18 |
19 | 20 | }> 21 | } /> 22 | } /> 23 | } /> 24 | } /> 25 | 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /client/Components/Dashboard/Dashboard.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import Dashboard from './Dashboard' 3 | import '@testing-library/jest-dom'; 4 | import React from 'react'; 5 | 6 | describe('Renders Element on Dashboard', () => { 7 | const renderDashboard= () => { 8 | render() 13 | } 14 | test("Renders Element on Dashboard", () => { 15 | renderDashboard(); 16 | expect(screen.getByText('Namespaces')).toBeInTheDocument() 17 | expect(screen.getByText('Nodes')).toBeInTheDocument() 18 | expect(screen.getByText('Pods')).toBeInTheDocument() 19 | expect(screen.getByText('2')).toBeInTheDocument() 20 | expect(screen.getByText('1')).toBeInTheDocument() 21 | expect(screen.getByText('4')).toBeInTheDocument() 22 | expect(document.getElementById('1')).toHaveTextContent('Namespaces') 23 | expect(document.getElementById('1')).toHaveTextContent('1') 24 | expect(document.getElementById('2')).toHaveTextContent('Nodes') 25 | expect(document.getElementById('2')).toHaveTextContent('4') 26 | expect(document.getElementById('3')).toHaveTextContent('Pods') 27 | expect(document.getElementById('3')).toHaveTextContent('2') 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | }); 38 | }); -------------------------------------------------------------------------------- /client/Components/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StatsDataProps, StatsData, ReactElementsArray } from '../../types' 3 | 4 | export default function Dashboard({ data = [] }: StatsDataProps) { 5 | 6 | 7 | const stats: ReactElementsArray = data.map((datum: StatsData) => ( 8 |
13 |
14 | {datum.name} 15 |
16 | 17 |
18 | {datum.value.toString()} 19 |
20 |
21 | )) 22 | 23 | return ( 24 |
25 |
26 |
27 |
28 | {stats} 29 |
30 |
31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /client/Components/Dashboard/LogsDashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LogTable from '../LogTable/LogTable'; 3 | import { type LogsData } from '../../types.js'; 4 | 5 | type LogsDashboardProps = { 6 | logs: LogsData[]; 7 | }; 8 | 9 | export default function LogsDashboard({ logs }: LogsDashboardProps) { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /client/Components/Dropdown/DropdownButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, Dispatch, SetStateAction, MouseEvent } from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Menu from '@mui/material/Menu'; 4 | import MenuItem from '@mui/material/MenuItem'; 5 | import Fade from '@mui/material/Fade'; 6 | import Box from '@mui/material/Box'; 7 | import { alpha } from '@mui/system'; 8 | 9 | type DropdownMenuProps = { 10 | namespaces: string[]; 11 | changeNamespace: Dispatch>; 12 | } 13 | export default function DropdownMenu({ changeNamespace, namespaces = [] }: DropdownMenuProps) { 14 | const [anchorEl, setAnchorEl] = useState(null); 15 | 16 | const open = Boolean(anchorEl); 17 | const handleClick = (event: MouseEvent) => { 18 | setAnchorEl(event.currentTarget); 19 | }; 20 | const handleClose = () => { 21 | setAnchorEl(null); 22 | }; 23 | 24 | return ( 25 | 26 |
27 | 37 | 47 | {namespaces && namespaces.map((namespace) => 48 | ( 49 | { 50 | changeNamespace(namespace); 51 | handleClose(); 52 | }}> 53 | {namespace} 54 | 55 | ))} 56 | 57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /client/Components/Dropdown/DropdownPods.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, Dispatch, SetStateAction, MouseEvent } from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Menu from '@mui/material/Menu'; 4 | import MenuItem from '@mui/material/MenuItem'; 5 | import Fade from '@mui/material/Fade'; 6 | import Box from '@mui/material/Box'; 7 | import { alpha } from '@mui/system'; 8 | 9 | type DropdownPodsProps = { 10 | changePod: Dispatch>; 11 | pods: string[]; 12 | } 13 | export default function DropdownPods({ changePod, pods = [] }: DropdownPodsProps) { 14 | const [anchorEl, setAnchorEl] = useState(null); 15 | const open = Boolean(anchorEl); 16 | const handleClick = (event: MouseEvent) => { 17 | setAnchorEl(event.currentTarget); 18 | }; 19 | const handleClose = () => { 20 | setAnchorEl(null); 21 | }; 22 | 23 | return ( 24 | 25 |
26 | 36 | 46 | {pods && 47 | pods.map((pod) => ( 48 | { 51 | changePod(pod); 52 | handleClose(); 53 | }} 54 | > 55 | {pod} 56 | 57 | ))} 58 | 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /client/Components/LineGraph/LineGraph.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | import { nest } from 'd3-collection'; 3 | import * as d3 from 'd3'; 4 | import { MappedNodeMetrics, MappedPodMetrics } from '../../types'; 5 | type LineGraphProps = { 6 | dataRef: { 7 | current: Array 8 | }; 9 | yaxis: string; 10 | propertyName: 'cpuCurrentUsage' | 'memoryCurrentUsage' | 'cpuPercentage' | 'memoryPercentage'; 11 | legendName: string; 12 | title: string; 13 | } 14 | 15 | type GraphVars = [ 16 | graph: d3.Selection, 17 | circleGroup: d3.Selection, 18 | xScaleGroup: d3.Selection, 19 | yScaleGroup: d3.Selection, 20 | width: number, 21 | margin: { 22 | top: number; 23 | right: number; 24 | bottom: number; 25 | left: number; 26 | } 27 | ] 28 | 29 | const LineGraph = ({ dataRef, yaxis, propertyName, legendName, title }: LineGraphProps) => { 30 | const svgRef = useRef(); 31 | function initialize(width: number, height: number): GraphVars { 32 | 33 | const margin = { top: 20, right: 175, bottom: 50, left: 100 }; 34 | width = width - margin.left - margin.right; 35 | height = height - margin.top - margin.bottom; 36 | 37 | const graph = d3 38 | .select(svgRef.current) //select the svg element from the virtual DOM. 39 | .attr("width", width + margin.left + margin.right) 40 | .attr("height", height + margin.top + margin.bottom) 41 | .attr("transform", `translate(${margin.left}, ${margin.top})`); 42 | 43 | const circleGroup = graph.append('g'); 44 | 45 | const xScaleGroup = graph.append('g'); 46 | 47 | const yScaleGroup = graph.append('g'); 48 | 49 | graph 50 | .append('clipPath') // define a clip path 51 | .attr('id', 'rectangle-clip') // give the clipPath an ID 52 | .append('rect') // shape it as an ellipse 53 | .attr('x', 101) // position the x-center 54 | .attr('y', 0) // position the y-center 55 | .attr('width', width * 2 + 300) 56 | .attr('height', height); 57 | 58 | 59 | graph.append("text") 60 | .attr("text-anchor", "end") 61 | .attr("x", width / 2 + margin.left) 62 | .attr("y", height + margin.bottom / 2) 63 | .text("Time (seconds)") 64 | .style("fill", "white") 65 | .attr('font-size', 12) 66 | 67 | graph 68 | .append("text") 69 | .attr("text-anchor", "end") 70 | .attr("transform", "rotate(-90)") 71 | .attr("x", -height / 2 + margin.bottom) 72 | .attr("y", margin.left / 6) 73 | .text(`${yaxis}`) 74 | .style("fill", "white") 75 | .attr('font-size', 12) 76 | 77 | graph.append("text") 78 | .attr("text-anchor", "start") 79 | .attr("x", width + margin.right / 8) 80 | .attr("y", 20) 81 | .text(legendName) 82 | .style("fill", "white") 83 | .attr('font-size', 12) 84 | 85 | return [graph, circleGroup, xScaleGroup, yScaleGroup, width, margin]; // returns an array of the variables, giving you the reference to the variable. 86 | } 87 | 88 | // updates the graph. data is our data, now is the end time, and lookback is the start time, graph vars is the array of reference of what we returned in initialize. 89 | function render(data: Array, now: Date, lookback: Date, graphVars: GraphVars) { 90 | const room_for_axis = 100; // padding for axis 91 | 92 | const [graph, circleGroup, xScaleGroup, yScaleGroup, width] = graphVars; 93 | 94 | const radius = +graph.attr('width') / 200.0; // for the circle 95 | 96 | 97 | const sumStat = nest() 98 | .key(function ({ name }) { return name }) 99 | .entries(data); 100 | 101 | const color = d3.scaleOrdinal(d3.schemeCategory10); 102 | 103 | const xScale = d3 104 | .scaleTime() //accepts a date as a value and helps us compare the time 105 | .domain([lookback, now]) // min time vs max time of the pods 106 | // Add a little extra room for y axis 107 | .range([room_for_axis + 5, width]); 108 | 109 | const isMappedNodeMetricsArray = (data: Array): data is MappedNodeMetrics[] => { 110 | return data.length > 0 && 'node' in data[0]; 111 | } 112 | 113 | const minValue = isMappedNodeMetricsArray(data) 114 | ? d3.min(data as MappedNodeMetrics[], (d) => { 115 | if (propertyName === 'cpuCurrentUsage') { 116 | return -0.00000001; 117 | } 118 | return +d[propertyName] / 4; 119 | }) 120 | : d3.min(data as MappedPodMetrics[], (d) => { 121 | if (propertyName === 'cpuCurrentUsage') { 122 | return -0.00000001; 123 | } 124 | return +d[propertyName] / 4; 125 | }); 126 | 127 | const maxValue = isMappedNodeMetricsArray(data) 128 | ? d3.max(data as MappedNodeMetrics[], (d) => +d[propertyName] * 1.2) 129 | : d3.max(data as MappedPodMetrics[], (d) => +d[propertyName] * 1.2); 130 | 131 | const yScale = d3 132 | .scaleLinear() 133 | .domain([minValue, maxValue]) 134 | .range([+graph.attr('height') - room_for_axis, 0]); 135 | 136 | const colorScale = d3 137 | .scaleTime() 138 | .domain([lookback, now]) 139 | .range(['blue', 'red']); 140 | 141 | circleGroup.selectAll('path').data(sumStat).exit().remove(); 142 | graph.selectAll('text.pod-name').remove(); 143 | graph.selectAll('.circle-pod-name').remove(); 144 | 145 | graph.selectAll('.circlelegend') 146 | .data(sumStat) 147 | .join("circle") 148 | .attr('class', 'circle-pod-name') 149 | .attr("cx", width + 25) 150 | .attr("cy", function (d, i) { return 60 + i * 40 - 2 }) // 100 is where the first dot appears. 25 is the distance between dots 151 | .attr("r", 5) 152 | .style("fill", function (d) { return color(d.key) }) 153 | graph.selectAll('.pod-name-temp') 154 | .data(sumStat) 155 | .join('text') 156 | .attr('class', 'pod-name') 157 | .text(function (d) { 158 | return d.key 159 | }) 160 | .style('fill', function (d) { 161 | return (color(d.key)) 162 | }) 163 | .attr('x', width + 28) 164 | .attr('y', function (d, i) { 165 | return 60 + i * 40 166 | }) 167 | .attr('alignment-baseline', 'middle') 168 | .attr('dx', 5) 169 | .attr('font-size', 12) 170 | 171 | //subtracted 5 seconds from lookback to create additional buffer on the clip. 172 | const adjustLookback = new Date(lookback) 173 | adjustLookback.setSeconds(adjustLookback.getSeconds() - 5); 174 | // specifes what date from the plot that is older than the lookback. 175 | const to_remove = data.filter((a) => a.timestamp < adjustLookback); 176 | circleGroup.selectAll('circle').data(to_remove).exit().remove(); 177 | 178 | 179 | const valueLine = d3 180 | .line() 181 | .x((d) => { 182 | return xScale(d.timestamp); 183 | }) 184 | .y((d) => { 185 | return yScale(d[propertyName]); 186 | }); 187 | circleGroup 188 | .selectAll('.temp-path') 189 | .data(sumStat) 190 | .join('path') 191 | .attr('d', function (d) { 192 | return valueLine(d.values) 193 | }) 194 | .attr("fill", "none") 195 | .attr("clip-path", "url(#rectangle-clip)") // clip the rectangle 196 | .attr("stroke", function (d) { 197 | return (color(d.key)); 198 | }) 199 | .attr("stroke-width", 4.5) 200 | 201 | data = (data as Array).filter((a) => a.timestamp > adjustLookback); 202 | circleGroup 203 | .selectAll('g').data(data).enter().append('circle'); 204 | 205 | circleGroup 206 | .selectAll('circle') 207 | .attr('cx', function (d: MappedPodMetrics | MappedNodeMetrics) { 208 | // cx = circle's x position (specific svg attribute) 209 | return xScale(d.timestamp); //tells us where on the graph that the plot should be relative to the chart's width. 210 | }) 211 | .attr('cy', function (d: MappedPodMetrics | MappedNodeMetrics) { 212 | return yScale(d[propertyName]); // tells us where on the graph that the plot should be relative to chart's height. 213 | }) 214 | .attr('r', radius) 215 | .attr('clip-path', 'url(#rectangle-clip)') // clip the rectangle 216 | .attr('fill', function (d: MappedPodMetrics | MappedNodeMetrics) { 217 | return colorScale(d.timestamp); 218 | }); 219 | 220 | const x_axis = d3.axisBottom(xScale); 221 | xScaleGroup 222 | .attr( 223 | 'transform', 224 | 'translate(0,' + (+graph.attr('height') - room_for_axis) + ')' 225 | ) 226 | .call(x_axis); 227 | 228 | const y_axis = d3.axisLeft(yScale); 229 | yScaleGroup 230 | .attr('transform', 'translate(' + room_for_axis + ',0)') 231 | .call(y_axis); 232 | 233 | return data; // return the updated filter data. 234 | } 235 | 236 | useEffect(() => { 237 | const lookback_s = 30; 238 | 239 | // initialize 240 | let now = new Date(); 241 | const width = 750; 242 | const graphVars = initialize(width, width * 0.6); 243 | 244 | let lookback = new Date(now); // creates a copy of now's date 245 | lookback.setSeconds(lookback.getSeconds() - lookback_s); // go back in time by 30 seconds 246 | 247 | render(dataRef.current, now, lookback, graphVars); // invoke to show first graph 248 | 249 | const updateIntervalMs = 200; 250 | const intervalID = setInterval(async function () { 251 | // Move time forward 252 | now = new Date(); 253 | lookback = new Date(now); 254 | lookback.setSeconds(lookback.getSeconds() - lookback_s); 255 | 256 | dataRef.current = render(dataRef.current, now, lookback, graphVars); // reassigning data with the newly pushed data. 257 | }, updateIntervalMs); 258 | return () => { 259 | clearInterval(intervalID); // once the component is removed, it will perform a clean up. Don't want the setInterval to run in the background even if the component is running in the background. 260 | }; 261 | }, []); 262 | 263 | return ( 264 |
265 |

{title}

266 | 267 |
268 | ) 269 | }; 270 | 271 | export default LineGraph; 272 | -------------------------------------------------------------------------------- /client/Components/LogTable/LogTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import { DataGrid, GridToolbar } from '@mui/x-data-grid'; 4 | // const VISIBLE_FIELDS = ['headers', 'message']; 5 | import { type LogsData } from '../../types'; 6 | 7 | type LogsProps = { 8 | logs: LogsData[] 9 | } 10 | export default function LogTable({ logs }: LogsProps) { 11 | const columns = [ 12 | { field: 'header', headerName: 'Header', width: 500 }, 13 | { 14 | field: 'message', 15 | headerName: 'Message', 16 | width: 1500, 17 | }, 18 | ]; 19 | 20 | return ( 21 | 22 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /client/Components/Logout/Logout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | export default function Logout() { 5 | const navigate = useNavigate() 6 | const logout = () => { 7 | navigate('*') 8 | } 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } -------------------------------------------------------------------------------- /client/Components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Outlet, Link } from 'react-router-dom'; 3 | 4 | const NavBar = () => { 5 | return ( 6 |
7 | <> 8 | 24 | 25 | 26 |
27 | ); 28 | }; 29 | 30 | export default NavBar; 31 | -------------------------------------------------------------------------------- /client/Pages/Home/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import LogsDashboard from '../../Components/Dashboard/LogsDashboard'; 3 | import Dashboard from '../../Components/Dashboard/Dashboard'; 4 | import LineGraph from '../../Components/LineGraph/LineGraph'; 5 | import DropdownMenu from '../../Components/Dropdown/DropdownButton'; 6 | import * as d3 from 'd3'; 7 | import DropdownPods from '../../Components/Dropdown/DropdownPods'; 8 | import { type Socket } from 'socket.io-client'; 9 | import { type StatsData, type LogsData } from '../../types'; 10 | import Logout from '../../Components/Logout/Logout'; 11 | type HomePageProps = { 12 | socket: Socket; 13 | }; 14 | 15 | type NodeMetrics = { 16 | node: string; 17 | cpuCurrentUsage: number; 18 | cpuTotal: number; 19 | memoryCurrentUsage: number; 20 | memoryTotal: number; 21 | }; 22 | 23 | type PodMetrics = { 24 | pod: string; 25 | cpuCurrentUsage: number; 26 | memoryCurrentUsage: number; 27 | }; 28 | 29 | const initialStats: StatsData[] = [ 30 | { id: '1', name: 'Namespaces', value: 0 }, 31 | { id: '2', name: 'Nodes', value: 0 }, 32 | { id: '3', name: 'Pods', value: 0 }, 33 | ]; 34 | 35 | const initialNamespaces: string[] = []; 36 | 37 | const initialLogs: LogsData[] = [{ id: '1', header: '', message: '' }]; 38 | 39 | export default function HomePage({ socket }: HomePageProps) { 40 | const [stats, setStats] = useState(initialStats); 41 | 42 | const [currentPod, setCurrentPod] = useState(''); 43 | const [currentNamespace, setCurrentNamespace] = useState('default'); 44 | const namespacesRef = useRef(initialNamespaces); 45 | const podRef = useRef([]); 46 | const podNamesRef = useRef([]); 47 | const nodeRef = useRef([]); 48 | const [logs, setLogs] = useState(initialLogs); 49 | 50 | useEffect(() => { 51 | socket.emit('metrics', { 52 | namespace: currentNamespace, 53 | }); 54 | }, [currentNamespace]); 55 | 56 | useEffect(() => { 57 | if (currentPod !== '') { 58 | socket.emit('logs', { 59 | namespace: currentNamespace, 60 | podname: currentPod, 61 | }); 62 | } 63 | }, [currentPod]); 64 | 65 | useEffect(() => { 66 | socket.on('logs', log => { 67 | setLogs(log); 68 | }); 69 | }, [currentPod]); 70 | 71 | useEffect(() => { 72 | const strictIsoParse = d3.utcParse('%Y-%m-%dT%H:%M:%S.%LZ'); // Need to use d3's isoParse: https://github.com/d3/d3-time-format 73 | 74 | socket.on('metrics', metrics => { 75 | const nodes = metrics.topNodes.map((el: NodeMetrics) => ({ 76 | name: el.node, 77 | cpuCurrentUsage: el.cpuCurrentUsage, 78 | cpuTotal: el.cpuTotal, 79 | cpuPercentage: (el.cpuCurrentUsage / el.cpuTotal), 80 | memoryCurrentUsage: el.memoryCurrentUsage, 81 | memoryTotal: el.memoryTotal, 82 | memoryPercentage: (el.memoryCurrentUsage / el.memoryTotal), 83 | timestamp: strictIsoParse(new Date().toISOString()), 84 | })); 85 | const pods = metrics.topPods.map((el: PodMetrics) => ({ 86 | name: el.pod, 87 | cpuCurrentUsage: el.cpuCurrentUsage, 88 | memoryCurrentUsage: el.memoryCurrentUsage, 89 | timestamp: strictIsoParse(new Date().toISOString()), 90 | })); 91 | podRef.current.push(...pods); 92 | nodeRef.current.push(...nodes); 93 | const newPods = metrics.topPods.map((el: PodMetrics) => el.pod); 94 | if (podNamesRef.current !== newPods) { 95 | podNamesRef.current = [...newPods]; 96 | } 97 | }); 98 | }, [socket]); 99 | 100 | useEffect(() => { 101 | socket.on('stats', data => { 102 | const newStats: StatsData[] = [ 103 | { id: '1', name: 'Namespaces', value: data.totalNamespaces }, 104 | { id: '2', name: 'Nodes', value: data.totalNodes }, 105 | { id: '3', name: 'Pods', value: data.totalPods }, 106 | ]; 107 | setStats(newStats); 108 | 109 | const newNamespaces = data.namespaces; 110 | // Update namespacesRef.current only if there are new namespaces 111 | if ( 112 | JSON.stringify(namespacesRef.current) !== JSON.stringify(newNamespaces) 113 | ) { 114 | namespacesRef.current = [...newNamespaces]; 115 | } 116 | }); 117 | }, [socket]); 118 | 119 | useEffect(() => { 120 | // Empty data from chart for pods in last namespace 121 | podRef.current = []; 122 | nodeRef.current = []; 123 | // Empty log data from last namespace 124 | setLogs([{ id: '1', header: '', message: '' }]); 125 | }, [currentNamespace]); 126 | 127 | return ( 128 | <> 129 | 130 | 131 | 135 |
136 | 137 | 138 |
139 |
140 | 141 | 142 |
143 |
144 | 145 | 146 |
147 | 148 | 149 | 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /client/Pages/Login/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import Logo from '../../../assets/KuberSee-t.png' 4 | 5 | type AuthBody = { 6 | username: string; 7 | password: string; 8 | } 9 | 10 | export default function LoginPage() { 11 | const navigate = useNavigate(); 12 | const login = async (e: MouseEvent) => { 13 | e.preventDefault(); 14 | const user = (document.getElementById('loginUsername') as HTMLInputElement).value; 15 | const pass = (document.getElementById('loginPassword') as HTMLInputElement).value; 16 | 17 | const info: AuthBody = { 18 | username: user, 19 | password: pass, 20 | }; 21 | 22 | const res = await fetch('http://localhost:3000/auth/signin', { 23 | method: 'POST', 24 | body: JSON.stringify(info), 25 | headers: { 26 | 'Content-Type': 'application/json', 27 | }, 28 | }); 29 | 30 | const answers = await res.json(); 31 | if (answers === 'wrong') { 32 | alert('no user found retry'); 33 | } else { 34 | navigate('/home'); 35 | } 36 | }; 37 | return ( 38 |
39 |
40 | 41 |
42 |

43 | Login 44 |

45 |
46 |
47 | 50 | 56 |
57 |
58 | 61 | 67 |
68 |
69 | 72 |
73 | 74 | Don't have an account ? 75 | 76 | Sign up 77 | 78 | {/* 82 | Signup 83 | */} 84 | 85 |
86 |
87 |
88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /client/Pages/NotFound/NotFound.test.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { render, screen } from '@testing-library/react'; 3 | import React from 'react'; 4 | import NotFound from './NotFound'; 5 | 6 | describe('NotFound tests', () => { 7 | it('should contains the heading 1', () => { 8 | render( 9 | 10 | ) 11 | const heading = screen.getByText(/Not Found/i); 12 | expect(heading).toBeInTheDocument() 13 | }); 14 | }); -------------------------------------------------------------------------------- /client/Pages/NotFound/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function NotFound() { 4 | return ( 5 |

Not Found

6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /client/Pages/Signup/SignupPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, MouseEvent } from 'react'; 2 | import { Link, Navigate, useNavigate } from 'react-router-dom'; 3 | import Logo from '../../../assets/KuberSee-t.png' 4 | 5 | //Creating a SALT when signing up 6 | // const salt = bcrypt.genSaltSync(10) 7 | 8 | export default function SignupPage() { 9 | //hook to handle input boxes 10 | //save us from using onClick on every input field 11 | const useInput = (init: string) => { 12 | const [value, setValue] = useState(init); 13 | const onChange = (e: React.ChangeEvent) => { 14 | const { target } = e; 15 | setValue((target as HTMLInputElement).value); 16 | }; 17 | return [value, onChange] as const; 18 | }; 19 | //instantiate usenavigate 20 | const navigate = useNavigate(); 21 | 22 | //state for our input fields; 23 | const [username, setUsername] = useInput(''); 24 | const [password, setPassword] = useInput(''); 25 | 26 | //state for checking any errors 27 | const [submitted, setSubmitted] = useState(false); 28 | const [error, setError] = useState({}); 29 | //to handle when the form is being submitted 30 | const saveUser = async (e: MouseEvent) => { 31 | e.preventDefault(); 32 | if (username === '' || password === '') setError(true); 33 | else { 34 | setSubmitted(true); 35 | setError(false); 36 | } 37 | const body = { 38 | username, 39 | password, 40 | }; 41 | 42 | try { 43 | const res = await fetch('http://localhost:3000/auth/signup', { 44 | method: 'POST', 45 | headers: { 46 | 'content-type': 'Application/JSON', 47 | }, 48 | body: JSON.stringify(body), 49 | }); 50 | navigate('/home'); 51 | const data = await res.json(); 52 | console.log(data, 'success'); 53 | } catch (error) { 54 | console.log('saveUser fetch /api/signup: ERROR: ', error); 55 | } 56 | }; 57 | //message when form is properly submitted 58 | return ( 59 |
60 | 61 |
64 | 65 |
66 |

67 | Sign Up 68 |

69 |
70 |
71 | 75 |
76 |
77 | ) => { 83 | setUsername(e); 84 | }} 85 | /> 86 | 90 |
91 | 99 |
100 | 103 |
104 |
105 |
106 |
107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { BrowserRouter } from "react-router-dom"; 3 | import * as ReactDOM from "react-dom/client"; 4 | import App from "./Components/App/App.tsx"; 5 | const root = ReactDOM.createRoot(document.getElementById("root")); 6 | 7 | root.render( 8 | // 9 | 10 | 11 | 12 | // {/* */} 13 | // 14 | // document.getElementById('root') 15 | ); 16 | -------------------------------------------------------------------------------- /client/styling/styles.css: -------------------------------------------------------------------------------- 1 | .chartSelect{ 2 | background-color: aliceblue; 3 | margin-left: 20%; 4 | } -------------------------------------------------------------------------------- /client/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | export type StatsDataProps = { 5 | data: StatsData[] 6 | } 7 | 8 | export type StatsData = { 9 | id: string, 10 | name: string, 11 | value: number, 12 | } 13 | 14 | export type LogsData = { 15 | id: string, 16 | header: string, 17 | message: string 18 | } 19 | 20 | export type ReactElementsArray = React.ReactElement[]; 21 | 22 | export type MappedNodeMetrics = { 23 | name: string; 24 | node: string; 25 | cpuCurrentUsage: number; 26 | cpuPercentage: number; 27 | cpuTotal: number; 28 | memoryCurrentUsage: number; 29 | memoryTotal: number; 30 | memoryPercentage: number; 31 | timestamp: Date; 32 | }; 33 | 34 | export type MappedPodMetrics = { 35 | pod: string; 36 | cpuCurrentUsage: number; 37 | memoryCurrentUsage: number; 38 | timestamp: Date; 39 | cpuPercentage: number; 40 | memoryPercentage: number; 41 | }; 42 | -------------------------------------------------------------------------------- /electron/main.ts: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require("electron"); 2 | const path = require("path"); 3 | const url = require("url"); 4 | const { fork } = require('child_process'); 5 | const ps = fork(path.join(__dirname, '../server/server.js')); 6 | 7 | const windowConfig = { 8 | width: 1100, 9 | height: 800, 10 | show: false, 11 | autoHideMenuBar: true, 12 | frame: false, 13 | webPreferences: { 14 | nodeIntegration: true, 15 | contextIsolation: false, 16 | webSecurity: false, 17 | }, 18 | }; 19 | 20 | function createWindow() { 21 | let mainWindow = new BrowserWindow(windowConfig); 22 | 23 | mainWindow.loadFile(path.join(__dirname, "./build/index.html")); 24 | 25 | mainWindow.once("ready-to-show", () => { 26 | mainWindow.show(); 27 | }); 28 | // Automatically open Chrome's DevTools in development mode. 29 | if (!app.isPackaged) { 30 | mainWindow.webContents.openDevTools(); 31 | } 32 | } 33 | 34 | // This method will be called when Electron has finished its initialization and 35 | // is ready to create the browser windows. 36 | // Some APIs can only be used after this event occurs. 37 | app.whenReady().then(() => { 38 | createWindow(); 39 | // setupLocalFilesNormalizerProxy(); 40 | 41 | app.on("activate", function () { 42 | // On macOS it's common to re-create a window in the app when the 43 | // dock icon is clicked and there are no other windows open. 44 | if (BrowserWindow.getAllWindows().length === 0) { 45 | createWindow(); 46 | } 47 | }); 48 | }); 49 | 50 | app.whenReady().then(() => { 51 | createWindow(); 52 | app.on("activate", () => { 53 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 54 | }); 55 | }); 56 | 57 | app.on("window-all-closed", () => { 58 | if (process.platform !== "darwin") app.quit(); 59 | }); 60 | -------------------------------------------------------------------------------- /fileTransformer.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | process(sourceText, sourcePath) { 5 | return { 6 | code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`, 7 | }; 8 | }, 9 | }; -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg' { 2 | const jpgPath: string; 3 | export default jpgPath; 4 | } 5 | declare module '*.jpeg' { 6 | const jpegPath: string; 7 | export default jpegPath; 8 | } 9 | declare module '*.svg' { 10 | const svgPath: string; 11 | export default svgPath; 12 | } 13 | declare module '*.png' { 14 | const pngPath: string; 15 | export default pngPath; 16 | } 17 | // to notify typescript of type for images. 18 | 19 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "transformIgnorePatterns": [ 3 | "node_modules/(?!@ngrx|(?!deck.gl)|ng-dynamic)" 4 | ], 5 | moduleNameMapper: { 6 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 7 | '/__mocks__/fileMock.js', 8 | '\\.(css|less)$': '/__mocks__/styleMock.js', 9 | }, 10 | transform: { 11 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 12 | '/fileTransformer.js', 13 | "\\.[jt]sx?$": "babel-jest", 14 | }, 15 | moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], 16 | moduleDirectories: ['node_modules'], 17 | preset: "ts-jest", 18 | testEnvironment: "jsdom", 19 | }; -------------------------------------------------------------------------------- /kubectl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KuberSee/abe326df25f43a056287c4e07ec7c43df98cac1d/kubectl -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubersee", 3 | "product_name": "KuberSee", 4 | "homepage": "./", 5 | "version": "1.0.0", 6 | "description": "Kubernetes Visualizer", 7 | "main": "electron/main.ts", 8 | "build": { 9 | "appId": "com.KuberSee.app" 10 | }, 11 | "scripts": { 12 | "test": "jest", 13 | "start": "nodemon server/server.js", 14 | "electron": "electron ./electron/main.ts", 15 | "start-server-electron": "concurrently \"npm run start\" \"npm run electron\"", 16 | "dev": "webpack-dev-server --open & nodemon server/server.js", 17 | "build": "NODE_ENV=production webpack", 18 | "dist": "electron-builder" 19 | }, 20 | "author": "Joey Cheng, Elinor Weissberg, Jordan Lopez, Daniel Zhao, and Alexis Contreras", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@babel/core": "^7.22.5", 24 | "@babel/preset-env": "^7.22.5", 25 | "@babel/preset-react": "^7.22.5", 26 | "@babel/preset-typescript": "^7.23.0", 27 | "@testing-library/react": "^14.0.0", 28 | "@types/d3": "^7.4.0", 29 | "@types/d3-collection": "^1.0.10", 30 | "@types/jest": "^29.5.5", 31 | "@types/node": "^20.4.1", 32 | "@typescript-eslint/eslint-plugin": "^6.4.1", 33 | "@typescript-eslint/parser": "^6.4.1", 34 | "babel-loader": "^9.1.2", 35 | "concurrently": "^8.2.0", 36 | "css-loader": "^6.8.1", 37 | "daisyui": "^3.2.1", 38 | "electron": "^25.3.1", 39 | "electron-builder": "^24.6.3", 40 | "eslint": "^8.48.0", 41 | "eslint-config-xo": "^0.43.1", 42 | "eslint-config-xo-typescript": "^1.0.1", 43 | "eslint-plugin-react": "^7.33.2", 44 | "file-loader": "^6.2.0", 45 | "html-webpack-plugin": "^5.5.3", 46 | "jest": "^29.6.1", 47 | "nodemon": "^2.0.22", 48 | "prisma": "^4.16.2", 49 | "react-router-dom": "^6.14.1", 50 | "sass": "^1.63.6", 51 | "sass-loader": "^13.3.2", 52 | "style-loader": "^3.3.3", 53 | "tailwindcss": "^3.3.2", 54 | "ts-jest": "^29.1.1", 55 | "ts-node": "^10.9.1", 56 | "typescript": "^5.1.6", 57 | "webpack": "^5.88.1", 58 | "webpack-cli": "^5.1.4", 59 | "webpack-dev-server": "^4.15.1" 60 | }, 61 | "dependencies": { 62 | "@emotion/react": "^11.11.1", 63 | "@emotion/styled": "^11.11.0", 64 | "@kubernetes/client-node": "^0.18.1", 65 | "@mui/material": "^5.14.0", 66 | "@mui/x-data-grid": "^6.10.0", 67 | "@mui/x-data-grid-generator": "^6.10.0", 68 | "@testing-library/jest-dom": "^6.1.3", 69 | "@types/react": "^18.2.21", 70 | "@types/react-dom": "^18.2.7", 71 | "babel-jest": "^29.7.0", 72 | "bcrypt": "^5.1.0", 73 | "bcryptjs": "^2.4.3", 74 | "cookie-parser": "^1.4.6", 75 | "cookieparser": "^0.1.0", 76 | "cors": "^2.8.5", 77 | "crypto-browserify": "^3.12.0", 78 | "d3": "^7.8.5", 79 | "d3-collection": "^1.0.7", 80 | "d3-selection": "^3.0.0", 81 | "dotenv": "^16.3.1", 82 | "electron-squirrel-startup": "^1.0.0", 83 | "express": "^4.18.2", 84 | "express-session": "^1.17.3", 85 | "http": "^0.0.1-security", 86 | "jest-environment-jsdom": "^29.7.0", 87 | "mongodb": "^5.6.0", 88 | "mongoose": "^7.3.1", 89 | "pg": "^8.11.1", 90 | "react": "^18.2.0", 91 | "react-dom": "^18.2.0", 92 | "socket.io": "^4.7.1", 93 | "socket.io-client": "^4.7.1", 94 | "stream": "^0.0.2", 95 | "ts-loader": "^9.4.4" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /server/controllers/sessionController.js: -------------------------------------------------------------------------------- 1 | const sessionController = {}; 2 | 3 | sessionController.createSession = (req, res, next) => { 4 | session = req.session; 5 | session.userid = req.body.username; 6 | console.log(req.session); 7 | return next(); 8 | }; 9 | 10 | module.exports = sessionController; 11 | -------------------------------------------------------------------------------- /server/controllers/socketController.js: -------------------------------------------------------------------------------- 1 | const socketController = {}; 2 | const k8s = require("@kubernetes/client-node"); 3 | 4 | socketController.getMetricsMiddleware = async function (data) { 5 | try { 6 | const kc = new k8s.KubeConfig(); 7 | kc.loadFromDefault(); 8 | 9 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 10 | const metricsClient = new k8s.Metrics(kc); 11 | 12 | const namespace = data.namespace; // Assuming data.namespace is sent from the client 13 | const locals = {}; 14 | locals.topNodes = []; 15 | locals.topPods = []; 16 | const currentTime = new Date(); 17 | 18 | await k8s.topNodes(k8sApi, metricsClient, namespace).then((nodes) => { 19 | nodes.map((node) => { 20 | locals.topNodes.push({ 21 | node: node.Node.metadata.name, 22 | cpuCurrentUsage: node.CPU.RequestTotal.toString(), 23 | cpuTotal: node.CPU.Capacity.toString(), 24 | memoryCurrentUsage: node.Memory.RequestTotal.toString(), 25 | memoryTotal: node.Memory.Capacity.toString(), 26 | timestamp: currentTime, 27 | }); 28 | }); 29 | }); 30 | 31 | await k8s.topPods(k8sApi, metricsClient, namespace).then((pods) => { 32 | pods.map((pod) => { 33 | let cpuPercentage = (pod.CPU.CurrentUsage / pod.CPU.LimitTotal) * 100; 34 | if ( 35 | cpuPercentage === Infinity || 36 | typeof cpuPercentage === "undefined" 37 | ) { 38 | cpuPercentage = 0; 39 | } 40 | 41 | locals.topPods.push({ 42 | pod: pod.Pod.metadata.name, 43 | cpuCurrentUsage: pod.CPU.CurrentUsage, 44 | memoryCurrentUsage: pod.Memory.CurrentUsage.toString(), 45 | timestamp: currentTime, 46 | }); 47 | }); 48 | }); 49 | 50 | return { 51 | topNodes: locals.topNodes, 52 | topPods: locals.topPods, 53 | }; // Call next to proceed to the next middleware or the event handler 54 | } catch (error) { 55 | console.error("Error fetching logs:", error); 56 | // Emit an error event or handle it in the event handler if needed 57 | return { 58 | topNodes: [], 59 | topPods: [], 60 | }; 61 | } 62 | }; 63 | 64 | socketController.getStatsMiddleware = async () => { 65 | try { 66 | const kc = new k8s.KubeConfig(); 67 | kc.loadFromDefault(); 68 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 69 | 70 | const namespaces = []; 71 | 72 | const totalPods = await k8sApi 73 | .listPodForAllNamespaces() 74 | .then((data) => data.body.items.length); 75 | 76 | const totalNodes = await k8sApi 77 | .listNode() 78 | .then((data) => data.body.items.length); 79 | 80 | await k8sApi.listNamespace().then((data) => { 81 | for (let i in data.body.items) { 82 | namespaces.push(data.body.items[i].metadata.name); 83 | } 84 | }); 85 | 86 | return { 87 | namespaces, 88 | totalNodes, 89 | totalPods, 90 | }; 91 | } catch (error) { 92 | console.error("Error fetching logs:", error); 93 | return { 94 | namespaces: [], 95 | totalNodes: 0, 96 | totalPods: 0, 97 | }; 98 | } 99 | }; 100 | 101 | socketController.getLogs = async (body) => { 102 | try { 103 | const kc = new k8s.KubeConfig(); 104 | kc.loadFromDefault(); 105 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 106 | 107 | const namespace = body.namespace; 108 | const podName = body.podname; 109 | const containerName = ""; 110 | const tailLines = 100; // Number of lines to fetch 111 | 112 | const logsResponse = await k8sApi.readNamespacedPodLog( 113 | podName, 114 | namespace, 115 | containerName, 116 | undefined, 117 | undefined, 118 | undefined, 119 | tailLines 120 | ); 121 | const data = logsResponse.body; 122 | const logs = data.split("\n"); 123 | //write logic to parse res.locals.log 124 | const newArray = []; 125 | logs.forEach((el, i) => { 126 | const splitLog = el.split(/\]/); 127 | if (splitLog[1] && splitLog[1].length) { 128 | splitLog[1] = splitLog[1].trim(); 129 | } 130 | const log = { 131 | id: i, 132 | header: splitLog[0], 133 | message: splitLog[1], 134 | }; 135 | newArray.push(log); 136 | }); 137 | // console.log("new Array", newArray); 138 | return newArray; 139 | } catch (error) { 140 | console.error("Error fetching logs:", error); 141 | return []; 142 | } 143 | }; 144 | 145 | module.exports = socketController; 146 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const db = require("../model/userModels"); 2 | const bcrypt = require("bcryptjs"); 3 | const userController = {}; 4 | 5 | userController.signup = async (req, res, next) => { 6 | console.log("singingup"); 7 | try { 8 | const addUser = 'INSERT INTO "User" (username, password) VALUES ($1, $2)'; 9 | const { username, password } = req.body; 10 | const saltRounds = 10; 11 | const hashedPassword = await bcrypt.hash(password, saltRounds); 12 | const values = [username, hashedPassword]; 13 | console.log(values); 14 | 15 | // console.log(query); 16 | const resp = await db.query(addUser, values); 17 | console.log(res, nextp); 18 | res.locals.users = resp.rows[0]; 19 | return next(); 20 | } catch (error) { 21 | console.log("error"); 22 | console.log(error); 23 | return next({ 24 | log: "Error from userController.signup middleware", 25 | message: { err: "Error from userController.signup middleware" }, 26 | }); 27 | } 28 | }; 29 | 30 | userController.signin = async (req, res, next) => { 31 | console.log("signin"); 32 | // console.log(req.body.password); 33 | 34 | try { 35 | const user = [req.body.username]; 36 | const query = `SELECT * FROM "User" where username=$1`; 37 | const resp = await db.query(query, user); 38 | console.log(resp); 39 | console.log(resp.rows.length); 40 | if (resp.rows.length === 0) { 41 | console.log("none found"); 42 | return res.status(400).json("wrong"); 43 | } 44 | const saltRounds = 10; 45 | const hashedPassword = await bcrypt.hash(req.body.password, saltRounds); 46 | if (resp.rows[0].password === hashedPassword) { 47 | // console.log("found users"); 48 | res.locals.users = resp.rows[0]; 49 | // console.log(res.locals.users); 50 | return next(); 51 | } 52 | res.locals.users = resp.rows[0]; 53 | // console.log(res.locals.users); 54 | return next(); 55 | } catch (error) { 56 | return next(error); 57 | } 58 | }; 59 | 60 | module.exports = userController; 61 | -------------------------------------------------------------------------------- /server/model/logsModel.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KuberSee/abe326df25f43a056287c4e07ec7c43df98cac1d/server/model/logsModel.js -------------------------------------------------------------------------------- /server/model/sessionModel.js: -------------------------------------------------------------------------------- 1 | // const { Client } = require("pg"); 2 | // require("dotenv").config(); 3 | // const { Pool } = require("pg"); 4 | 5 | // const pool = new Pool({ 6 | // connectionString: process.env.DATABASE_URL, 7 | // }); 8 | 9 | // pool.query( 10 | // 'CREATE TABLE IF NOT EXISTS "Sessions" (cookieId: String NOT NULL PRIMARY KEY, )' 11 | // ); 12 | 13 | // pool.on("connect", (client) => { 14 | // client.query("SET DATESTYLE = iso, mdy"); 15 | // }); 16 | // // 17 | -------------------------------------------------------------------------------- /server/model/userModels.js: -------------------------------------------------------------------------------- 1 | // import * as dotenv from "dotenv"; 2 | const { Client } = require("pg"); 3 | require("dotenv").config(); 4 | const { Pool } = require("pg"); 5 | 6 | const pool = new Pool({ 7 | connectionString: process.env.DATABASE_URL, 8 | }); 9 | 10 | pool.on("connect", (client) => { 11 | client.query("SET DATESTYLE = iso, mdy"); 12 | }); 13 | 14 | if (pool) { 15 | // mysql is started && connected successfully. 16 | console.log("Connection Success"); 17 | } 18 | 19 | module.exports = { 20 | query: (text, params, callback) => { 21 | // console.log("executed query", text); 22 | return pool.query(text, params, callback); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /server/routes/authRoute.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const userController = require("../controllers/userController"); 4 | const sessionController = require("../controllers/sessionController"); 5 | const { error } = require("console"); 6 | 7 | router.post( 8 | "/signin", 9 | userController.signin, 10 | // sessionController.createSession, 11 | (req, res) => { 12 | // res.redirect("/"); 13 | res.status(200).json(res.locals.users); 14 | } 15 | ); 16 | 17 | router.post("/signup", userController.signup, (req, res) => { 18 | res.status(200).json(res.locals.users); 19 | }); 20 | 21 | module.exports = router; 22 | -------------------------------------------------------------------------------- /server/routes/socketRoutes.js: -------------------------------------------------------------------------------- 1 | // const express = require("express"); 2 | // const router = express.Router(); 3 | 4 | // // Middleware function to log Socket.io events and data 5 | // function logSocketEvent(socket, next) { 6 | // console.log(`Event: ${socket.event}`); 7 | // console.log("Data:", socket.data); 8 | // next(); // Call next() to proceed to the next middleware or the event handler 9 | // } 10 | 11 | // // Route to handle Socket.io connection event 12 | // router.io.on("connection", (socket) => { 13 | // console.log(`New client connected: ${socket.id}`); 14 | 15 | // // Use the middleware for a specific event 16 | // socket.use(logSocketEvent); 17 | 18 | // setInterval(() => { 19 | // socket.emit("logs", { 20 | // pod: "prometheus-kube-state-metrics-799f44d4db-wvm2t", 21 | // cpuCurrentUsage: "0.001241719", 22 | // memoryCurrentUsage: "12636160", 23 | // timestamp: 1688849973965, 24 | // }); 25 | // }, 1000); 26 | // // Handle custom Socket.io events here 27 | // // socket.on('logs', (data) => { 28 | // // console.log(`Received message: ${data}`); 29 | // // // Broadcast the message to all connected clients except the sender 30 | // // socket.broadcast.emit('logs', data); 31 | // // }); 32 | 33 | // // Handle disconnection event 34 | // socket.on("disconnect", () => { 35 | // console.log(`Client disconnected: ${socket.id}`); 36 | // }); 37 | // }); 38 | 39 | // module.exports = router; 40 | 41 | const express = require("express"); 42 | const router = express.Router(); 43 | 44 | // Middleware function to log Socket.io events and data 45 | function logSocketEvent(socket, next) { 46 | console.log(`Event: ${socket.event}`); 47 | console.log("Data:", socket.data); 48 | next(); // Call next() to proceed to the next middleware or the event handler 49 | } 50 | 51 | // Export a function that sets up Socket.io events 52 | function setupSocket(io) { 53 | io.on("connection", (socket) => { 54 | console.log(`New client connected: ${socket.id}`); 55 | 56 | // Use the middleware for a specific event 57 | socket.use(logSocketEvent); 58 | 59 | setInterval(() => { 60 | socket.emit("logs", { 61 | pod: "prometheus-kube-state-metrics-799f44d4db-wvm2t", 62 | cpuCurrentUsage: "0.001241719", 63 | memoryCurrentUsage: "12636160", 64 | timestamp: 1688849973965, 65 | }); 66 | }, 1000); 67 | 68 | // Handle disconnection event 69 | socket.on("disconnect", () => { 70 | console.log(`Client disconnected: ${socket.id}`); 71 | }); 72 | }); 73 | 74 | return router; 75 | } 76 | 77 | module.exports = setupSocket; 78 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | const path = require("path"); 4 | const cors = require("cors"); 5 | const PORT = 3000; 6 | const authRoute = require("./routes/authRoute"); 7 | const socketController = require("./controllers/socketController"); 8 | const http = require("http"); 9 | const socketIO = require("socket.io"); 10 | 11 | app.use(express.urlencoded({ extended: true })); 12 | app.use(express.json()); 13 | app.use(cors({ origin: "http://localhost:8080" })); 14 | 15 | app.use("/", express.static(path.resolve(__dirname, "../build"))); 16 | 17 | // connect express server with socket.io 18 | const server = http.createServer(app); 19 | const io = socketIO(server, { 20 | cors: { 21 | origin: "*", 22 | methods: ["GET", "POST", "PATCH", "PUT", "DELETE"], 23 | }, 24 | }); 25 | 26 | app.use("/auth", authRoute); 27 | 28 | // app.use(cookieParser()); 29 | // const oneDay = 1000 * 60 * 60 * 24; 30 | // app.use( 31 | // sessions({ 32 | // secret: process.env.SECRET, 33 | // saveUninitialized: true, 34 | // cookie: { maxAge: oneDay }, 35 | // resave: false, 36 | // }) 37 | // ); 38 | 39 | // looks for an event with "connection" and when you listen, you need a callback function. for future ref when someone connects to the server. 40 | io.on("connection", (socket) => { 41 | console.log(`New user connected: ${socket.id}`); 42 | let metricsInterval; 43 | socket.on("metrics", async (data) => { 44 | try { 45 | if (metricsInterval) { 46 | clearInterval(metricsInterval); 47 | } 48 | // Send metrics data to the client every 1000ms after the middleware has been set up 49 | metricsInterval = setInterval(async () => { 50 | const metrics = await socketController.getMetricsMiddleware(data); 51 | socket.emit("metrics", { 52 | topPods: metrics.topPods, 53 | topNodes: metrics.topNodes, 54 | }); 55 | }, 1000); 56 | } catch (error) { 57 | console.error("Error fetching metrics:", error); 58 | // Emit an error event or handle it in the event handler if needed 59 | socket.emit("error", "Error fetching metrics"); 60 | } 61 | }); 62 | 63 | socket.on("stats", async () => { 64 | let statsInterval; 65 | try { 66 | if (statsInterval) { 67 | clearInterval(statsInterval); 68 | } 69 | statsInterval = setInterval(async () => { 70 | const stats = await socketController.getStatsMiddleware(); 71 | socket.emit("stats", { 72 | namespaces: stats.namespaces, 73 | totalNamespaces: stats.namespaces.length, 74 | totalPods: stats.totalPods, 75 | totalNodes: stats.totalNodes, 76 | }); 77 | }, 1000); 78 | } catch (error) { 79 | console.error("Error fetching stats:", error); 80 | // Emit an error event or handle it in the event handler if needed 81 | socket.emit("error", "Error fetching stats"); 82 | } 83 | }); 84 | 85 | socket.on("logs", async (body) => { 86 | try { 87 | const data = await socketController.getLogs(body); 88 | 89 | socket.emit("logs", data); 90 | } catch (error) { 91 | console.error("Error fetching logs:", error); 92 | // Emit an error event or handle it in the event handler if needed 93 | socket.emit("error", "Error fetching logs"); 94 | } 95 | }); 96 | 97 | // listens for when a user disconnects from the server. 98 | socket.on("disconnect", () => { 99 | console.log(`User Disconnected: ${socket.id}`); 100 | }); 101 | }); 102 | 103 | app.get("/*", function (req, res) { 104 | res.sendFile(path.resolve(__dirname, "../index.html"), function (err) { 105 | if (err) { 106 | res.status(500).send(err); 107 | } 108 | }); 109 | }); 110 | 111 | server.listen(PORT, () => { 112 | console.log(`WebSocket server running on ${PORT}`); 113 | }); 114 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{js,jsx,html,ts,tsx}'], 4 | daisyui: { 5 | themes: ['light', 'dark', 'cupcake', 'emerald', 'night'], 6 | }, 7 | 8 | plugins: [require('daisyui')], 9 | }; 10 | -------------------------------------------------------------------------------- /target/npmlist.json: -------------------------------------------------------------------------------- 1 | {"version":"1.0.0","name":"kubersee","dependencies":{"@emotion/react":{"version":"11.11.1"},"@emotion/styled":{"version":"11.11.0"},"@kubernetes/client-node":{"version":"0.18.1"},"@mui/material":{"version":"5.14.0"},"@mui/x-data-grid-generator":{"version":"6.10.0"},"@mui/x-data-grid":{"version":"6.10.0"},"@prisma/client":{"version":"4.16.2"},"bcrypt":{"version":"5.1.0"},"bcryptjs":{"version":"2.4.3"},"cookie-parser":{"version":"1.4.6"},"cookieparser":{"version":"0.1.0"},"cors":{"version":"2.8.5"},"crypto-browserify":{"version":"3.12.0"},"d3-collection":{"version":"1.0.7"},"d3-selection":{"version":"3.0.0"},"d3":{"version":"7.8.5"},"dotenv":{"version":"16.3.1"},"express-session":{"version":"1.17.3"},"express":{"version":"4.18.2"},"http":{"version":"0.0.1-security"},"mongodb":{"version":"5.6.0"},"mongoose":{"version":"7.3.1"},"pg":{"version":"8.11.1"},"react-dom":{"version":"18.2.0"},"react":{"version":"18.2.0"},"socket.io-client":{"version":"4.7.1"},"socket.io":{"version":"4.7.1"},"stream":{"version":"0.0.2"}}} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "sourceMap": true, 5 | "target": "ES6", 6 | "esModuleInterop": true, 7 | "jsx": "react", 8 | "noImplicitAny": true, 9 | "allowJs": true, 10 | "moduleResolution": "Node", 11 | }, 12 | "include": [ 13 | "electron/main.ts", 14 | "client/**/**/*.ts", 15 | "client/**/**/*.tsx", 16 | "server/**/*.ts", 17 | "server/**/*.tsx", 18 | "index.d.ts" 19 | ] 20 | } -------------------------------------------------------------------------------- /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 || 'development', 6 | entry: './client/index.js', 7 | // Target: 'electron-renderer', 8 | output: { 9 | filename: 'bundle.js', 10 | path: path.resolve(__dirname, './electron/build'), 11 | }, 12 | plugins: [ 13 | new HtmlWebpackPlugin({ 14 | filename: 'index.html', 15 | template: path.resolve(__dirname, 'index.html'), 16 | }), 17 | ], 18 | performance: { 19 | hints: false, 20 | }, 21 | devServer: { 22 | historyApiFallback: true, 23 | port: 8080, 24 | proxy: { 25 | '/api': { 26 | target: 'http://localhost:8080', 27 | router: () => 'http://localhost:3000', 28 | }, 29 | }, 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.(js|jsx)$/, 35 | exclude: /node_modules/, 36 | use: { 37 | loader: 'babel-loader', 38 | options: { 39 | presets: ['@babel/preset-env', '@babel/preset-react'], 40 | }, 41 | }, 42 | }, 43 | { 44 | test: /\.(ts|tsx)$/, 45 | exclude: /node_modules/, 46 | use: 'ts-loader', 47 | }, 48 | { 49 | test: /\.s[ac]ss$/i, 50 | use: ['style-loader', 'css-loader', 'sass-loader'], 51 | }, 52 | { 53 | test: /\.(png|jp(e*)g|svg|gif)$/, 54 | loader: 'file-loader', 55 | }, 56 | ], 57 | }, 58 | resolve: { 59 | extensions: ['.jsx', '.js'], 60 | }, 61 | resolve: { 62 | extensions: ['.tsx', '.ts', '.js'], 63 | }, 64 | }; 65 | --------------------------------------------------------------------------------