├── .eslintrc ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── Procfile ├── README.md ├── __tests__ ├── server │ └── server.test.js ├── setup.js └── src │ ├── App.test.jsx │ ├── component │ └── Navbar.test.jsx │ └── containers │ ├── Dashboard.test.jsx │ └── login.test.jsx ├── index.html ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── vite.svg ├── server ├── controllers │ ├── metricsController.js │ ├── tokenController.js │ └── userController.js ├── demo │ ├── Demo.md │ ├── consumer.js │ ├── producer.js │ ├── prometheus.yml │ └── utility.js ├── models │ └── userModel.js ├── routes │ ├── metricsRouter.js │ └── userRouter.js └── server.js ├── src ├── App.css ├── App.jsx ├── Hooks │ ├── useAuthenticate.jsx │ ├── useDarkMode.jsx │ ├── useMetricStore.jsx │ └── useScroll.jsx ├── assets │ ├── LinkedIn-Logos │ │ ├── LI-In-Bug.png │ │ └── LI-Logo.png │ ├── connorpic.png │ ├── davidpic.png │ ├── demo.gif │ ├── dominicpic.png │ ├── github-mark │ │ ├── GitHub_Logo.png │ │ ├── github-mark-white.png │ │ ├── github-mark-white.svg │ │ ├── github-mark.png │ │ └── github-mark.svg │ ├── grape.png │ ├── klusterfunklogo.png │ ├── klusterfunklogo2.png │ ├── medium-logo.png │ ├── react.svg │ └── wilsonpic.png ├── component │ ├── LineGraph.jsx │ ├── Links.jsx │ ├── Navbar.jsx │ ├── PromAddress.jsx │ ├── ReadMe.jsx │ └── TeamInfo.jsx ├── containers │ ├── Dashboard.jsx │ ├── Homepage.jsx │ └── login.jsx ├── index.css └── main.jsx ├── tailwind.config.js └── vite.config.js /.eslintrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/.eslintrc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | package-lock.json 4 | 5 | # dependencies 6 | node_modules/ 7 | 8 | # logs 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | data 13 | 14 | # output from webpack 15 | dist/ 16 | 17 | # zip file for initial aws deployment 18 | *.zip 19 | 20 | # jest coverage 21 | coverage/ 22 | 23 | .env 24 | 25 | .vscode/ 26 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "jsxSingleQuote": true, "singleQuote": true } 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 KlusterFunk 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: npm run start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![KlusterFunk Logo](./src/assets/klusterfunklogo2.png) 2 | 3 | KlusterFunk is a monitoring tool, built to visualize metrics from local kafka clusters, showing you a real-time, live-updating graph of those metrics. 4 | 5 | You can find and use our application at https://klusterfunk-b05ffb62bc07.herokuapp.com/ 6 | 7 | ## Overview 8 | 9 | This is a tool meant for developers that are familiar with Kafka and how to export metrics. To use this tool we assume you have: 10 | 11 | 1. Implemented Prometheus' JMX exporter on your running Kafka cluster(s). 12 | 2. Set up a Prometheus instance including a yaml config file that is pulling metrics from the port where JMX exporter metrics are being exposed. 13 | 3. Port forward your Prometheus port so you can input the link in our app. 14 | 15 | If you have not yet set up these tools for your clusters, follow the steps in **[Setup](#setup)** 16 | 17 | ## App in Action 18 | 19 | 20 | 21 | ## Features 22 | 23 | | Feature | Status | 24 | | -------------------------------------------------------------------------------------------------------- | ------ | 25 | | Login authorization using JWT tokens | ✅ | 26 | | Prometheus API | ✅ | 27 | | Build in functionality to have users simply input kafka cluster URIs and link up metrics on the backend | ⏳ | 28 | | Allow users to choose from list of metrics they would like to see or even input metrics they want to see | ⏳ | 29 | | Switch between Kafka clusters | ⏳ | 30 | | Dark Mode | ⏳ | 31 | | More styling using Tailwind | 🙏🏻 | 32 | | Save in database location of kafka clusters and prometheus address | 🙏🏻 | 33 | 34 | - ✅ = Ready to use 35 | - ⏳ = In progress 36 | - 🙏🏻 = Looking for contributors 37 | 38 | ## Setup 39 | 40 | ### To setup JMX exporter 41 | 42 | - Build exporter: 43 | 44 | ```shell 45 | git clone https://github.com/prometheus/jmx_exporter.git 46 | cd jmx_exporter 47 | mvn package 48 | ``` 49 | 50 | - Start zookeeper: 51 | 52 | ```shell 53 | /usr/local/opt/kafka/bin/zookeeper-server-start /usr/local/etc/zookeeper/zoo.cfg 54 | ``` 55 | 56 | - Setup JMX exporter to run on Kafka (run from root) \ 57 | 58 | ```shell 59 | export EXTRA_ARGS="-Dcom.sun.management.jmxremote \ 60 | -Dcom.sun.management.jmxremote.authenticate=false \ 61 | -Dcom.sun.management.jmxremote.ssl=false \ 62 | -Djava.util.logging.config.file=logging.properties \ 63 | -javaagent:/Users//jmx_exporter/jmx_prometheus_javaagent/target/jmx_prometheus_javaagent-0.20.1-SNAPSHOT.jar=8081:/Users//jmx_exporter/example_configs/kafka-2_0_0.yml" 64 | ``` 65 | 66 | - Start kafka 67 | 68 | ```shell 69 | /usr/local/opt/kafka/bin/kafka-server-start /usr/local/etc/kafka/server.properties 70 | ``` 71 | 72 | Localhost:8081 should now be displaying JMX metrics 73 | 74 | - Run Prometheus and point it to scrape at port 8081 (or whatever port you configured the JMX exporter ) 75 | 76 | ## Contributing 77 | 78 | Feel free to use the GitHub Issues tab to let us know what features you want and what you'd like to see next from the project! 79 | 80 | If you would like to work on the open source code, please do feel free to submit a pull request! Make sure you're following Javascript ES6 syntax and modularize your code as much as possible. 81 | 82 | To get started, first _fork_ and clone the repo, then run the following commands: 83 | 84 | 85 | ```shell 86 | npm install 87 | ``` 88 | 89 | ```shell 90 | npm run dev 91 | ``` 92 | 93 | ## Stack 94 | 95 | Apache Kafka, JMX Exporter, Prometheus, Node.js, Vite, MongoDB, Mongoose, Express, React, Chart.js, TailwindCSS, Vitest 96 | 97 | ## Contact Us 98 | 99 |

Dominic Kenny - 100 | Github 101 | | 102 | LinkedIn 103 | 104 |

105 |

Connor Donahue - 106 | Github 107 | | 108 | LinkedIn 109 | 110 |

111 |

Wilson Wu - 112 | Github 113 | | 114 | LinkedIn 115 | 116 |

117 |

David Tezza - 118 | Github 119 | | 120 | LinkedIn 121 | 122 |

123 | 124 | ## License 125 | 126 | [MIT License](./LICENSE.md) 127 | -------------------------------------------------------------------------------- /__tests__/server/server.test.js: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | 3 | import app from '../../server/server.js'; 4 | const server = 'http://localhost:3030'; 5 | 6 | describe('Route Integration', () => { 7 | describe('/404Route', () => { 8 | test('respond with 404 status and text/html content type', () => { 9 | return request(server) 10 | .get('/fakeroute') 11 | .expect('Content-Type', /text\/html/) 12 | .expect(404); 13 | }); 14 | }); 15 | 16 | describe('/user', () => { 17 | describe('/user/login', () => { 18 | test('respond with 400 status for missing password to /login', () => { 19 | return request(server) 20 | .post('/user/login') 21 | .send({ username: 'KlusterFunk' }) 22 | .expect('Content-Type', /json/) 23 | .expect(400, { err: 'Username and password required' }); 24 | }); 25 | test('respond with 400 status for missing username to /login', () => { 26 | return request(server) 27 | .post('/user/login') 28 | .send({ password: 'KlusterFunk' }) 29 | .expect('Content-Type', /json/) 30 | .expect(400, { err: 'Username and password required' }); 31 | }); 32 | }); 33 | 34 | describe('/user/signup', () => { 35 | test('respond with 400 status for missing password to /signup', () => { 36 | return request(server) 37 | .post('/user/signup') 38 | .send({ username: 'KlusterFunk' }) 39 | .expect('Content-Type', /json/) 40 | .expect(400, { err: 'Username and password required' }); 41 | }); 42 | test('respond with 400 status for missing username to /signup', () => { 43 | return request(server) 44 | .post('/user/signup') 45 | .send({ password: 'KlusterFunk' }) 46 | .expect('Content-Type', /json/) 47 | .expect(400, { err: 'Username and password required' }); 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/setup.js: -------------------------------------------------------------------------------- 1 | import { afterEach } from 'vitest'; 2 | import { cleanup } from '@testing-library/react'; 3 | import '@testing-library/jest-dom/vitest'; 4 | 5 | // runs a cleanup after each test case (e.g. clearing jsdom) 6 | afterEach(() => { 7 | cleanup(); 8 | }); 9 | -------------------------------------------------------------------------------- /__tests__/src/App.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { BrowserRouter, MemoryRouter } from 'react-router-dom'; 3 | 4 | import App from '/Users/Donahue/Documents/gits/ClusterFunk/src/App.jsx'; 5 | import 'react-router-dom'; 6 | 7 | describe('App', () => { 8 | test('renders App', () => { 9 | render( 10 | 11 | 12 | 13 | ); 14 | 15 | screen.debug(); 16 | 17 | // check if App components renders headline 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/src/component/Navbar.test.jsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react'; 2 | import Navbar from '@/component/Navbar.jsx'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | import { beforeEach } from 'vitest'; 5 | 6 | describe('Navbar', () => { 7 | beforeEach(async () => { 8 | render( 9 | 10 | 11 | 12 | ); 13 | }); 14 | 15 | test('renders Navbar', () => { 16 | expect(screen.getByText('Home')).toBeInTheDocument(); 17 | expect(screen.getByText('Features')).toBeInTheDocument(); 18 | expect(screen.getByText('Team')).toBeInTheDocument(); 19 | expect(screen.getByText('Blog')).toBeInTheDocument(); 20 | expect(screen.getByText('Login')).toBeInTheDocument(); 21 | }); 22 | 23 | // think this needs to be a part of integration test? don't know how to change state since 24 | // no event on this component to trigger the state change 25 | // test('renders Signout instead of login if user is no signed in', () => {}); 26 | }); 27 | -------------------------------------------------------------------------------- /__tests__/src/containers/Dashboard.test.jsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react'; 2 | import Dashboard from '@/containers/Dashboard.jsx'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | import { beforeEach } from 'vitest'; 5 | import request from 'supertest'; 6 | 7 | describe('Login', () => { 8 | beforeEach(async () => { 9 | render( 10 | 11 | 12 | 13 | ); 14 | }); 15 | describe('navbar', () => { 16 | test('navbar redners', () => { 17 | expect(screen.getByText('Blog')).toBeInTheDocument(); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/src/containers/login.test.jsx: -------------------------------------------------------------------------------- 1 | import { fireEvent, render, screen } from '@testing-library/react'; 2 | import Login from '@/containers/login.jsx'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | import { beforeEach } from 'vitest'; 5 | import { request } from 'supertest'; 6 | 7 | describe('Login', () => { 8 | beforeEach(async () => { 9 | render( 10 | 11 | 12 | 13 | ); 14 | }); 15 | test('renders login', () => { 16 | expect(screen.getByText('Login')).toBeInTheDocument(); 17 | }); 18 | 19 | test('Warning appears if password and confirm password do not match', () => { 20 | // Locating the sign up button and clicking it 21 | const signUpButton = screen.getByRole('button', { name: 'Signup' }); 22 | fireEvent.click(signUpButton); 23 | 24 | // Locating pw input and using fireEvent.change to put in pw value 25 | const passwordInput = screen.getByPlaceholderText('Password'); 26 | fireEvent.change(passwordInput, { target: { value: 'sandwich' } }); 27 | 28 | // Locating confirm pw input and using fireEvent.change to put in incorrect confirm pw value 29 | const confirmPasswordInput = 30 | screen.getByPlaceholderText('Confirm password'); 31 | fireEvent.change(confirmPasswordInput, { target: { value: 'sandwic' } }); 32 | 33 | // Locating infoMatch message and confirming that a warning shows up 34 | const infoMatch = screen.getByRole('infoMatch'); 35 | expect(infoMatch).toHaveTextContent('passwords do not match'); 36 | }); 37 | 38 | test('Login or Signup buttons send fetch request', () => {}); 39 | }); 40 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | KlusterFunk 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klusterfunk", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "set NODE_ENV=production && node server/server.js", 8 | "dev": "concurrently \"vite\" \"nodemon server/server.js\" --open", 9 | "build": "vite build", 10 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview", 12 | "test": "vitest" 13 | }, 14 | "dependencies": { 15 | "@christiangalsterer/kafkajs-prometheus-exporter": "^3.0.0", 16 | "base-64": "^1.0.0", 17 | "bcrypt": "^5.1.1", 18 | "chart.js": "^4.4.0", 19 | "concurrently": "^8.2.2", 20 | "cookie-parser": "^1.4.6", 21 | "cors": "^2.8.5", 22 | "dotenv": "^16.3.1", 23 | "express": "^4.18.2", 24 | "flowbite": "^2.1.1", 25 | "flowbite-react": "^0.6.4", 26 | "jsonwebtoken": "^9.0.2", 27 | "mongodb": "^6.8.0", 28 | "mongoose": "^7.8.1", 29 | "nodemon": "^3.0.1", 30 | "react": "^18.2.0", 31 | "react-chartjs-2": "^5.2.0", 32 | "react-dom": "^18.2.0", 33 | "react-router": "^6.17.0", 34 | "react-router-dom": "^6.17.0", 35 | "vite": "^4.5.0", 36 | "vite-plugin-eslint": "^1.8.1" 37 | }, 38 | "devDependencies": { 39 | "@testing-library/jest-dom": "^6.1.4", 40 | "@testing-library/react": "^14.1.0", 41 | "@types/react": "^18.2.15", 42 | "@types/react-dom": "^18.2.7", 43 | "@vitejs/plugin-react": "^4.0.3", 44 | "autoprefixer": "^10.4.16", 45 | "eslint": "^8.53.0", 46 | "eslint-config-react-app": "^7.0.1", 47 | "eslint-plugin-react": "^7.32.2", 48 | "eslint-plugin-react-hooks": "^4.6.0", 49 | "eslint-plugin-react-refresh": "^0.4.3", 50 | "helmet": "^7.0.0", 51 | "jest": "^29.7.0", 52 | "jsdom": "^22.1.0", 53 | "kafkajs": "^2.2.4", 54 | "postcss": "^8.4.31", 55 | "prom-client": "^15.0.0", 56 | "supertest": "^6.3.3", 57 | "tailwindcss": "^3.3.5", 58 | "vitest": "^0.34.6" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/public/favicon.ico -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/controllers/metricsController.js: -------------------------------------------------------------------------------- 1 | const metricsController = {}; 2 | 3 | metricsController.getDefaultMetrics = async (req, res, next) => { 4 | // Needs error handling for if promAddress doesn't start with 'http:' 5 | const defaultMetrics = {}; 6 | const { promAddress } = req.query; 7 | const getMetric = async (promQuery) => { 8 | let response = await fetch( 9 | `${promAddress}/api/v1/query?query=${promQuery}` 10 | ); 11 | response = await response.json(); 12 | return response.data.result[0].value; 13 | }; 14 | 15 | try { 16 | defaultMetrics.bytesIn = await getMetric( 17 | 'sum(rate(kafka_server_brokertopicmetrics_bytesin_total[1m]))' 18 | ); 19 | defaultMetrics.bytesOut = await getMetric( 20 | 'sum(rate(kafka_server_brokertopicmetrics_bytesout_total[1m]))' 21 | ); 22 | defaultMetrics.cpuUsage = await getMetric( 23 | 'sum(rate(process_cpu_seconds_total[1m])) * 100' 24 | ); 25 | defaultMetrics.brokerCount = await getMetric( 26 | 'kafka_controller_kafkacontroller_activebrokercount' 27 | ); 28 | // console.log(defaultMetrics); 29 | 30 | res.locals.defaultMetrics = defaultMetrics; 31 | return next(); 32 | } catch (err) { 33 | return next({ 34 | log: 'Error in metrics.Controller in getDefaultMetrics', 35 | message: { err: 'Error when fetching metrics from Prom API' }, 36 | }); 37 | } 38 | }; 39 | 40 | export default metricsController; 41 | -------------------------------------------------------------------------------- /server/controllers/tokenController.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import 'dotenv/config'; 3 | import base64 from 'base-64'; 4 | 5 | const tokenController = {}; 6 | 7 | /* 8 | //// Create Token Controller \\\\ 9 | 10 | Takes username from res.locals 11 | Generates JWT token with username and JWT_SECRET 12 | Assigns token to the response's HTTP-only cookie, labeled 'token' 13 | Saves new token to res.locals.token --> next(); 14 | 15 | */ 16 | 17 | tokenController.createToken = async (req, res, next) => { 18 | const { username } = res.locals.user; 19 | const token = jwt.sign({ username: username }, process.env.JWT_SECRET, { 20 | expiresIn: '12h', 21 | }); 22 | res.cookie('token', token, { httpOnly: true }); 23 | res.locals.token = token; 24 | return next(); 25 | }; 26 | 27 | /* 28 | //// Verify Token Controller \\\\ 29 | 30 | Takes token from req.cookies 31 | If no token in req.cookies, throws error 32 | Decodes the JWT using the token and the secret 33 | Deconstructs the username from the decoded token 34 | Stored in res.locals.username 35 | 36 | */ 37 | 38 | tokenController.verifyToken = async (req, res, next) => { 39 | const { token } = req.cookies; 40 | // Throws error if no token in cookies 41 | if (!token) { 42 | return next({ 43 | log: 'tokenController.verifyToken: ERROR: no token in req.cookies', 44 | message: { err: 'Unable to verify' }, 45 | status: 401, 46 | }); 47 | } 48 | try { 49 | // Decodes JWT token, pulls username and stores it in res.locals. 50 | const decoded = await jwt.verify(token, process.env.JWT_SECRET); 51 | const { username } = decoded; 52 | res.locals.username = username; 53 | return next(); 54 | } catch (err) { 55 | return next({ 56 | log: `tokenController.verifyToken: ERROR: ${err}`, 57 | status: 401, 58 | message: { err: 'Unable to verify' }, 59 | }); 60 | } 61 | }; 62 | 63 | tokenController.deleteToken = async (req, res, next) => { 64 | const { token } = req.cookies; 65 | console.log('in delete token') 66 | if (!token) { 67 | return next({ 68 | log: 'tokenController.verifyToken: ERROR: no token in req.cookies', 69 | message: { err: 'Unable to verify' }, 70 | status: 401, 71 | }); 72 | } 73 | res.clearCookie('token'); 74 | return next(); 75 | } 76 | 77 | export default tokenController; 78 | 79 | /* 80 | 81 | /// Code Storehouse \\\ 82 | 83 | Putting some old code here not ready to delete, DON'T PUSH THIS TO MAIN 84 | 85 | 86 | David's approach on decoding the JWT token: 87 | Takes token from req.cookies 88 | Splits token into segments [Header, Payload, Signature] 89 | Decodes the payload using Base64 and uses object destructuring to pull username 90 | Stores username in res.locals.username 91 | 92 | 93 | tokenController.verifyToken = async (req, res, next) => { 94 | const { token } = req.cookies; 95 | // if (token) { 96 | // const splitToken = token.split('.'); 97 | // const { username } = JSON.parse(base64.decode(splitToken[1])); 98 | // res.locals.username = username; 99 | // } 100 | 101 | 102 | */ 103 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | import User from '../models/userModel.js'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | const userController = {}; 5 | 6 | /* 7 | //// Login Controller \\\\ 8 | 9 | Takes username and password from req.body. 10 | Checks to see if username exists in MongoDB 11 | Checks password against Bcrypt hash. 12 | If both checks pass: stores user object to res.locals -> next(); 13 | 14 | */ 15 | userController.login = async (req, res, next) => { 16 | const { username, password } = req.body; 17 | // Confirms req.body includes username and password 18 | if (!username || !password) { 19 | return next({ 20 | log: 'userController.createUser: ERROR: missing username or password', 21 | status: 400, 22 | message: { err: 'Username and password required' }, 23 | }); 24 | } 25 | try { 26 | // Searches MongoDB for user document with matching username 27 | const user = await User.findOne({ username: username }); 28 | if (!user) { 29 | return next({ 30 | log: 'userController.login: ERROR: unable to find user in database', 31 | status: 401, 32 | message: { err: 'Invalid username or password' }, 33 | }); 34 | } 35 | // Compares req.body password with the User document's password (returns boolean) 36 | const result = await bcrypt.compare(password, user.password); 37 | if (!result) { 38 | return next({ 39 | log: 'userController.login: ERROR: invalid password', 40 | status: 401, 41 | message: { err: 'Invalid username or password' }, 42 | }); 43 | } else { 44 | // Stores user object to res.locals.user 45 | res.locals.user = user; 46 | return next(); 47 | } 48 | } catch (err) { 49 | return next({ 50 | log: `userController.login: ERROR: ${err}`, 51 | message: { err: 'Something went wrong! Whoops!' }, 52 | status: 500, 53 | }); 54 | } 55 | }; 56 | 57 | /* 58 | //// Signup Controller \\\\ 59 | 60 | Takes username and password from req.body. 61 | Checks to see if username is available by seeing if it alread exists in MongoDB 62 | Hashes password using Bcrypt 63 | Creates new user in MongoDB using username and hashed password 64 | Saves new user object to res.locals.user --> next(); 65 | 66 | */ 67 | 68 | userController.signup = async (req, res, next) => { 69 | const { username, password } = req.body; 70 | // Confirms req.body includes username and password 71 | if (!username || !password) { 72 | return next({ 73 | log: 'userController.signup: ERROR: missing username or password', 74 | status: 400, 75 | message: { err: 'Username and password required' }, 76 | }); 77 | } 78 | 79 | try { 80 | // Checks to see if username is available: 81 | const findUser = await User.findOne({ username: username }); 82 | if (findUser) { 83 | return next({ 84 | log: 'userController.signup: ERROR: username already exists', 85 | status: 400, 86 | message: { err: 'Username is taken' }, 87 | }); 88 | } 89 | // Hashes password using Bcrypt 90 | const salt = await bcrypt.genSalt(); 91 | const hashedPassword = await bcrypt.hash(password, salt); 92 | const user = await User.create({ 93 | username, 94 | password: hashedPassword, 95 | }); 96 | // Saves new user object to res.locals.user 97 | res.locals.user = user; 98 | return next(); 99 | } catch (err) { 100 | return next({ 101 | log: `userController.signup: ERROR: ${err}`, 102 | message: { err: 'Something went wrong! Whoops!' }, 103 | status: 500, 104 | }); 105 | } 106 | }; 107 | 108 | export default userController; 109 | -------------------------------------------------------------------------------- /server/demo/Demo.md: -------------------------------------------------------------------------------- 1 | # Setting up Kafka 2 | 3 | ## Download Kafka with Homebrew: 4 | 5 | `$ brew install kafka` 6 | 7 | ## Starting a Session: 8 | 9 | **Open zookeeper:** 10 | 11 | `$ bin/zookeeper-server-start.sh config/zookeeper.properties` 12 | 13 | **--M1 Chips:** 14 | bins and configs are stored in a different spot, so use this instead: 15 | `/opt/homebrew/bin/zookeeper-server-start /opt/homebrew/etc/zookeeper/zoo.cfg` 16 | 17 | **In separate terminal, open Kafka:** 18 | 19 | `$ bin/kafka-server-start.sh config/server.properties` 20 | 21 | **-- M1 Chips:** 22 | `/opt/homebrew/bin/kafka-server-start /opt/homebrew/etc/kafka/server.properties` 23 | 24 | ## How to create a topic: 25 | 26 | `kafka-topics --create --bootstrap-server localhost: --replication-factor <# of factor> --partitions <# of partitions. --topic ` 27 | 28 | **For the demo, please run this command to create a topic, "test-topic":** 29 | `kafka-topics --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic test-topic` 30 | 31 | **List created topics:** 32 | to confirm you created a topic correctly, please run: 33 | `kafka-topics --list --bootstrap-server localhost:9092` 34 | 35 | **Run Prometheus** 36 | `/opt/homebrew/bin/prometheus --config.file=../ClusterFunk/server/demo/prometheus.yml` 37 | -------------------------------------------------------------------------------- /server/demo/consumer.js: -------------------------------------------------------------------------------- 1 | import { kafka } from './utility.js'; 2 | 3 | /* 4 | /// Consumer \\\ 5 | 6 | Here we define a consumer and set it to subscribe to the same topic that the producer sends messages to. 7 | 8 | When operating the demo: 9 | - run node server/demo/producer first, then 10 | - run node server/demo/consumer 11 | - after a few moments, the console should log the messages sent by the producer to the test-topic 12 | - the consumer remains connected and subscribed 13 | - to stream more data: run node/server/producer.js 14 | - to close out, type control-c to send SIGINT 15 | */ 16 | 17 | const consumer = kafka.consumer({ groupId: 'Demo-App_Group-01' }); 18 | await consumer.connect(); 19 | 20 | /* 21 | Consumer groups fetch messages from the latest commited offset 22 | If the offset is invalid or not defined: 23 | - fromBeginning set to true, the group will use the earliest offset. 24 | - fromBeggining set to false (default) they use the latest offset. 25 | */ 26 | 27 | await consumer.subscribe({ topic: 'test-topic', fromBeginning: true }); 28 | 29 | await consumer.run({ 30 | eachMessage: async ({ topic, partition, message }) => { 31 | await console.log({ 32 | value: message.value.toString(), 33 | }); 34 | }, 35 | }); 36 | 37 | export default consumer; 38 | -------------------------------------------------------------------------------- /server/demo/producer.js: -------------------------------------------------------------------------------- 1 | import { kafka } from './utility.js'; 2 | 3 | /* 4 | /// Producer \\\ 5 | 6 | Here we define a producer and set up some basic functionality to imitate a data stream: 7 | - a random number generator 8 | - a setInterval that sends a random number every second 9 | - a setTimeout that clears that interval after 10 seconds 10 | 11 | When operating the demo: 12 | - run node server/demo/producer 13 | - this connects the producer, emits 10 messages, then disconnects 14 | - run the node command again to send more data, or 15 | */ 16 | 17 | // Define and connect a Kafka producer 18 | const producer = kafka.producer(); 19 | await producer.connect(); 20 | console.log('producer connected'); 21 | 22 | // Function that returns random number 23 | function randomNums() { 24 | return `${Math.floor(Math.random() * 1017)}`; 25 | } 26 | 27 | // Send method designates on which topic to send data (in this case, 'test-topic') 28 | // Message payload sets the invocation of randomNums as the value 29 | async function sendMessages() { 30 | await producer.send({ 31 | topic: 'test-topic', 32 | messages: [{ value: randomNums() }], 33 | }); 34 | } 35 | 36 | const dataStream = setInterval(async () => { 37 | await sendMessages(); 38 | }, 1000); 39 | 40 | setTimeout(async () => { 41 | clearInterval(dataStream); 42 | await producer.disconnect(); 43 | console.log('producer disconnected'); 44 | }, 10000); 45 | 46 | export default producer; 47 | -------------------------------------------------------------------------------- /server/demo/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 3 | 4 | # Attach these labels to any time series or alerts when communicating with 5 | # external systems (federation, remote storage, Alertmanager). 6 | external_labels: 7 | monitor: "codelab-monitor" 8 | 9 | # A scrape configuration containing exactly one endpoint to scrape: 10 | # Here it's Prometheus itself. 11 | scrape_configs: 12 | # The job name is added as a label `job=` to any timeseries scraped from this config. 13 | - job_name: "demo-app" 14 | 15 | # Override the global default and scrape targets from this job every 5 seconds. 16 | scrape_interval: 2s 17 | 18 | static_configs: 19 | - targets: ["localhost:8081"] 20 | 21 | ###### 22 | -------------------------------------------------------------------------------- /server/demo/utility.js: -------------------------------------------------------------------------------- 1 | import { Kafka } from 'kafkajs'; 2 | 3 | /* 4 | Instantiate a KafkaJS client and point it to our demo broker (see DEMO.md for setup instructions) 5 | KafkaJS allows us to manipulate Kafka with Javascript! 6 | We have the brokers location set to 'localhost:9092' by default 7 | - change this value if you set up your Kafka instance on a different port! 8 | */ 9 | const clientId = 'demo-app'; 10 | const kafka = new Kafka({ 11 | clientId, 12 | brokers: ['localhost:9092'], 13 | }); 14 | 15 | 16 | export { kafka }; 17 | -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | const Schema = mongoose.Schema; 3 | 4 | const userSchema = new Schema({ 5 | username: { type: String, required: true, unique: true }, 6 | password: { type: String, required: true }, 7 | cluster: { type: String }, 8 | }); 9 | 10 | export default mongoose.model('user', userSchema); -------------------------------------------------------------------------------- /server/routes/metricsRouter.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import metricsController from '../controllers/metricsController.js'; 3 | const router = express.Router(); 4 | 5 | router.get('/default', metricsController.getDefaultMetrics, (req, res) => { 6 | return res.status(200).json(res.locals.defaultMetrics); 7 | }); 8 | 9 | export { router as metricsRouter }; 10 | -------------------------------------------------------------------------------- /server/routes/userRouter.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import userController from '../controllers/userController.js'; 3 | import tokenController from '../controllers/tokenController.js'; 4 | const router = express.Router(); 5 | 6 | router.post('/signup', userController.signup, tokenController.createToken, (req, res) => { 7 | return res.status(201).json({ message: 'registration successful' }); 8 | }) 9 | 10 | router.post('/login', userController.login, tokenController.createToken, (req, res) => { 11 | return res.status(202).json({ message: 'login successul' }); 12 | }) 13 | 14 | router.get('/auth', tokenController.verifyToken, (req, res) => { 15 | return res.status(202).json({ username: res.locals.username}); 16 | }) 17 | 18 | router.get('/signout', tokenController.deleteToken, (req, res) => { 19 | return res.status(202).json({ message: 'token deleted' }) 20 | }) 21 | 22 | export {router as userRouter}; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import 'dotenv/config'; 4 | import { mongoose } from 'mongoose'; 5 | import { MongoClient } from "mongodb"; 6 | import cookieParser from 'cookie-parser'; 7 | import cors from 'cors'; 8 | import { userRouter } from './routes/userRouter.js'; 9 | import { metricsRouter } from './routes/metricsRouter.js'; 10 | import { fileURLToPath } from 'url'; 11 | import { dirname, join } from 'path'; 12 | 13 | const app = express(); 14 | const PORT = process.env.PORT || 3030; 15 | const __filename = fileURLToPath(import.meta.url); 16 | const __dirname = dirname(__filename); 17 | const uri = process.env.MONGO_URI; 18 | 19 | console.log(process.env.MONGO_URI); 20 | 21 | app.use(express.json()); 22 | app.use(cookieParser()); 23 | app.use(cors()); 24 | app.use(express.urlencoded({ extended: true })); 25 | app.use(express.static(join(__dirname, '../dist'))); 26 | 27 | // Connect to database 28 | // const client = new MongoClient(uri); 29 | 30 | // async function run() { 31 | // try { 32 | // // Connect the client to the server (optional starting in v4.7) 33 | // await client.connect(); 34 | // // Send a ping to confirm a successful connection 35 | // await client.db("admin").command({ ping: 1 }); 36 | // console.log("Pinged your deployment. You successfully connected to MongoDB!"); 37 | // } finally { 38 | // // Ensures that the client will close when you finish/error 39 | // await client.close(); 40 | // } 41 | // } 42 | 43 | // run().catch(console.dir); 44 | 45 | mongoose 46 | .connect(process.env.MONGO_URI) 47 | .then(() => console.log(`Connected to Mongo DB using ${process.env.MONGO_URI}`)) 48 | .catch((err) => console.log(err)); 49 | 50 | // Set up routers 51 | app.use('/user', userRouter); 52 | app.use('/metrics', metricsRouter); 53 | 54 | // if (process.env.NODE_ENV === 'production') { 55 | // } 56 | 57 | app.use('/', (req, res, next) => { 58 | return res.sendFile(join(__dirname, '../dist/index.html')); 59 | }); 60 | 61 | // Unknown route handler 62 | app.get('/*', (req, res) => { 63 | return res.status(404).send('Page not found'); 64 | }); 65 | 66 | // Global error handler 67 | app.use((err, req, res, next) => { 68 | const defaultErr = { 69 | log: 'Express error handler caught unknown middleware error', 70 | status: 500, 71 | message: { err: 'An error occurred' }, 72 | }; 73 | const errorObj = Object.assign({}, defaultErr, err); 74 | console.log(errorObj.log); 75 | return res.status(errorObj.status).json(errorObj.message); 76 | }); 77 | 78 | app.listen(PORT, () => { 79 | console.log(`Server listening on port: ${PORT}...`); 80 | }); 81 | 82 | export default app; 83 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | #root { 6 | max-width: 1280px; 7 | margin: 0 auto; 8 | padding: 2rem; 9 | text-align: center; 10 | } 11 | 12 | .logo { 13 | height: 6em; 14 | padding: 1.5em; 15 | will-change: filter; 16 | transition: filter 300ms; 17 | } 18 | .logo:hover { 19 | filter: drop-shadow(0 0 2em #646cffaa); 20 | } 21 | .logo.react:hover { 22 | filter: drop-shadow(0 0 2em #61dafbaa); 23 | } 24 | 25 | @keyframes logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | 34 | @media (prefers-reduced-motion: no-preference) { 35 | a:nth-of-type(2) .logo { 36 | animation: logo-spin infinite 20s linear; 37 | } 38 | } 39 | 40 | .card { 41 | padding: 2em; 42 | } 43 | 44 | .read-the-docs { 45 | color: #888; 46 | } 47 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | import reactLogo from './assets/react.svg'; 4 | import viteLogo from '/vite.svg'; 5 | import './App.css'; 6 | import Homepage from './containers/Homepage.jsx'; 7 | import Login from './containers/login.jsx'; 8 | import Dashboard from './containers/Dashboard.jsx'; 9 | 10 | 11 | function App() { 12 | return ( 13 | 14 | } /> 15 | } /> 16 | } /> 17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/Hooks/useAuthenticate.jsx: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect } from "react"; 2 | import { useNavigate, useLocation } from "react-router"; 3 | 4 | const useAuthenticate = (setUser) => { 5 | const navigate = useNavigate(); 6 | const location = useLocation(); 7 | 8 | const verifyToken = async () => { 9 | const verifyEndpoint = '/user/auth'; 10 | try { 11 | const res = await fetch(verifyEndpoint); 12 | if (res.status === 401 && location.pathname === '/dashboard') { 13 | return navigate('/login'); 14 | } else if (res.status === 401 && location.pathname === '/') { 15 | return; 16 | } 17 | if (!res.ok) { 18 | throw Error('failed to authenticate user'); 19 | } 20 | const { username } = await res.json(); 21 | setUser(username) 22 | } catch (error) { 23 | console.error(error.message); 24 | } 25 | } 26 | 27 | useLayoutEffect(() => { verifyToken() }, []); 28 | 29 | const signout = async () => { 30 | try { 31 | const res = await fetch('/user/signout') 32 | if (res.status === 202) { 33 | verifyToken(); 34 | setUser(''); 35 | return; 36 | } 37 | if (!res.ok) throw Error('failed to signout'); 38 | } catch (error) { 39 | console.error(error.message); 40 | } 41 | } 42 | 43 | return [ signout ]; 44 | } 45 | 46 | export default useAuthenticate; 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/Hooks/useDarkMode.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | const useDarkMode = () => { 4 | const [ darkMode, setDarkMode ] = useState(false); 5 | 6 | useEffect(() => { 7 | const preferDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 8 | setDarkMode(preferDark) 9 | }, []) 10 | 11 | const toggleDarkMode = () => { 12 | setDarkMode(!darkMode) 13 | localStorage.setItem('darkMode', JSON.stringify(!darkMode)) 14 | } 15 | 16 | return [darkMode, toggleDarkMode]; 17 | } 18 | 19 | export default useDarkMode; -------------------------------------------------------------------------------- /src/Hooks/useMetricStore.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useMetricStore = (promAddress) => { 4 | const initialState = { 5 | bytesIn: [ 6 | ['-', '-'], 7 | ['-', '-'], 8 | ['-', '-'], 9 | ['-', '-'], 10 | ['-', '-'], 11 | ['-', '-'], 12 | ['-', '-'], 13 | ['-', '-'], 14 | ['-', '-'], 15 | ['-', '-'], 16 | ['-', '-'], 17 | ['-', '-'], 18 | ], 19 | bytesOut: [ 20 | ['-', '-'], 21 | ['-', '-'], 22 | ['-', '-'], 23 | ['-', '-'], 24 | ['-', '-'], 25 | ['-', '-'], 26 | ['-', '-'], 27 | ['-', '-'], 28 | ['-', '-'], 29 | ['-', '-'], 30 | ['-', '-'], 31 | ['-', '-'], 32 | ], 33 | cpuUsage: [ 34 | ['-', '-'], 35 | ['-', '-'], 36 | ['-', '-'], 37 | ['-', '-'], 38 | ['-', '-'], 39 | ['-', '-'], 40 | ['-', '-'], 41 | ['-', '-'], 42 | ['-', '-'], 43 | ['-', '-'], 44 | ['-', '-'], 45 | ['-', '-'], 46 | ], 47 | brokerCount: [], 48 | }; 49 | 50 | const [metricStore, setMetricStore] = useState(initialState); 51 | 52 | useEffect(() => { 53 | const updateMetrics = async () => { 54 | const endPoint = `/metrics/default?promAddress=${promAddress}`; 55 | 56 | try { 57 | const res = await fetch(endPoint); 58 | 59 | if (!res.ok) { 60 | throw Error(`Failed to fetch metrics with promAddress ${promAddress}`) 61 | } 62 | 63 | const metrics = await res.json(); 64 | 65 | setMetricStore((prevStore) => { 66 | const newStore = structuredClone(prevStore); 67 | 68 | newStore.bytesIn = newStore.bytesIn.slice(1); 69 | newStore.bytesIn.push(metrics.bytesIn); 70 | newStore.bytesOut = newStore.bytesOut.slice(1); 71 | newStore.bytesOut.push(metrics.bytesOut); 72 | newStore.cpuUsage = newStore.cpuUsage.slice(1); 73 | newStore.cpuUsage.push(metrics.cpuUsage); 74 | newStore.brokerCount = metrics.brokerCount 75 | 76 | return newStore; 77 | }) 78 | } catch (error) { 79 | console.error(error.message); 80 | } 81 | } 82 | 83 | const interval = setInterval(updateMetrics, 2000); 84 | 85 | return () => clearInterval(interval); 86 | }, [promAddress, metricStore]) 87 | 88 | const resetMetricStore = () => { 89 | setMetricStore(initialState) 90 | } 91 | 92 | return [metricStore, resetMetricStore]; 93 | } 94 | 95 | export default useMetricStore; -------------------------------------------------------------------------------- /src/Hooks/useScroll.jsx: -------------------------------------------------------------------------------- 1 | const useScroll = () => { 2 | 3 | const scroll = (ref) => { 4 | if (ref && ref.current) { 5 | ref.current.scrollIntoView({behavior: 'smooth'}) 6 | } 7 | } 8 | 9 | return scroll; 10 | } 11 | 12 | export default useScroll; -------------------------------------------------------------------------------- /src/assets/LinkedIn-Logos/LI-In-Bug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/LinkedIn-Logos/LI-In-Bug.png -------------------------------------------------------------------------------- /src/assets/LinkedIn-Logos/LI-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/LinkedIn-Logos/LI-Logo.png -------------------------------------------------------------------------------- /src/assets/connorpic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/connorpic.png -------------------------------------------------------------------------------- /src/assets/davidpic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/davidpic.png -------------------------------------------------------------------------------- /src/assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/demo.gif -------------------------------------------------------------------------------- /src/assets/dominicpic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/dominicpic.png -------------------------------------------------------------------------------- /src/assets/github-mark/GitHub_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/github-mark/GitHub_Logo.png -------------------------------------------------------------------------------- /src/assets/github-mark/github-mark-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/github-mark/github-mark-white.png -------------------------------------------------------------------------------- /src/assets/github-mark/github-mark-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/github-mark/github-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/github-mark/github-mark.png -------------------------------------------------------------------------------- /src/assets/github-mark/github-mark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/grape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/grape.png -------------------------------------------------------------------------------- /src/assets/klusterfunklogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/klusterfunklogo.png -------------------------------------------------------------------------------- /src/assets/klusterfunklogo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/klusterfunklogo2.png -------------------------------------------------------------------------------- /src/assets/medium-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/medium-logo.png -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/wilsonpic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterFunk/77a22d49c8b92cf1a006d7d9bb9dc943a299df15/src/assets/wilsonpic.png -------------------------------------------------------------------------------- /src/component/LineGraph.jsx: -------------------------------------------------------------------------------- 1 | import { Line } from 'react-chartjs-2'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | PointElement, 7 | LineElement, 8 | Title, 9 | Tooltip, 10 | Legend, 11 | Colors, 12 | } from 'chart.js'; 13 | 14 | ChartJS.register( 15 | CategoryScale, 16 | LinearScale, 17 | PointElement, 18 | LineElement, 19 | Title, 20 | Tooltip, 21 | Legend, 22 | Colors 23 | ); 24 | 25 | const LineGraph = ({ 26 | graphTitle, 27 | metricStore, 28 | timeLabels, 29 | dataLabel, 30 | }) => { 31 | const labels = []; 32 | for (let i = 0; i < metricStore.length; i++) { 33 | if (metricStore[i][0] === '-') continue; 34 | else { 35 | let time = new Date(metricStore[i][0] * 1000); 36 | time = time.toTimeString().slice(0, 8); 37 | labels.push(time); 38 | } 39 | // console.log('label time: ', metric[i][0]); 40 | } 41 | // console.log(labels); 42 | 43 | const dataArr = []; 44 | for (let i = 0; i < metricStore.length; i++) { 45 | if (metricStore[i][0] === '-') continue; 46 | dataArr.push(metricStore[i][1]); 47 | } 48 | // console.log(dataArr); 49 | // ChartJS.defaults.backgroundColor = '#9BD0F5'; 50 | // ChartJS.defaults.borderColor = '#36A2EB'; 51 | // ChartJS.defaults.color = '#000'; 52 | 53 | const options = { 54 | responsive: true, 55 | plugins: { 56 | legend: { position: 'top' }, 57 | title: { 58 | display: true, 59 | text: graphTitle, 60 | }, 61 | }, 62 | }; 63 | 64 | const data = { 65 | labels, 66 | datasets: [ 67 | { 68 | label: dataLabel, 69 | data: dataArr, 70 | borderColor: '#520049', 71 | backgroundColor: '#520049', 72 | }, 73 | ], 74 | }; 75 | 76 | return ; 77 | } 78 | 79 | export default LineGraph; 80 | -------------------------------------------------------------------------------- /src/component/Links.jsx: -------------------------------------------------------------------------------- 1 | const Links = (props) => { 2 | const { logo, text, link } = props; 3 | return ( 4 |
5 | {text} 6 | 7 | 8 | 9 |
10 | ); 11 | }; 12 | 13 | export default Links; 14 | -------------------------------------------------------------------------------- /src/component/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { useLocation, useNavigate } from 'react-router-dom'; 2 | import { Avatar, Dropdown, Navbar, Button } from 'flowbite-react'; 3 | import useScroll from '../Hooks/useScroll.jsx'; 4 | import useDarkMode from '../Hooks/useDarkMode.jsx'; 5 | import grape from '@/assets/grape.png'; 6 | 7 | const Nav = ({ promAddress, user, signout, refs, reset }) => { 8 | const navigate = useNavigate(); 9 | const location = useLocation(); 10 | const scroll = useScroll(); 11 | const [darkMode, toggleDarkMode] = useDarkMode(); 12 | 13 | return ( 14 | 19 | 20 | 21 | 22 | {user && ( 23 |
24 | } 28 | > 29 | 30 | Hello, {user}! 31 | 32 | navigate('/dashboard')}> 33 | Dashboard 34 | 35 | toggleDarkMode()}> 36 | {darkMode ? 'Light Mode' : 'Dark Mode'} 37 | 38 | 39 | signout()}>Sign out 40 | 41 | 42 |
43 | )} 44 | 45 | {location.pathname === '/' && ( 46 | <> 47 | { 51 | scroll(refs.featuresRef); 52 | }} 53 | > 54 | About 55 | 56 | { 60 | scroll(refs.aboutMeRef); 61 | }} 62 | > 63 | Contact 64 | 65 | 66 | )} 67 | {!user && ( 68 | 75 | )} 76 | {location.pathname === '/dashboard' && ( 77 |
78 | Prometheus: {promAddress} 79 |
80 | )} 81 |
82 |
83 | ); 84 | }; 85 | 86 | export default Nav; 87 | -------------------------------------------------------------------------------- /src/component/PromAddress.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const PromAddress = ({ setPromAddress }) => { 4 | const [cluster, setCluster] = useState('') 5 | 6 | useEffect(() => { 7 | if (sessionStorage.getItem('prometheus')) { 8 | setPromAddress(sessionStorage.getItem('prometheus')) 9 | } 10 | }, []) 11 | 12 | return ( 13 |
14 | 17 | { setCluster(e.target.value) }} 23 | className='border border-gray-300 rounded-lg px-4 py-2 mb-4 w-full max-w-sm' 24 | /> 25 | 36 |
37 | ); 38 | } 39 | 40 | export default PromAddress; -------------------------------------------------------------------------------- /src/component/ReadMe.jsx: -------------------------------------------------------------------------------- 1 | import demo from '@/assets/demo.gif'; 2 | 3 | const ReadMe = () => { 4 | return ( 5 |
6 |

7 | KlusterFunk is a monitoring tool, built to visualize metrics from local 8 | kafka clusters, showing you a real-time, live-updating graph of those 9 | metrics. 10 |

11 |
12 |
13 | Features: 14 |
    15 |
  • Sign in/sign up for individual user accounts
  • 16 |
  • Visualizes Metrics pulled from user’s Prometheus
  • 17 |
  • Interactive, live-updating graphs of data points
  • 18 |
  • Reset button to refresh graphs
  • 19 |
  • Easy to use interface and simple display
  • 20 |
  • Funky, groovy color scheme
  • 21 |
22 | 23 |
24 |
25 |
26 | ); 27 | }; 28 | 29 | export default ReadMe; 30 | -------------------------------------------------------------------------------- /src/component/TeamInfo.jsx: -------------------------------------------------------------------------------- 1 | const TeamInfo = (props) => { 2 | const { member, pic, githubLink, linkedinLink, githubIcon, linkedinIcon } = 3 | props; 4 | 5 | return ( 6 |
7 |

{member}

8 | 9 | 17 |
18 | ); 19 | }; 20 | 21 | export default TeamInfo; 22 | -------------------------------------------------------------------------------- /src/containers/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import LineGraph from '../component/LineGraph.jsx'; 3 | import PromAddress from '../component/PromAddress.jsx'; 4 | import Nav from '../component/Navbar.jsx'; 5 | import useMetricStore from '../Hooks/useMetricStore.jsx'; 6 | import useAuthenticate from '../Hooks/useAuthenticate.jsx'; 7 | 8 | 9 | const Dashboard = () => { 10 | const [ user, setUser ] = useState(''); 11 | 12 | // created custom hook to modularize user authentication actions such as verifying user session and signout 13 | const [ signout ] = useAuthenticate(setUser); 14 | const [ promAddress, setPromAddress ] = useState(null); 15 | 16 | // created custom hook to modularize the current state of graphs so that the dashboard looks cleaner and metric state hook can be reuseable 17 | const [ metricStore, resetMetricStore ] = useMetricStore(promAddress); 18 | 19 | // pushing graph components into an array so it is easy to add more metrics 20 | const graphArray = []; 21 | for (let i in metricStore) { 22 | if (i === 'brokerCount') continue; 23 | graphArray.push( 24 | 30 | ); 31 | } 32 | 33 | return ( 34 |
35 |
53 | ); 54 | } 55 | 56 | export default Dashboard; 57 | -------------------------------------------------------------------------------- /src/containers/Homepage.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import useAuthenticate from '../Hooks/useAuthenticate.jsx'; 3 | import Nav from '../component/Navbar.jsx'; 4 | import TeamInfo from '../component/TeamInfo.jsx'; 5 | import ReadMe from '../component/ReadMe.jsx'; 6 | import Links from '../component/Links.jsx'; 7 | import davidPic from '@/assets/davidpic.png'; 8 | import connorPic from '@/assets/connorpic.png'; 9 | import wilsonPic from '@/assets/wilsonpic.png'; 10 | import dominicPic from '@/assets/dominicpic.png'; 11 | import githubIcon from '@/assets/github-mark/github-mark.png'; 12 | import linkedinIcon from '@/assets/LinkedIn-Logos/LI-In-Bug.png'; 13 | import linkedinLogo from '@/assets/LinkedIn-Logos/LI-Logo.png'; 14 | import githubLogo from '@/assets/github-mark/GitHub_Logo.png'; 15 | import mediumLogo from '@/assets/medium-logo.png'; 16 | import logo from '@/assets/klusterfunklogo2.png'; 17 | 18 | const Homepage = () => { 19 | const [user, setUser] = useState(''); 20 | const [signout] = useAuthenticate(setUser); 21 | const featuresRef = useRef(null); 22 | const aboutMeRef = useRef(null); 23 | 24 | // put both features section and about-me section into an object to easily pass it down to Navbar component as a prop 25 | const refs = { 26 | featuresRef: featuresRef, 27 | aboutMeRef: aboutMeRef, 28 | }; 29 | 30 | const memberInfo = { 31 | 'David Tezza': [ 32 | davidPic, 33 | 'https://github.com/dtezz', 34 | 'https://www.linkedin.com/in/david-tezza/', 35 | ], 36 | 'Wilson Wu': [ 37 | wilsonPic, 38 | 'https://github.com/jwu8475', 39 | 'www.linkedin.com/in/wilsonwuu8', 40 | ], 41 | 'Connor Donahue': [ 42 | connorPic, 43 | 'https://github.com/conniedonahue', 44 | 'https://www.linkedin.com/in/connordonahue09/', 45 | ], 46 | 'Dominic Kenny': [ 47 | dominicPic, 48 | 'https://github.com/dominicjkenny', 49 | 'https://www.linkedin.com/in/dominicjkenny/', 50 | ], 51 | }; 52 | const teamInfo = []; 53 | for (const member in memberInfo) { 54 | teamInfo.push( 55 | 64 | ); 65 | } 66 | const linkInfo = { 67 | github: [ 68 | 'For instructions on how to get started with KlusterFunk, visit our Github repo!', 69 | 'https://github.com/oslabs-beta/KlusterFunk', 70 | githubLogo, 71 | ], 72 | medium: [ 73 | 'To read more about our Product, head over to our Medium article!', 74 | 'https://medium.com/@connordonahue09/klusterfunk-564ec1c78e36', 75 | mediumLogo, 76 | ], 77 | linkedin: [ 78 | 'Connect with us on LinkedIn!', 79 | 'https://www.linkedin.com', 80 | linkedinLogo, 81 | ], 82 | }; 83 | const links = []; 84 | for (const link in linkInfo) { 85 | links.push( 86 | 92 | ); 93 | } 94 | 95 | return ( 96 |
97 |
134 | ); 135 | }; 136 | 137 | export default Homepage; 138 | -------------------------------------------------------------------------------- /src/containers/login.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | import { useState, useEffect } from 'react'; 3 | import { TextInput } from 'flowbite-react'; 4 | import logo from '@/assets/klusterfunklogo2.png'; 5 | 6 | const Login = () => { 7 | const [ auth, setAuth ] = useState('login'); 8 | const [ username, setUsername ] = useState(''); 9 | const [ password, setPassword ] = useState(''); 10 | const [ confirmPassword, setConfirmPassword ] = useState(''); 11 | const [ infoMatch, setInfoMatch] = useState(''); 12 | const navigate = useNavigate(); 13 | 14 | useEffect(() => { 15 | confirmPassword.length > 0 && password !== confirmPassword 16 | ? setInfoMatch('passwords do not match') 17 | : setInfoMatch(''); 18 | }, [confirmPassword]); 19 | 20 | // login or signup requested 21 | const handleSubmit = async (e) => { 22 | e.preventDefault(); 23 | const loginEndpoint = `/user/${auth}`; 24 | 25 | if (password === confirmPassword || auth === 'login') { 26 | const res = await fetch(loginEndpoint, { 27 | method: 'POST', 28 | body: JSON.stringify({ 29 | username: username, 30 | password: password, 31 | }), 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | }); 36 | if (!res.ok) { 37 | throw new Error('failed to fetch at auth'); 38 | } 39 | if (res.status === 201 || res.status === 202) { 40 | navigate('/dashboard'); 41 | } 42 | } 43 | }; 44 | 45 | return ( 46 |
47 | 48 |
49 |
{ 53 | switch (e.target.name) { 54 | case 'username': 55 | setUsername(e.target.value); 56 | case 'password': 57 | setPassword(e.target.value); 58 | case 'confirmPassword': 59 | setConfirmPassword(e.target.value); 60 | } 61 | }} 62 | > 63 | 70 |
71 | 77 |
78 | {auth === 'signup' && ( 79 | <> 80 | 86 |
87 |
88 | {infoMatch} 89 |
90 |
91 | 92 | )} 93 | {auth === 'login' && ( 94 | 101 | )} 102 | 112 | 113 | {auth === 'signup' && ( 114 | setAuth('login')}> 115 | Or sign in 116 |
117 |
118 | )} 119 |
120 |
121 | ); 122 | }; 123 | 124 | export default Login; 125 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 7 | line-height: 1.5; 8 | font-weight: 400; 9 | 10 | color-scheme: light dark; 11 | color: rgba(255, 255, 255, 0.87); 12 | background-color: #242424; 13 | 14 | font-synthesis: none; 15 | text-rendering: optimizeLegibility; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | -webkit-text-size-adjust: 100%; 19 | } 20 | 21 | a { 22 | font-weight: 500; 23 | color: #646cff; 24 | text-decoration: inherit; 25 | } 26 | a:hover { 27 | color: #535bf2; 28 | } 29 | 30 | body { 31 | margin: 0; 32 | display: flex; 33 | place-items: center; 34 | min-width: 320px; 35 | min-height: 100vh; 36 | background-color: #4a044e; 37 | } 38 | 39 | h1 { 40 | font-size: 3.2em; 41 | line-height: 1.1; 42 | } 43 | 44 | button { 45 | border-radius: 8px; 46 | border: 1px solid transparent; 47 | padding: 0.6em 1.2em; 48 | font-size: 1em; 49 | font-weight: 500; 50 | font-family: inherit; 51 | background-color: #1a1a1a; 52 | cursor: pointer; 53 | transition: border-color 0.25s; 54 | } 55 | button:hover { 56 | border-color: #646cff; 57 | } 58 | button:focus, 59 | button:focus-visible { 60 | outline: 4px auto -webkit-focus-ring-color; 61 | } 62 | 63 | @media (prefers-color-scheme: light) { 64 | :root { 65 | color: #213547; 66 | background-color: #ffffff; 67 | } 68 | a:hover { 69 | color: #747bff; 70 | } 71 | button { 72 | background-color: #f9f9f9; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { BrowserRouter } from 'react-router-dom' 4 | import App from './App.jsx' 5 | import './index.css' 6 | 7 | ReactDOM.createRoot(document.getElementById('root')).render( 8 | 9 | 10 | 11 | 12 | , 13 | ) 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | "node_modules/flowbite-react/**/*.{js,jsx,ts,tsx}", 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [require('flowbite/plugin')], 12 | } 13 | 14 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import path from 'path'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | server: { 9 | proxy: { 10 | '/user': { 11 | target: 'http://localhost:3030', 12 | changeOrigin: true, 13 | secure: false, 14 | }, 15 | '/metrics': { 16 | target: 'http://localhost:3030', 17 | changeOrigin: true, 18 | secure: false, 19 | }, 20 | }, 21 | port: 8080, 22 | }, 23 | test: { 24 | globals: true, 25 | environment: 'jsdom', 26 | setupFiles: './__tests__/setup.js', 27 | }, 28 | resolve: { 29 | alias: { 30 | '@': path.resolve(__dirname, './src'), 31 | }, 32 | }, 33 | }); 34 | --------------------------------------------------------------------------------