├── .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 | 
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 | 
31 | 
32 | 
33 | 
34 | 
35 | 
36 | 
37 | 
38 | 
39 | 
40 | 
41 | 
42 | 
43 | 
44 | 
45 | 
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 |
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 |
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 |
20 |
21 |
22 |
23 | Check our LinkedIn
24 |
25 |
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 |
82 | Log in
83 |
84 |
85 |
86 | {error && {error}
}
87 |
88 |
89 |
Don't have an account?
90 |
91 |
96 | Register
97 |
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 |
82 | Register
83 |
84 |
85 |
86 | {error && (
87 |
91 | {error}
92 |
93 | )}
94 |
95 |
Have an account?
96 |
97 |
102 | Login
103 |
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 |
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 | Features
89 | Get Started
90 | Team
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 |
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 |
--------------------------------------------------------------------------------