├── .eslintrc.cjs ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── _sampleconfig_ ├── config.yml └── prometheus.yml ├── archive ├── NotFound.tsx ├── mainpageArch.ts ├── sessionController.ts └── static.json ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── server ├── controllers │ ├── clusterController.ts │ ├── cookieController.ts │ └── userController.ts ├── database │ ├── sessionDatabase.ts │ └── userDatabase.ts ├── routes │ ├── clusterRouter.ts │ └── loginRouter.ts └── server.ts ├── src ├── App.css ├── App.tsx ├── assets │ ├── AllenHui.jpeg │ ├── Charts.gif │ ├── DanielLee.jpeg │ ├── Demo.gif │ ├── SamJohnson.png │ ├── WanluDing.jpeg │ ├── favicon.png │ ├── githubIcon.png │ ├── linkedin.png │ ├── logo2.png │ └── team.png ├── components │ ├── Chart.tsx │ ├── MeetTeam.tsx │ ├── NavBar.tsx │ ├── RepoSection.tsx │ ├── SignIn.tsx │ └── SignUp.tsx ├── containers │ ├── MainPage.tsx │ └── WelcomePage.tsx ├── index.css ├── main.tsx ├── stylesheets │ └── index.css └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── types.ts └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | // 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ClusterSense 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: tsx server/server.ts -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://hackmd.io/_uploads/HJtzS2el6.png) 2 | # ClusterSense - Kafka Cluster Management Tool 3 | 4 | --- 5 | ## Table of Contents 6 | 7 | - [Product Description](#product-description) 8 | - [Web App](#Web-App) 9 | - [Install Locally](#install-locally) 10 | - [Contribute](#contribute) 11 | - [Our Team](#Contributors) 12 | - [License](#license) 13 | 14 | --- 15 | ## Product Description 16 | 17 | Cluster Sense is an open-source product dedicated to developing a Kafka cluster visualization tool. Our tool is designed with developers in mind, to help our users visualize their metrics and have an understanding of their clusters' health. 18 | 19 | # Features 20 | Real-time Metrics and Charts: ClusterSense provides a GUI with important metrics, updated in real-time for insights into your Kafka clusters' health and performance through visually appealing charts. 21 | 22 | Seamless Prometheus Integration: Setting up Prometheus to scrape Kafka metrics has never been easier. View our sample YML file to guide you through the prometheus configuration, ensuring that the process is a smooth experience. 23 | 24 | Profile Port Selection: The application's top navbar has a dropdown that stores your previous port selections for easy navigation between seperate Kafka Instances for seamless monitoring of multiple brokers or clusters. 25 | 26 | ## Tech Stack 27 | 28 |
29 | 30 | ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white) 31 | ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) 32 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 33 | ![Node](https://img.shields.io/badge/-node-339933?style=for-the-badge&logo=node.js&logoColor=white) 34 | ![Express](https://img.shields.io/badge/express-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 35 | ![Prometheus](https://img.shields.io/badge/Prometheus-E7532D?style=for-the-badge&logo=prometheus&logoColor=white) 36 | ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4EA94B?style=for-the-badge&logo=postgres&logoColor=white) 37 | ![Apache Kafka](https://img.shields.io/badge/apache%20kafka-%2320232a.svg?style=for-the-badge&logo=apachekafka&logoColor=white) 38 | ![Tailwind](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white) 39 | ![Jest](https://img.shields.io/badge/Jest-323330?style=for-the-badge&logo=Jest&logoColor=white) 40 | ![Testing Library](https://img.shields.io/badge/testing%20library-323330?style=for-the-badge&logo=testing-library&logoColor=red) 41 | ![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white) 42 | ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) 43 | ![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white) 44 | ![Chart.js](https://img.shields.io/badge/chart.js-F5788D.svg?style=for-the-badge&logo=chart.js&logoColor=white) 45 | ![Heroku](https://img.shields.io/static/v1?style=for-the-badge&message=Heroku&color=430098&logo=Heroku&logoColor=FFFFFF&label=) 46 |
47 | 48 | --- 49 | 50 | ## Web App 51 | 52 | To begin using ClusterSense, navigate to ClusterSense.org and create an account. 53 | 54 | - Ensure prometheus is running and connected with your broker/cluster 55 | - Enter the port prometheus is currently occupying into the form and hit submit; we have included a sample prometheus.yml file in the _sampleconfig_ directory to streamline your prometheus configuration. 56 | - View real-time data of your Apache-Kafka instance 57 | 58 | 59 | ### Install Locally 60 | 61 | Alternatively, if you would prefer to run ClusterSense locally, you may fork and clone our Github repository. 62 | 63 | - Create a .env file in the main directory and create a variable PG_URL set equal to your PostgreSQL server instance URI 64 | - Instantiate the database using the CLI with these table formats: 65 | - `CREATE TABLE "users" ( 66 | user_id serial PRIMARY KEY, 67 | username varchar(50) NOT NULL, 68 | password varchar(255), 69 | oauth_provider varchar(255), 70 | oauth_id varchar(255), 71 | oauth_access_token varchar(255), 72 | oauth_refresh_token varchar(255), 73 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 74 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);` 75 | - `CREATE TABLE cluster( 76 | id SERIAL PRIMARY KEY, 77 | user_id SERIAL REFERENCES users(user_id), 78 | cluster_port INTEGER NOT NULL, 79 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)` 80 | - In the terminal run: 81 | - `npm install` 82 | - `npm run dev` 83 | - Once the application is running, navigate to localhost:3030, create an account, and input the port of your Prometheus server. 84 | 85 | --- 86 | 87 | ## Contribute 88 | If you would like to contribute to this product and improve upon it's current functionality or add a feature, please fork the repository and submit a pull request. 89 | Some of our planned features for ClusterSense include: 90 | - Transition to Websockets for Real-Time Prometheus Data 91 | - Open Authorization 92 | - Light/Dark Mode 93 | - Adding User-Defined Charts to GUI 94 | - Alerting with user-defined service level indicators 95 | 96 | --- 97 | 98 | ## Contributors 99 |
100 | 101 | 102 | 103 | 112 | 121 | 130 | 139 |
104 | 105 |
106 | Allen Hui 107 |
108 | 109 | 110 |
111 |
113 | 114 |
115 | Sam Johnson 116 |
117 | 118 | 119 |
120 |
122 | 123 |
124 | Wanlu Ding 125 |
126 | 127 | 128 |
129 |
131 | 132 |
133 | Daniel (Jung Tae) Lee 134 |
135 | 136 | 137 |
138 |
140 |
141 | 142 | --- 143 | 144 | # License 145 | This project is licensed under the [**MIT License**](https://choosealicense.com/licenses/mit/) 146 | -------------------------------------------------------------------------------- /_sampleconfig_/config.yml: -------------------------------------------------------------------------------- 1 | #exporter rules 2 | hostPort: kafka:9991 3 | lowercaseOutputName: true 4 | 5 | rules: 6 | # Special cases and very specific rules 7 | - pattern : kafka.server<>Value 8 | name: kafka_server_$1_$2 9 | type: GAUGE 10 | labels: 11 | clientId: "$3" 12 | topic: "$4" 13 | partition: "$5" 14 | - pattern : kafka.server<>Value 15 | name: kafka_server_$1_$2 16 | type: GAUGE 17 | labels: 18 | clientId: "$3" 19 | broker: "$4:$5" 20 | - pattern : kafka.coordinator.(\w+)<>Value 21 | name: kafka_coordinator_$1_$2_$3 22 | type: GAUGE 23 | 24 | # Generic per-second counters with 0-2 key/value pairs 25 | - pattern: kafka.(\w+)<>Count 26 | name: kafka_$1_$2_$3_total 27 | type: COUNTER 28 | labels: 29 | "$4": "$5" 30 | "$6": "$7" 31 | - pattern: kafka.(\w+)<>Count 32 | name: kafka_$1_$2_$3_total 33 | type: COUNTER 34 | labels: 35 | "$4": "$5" 36 | - pattern: kafka.(\w+)<>Count 37 | name: kafka_$1_$2_$3_total 38 | type: COUNTER 39 | 40 | # Quota specific rules 41 | - pattern: kafka.server<>([a-z-]+) 42 | name: kafka_server_quota_$4 43 | type: GAUGE 44 | labels: 45 | resource: "$1" 46 | user: "$2" 47 | clientId: "$3" 48 | - pattern: kafka.server<>([a-z-]+) 49 | name: kafka_server_quota_$3 50 | type: GAUGE 51 | labels: 52 | resource: "$1" 53 | clientId: "$2" 54 | - pattern: kafka.server<>([a-z-]+) 55 | name: kafka_server_quota_$3 56 | type: GAUGE 57 | labels: 58 | resource: "$1" 59 | user: "$2" 60 | 61 | # Generic gauges with 0-2 key/value pairs 62 | - pattern: kafka.(\w+)<>Value 63 | name: kafka_$1_$2_$3 64 | type: GAUGE 65 | labels: 66 | "$4": "$5" 67 | "$6": "$7" 68 | - pattern: kafka.(\w+)<>Value 69 | name: kafka_$1_$2_$3 70 | type: GAUGE 71 | labels: 72 | "$4": "$5" 73 | - pattern: kafka.(\w+)<>Value 74 | name: kafka_$1_$2_$3 75 | type: GAUGE 76 | 77 | # Emulate Prometheus 'Summary' metrics for the exported 'Histogram's. 78 | # 79 | # Note that these are missing the '_sum' metric! 80 | - pattern: kafka.(\w+)<>Count 81 | name: kafka_$1_$2_$3_count 82 | type: COUNTER 83 | labels: 84 | "$4": "$5" 85 | "$6": "$7" 86 | - pattern: kafka.(\w+)<>(\d+)thPercentile 87 | name: kafka_$1_$2_$3 88 | type: GAUGE 89 | labels: 90 | "$4": "$5" 91 | "$6": "$7" 92 | quantile: "0.$8" 93 | - pattern: kafka.(\w+)<>Count 94 | name: kafka_$1_$2_$3_count 95 | type: COUNTER 96 | labels: 97 | "$4": "$5" 98 | - pattern: kafka.(\w+)<>(\d+)thPercentile 99 | name: kafka_$1_$2_$3 100 | type: GAUGE 101 | labels: 102 | "$4": "$5" 103 | quantile: "0.$6" 104 | - pattern: kafka.(\w+)<>Count 105 | name: kafka_$1_$2_$3_count 106 | type: COUNTER 107 | - pattern: kafka.(\w+)<>(\d+)thPercentile 108 | name: kafka_$1_$2_$3 109 | type: GAUGE 110 | labels: 111 | quantile: "0.$4" 112 | 113 | # Generic gauges for MeanRate Percent 114 | # Ex) kafka.server<>MeanRate 115 | - pattern: kafka.(\w+)<>MeanRate 116 | name: kafka_$1_$2_$3_percent 117 | type: GAUGE 118 | - pattern: kafka.(\w+)<>Value 119 | name: kafka_$1_$2_$3_percent 120 | type: GAUGE 121 | - pattern: kafka.(\w+)<>Value 122 | name: kafka_$1_$2_$3_percent 123 | type: GAUGE 124 | labels: 125 | "$4": "$5" -------------------------------------------------------------------------------- /_sampleconfig_/prometheus.yml: -------------------------------------------------------------------------------- 1 | #scrape time 2 | global: 3 | scrape_interval: 5s 4 | evaluation_interval: 30s 5 | 6 | #dictionaries for prometheus and the exporter 7 | scrape_configs: 8 | - job_name: 'prometheus' 9 | static_configs: 10 | - targets: ['localhost:9090'] 11 | 12 | - job_name: 'jmx-kafka' 13 | static_configs: 14 | - targets: ['jmx-kafka:5556'] 15 | -------------------------------------------------------------------------------- /archive/NotFound.tsx: -------------------------------------------------------------------------------- 1 | // const NotFound = () => { 2 | // return ( 3 | //
4 | // 5 | // It appears you have wandered astray. Head back home. 6 | //
7 | // ); 8 | // }; 9 | 10 | // export default NotFound; -------------------------------------------------------------------------------- /archive/mainpageArch.ts: -------------------------------------------------------------------------------- 1 | // useEffect that will fetch data from prometheus/backend once the form is submitted using the port 2 | 3 | // const getMetricData = async (metric: string): void => { 4 | // if (!metricList.includes(metric)) return setMetricData([]); 5 | // try { 6 | // const response = await axios.get( 7 | // `http://${server}/api/v1/query?query=${metric}` 8 | // ); 9 | // setMetricData(response.data.data.result); 10 | // } catch (err) { 11 | // console.log(err); 12 | // } 13 | // }; 14 | -------------------------------------------------------------------------------- /archive/sessionController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { pool } from "../database/userDatabase"; 3 | // import { OAuthUser } from "../../types"; 4 | 5 | /* 6 | CREATE TABLE sessions ( 7 | id SERIAL PRIMARY KEY, 8 | cookieId VARCHAR(255) NOT NULL UNIQUE, 9 | createdAt TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP 10 | ); 11 | */ 12 | 13 | const sessionController = { 14 | // startSession - create and save a new Session into the database. 15 | startSession: async ( 16 | _req: Request, 17 | res: Response, 18 | next: NextFunction, 19 | ): Promise => { 20 | 21 | const { user_id } = res.locals; 22 | console.log(`a new session created for user ${user_id}`) 23 | // check if session already exists for user 24 | try { 25 | // check if session already exists for user 26 | const existingSessionQuery = `SELECT * FROM sessions WHERE cookieId = $1`; 27 | const sessionResult = await pool.query(existingSessionQuery, [user_id]); 28 | if (sessionResult.rows.length > 0) { 29 | // Session already exists, move on. 30 | console.log('session exists') 31 | return next(); 32 | } else { 33 | // No session exists, create one. 34 | const createSessionQuery = `INSERT INTO sessions (cookieId) VALUES ($1)`; 35 | await pool.query(createSessionQuery, [user_id]); 36 | console.log('session created') 37 | return next(); 38 | 39 | } 40 | } catch (err) { 41 | return next({ 42 | log: `Error occurred in sessionController.startSession ${err}`, 43 | status: 500, 44 | message: { err: "Unable to create session" }, 45 | }); 46 | } 47 | }, 48 | 49 | // isLoggedIn - find the appropriate session for this request in the DB and verify whether or not the session is still valid 50 | isLoggedIn: async ( 51 | req: Request, 52 | res: Response, 53 | next: NextFunction, 54 | ): Promise => { 55 | const { ssid } = req.cookies; 56 | 57 | try { 58 | // Use the pg pool to query the sessions table and find a session with the matching cookieId 59 | const result = await pool.query('SELECT * FROM sessions WHERE cookieId = $1', [ssid]); 60 | 61 | // If session is not found, send a status code 303 to the front-end 62 | if (result.rows.length === 0) { 63 | res.status(303).json("No active session exists"); 64 | } else { 65 | // If the session is found, save the cookie ssid in res.locals and return next() 66 | res.locals.userId = ssid; 67 | return next(); 68 | } 69 | } catch (err) { 70 | return next({ 71 | log: `Error occurred in sessionController.isLoggedIn ${err}`, 72 | status: 500, 73 | message: { err: "Unable to find session" }, 74 | }); 75 | } 76 | }, 77 | 78 | 79 | // old session verification 80 | // loginRouter.get( 81 | // "/isLoggedIn", 82 | // sessionController.isLoggedIn, 83 | // (_req: Request, res: Response) => { 84 | // return res.status(201).json({ message: "login successful" }); 85 | // } 86 | // ); 87 | 88 | 89 | 90 | 91 | 92 | 93 | }; 94 | export { sessionController }; -------------------------------------------------------------------------------- /archive/static.json: -------------------------------------------------------------------------------- 1 | // { 2 | // "root": "./dist", 3 | // "clean_urls": true, 4 | // "routes": { 5 | // "/**": "index.html" 6 | // } 7 | // } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ClusterSense 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clustersense", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "engines": { 7 | "node": "16.18.1" 8 | }, 9 | "scripts": { 10 | "build": "cross-env NODE_ENV=production vite build", 11 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 12 | "preview": "vite preview", 13 | "start": "cross-env NODE_ENV=production tsx server/server.ts", 14 | "test": "cross-env NODE_ENV=test vitest", 15 | "start:server": "ts-node server/server.ts", 16 | "dev": "cross-env NODE_ENV=development vite & nodemon --watch './**/*.ts' --exec 'node --experimental-specifier-resolution=node --loader ts-node/esm --no-warnings' server/server.ts" 17 | }, 18 | "dependencies": { 19 | "@emotion/react": "^11.11.1", 20 | "@emotion/styled": "^11.11.0", 21 | "@fortawesome/fontawesome-svg-core": "^6.4.2", 22 | "@fortawesome/free-brands-svg-icons": "^6.4.2", 23 | "@fortawesome/react-fontawesome": "^0.2.0", 24 | "@mui/icons-material": "^5.14.9", 25 | "@mui/material": "^5.14.11", 26 | "@mui/x-date-pickers": "^6.13.0", 27 | "axios": "^1.5.0", 28 | "bcryptjs": "^2.4.3", 29 | "chart.js": "^4.4.0", 30 | "chartjs": "^0.3.24", 31 | "cookie-parser": "^1.4.6", 32 | "cors": "^2.8.5", 33 | "dotenv": "^16.3.1", 34 | "express": "^4.18.2", 35 | "nodemon": "^3.0.1", 36 | "passport": "^0.6.0", 37 | "path": "^0.12.7", 38 | "pg": "^8.11.3", 39 | "react": "^18.2.0", 40 | "react-chartjs-2": "^5.2.0", 41 | "react-dom": "^18.2.0", 42 | "react-iframe": "^1.8.5", 43 | "react-router-dom": "^6.15.0", 44 | "react-select": "^5.7.4", 45 | "react-type-animation": "^3.1.0", 46 | "ts-node": "^10.9.1", 47 | "tsx": "^3.13.0", 48 | "tw-elements": "^1.0.0", 49 | "tw-elements-react": "^1.0.0-alpha1" 50 | }, 51 | "devDependencies": { 52 | "@types/bcryptjs": "^2.4.3", 53 | "@types/cookie-parser": "^1.4.4", 54 | "@types/cors": "^2.8.14", 55 | "@types/express": "^4.17.17", 56 | "@types/node": "^20.5.9", 57 | "@types/pg": "^8.10.2", 58 | "@types/react": "^18.2.21", 59 | "@types/react-dom": "^18.2.7", 60 | "@typescript-eslint/eslint-plugin": "^6.0.0", 61 | "@typescript-eslint/parser": "^6.0.0", 62 | "@vitejs/plugin-react-swc": "^3.3.2", 63 | "autoprefixer": "^10.4.15", 64 | "concurrently": "^8.2.1", 65 | "cross-env": "^7.0.3", 66 | "eslint": "^8.45.0", 67 | "eslint-plugin-react-hooks": "^4.6.0", 68 | "eslint-plugin-react-refresh": "^0.4.3", 69 | "postcss": "^8.4.29", 70 | "sass": "^1.68.0", 71 | "sass-loader": "^13.3.2", 72 | "tailwindcss": "^3.3.3", 73 | "ts-node": "^10.9.1", 74 | "typescript": "^5.2.2", 75 | "vite": "^4.4.5" 76 | }, 77 | "description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.", 78 | "main": "index.js", 79 | "repository": { 80 | "type": "git", 81 | "url": "git+https://github.com/oslabs-beta/ClusterSense.git" 82 | }, 83 | "keywords": [], 84 | "author": "", 85 | "license": "ISC", 86 | "bugs": { 87 | "url": "https://github.com/oslabs-beta/ClusterSense/issues" 88 | }, 89 | "homepage": "https://github.com/oslabs-beta/ClusterSense#readme" 90 | } 91 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /server/controllers/clusterController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { pool } from "../database/userDatabase"; 3 | 4 | /** 5 | * The clusterController handles interactions with the "cluster" table in the database, 6 | * allowing users to store and retrieve cluster data based on user IDs and associated ports. 7 | */ 8 | const clusterController = { 9 | 10 | /** 11 | * @function storeCluster 12 | * Store a new cluster in the database, associating it with a user ID and port number. 13 | * 14 | * @param req - Express request object. 15 | * @param res - Express response object. 16 | * @param next - Express next middleware function. 17 | */ 18 | storeCluster: async ( 19 | req: Request, res: Response, next: NextFunction 20 | ): Promise => { 21 | try { 22 | const { port } = req.body; 23 | const user_id = req.cookies.ssid; 24 | const checkExisting = `SELECT * FROM cluster WHERE user_id=$1 AND cluster_port=$2`; 25 | const existingEntry = await pool.query(checkExisting, [user_id, port]); 26 | 27 | // If the port number is not already associated with the user ID, insert into the database. 28 | if (existingEntry.rows.length === 0) { 29 | const queryCluster = `INSERT INTO cluster (user_id, cluster_port) VALUES ($1, $2)`; 30 | const clusterResult = await pool.query(queryCluster, [user_id, port]); 31 | res.locals.clusterResult = clusterResult; 32 | } else { 33 | res.locals.clusterResult = 'it exists'; 34 | } 35 | 36 | return next(); 37 | } catch (err) { 38 | return next({ 39 | log: `Error occurred in clusterController.storeCluster: ${err}`, 40 | status: 500, 41 | message: { err: "Unable to save cluster" }, 42 | }); 43 | } 44 | }, 45 | 46 | /** 47 | * @function fetchCluster 48 | * Retrieve all clusters associated with a given user ID. 49 | * 50 | * @param req - Express request object. 51 | * @param res - Express response object. 52 | * @param next - Express next middleware function. 53 | */ 54 | fetchCluster: async ( 55 | req: Request, res: Response, next: NextFunction 56 | ): Promise => { 57 | try { 58 | const user_id = req.cookies.ssid; 59 | const queryClusters = `SELECT cluster_port FROM "cluster" WHERE user_id = $1`; 60 | const clusterResult = await pool.query(queryClusters, [user_id]); 61 | 62 | res.locals.clusters = clusterResult.rows; 63 | return next(); 64 | } catch (err) { 65 | return next({ 66 | log: `Error occurred in clusterController.fetchCluster: ${err}`, 67 | status: 500, 68 | message: { err: "Unable to fetch clusters" }, 69 | }); 70 | } 71 | }, 72 | } 73 | 74 | export { clusterController }; 75 | -------------------------------------------------------------------------------- /server/controllers/cookieController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express'; 2 | 3 | // Controller for handling cookie operations. 4 | const cookieController = { 5 | 6 | /** 7 | * Set the user ID in an 'ssid' cookie. 8 | * @param _req - Express Request object 9 | * @param res - Express Response object 10 | * @param next - Next middleware function 11 | */ 12 | setSSIDCookie: (_req: Request, res: Response, next: NextFunction): void => { 13 | const { user_id } = res.locals; 14 | 15 | // Set the 'ssid' cookie for 12 hours 16 | res.cookie('ssid', user_id, { 17 | maxAge: 12 * 60 * 60 * 1000, 18 | httpOnly: true, 19 | sameSite: 'none', 20 | secure: true, 21 | }); 22 | 23 | return next(); 24 | }, 25 | }; 26 | 27 | export { cookieController }; 28 | -------------------------------------------------------------------------------- /server/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import { pool } from "../database/userDatabase"; 3 | import bcrypt from 'bcryptjs'; 4 | 5 | /** 6 | * Database table structure: 7 | * 8 | * CREATE TABLE "users" ( 9 | * user_id serial PRIMARY KEY, 10 | * username varchar(50) NOT NULL, 11 | * password varchar(255), 12 | * oauth_provider varchar(255), 13 | * oauth_id varchar(255), 14 | * oauth_access_token varchar(255), 15 | * oauth_refresh_token varchar(255), 16 | * created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 17 | * updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 18 | * ); 19 | */ 20 | 21 | const userController = { 22 | 23 | /** 24 | * createUser - Handle user registration. 25 | * @param req - Express request object 26 | * @param res - Express response object 27 | * @param next - Express next function 28 | * @returns Promise 29 | */ 30 | createUser: async ( 31 | req: Request, res: Response, next: NextFunction 32 | ): Promise => { 33 | const { username, password } = req.body; 34 | 35 | // Validate input fields 36 | if (!username || !password) { 37 | return next({ 38 | log: "Error in userController.createUser: Missing input fields", 39 | status: 400, 40 | message: { err: "All fields required" }, 41 | }); 42 | } 43 | 44 | try { 45 | // Query to check if the username already exists in the database 46 | const existingUserQuery = `SELECT * FROM "users" WHERE username = $1`; 47 | const existingUserValues = [req.body.username]; 48 | const existingUserResult = await pool.query( 49 | existingUserQuery, 50 | existingUserValues 51 | ); 52 | 53 | // If user exists, send an error response 54 | if (existingUserResult.rows.length > 0) { 55 | res.status(409).json({ error: 'Username already exists' }); 56 | return next(); 57 | } 58 | 59 | // Encrypt user password and insert the new user into the database 60 | const salt = await bcrypt.genSalt(10); 61 | const hashedPassword = await bcrypt.hash(password, salt); 62 | const insertQuery = `INSERT INTO "users" (username, password) VALUES ($1, $2) RETURNING *`; 63 | const insertValues = [username, hashedPassword]; 64 | const createUser = await pool.query(insertQuery, insertValues); 65 | 66 | // Store the user's id in res.locals for subsequent middleware 67 | res.locals.user_id = createUser.rows[0].user_id; 68 | return next(); 69 | } catch (err) { 70 | return next({ 71 | log: `Error in userController.createUser: ${err}`, 72 | status: 500, 73 | message: { err: "Unable to create user" }, 74 | }); 75 | } 76 | }, 77 | 78 | /** 79 | * verifyUser - Handle user authentication. 80 | * @param req - Express request object 81 | * @param res - Express response object 82 | * @param next - Express next function 83 | * @returns Promise 84 | */ 85 | verifyUser: async (req: Request, res: Response, next: NextFunction): Promise => { 86 | const { username, password } = req.body; 87 | 88 | // Validate input fields 89 | if (!username || !password) { 90 | return next({ 91 | log: "Error in userController.verifyUser: Missing input fields", 92 | status: 400, 93 | message: { err: "All fields required" }, 94 | }); 95 | } 96 | 97 | try { 98 | // Query to retrieve the user based on the provided username 99 | const existingUsernameQuery = `SELECT * FROM "users" WHERE username = $1`; 100 | const userResult = await pool.query(existingUsernameQuery, [username]); 101 | 102 | // If no match is found for the username 103 | if (userResult.rows.length === 0) { 104 | res.status(409).json({ error: 'Invalid Username or Password' }); 105 | } 106 | 107 | // Validate the provided password against the stored hash 108 | const user = userResult.rows[0]; 109 | const isPasswordMatch = await bcrypt.compare(password, user.password); 110 | if (!isPasswordMatch) { 111 | res.status(401).json({ error: 'Invalid Username or Password' }); 112 | } 113 | 114 | // Store the user's id in res.locals for subsequent middleware 115 | res.locals.user_id = user.user_id; 116 | return next(); 117 | } catch (err) { 118 | return next({ 119 | log: `Error in userController.verifyUser: ${err}`, 120 | status: 500, 121 | message: { err: "Unable to verify user" }, 122 | }); 123 | } 124 | }, 125 | } 126 | 127 | export { userController }; 128 | -------------------------------------------------------------------------------- /server/database/sessionDatabase.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import pg from 'pg'; 3 | 4 | dotenv.config(); 5 | 6 | const { Pool } = pg; 7 | 8 | // Establish a new database connection using PG_URL from environment variables 9 | const pool = new Pool({ 10 | connectionString: process.env.PG_URL 11 | }); 12 | 13 | /** 14 | * Execute a database query. 15 | * @param text - SQL query string. 16 | * @param params - Query parameters. 17 | * @param callback - Function to handle the result or error. 18 | */ 19 | export const query = ( 20 | text: string, 21 | params: Array, 22 | callback: (error: Error, result: unknown) => void 23 | ) => pool.query(text, params, callback); 24 | 25 | export { pool }; 26 | -------------------------------------------------------------------------------- /server/database/userDatabase.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import pg from 'pg'; 3 | 4 | dotenv.config(); 5 | 6 | const { Pool } = pg; 7 | 8 | // Create a new connection pool using PG_URL environment variable. 9 | const pool = new Pool({ 10 | connectionString: process.env.PG_URL 11 | }); 12 | 13 | /** 14 | * Execute a query against the database. 15 | * @param text - SQL query. 16 | * @param params - Query parameters. 17 | * @param callback - Handle results or errors. 18 | */ 19 | export const query = ( 20 | text: string, 21 | params: Array, 22 | callback: (error: Error, result: unknown) => void 23 | ) => pool.query(text, params, callback); 24 | 25 | export { pool }; 26 | -------------------------------------------------------------------------------- /server/routes/clusterRouter.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { Request, Response } from 'express'; 3 | import { clusterController } from "../controllers/clusterController"; 4 | 5 | const clusterRouter = express.Router(); 6 | 7 | /** 8 | * @route POST /cluster 9 | * @desc Store cluster data from a form submission. 10 | * @access Public (You can adjust this based on your application's authentication flow.) 11 | */ 12 | clusterRouter.post( 13 | "/", 14 | clusterController.storeCluster, 15 | (_req: Request, res: Response) => { 16 | return res.status(200).send(); 17 | }, 18 | ); 19 | 20 | /** 21 | * @route GET /cluster/DB 22 | * @desc Retrieve cluster data to be displayed, typically triggered from the navigation bar. 23 | * @access Public (You can adjust this based on your application's authentication flow.) 24 | */ 25 | clusterRouter.get( 26 | "/DB", 27 | clusterController.fetchCluster, 28 | (_req: Request, res: Response) => { 29 | const { clusters } = res.locals; 30 | return res.status(200).json([...clusters]); 31 | }, 32 | ); 33 | 34 | export { clusterRouter }; 35 | -------------------------------------------------------------------------------- /server/routes/loginRouter.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { Request, Response } from 'express'; 3 | import { userController } from "../controllers/userController"; 4 | import { cookieController } from "../controllers/cookieController"; 5 | 6 | const loginRouter = express.Router(); 7 | 8 | /** 9 | * @route POST /signupRequest 10 | * @desc Registers a new user and sets an SSID cookie upon successful registration. 11 | * @access Public 12 | */ 13 | loginRouter.post( 14 | "/signupRequest", 15 | userController.createUser, 16 | cookieController.setSSIDCookie, 17 | (_req: Request, res: Response) => { 18 | if (res.locals.user_id) { 19 | console.log('User successfully registered'); 20 | return res.status(200).send('You are logged in'); 21 | } else { 22 | return res.status(500).json({ error: 'User ID not found' }); 23 | } 24 | }, 25 | ); 26 | 27 | /** 28 | * @route GET /verify 29 | * @desc Verifies if the user is logged in based on the presence of the SSID cookie. 30 | * @access Public 31 | */ 32 | loginRouter.get( 33 | "/verify", 34 | (_req: Request, res: Response) => { 35 | const loggedIn = _req.cookies.ssid ? true : false; 36 | return res.status(200).json({ status: loggedIn }); 37 | }, 38 | ); 39 | 40 | /** 41 | * @route POST /loginRequest 42 | * @desc Authenticates user login credentials and sets an SSID cookie upon successful authentication. 43 | * @access Public 44 | */ 45 | loginRouter.post( 46 | "/loginRequest", 47 | userController.verifyUser, 48 | cookieController.setSSIDCookie, 49 | (_req: Request, res: Response) => { 50 | return res.status(200).send("You are logged in"); 51 | }, 52 | ); 53 | 54 | export { loginRouter }; 55 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import dotenv from 'dotenv'; 3 | import path from 'path'; 4 | import cors from 'cors'; 5 | import cookieParser from 'cookie-parser'; 6 | import { ServerError } from '../types'; 7 | import { loginRouter } from "./routes/loginRouter"; 8 | import { clusterRouter } from "./routes/clusterRouter"; 9 | 10 | 11 | dotenv.config(); 12 | 13 | const app = express(); 14 | 15 | // Middleware 16 | app.use(express.urlencoded({ extended: true })); 17 | app.use(express.json()); 18 | app.use(cors({ 19 | origin: 'http://localhost:3030', 20 | credentials: true 21 | })); 22 | app.use(cookieParser()); 23 | 24 | const PORT = process.env.PORT || 4000; 25 | 26 | // Route handlers 27 | app.use('/login', loginRouter); 28 | app.use('/cluster', clusterRouter); 29 | app.get('/logout', (_req: Request, res: Response) => { 30 | return res.cookie('ssid', '', { 31 | expires: new Date(0), 32 | httpOnly: true, 33 | sameSite: 'none', 34 | secure: true 35 | }).status(200).send('session ended'); 36 | }); 37 | 38 | if (process.env.NODE_ENV === "production") { 39 | app.use(express.static(path.join(path.resolve(), "dist"))); 40 | app.get("/*", function (_req, res) { 41 | return res.sendFile(path.join(path.resolve(), "dist", "index.html")); 42 | }); 43 | } 44 | 45 | app.use((_req: Request, res: Response) => { 46 | return res.status(404).send("Invalid endpoint"); 47 | }); 48 | 49 | app.use((err: ServerError, _req: Request, res: Response) => { 50 | const defaultErr: ServerError = { 51 | log: "Express error handler caught unknown middleware error", 52 | status: 500, 53 | message: { err: "An error occurred" }, 54 | }; 55 | const errorObj: ServerError = Object.assign({}, defaultErr, err); 56 | console.log(errorObj.log); 57 | return res.status(errorObj.status).json(errorObj.message); 58 | }); 59 | 60 | app.listen(PORT, () => { 61 | console.log(`app is listening on port: ${PORT}...`); 62 | }); 63 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | 15 | .logo:hover { 16 | filter: drop-shadow(0 0 2em #646cffaa); 17 | } 18 | 19 | .logo.react:hover { 20 | filter: drop-shadow(0 0 2em #61dafbaa); 21 | } 22 | 23 | @keyframes logo-spin { 24 | from { 25 | transform: rotate(0deg); 26 | } 27 | 28 | to { 29 | transform: rotate(360deg); 30 | } 31 | } 32 | 33 | @media (prefers-reduced-motion: no-preference) { 34 | a:nth-of-type(2) .logo { 35 | animation: logo-spin infinite 20s linear; 36 | } 37 | } 38 | 39 | .card { 40 | padding: 2em; 41 | } 42 | 43 | .read-the-docs { 44 | color: #888; 45 | } 46 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 2 | import MainPage from "./containers/MainPage" 3 | import './stylesheets/index.css' 4 | import WelcomePage from './containers/WelcomePage'; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 | 11 | }/> 12 | } /> 13 | 14 | 15 |
16 | ) 17 | } 18 | 19 | export default App 20 | -------------------------------------------------------------------------------- /src/assets/AllenHui.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/AllenHui.jpeg -------------------------------------------------------------------------------- /src/assets/Charts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/Charts.gif -------------------------------------------------------------------------------- /src/assets/DanielLee.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/DanielLee.jpeg -------------------------------------------------------------------------------- /src/assets/Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/Demo.gif -------------------------------------------------------------------------------- /src/assets/SamJohnson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/SamJohnson.png -------------------------------------------------------------------------------- /src/assets/WanluDing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/WanluDing.jpeg -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/favicon.png -------------------------------------------------------------------------------- /src/assets/githubIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/githubIcon.png -------------------------------------------------------------------------------- /src/assets/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/linkedin.png -------------------------------------------------------------------------------- /src/assets/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/logo2.png -------------------------------------------------------------------------------- /src/assets/team.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ClusterSense/ba7d36fe2867cf74c09b32cc832e7942cf6786f3/src/assets/team.png -------------------------------------------------------------------------------- /src/components/Chart.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import ReactElement from 'react'; 3 | import { 4 | Chart as ChartJS, 5 | LineElement, 6 | LinearScale, 7 | CategoryScale, 8 | PointElement, 9 | Title, 10 | } from 'chart.js'; 11 | import { Line } from 'react-chartjs-2'; 12 | import axios from 'axios'; 13 | 14 | type ChartProps = { 15 | port: number; 16 | query: string; 17 | title: string; 18 | }; 19 | 20 | const loading = { 21 | labels: [], 22 | datasets: [ 23 | { 24 | label: 'Loading', 25 | data: [], 26 | backgroundColor: 'rgba(255,0,0)', 27 | borderColor: 'rgba(255,0,0)', 28 | borderWidth: 2, 29 | pointRadius: 4, 30 | pointBackgroundColor: 'rgba(255,0,0)', 31 | }, 32 | ], 33 | }; 34 | type DataPoint = string[][]; 35 | type Data = string[] 36 | 37 | //rounds the value to 4 decimals and returns values for Charts.js to use 38 | const organizeData = (array: DataPoint) => { 39 | const value: string[] = []; 40 | array.forEach((el: Data) => { 41 | if (el[1].length > 5 && el[1].includes('.')) { 42 | el[1] = el[1].slice(0, 5); 43 | } 44 | value.push(el[1]); 45 | }); 46 | 47 | const newChartData = { 48 | labels: [ 49 | 'T-2:30', 50 | 'T-2:00', 51 | 'T-1:30', 52 | 'T-1:00', 53 | 'T-0:30', 54 | 'T-0:00', 55 | ], 56 | datasets: [ 57 | { 58 | label: 'Sample Line Chart', 59 | data: value, 60 | backgroundColor: 'rgba(255,0,0)', 61 | borderColor: 'rgba(255,0,0)', 62 | borderWidth: 2, 63 | pointRadius: 4, 64 | pointBackgroundColor: 'rgba(255,0,0)', 65 | }, 66 | ], 67 | }; 68 | return newChartData; 69 | }; 70 | 71 | const Chart: React.FC = ({ port, query, title }: ChartProps): ReactElement => { 72 | ChartJS.register( 73 | CategoryScale, 74 | LinearScale, 75 | PointElement, 76 | LineElement, 77 | Title 78 | ); 79 | const [data, setData] = useState(loading); 80 | 81 | const url = `http://localhost:${port}/api/v1/query?query=${query}[1m]`; 82 | 83 | useEffect(() => { 84 | if (!port || !data) return undefined; 85 | const fetchData = async () => { 86 | try { 87 | const response = await axios.get(url); 88 | if (response.data.data.result[0].values) { 89 | const array = response.data.data.result[0].values; 90 | setData(organizeData(array)); 91 | } 92 | } catch (err) { 93 | console.log(err); 94 | return; 95 | } 96 | }; 97 | fetchData(); 98 | const interval = setInterval(fetchData, 3000); 99 | return () => clearInterval(interval); 100 | }, [data, port]); 101 | 102 | const options = { 103 | maintainAspectRatio: false, 104 | responsive: true, 105 | animation: { 106 | duration: 1000, 107 | }, 108 | scales: { 109 | y: { 110 | ticks: { 111 | color: '#black', 112 | }, 113 | }, 114 | x: { 115 | ticks: { 116 | color: '#black', 117 | }, 118 | }, 119 | }, 120 | plugins: { 121 | title: { 122 | display: true, 123 | position: 'top' as const, 124 | text: `${title}`, 125 | color: '#black', 126 | align: 'start' as const, 127 | padding: { 128 | top: 10, 129 | bottom: 15, 130 | }, 131 | }, 132 | }, 133 | }; 134 | 135 | return ( 136 | <> 137 | 138 | 139 | ); 140 | }; 141 | 142 | export default Chart; 143 | -------------------------------------------------------------------------------- /src/components/MeetTeam.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DanielLee from '../assets/DanielLee.jpeg'; 3 | import WanluDing from '../assets/WanluDing.jpeg'; 4 | import SamJohnson from '../assets/SamJohnson.png'; 5 | import AllenHui from '../assets/AllenHui.jpeg'; 6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 7 | import { faLinkedin, faGithub } from '@fortawesome/free-brands-svg-icons'; 8 | 9 | // Array of team member details 10 | const teamMembers = [ 11 | // Individual members' information 12 | { 13 | name: "Allen Hui", 14 | photo: AllenHui, 15 | email: "huiallen92@gmail.com", 16 | linkedin: "https://www.linkedin.com/in/allen-hui-7b590b9a/", 17 | github: "https://github.com/Ahuim8", 18 | }, 19 | { 20 | name: "Daniel Lee", 21 | photo: DanielLee, 22 | email: "jungtaelee0128@gmail.com", 23 | linkedin: "https://www.linkedin.com/in/jungtaelee/", 24 | github: "https://github.com/jungtaelee0128", 25 | }, 26 | { 27 | name: "Sam Johnson", 28 | photo: SamJohnson, 29 | email: "SFJohnson24@gmail.com", 30 | linkedin: "https://www.linkedin.com/in/samuel-johnson-dpt/", 31 | github: "https://github.com/SFJohnson24", 32 | }, 33 | { 34 | name: "Wanlu Ding", 35 | photo: WanluDing, 36 | email: "wanlu.ding@gmail.com", 37 | linkedin: "https://www.linkedin.com/in/wanlu-ding/", 38 | github: "https://github.com/WanluD", 39 | }, 40 | ]; 41 | 42 | /** 43 | * MeetTeam Component 44 | * This component displays the 'Meet the Team' section, including 45 | * a list of team members and associated links to their GitHub and LinkedIn profiles. 46 | */ 47 | const MeetTeam: React.FC = () => { 48 | return ( 49 |
61 |

62 | Meet the Team 63 |

64 | 65 |
71 | {teamMembers.map((member, index) => ( 72 |
85 | {member.name} 86 | 87 |

{member.name}

88 | 89 |

{member.email}

90 | 91 | 99 |
100 | ))} 101 |
102 |
103 | ); 104 | }; 105 | 106 | export default MeetTeam; 107 | -------------------------------------------------------------------------------- /src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import AppBar from '@mui/material/AppBar'; 4 | import Toolbar from '@mui/material/Toolbar'; 5 | import Stack from '@mui/material/Stack'; 6 | import IconButton from '@mui/material/IconButton'; 7 | import Typography from '@mui/material/Typography'; 8 | import Menu from '@mui/material/Menu'; 9 | import MenuItem from '@mui/material/MenuItem'; 10 | import logo2 from '../assets/logo2.png'; 11 | import MouseEvent from 'react'; 12 | import AccountCircle from '@mui/icons-material/AccountCircle'; 13 | import Box from '@mui/material/Box'; 14 | import BubbleChartIcon from '@mui/icons-material/BubbleChart'; 15 | 16 | type cluster = {value: string, label: string} 17 | 18 | interface NavProps { 19 | setPort: (e: number) => void; 20 | formStatus: boolean; 21 | formSubmission: (e: boolean) => void; 22 | clusterOptions: cluster[]; 23 | setClustersOptions: (e: cluster[]) => void; 24 | } 25 | 26 | const NavBar = ({ 27 | setPort, 28 | formStatus, 29 | formSubmission, 30 | clusterOptions, 31 | setClustersOptions, 32 | }: NavProps) => { 33 | const [menuAnchorEl, setMenuAnchorEl] = useState(null); 34 | const [clusterMenuAnchorEl, setClusterMenuAnchorEl] = 35 | useState(null); 36 | 37 | const navigate = useNavigate(); 38 | 39 | const toHome = () => { 40 | formSubmission(false); 41 | const path: string = '/home'; 42 | navigate(path); 43 | }; 44 | 45 | const signOut = async () => { 46 | try { 47 | const response = await fetch('/logout', { 48 | method: 'GET', 49 | credentials: 'include', 50 | headers: { 'Content-Type': 'application/json' }, 51 | }); 52 | if (response.ok) { 53 | const path: string = '/'; 54 | navigate(path); 55 | } else { 56 | console.error('Error deleting session'); 57 | } 58 | } catch (error) { 59 | console.error('Error:', error); 60 | } 61 | }; 62 | 63 | function handleSelect(e: MouseEvent) { 64 | const chosenCluster = e.target.value; 65 | console.log('chosen', chosenCluster); 66 | if (chosenCluster !== undefined) { 67 | setPort(chosenCluster); 68 | formSubmission(true); 69 | } 70 | } 71 | 72 | interface clusterType { 73 | cluster_port: number; 74 | } 75 | useEffect(() => { 76 | const fetchClusters = async () => { 77 | try { 78 | const response = await fetch('/cluster/DB', { 79 | method: 'GET', 80 | credentials: 'include', 81 | }); 82 | if (response.ok) { 83 | const data = await response.json(); 84 | if (!data.length) { 85 | setClustersOptions([{ value: '', label: 'Empty' }]); 86 | } else { 87 | const convertData = data.map((cluster: clusterType) => { 88 | const value = cluster.cluster_port.toString(); 89 | const label = cluster.cluster_port.toString(); 90 | return { value: value, label: label }; 91 | }); 92 | await setClustersOptions(convertData); 93 | } 94 | } else { 95 | console.error('Error fetching clusters'); 96 | } 97 | } catch (error) { 98 | console.error('Error:', error); 99 | } 100 | }; 101 | fetchClusters(); 102 | }, [formStatus]); 103 | 104 | const handleMenuOpen = (event: React.MouseEvent) => { 105 | setMenuAnchorEl(event.currentTarget); 106 | }; 107 | 108 | const handleMenuClose = () => { 109 | setMenuAnchorEl(null); 110 | }; 111 | 112 | const handleClusterMenuOpen = (event: React.MouseEvent) => { 113 | setClusterMenuAnchorEl(event.currentTarget); 114 | }; 115 | 116 | const handleClusterMenuClose = () => { 117 | setClusterMenuAnchorEl(null); 118 | }; 119 | 120 | return ( 121 |
122 | 127 | 128 |
129 | 134 | Sign Out 135 | 136 | 143 | 144 | 145 | ClusterSense 146 | 147 |
148 | 149 | 155 | {clusterOptions.map((element: cluster, index : number) => ( 156 | 157 | {element.label} 158 | 159 | ))} 160 | 161 | 162 | 169 | 170 | 171 | 172 | 179 | 180 | 181 | 182 |
183 |
184 |
185 | ); 186 | }; 187 | export default NavBar; -------------------------------------------------------------------------------- /src/components/RepoSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import githubIcon from '../assets/githubIcon.png'; 3 | import linkedinIcon from '../assets/linkedin.png'; 4 | 5 | const RepoSection: React.FC = () => ( 6 |
16 |

17 | Check our Github 18 | 19 | GitHub Logo 20 | 21 |

22 |

23 | Check our LinkedIn 24 | 25 | LinkedIn Logo 26 | 27 |

28 |
29 | ); 30 | 31 | export default RepoSection; 32 | -------------------------------------------------------------------------------- /src/components/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { TERipple } from 'tw-elements-react'; 4 | import TextField from '@mui/material/TextField'; 5 | 6 | interface SignUpProps { 7 | showSignUp: () => void; 8 | } 9 | 10 | const SignIn = ({ 11 | showSignUp, 12 | }: SignUpProps) => { 13 | const [username, setUsername] = useState(''); 14 | const [password, setPassword] = useState(''); 15 | const [error, setError] = useState(''); 16 | 17 | //navigation paths 18 | const navigate = useNavigate(); 19 | const toHome = () => { 20 | const path = '/home'; 21 | navigate(path); 22 | }; 23 | 24 | const Submit = async () => { 25 | try { 26 | const data = { 27 | username: username, 28 | password: password, 29 | }; 30 | const response = await fetch(`/login/loginRequest`, { 31 | method: 'POST', 32 | credentials: 'include', 33 | headers: { 'Content-Type': 'application/json' }, 34 | body: JSON.stringify(data), 35 | }); 36 | if (response.ok) { 37 | toHome(); 38 | } else if (response.status === 400) { 39 | setError('All fields are required'); 40 | } else if (response.status === 409 || response.status === 401) { 41 | setError('Invalid username or password'); 42 | } else { 43 | console.log(response) 44 | setError('An error occurred.'); 45 | } 46 | } catch (err) { 47 | console.error(err); 48 | setError('An error occurred.'); 49 | } 50 | }; 51 | // Use the passed function to navigate to SignUp form 52 | const toSignUp = () => { 53 | showSignUp(); 54 | }; 55 | 56 | return ( 57 |
58 |
59 |
60 |

Login

61 | ) => setUsername(e.target.value)}/> 67 | 68 | ) => setPassword(e.target.value)}/> 74 |
75 | 76 | 77 | 84 | 85 |
86 | {error &&
{error}
} 87 | 88 |
89 |

Don't have an account?

90 | 91 | 98 | 99 |
100 |
101 |
102 |
103 | ); 104 | }; 105 | 106 | export default SignIn; -------------------------------------------------------------------------------- /src/components/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { TERipple } from 'tw-elements-react'; 4 | import TextField from '@mui/material/TextField'; 5 | 6 | interface SignUpProps { 7 | showSignIn: () => void; 8 | } 9 | 10 | const SignUp: React.FC = ({ showSignIn }) => { 11 | const [username, setUsername] = useState(''); 12 | const [password, setPassword] = useState(''); 13 | const [error, setError] = useState(''); 14 | 15 | const navigate = useNavigate(); 16 | const goLogin = () => { 17 | showSignIn(); 18 | }; 19 | const toHome = () => { 20 | const path = '/home'; 21 | navigate(path); 22 | }; 23 | // 24 | const handleClick = async () => { 25 | try { 26 | const body = { 27 | username: username, 28 | password: password, 29 | }; 30 | const data = await fetch(`/login/signupRequest`, { 31 | method: 'POST', 32 | headers: { 'Content-Type': 'application/json' }, 33 | body: JSON.stringify(body), 34 | }); 35 | if (data.ok) { 36 | toHome(); 37 | } else if (data.status === 400) { 38 | setError('All fields are required'); 39 | } else if (data.status === 409) { 40 | setError('Username already exists!'); 41 | } else { 42 | console.log(data); 43 | setError('Unable to create user. Please try again!'); 44 | } 45 | } catch (err) { 46 | console.log(err); 47 | setError('An error occurred.'); 48 | } 49 | }; 50 | 51 | return ( 52 | 53 |
54 |
55 |

Sign Up

56 |
57 | ) => setUsername(e.target.value)} 65 | /> 66 | ) => setPassword(e.target.value)} 74 | /> 75 |
76 | 77 | 84 | 85 |
86 | {error && ( 87 |
91 | {error} 92 |
93 | )} 94 |
95 |

Have an account?

96 | 97 | 104 | 105 |
106 |
107 |
108 |
109 | ); 110 | }; 111 | 112 | export default SignUp; -------------------------------------------------------------------------------- /src/containers/MainPage.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import MouseEvent from 'react'; 4 | import NavBar from '../components/NavBar'; 5 | import TextField from '@mui/material/TextField'; 6 | import Button from '@mui/material/Button'; 7 | import { Container } from '@mui/material'; 8 | import Chart from '../components/Chart.tsx'; 9 | 10 | const MainPage = () => { 11 | const navigate = useNavigate(); 12 | 13 | const toLogin = () => { 14 | const path: string = '/'; 15 | navigate(path); 16 | }; 17 | 18 | const [clusterOptions, setClustersOptions] = useState([]); 19 | const [port, setPort] = useState(); 20 | const [isFormSubmitted, setIsFormSubmitted] = useState(false); 21 | 22 | const handleSubmission = async (e: MouseEvent) => { 23 | e.preventDefault(); 24 | try { 25 | //this will put the cluster into the database 26 | const numPort = Number(port); 27 | const data = { port: numPort }; 28 | const response = await fetch('/cluster', { 29 | method: 'POST', 30 | credentials: 'include', 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | }, 34 | body: JSON.stringify(data), 35 | }); 36 | if (response.ok) { 37 | setIsFormSubmitted(true); 38 | const updatedClusters = [ 39 | ...clusterOptions, 40 | { value: port.toString(), label: port.toString() }, 41 | ]; 42 | setClustersOptions(updatedClusters); 43 | } 44 | } catch (error) { 45 | console.log('error: ', error); 46 | } 47 | }; 48 | 49 | //useEffect to check if cookies exist 50 | useEffect(() => { 51 | const checkSession = async () => { 52 | try { 53 | const isAuthenticated = await fetch('/login/verify'); 54 | const status = await isAuthenticated.json(); 55 | if (status.status === false) { 56 | toLogin(); 57 | } 58 | } catch (error) { 59 | console.error('Error checking session:', error); 60 | } 61 | }; 62 | checkSession(); 63 | }, []); 64 | 65 | return ( 66 |
67 | 74 |
82 | {isFormSubmitted ? ( 83 |
84 |
85 |
86 | 91 |
92 |
93 | 98 |
99 |
100 | 104 |
105 |
106 |
110 |
111 | 116 |
117 |
118 | 123 |
124 |
125 |
126 | ) : ( 127 | 137 |

138 | {' '} 139 | Welcome to your ClusterSense Dashboard! 140 |

141 |

142 |
143 |
144 |

145 | Enter your JMX port for your Kafka Cluster:{' '} 146 |

147 | ) => setPort(e.target.value)} 152 | /> 153 |
154 |
155 | 158 |
159 |
160 |
161 | )} 162 |
163 |
164 | ); 165 | }; 166 | 167 | export default MainPage; -------------------------------------------------------------------------------- /src/containers/WelcomePage.tsx: -------------------------------------------------------------------------------- 1 | import ReactElement from 'react'; 2 | import { useState, useEffect } from 'react'; 3 | import logo2 from '../assets/logo2.png'; 4 | import MeetTeam from '../components/MeetTeam'; 5 | import SignIn from '../components/SignIn'; 6 | import SignUp from '../components/SignUp'; 7 | import { AppBar, Toolbar, Container, Typography, Button, Box, Grid } from '@mui/material'; 8 | import styled from '@emotion/styled'; 9 | import { useTheme } from '@mui/material/styles'; 10 | import useMediaQuery from '@mui/material/useMediaQuery'; 11 | import { TypeAnimation } from 'react-type-animation'; 12 | import IconButton from '@mui/material/IconButton'; 13 | import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; 14 | import RepoSection from '../components/RepoSection'; 15 | import TimelineIcon from '@mui/icons-material/Timeline'; 16 | import VerifiedUserIcon from '@mui/icons-material/VerifiedUser'; 17 | import CableIcon from '@mui/icons-material/Cable'; 18 | import chartsVisual from '../assets/Charts.gif' 19 | 20 | const WelcomePage = (): ReactElement => { 21 | 22 | // State to toggle between SignUp and SignIn components 23 | const [showSignUp, setShowSignUp] = useState(false); 24 | const [showScrollTopButton, setShowScrollTopButton] = useState(false); 25 | 26 | // Handlers to toggle visibility of SignUp and SignIn components 27 | const handleShowSignIn = ():void => { 28 | setShowSignUp(false); 29 | }; 30 | 31 | const handleShowSignUp = ():void => { 32 | setShowSignUp(true); 33 | }; 34 | 35 | useEffect(() => { 36 | const handleScroll = ():void => { 37 | const meetTeamElem = document.getElementById("team"); 38 | const rect = meetTeamElem?.getBoundingClientRect(); 39 | if (rect && rect.top <= window.innerHeight) { 40 | setShowScrollTopButton(true); 41 | } else { 42 | setShowScrollTopButton(false); 43 | } 44 | }; 45 | 46 | window.addEventListener('scroll', handleScroll); 47 | 48 | // Cleanup function to remove the event listener 49 | return () => window.removeEventListener('scroll', handleScroll); 50 | }, []); 51 | 52 | // Sample text for demonstration 53 | const appIntro = `An intuitive and feature-rich GUI that simplifies Kafka cluster management, monitoring, and interaction to streamline operations and efficiency when working with Kafka clusters.`; 54 | const ourApp = `ClusterSense provides pre-built charts for the most important metrics in your Kafka Application. 55 | User port numbers are saved and available with our drop own menu.` 56 | // const loremParagraph3 = "Port information of existing users are stored in our database for immediate access to their metrics. Metrics are not stored in our database."; 57 | 58 | const CustomAppBar = styled(AppBar)` 59 | background-color: transparent; 60 | box-shadow: none; 61 | ` as unknown as typeof AppBar; 62 | 63 | const theme = useTheme(); 64 | const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); 65 | 66 | const containerStyle = { 67 | padding: isSmallScreen ? '0 1rem' : '0 5rem', 68 | marginBottom: '4rem', 69 | background: 'transparent' 70 | }; 71 | return ( 72 |
73 | {/* Navigation bar */} 74 | 75 | 76 | {/* Logo Display */} 77 | 78 | 84 | 85 | 86 | {/* Navigation Buttons */} 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 |
95 |
96 |
97 | 102 |

ClusterSense

103 | 104 |
105 |
106 | 118 |
119 |
120 |
121 | 122 | {showSignUp ? : } 123 | 124 |
125 |
126 |
127 |

Welcome to ClusterSense!

129 | 130 | {appIntro} 131 | 132 |
133 | 134 | 135 | Core Features 136 | 137 |

138 | 139 | 140 | 156 | 157 |
158 | 159 | Realtime charts 160 | 161 |
162 |
163 | 164 | 180 | 181 |
182 | 183 | User Authentication 184 | 185 |
186 |
187 | 188 | 204 | 205 |
206 | 207 | Revisit previous ports 208 | 209 |
210 |
211 | 212 | 213 |

Getting Started

{ourApp} 214 |
215 |
216 | 217 | Demo GIF 218 | 219 |
220 |
221 |

222 | 223 | 224 | 225 | 226 | {showScrollTopButton && ( 227 | window.scrollTo({ top: 0, behavior: 'smooth' })} 235 | > 236 | 237 | 238 | )} 239 |
240 | ); 241 | } 242 | 243 | export default WelcomePage; 244 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | 52 | button:hover { 53 | border-color: #646cff; 54 | } 55 | 56 | button:focus, 57 | button:focus-visible { 58 | outline: 4px auto -webkit-focus-ring-color; 59 | } 60 | 61 | @media (prefers-color-scheme: light) { 62 | :root { 63 | color: #213547; 64 | background-color: #ffffff; 65 | } 66 | 67 | a:hover { 68 | color: #747bff; 69 | } 70 | 71 | button { 72 | background-color: #f9f9f9; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './stylesheets/index.css' 5 | 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | , 11 | ) 12 | -------------------------------------------------------------------------------- /src/stylesheets/index.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | @import url('https://fonts.googleapis.com/css2?family=Cabin:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap'); 5 | 6 | /* @layer base { 7 | html { 8 | @apply text-neutral-800; 9 | } 10 | 11 | html.dark { 12 | @apply text-neutral-50; 13 | @apply bg-neutral-800; 14 | } 15 | } */ 16 | 17 | /*gradient setting add to class*/ 18 | .gradient-background { 19 | background: linear-gradient(to left, #3D2F91, #89278D); 20 | } 21 | .background-animate { 22 | background-size: 400%; 23 | 24 | -webkit-animation: AnimationName 3s ease infinite; 25 | -moz-animation: AnimationName 3s ease infinite; 26 | animation: AnimationName 3s ease infinite; 27 | } 28 | 29 | @keyframes AnimationName { 30 | 0%, 31 | 100% { 32 | background-position: 0% 50%; 33 | } 34 | 50% { 35 | background-position: 100% 50%; 36 | } 37 | } 38 | 39 | /* button format */ 40 | .button-color { 41 | display: inline-block; 42 | padding: 6px 12px; /* You can adjust the padding as needed */ 43 | font-size: 0.875rem; /* Equivalent to text-xs in Tailwind CSS */ 44 | font-weight: 500; /* Equivalent to font-medium in Tailwind CSS */ 45 | text-transform: uppercase; 46 | text-align: center; 47 | vertical-align: middle; 48 | border-width: 2px; 49 | border-style: solid; 50 | border-radius: 0.25rem; /* Equivalent to rounded in Tailwind CSS */ 51 | transition: all 150ms ease-in-out; 52 | } 53 | 54 | .button-color { 55 | border-color: #89278D; /* Equivalent to border-danger in Tailwind CSS */ 56 | color: #89278D; /* Equivalent to text-danger in Tailwind CSS */ 57 | background-color: transparent; /* Equivalent to bg-transparent in Tailwind CSS */ 58 | } 59 | 60 | .transparent{ 61 | background: 'transparent', 62 | } 63 | 64 | .signIn-up{ 65 | @apply h-screen bg-white flex-col items-center w-full h-full text-black 66 | rounded-lg 67 | lg:w-9/12 p-6 68 | } 69 | 70 | .chart-dots { 71 | display: none; 72 | } 73 | 74 | .chart-line { 75 | stroke: #3D2F91; /* Change to the desired line color */ 76 | stroke-width: 2px; /* Adjust line thickness as needed */ 77 | } 78 | 79 | .fontStyling { 80 | font-family: 'DM Sans', sans-serif; 81 | font: bold; 82 | } 83 | 84 | .text-custom { 85 | font-size: 20px; 86 | } 87 | 88 | .meetTeamContainer { 89 | margin-bottom: 50px; 90 | } 91 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module 'react/jsx-runtime' { 3 | export default any; 4 | } 5 | declare module 'react' { 6 | export default any; 7 | } 8 | declare module 'react-dom/client' { 9 | export default any; 10 | } 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}", "./node_modules/tw-elements/dist/js/**/*.js"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("tw-elements/dist/plugin.cjs")], 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ES2020", 7 | "skipLibCheck": true, 8 | /* Bundler mode */ 9 | "moduleResolution": "Bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | "esModuleInterop": true, 16 | "outDir": "./dist/", 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src", "server/**/*.ts", "types.ts"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | "vite.config.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | // import React from "react"; 2 | // import { Theme } from "@mui/material"; 3 | // import { PaletteMode } from "@mui/material"; 4 | 5 | export type ServerError = { 6 | log: string; 7 | status: number; 8 | message: { err: string }; 9 | }; 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | // css: { 8 | // preprocessorOptions: { 9 | // scss: { 10 | // additionalData: `@import "./src/stylesheets/index.scss";`, 11 | // }, 12 | // }, 13 | // }, 14 | server: { 15 | port: 3030, 16 | proxy: { 17 | "/cluster/DB" : "http://localhost:4000", 18 | "/cluster": "http://localhost:4000", 19 | "/login": "http://localhost:4000", 20 | "/logout": "http://localhost:4000", 21 | } 22 | }, 23 | optimizeDeps: { 24 | include: ['@emotion/styled'] 25 | } 26 | }); 27 | --------------------------------------------------------------------------------