├── client ├── src │ ├── __mocks__ │ │ ├── styleMock.js │ │ └── fileMock.js │ ├── assets │ │ ├── logo.png │ │ ├── headshots │ │ │ ├── ian-headshot.jpg │ │ │ ├── jeb-headshot.jpg │ │ │ ├── annie-headshot.jpg │ │ │ ├── hazel-headshot.jpg │ │ │ └── krystal-headshot.jpg │ │ ├── readme-icons │ │ │ ├── github-logo.png │ │ │ └── linkedIn-logo.png │ │ ├── animation.js │ │ └── testData.js │ ├── pages │ │ ├── RootPage │ │ │ ├── components │ │ │ │ ├── Header.jsx │ │ │ │ ├── Button.jsx │ │ │ │ ├── Link.jsx │ │ │ │ ├── Footer.jsx │ │ │ │ └── TextField.jsx │ │ │ └── RootPage.jsx │ │ ├── dashboardPage │ │ │ ├── components │ │ │ │ ├── Alert.jsx │ │ │ │ ├── Metric.jsx │ │ │ │ ├── UserMenu.jsx │ │ │ │ ├── BrokerIdForm.jsx │ │ │ │ ├── Broker.jsx │ │ │ │ ├── URPChart.jsx │ │ │ │ ├── BytesOutChart.jsx │ │ │ │ ├── BytesInChart.jsx │ │ │ │ └── Charts.jsx │ │ │ ├── containers │ │ │ │ ├── AlertsContainer.jsx │ │ │ │ ├── DashNav.jsx │ │ │ │ └── BrokersContainer.jsx │ │ │ └── DashboardPage.jsx │ │ ├── landingPage │ │ │ ├── LandingPage.jsx │ │ │ └── components │ │ │ │ ├── SingleFeature.jsx │ │ │ │ ├── Hero.jsx │ │ │ │ ├── Player.jsx │ │ │ │ ├── Features.jsx │ │ │ │ └── Team.jsx │ │ └── loginPage │ │ │ └── LoginPage.jsx │ ├── index.js │ ├── index.html │ ├── __tests__ │ │ ├── axeTest.js │ │ ├── PostgresTests.js │ │ ├── CookieControllerTests.js │ │ ├── AuthControllerTests.js │ │ ├── ServerTests.js │ │ ├── BrokerMetrics.test.js │ │ └── BrokersContainer.test.js │ ├── styles │ │ ├── variables.css │ │ └── styles.css │ └── App.jsx ├── jest-teardown.js ├── jest-setup.js ├── babel-plugin-macros.config.js ├── babel.config.js ├── postcss.config.js ├── webpack.config.js └── package.json ├── server ├── controllers │ ├── cookieController.js │ └── authController.js ├── models │ ├── cluster.js │ ├── index.js │ └── user.js ├── package.json ├── index.js └── package-lock.json ├── package.json ├── LICENSE ├── .gitignore └── README.md /client/src/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /client/src/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /client/jest-teardown.js: -------------------------------------------------------------------------------- 1 | module.exports = (globalConfig) => { 2 | testServer.close(); 3 | }; -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kafkalerts/HEAD/client/src/assets/logo.png -------------------------------------------------------------------------------- /client/jest-setup.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | global.testServer = require('./src/server/server.js'); 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /client/babel-plugin-macros.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'fontawesome-svg-core': { 3 | license: 'free', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | plugins: ['macros'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/assets/headshots/ian-headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kafkalerts/HEAD/client/src/assets/headshots/ian-headshot.jpg -------------------------------------------------------------------------------- /client/src/assets/headshots/jeb-headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kafkalerts/HEAD/client/src/assets/headshots/jeb-headshot.jpg -------------------------------------------------------------------------------- /client/src/assets/headshots/annie-headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kafkalerts/HEAD/client/src/assets/headshots/annie-headshot.jpg -------------------------------------------------------------------------------- /client/src/assets/headshots/hazel-headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kafkalerts/HEAD/client/src/assets/headshots/hazel-headshot.jpg -------------------------------------------------------------------------------- /client/src/assets/readme-icons/github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kafkalerts/HEAD/client/src/assets/readme-icons/github-logo.png -------------------------------------------------------------------------------- /client/src/assets/headshots/krystal-headshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kafkalerts/HEAD/client/src/assets/headshots/krystal-headshot.jpg -------------------------------------------------------------------------------- /client/src/assets/readme-icons/linkedIn-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kafkalerts/HEAD/client/src/assets/readme-icons/linkedIn-logo.png -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require('postcss-preset-env'); 2 | const postcssImport = require('postcss-import'); 3 | 4 | module.exports = { 5 | plugins: [postcssImport(), postcssPresetEnv({ stage: 1 })], 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/pages/RootPage/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import logo from '../../../assets/logo.png'; 2 | 3 | const Header = () => { 4 | return ( 5 |
6 |
7 | 8 |
9 |

kafkAlerts

10 |
11 | ); 12 | }; 13 | 14 | export default Header; 15 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { StrictMode } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { createRoot } from 'react-dom/client'; 4 | 5 | import App from './App.jsx'; 6 | const root = createRoot(document.getElementById('root')); 7 | 8 | root.render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /client/src/pages/RootPage/RootPage.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | import Header from './components/Header'; 3 | import Footer from './components/Footer'; 4 | 5 | const RootPage = () => { 6 | return ( 7 |
8 |
9 | 10 |
12 | ); 13 | }; 14 | 15 | export default RootPage; 16 | -------------------------------------------------------------------------------- /client/src/pages/RootPage/components/Button.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useButton } from 'react-aria'; 3 | 4 | export default function Button(props) { 5 | let ref = useRef(); 6 | let { buttonProps } = useButton(props, ref); 7 | let { children } = props; 8 | 9 | return ( 10 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/components/Alert.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-scroll'; 2 | 3 | const Alert = ({ broker }) => { 4 | return ( 5 | 13 | {broker[0]} 14 | 15 | ); 16 | }; 17 | 18 | export default Alert; 19 | -------------------------------------------------------------------------------- /client/src/pages/landingPage/LandingPage.jsx: -------------------------------------------------------------------------------- 1 | import Hero from './components/Hero'; 2 | import Features from './components/Features'; 3 | import Team from './components/Team'; 4 | const LandingPage = () => { 5 | return ( 6 |
7 |
8 | 9 | 10 | 11 |
12 |
13 | ); 14 | }; 15 | 16 | export default LandingPage; 17 | -------------------------------------------------------------------------------- /server/controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | const cookieController = {}; 2 | 3 | cookieController.setCookie = (req, res, next) => { 4 | // console.log('verified', res.locals.isVerified); 5 | // if (res.locals.isVerified) { 6 | // console.log('cookieID', res.locals.cookieID); 7 | // res.cookie('cookieID', res.locals.cookieID); 8 | // } 9 | return next(); 10 | }; 11 | export default cookieController; 12 | // module.exports = cookieController; 13 | -------------------------------------------------------------------------------- /client/src/pages/landingPage/components/SingleFeature.jsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | 3 | const SingleFeature = ({ feature }) => { 4 | const { icon, header, text } = feature; 5 | return ( 6 |
7 | 8 | {header} 9 |

{text}

10 |
11 | ); 12 | }; 13 | 14 | export default SingleFeature; 15 | -------------------------------------------------------------------------------- /client/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | kafkAlerts 8 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /client/src/__tests__/axeTest.js: -------------------------------------------------------------------------------- 1 | const React = require('react') 2 | const App = require('../App.jsx').default 3 | 4 | const { render } = require('@testing-library/react') 5 | const { axe, toHaveNoViolations } = require('jest-axe') 6 | expect.extend(toHaveNoViolations) 7 | 8 | it('should demonstrate this matcher`s usage with react testing library', async () => { 9 | const { container } = render() 10 | const results = await axe(container) 11 | 12 | expect(results).toHaveNoViolations() 13 | }) -------------------------------------------------------------------------------- /client/src/pages/landingPage/components/Hero.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router-dom'; 2 | 3 | const Hero = () => { 4 | const navigate = useNavigate(); 5 | 6 | return ( 7 |
8 | kafkAlerts 9 |

Driven by usability.

10 |

Broker metric monitoring and alerting for your Kafka cluster

11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Hero; 17 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/containers/AlertsContainer.jsx: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import Alert from '../components/Alert'; 3 | 4 | const AlertsContainer = ({ brokers }) => { 5 | const alertingBrokers = brokers.map((broker, index) => 6 | broker.alerts.length ? ( 7 | 8 | ) : null 9 | ); 10 | 11 | return ( 12 |
13 |

Alerting Brokers:

14 | {alertingBrokers} 15 |
16 | ); 17 | }; 18 | 19 | export default AlertsContainer; 20 | -------------------------------------------------------------------------------- /client/src/__tests__/PostgresTests.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const server = 'http://localhost:3000'; 3 | 4 | describe('Bcrypting passwords', () => { 5 | 6 | it('Cookie should be created', () => { 7 | request(server) 8 | .post('/signup') 9 | .send({"username": "test3", "password" : "password3"}) 10 | .type('form') 11 | // .end((err, res) => { 12 | // cookieController.setCookie({}, (err, user) => { 13 | // expect(user.cookieID).to.eql("test3"); 14 | // }); 15 | // }); 16 | }); 17 | 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /client/src/__tests__/CookieControllerTests.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const server = 'http://localhost:3000'; 3 | 4 | describe('Bcrypting passwords', () => { 5 | 6 | it('Cookie should be created', () => { 7 | request(server) 8 | .post('/signup') 9 | .send({"username": "test3", "password" : "password3"}) 10 | .type('form') 11 | // .end((err, res) => { 12 | // cookieController.setCookie({}, (err, user) => { 13 | // expect(user.cookieID).to.eql("test3"); 14 | // }); 15 | // }); 16 | }); 17 | 18 | }) 19 | 20 | -------------------------------------------------------------------------------- /server/models/cluster.js: -------------------------------------------------------------------------------- 1 | const getClusterModel = (sequelize, { DataTypes }) => { 2 | const Cluster = sequelize.define('cluster', { 3 | connection_string: { 4 | type: DataTypes.STRING, 5 | defaultValue: '', 6 | allowNull: false, 7 | validate: { 8 | notEmpty: true, 9 | }, 10 | }, 11 | broker_ids: { 12 | type: DataTypes.ARRAY(DataTypes.STRING), 13 | defaultValue: [], 14 | }, 15 | }); 16 | Cluster.associate = (models) => { 17 | Cluster.belongsTo(models.User); 18 | }; 19 | 20 | return Cluster; 21 | }; 22 | 23 | export default getClusterModel; 24 | -------------------------------------------------------------------------------- /client/src/pages/RootPage/components/Link.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useLink } from 'react-aria'; 3 | 4 | const Link = (props) => { 5 | let ref = useRef(null); 6 | let { linkProps, isPressed } = useLink( 7 | { ...props, elementType: 'span' }, 8 | ref 9 | ); 10 | 11 | return ( 12 | 21 | {props.children} 22 | 23 | ); 24 | }; 25 | export default Link; 26 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/components/Metric.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import URPChart from './URPChart'; 3 | import BytesInChart from './BytesInChart'; 4 | import BytesOutChart from './BytesOutChart'; 5 | 6 | const Metric = ({ name, result }) => { 7 | let chart = []; 8 | chart = 9 | name === 'Bytes In' 10 | ? BytesInChart(result) 11 | : name === 'Bytes Out' 12 | ? BytesOutChart(result) 13 | : URPChart(result); 14 | 15 | return ( 16 |
17 |

{name}

18 |
{chart}
19 |
20 | ); 21 | }; 22 | 23 | export default Metric; 24 | -------------------------------------------------------------------------------- /client/src/pages/landingPage/components/Player.jsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | 3 | const Player = ({ name, picSrc, github, linkedIn }) => { 4 | return ( 5 |
6 | 7 |

{name}

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Player; 21 | -------------------------------------------------------------------------------- /client/src/pages/RootPage/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 2 | 3 | const Footer = () => { 4 | return ( 5 | 17 | ); 18 | }; 19 | 20 | export default Footer; 21 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "nodemon -r dotenv/config index.js", 9 | "start": "node -r dotenv/config index.js " 10 | }, 11 | "author": "Annie Rosen, Hazel Bolivar, Ian Flynn, Jeb Stone, Krystal Fung", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.1.0", 15 | "cookie-parser": "^1.4.6", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.0.3", 18 | "express": "^4.18.2", 19 | "pg": "^8.10.0", 20 | "sequelize": "^6.32.1" 21 | }, 22 | "devDependencies": { 23 | "nodemon": "^3.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/models/index.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import getUserModel from './user.js'; 3 | import getClusterModel from './cluster.js'; 4 | 5 | const sequelize = new Sequelize( 6 | process.env.PG_DATABASE, 7 | process.env.PG_USER, 8 | process.env.PG_PASSWORD, 9 | { 10 | host: process.env.PG_HOST, 11 | dialect: 'postgres', 12 | } 13 | ); 14 | const models = { 15 | User: getUserModel(sequelize, Sequelize), 16 | Cluster: getClusterModel(sequelize, Sequelize), 17 | }; 18 | 19 | Object.keys(models).forEach((key) => { 20 | if ('associate' in models[key]) { 21 | models[key].associate(models); 22 | } 23 | }); 24 | 25 | export { sequelize }; 26 | export default models; 27 | -------------------------------------------------------------------------------- /client/src/pages/RootPage/components/TextField.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import { useTextField } from 'react-aria'; 3 | 4 | export default function TextField(props) { 5 | let { label } = props; 6 | let ref = useRef(null); 7 | let { labelProps, inputProps, descriptionProps, errorMessageProps } = 8 | useTextField(props, ref); 9 | 10 | return ( 11 |
12 | 13 | 14 | 15 | {props.errorMessage && ( 16 |
17 | {props.errorMessage} 18 |
19 | )} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/components/UserMenu.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from 'react-router'; 2 | import BrokerIdForm from './BrokerIdForm'; 3 | 4 | const UserMenu = ({ username, menuOpen, connectionString, handleSubmit }) => { 5 | const navigate = useNavigate(); 6 | return ( 7 |
8 |

9 | Welcome, {username} 10 |

11 |

12 | Connected: {connectionString} 13 |

14 | 15 | 18 |
19 | ); 20 | }; 21 | export default UserMenu; 22 | -------------------------------------------------------------------------------- /client/src/styles/variables.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@300;400;500&display=swap'); 2 | 3 | /* // === COLORS === */ 4 | :root { 5 | --cl-dark: #0e3d61; 6 | --cl-medium-dark: #1a659e; 7 | --cl-medium: #ff6b35; 8 | --cl-medium-light: #f7c59f; 9 | --cl-light: #ffffff; 10 | --cl-text: #ffffff; 11 | 12 | --cl-accent: #f94b06; 13 | --cl-accent-light: #f8bb2a; 14 | 15 | /* // === FONTS === */ 16 | --ff-main: 'Noto Sans', Arial, Helvetica, sans-serif; 17 | --ff-logo: 'Noto Sans', Arial, Helvetica, sans-serif; 18 | 19 | --br-regular: var(--cl-medium-dark) 3px solid; 20 | --br-small-dark: var(--cl-medium-dark) 2px solid; 21 | --br-small-light: var(--cl-medium-light) 2px solid; 22 | 23 | --breakpoint-phone: 500px; 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kafkalerts", 3 | "version": "1.0.0", 4 | "description": "Kafka cluster monitoring tool", 5 | "main": "./server/index.js", 6 | "scripts": { 7 | "start": "cd server && npm run start", 8 | "client-install": "cd client && npm install", 9 | "server-install": "cd server && npm install", 10 | "dev": "concurrently \"cd server && npm run dev\" \"cd client && npm run dev\"" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/oslabs-beta/kafkalerts.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/oslabs-beta/kafkalerts/issues" 20 | }, 21 | "homepage": "kafkalerts.com", 22 | "dependencies": { 23 | "concurrently": "^8.0.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | const getUserModel = (sequelize, { DataTypes }) => { 2 | const User = sequelize.define('user', { 3 | username: { 4 | type: DataTypes.STRING, 5 | unique: true, 6 | allowNull: false, 7 | validate: { 8 | notEmpty: true, 9 | }, 10 | primaryKey: true, 11 | }, 12 | password: { 13 | type: DataTypes.STRING, 14 | validate: { 15 | notEmpty: true, 16 | }, 17 | allowNull: false, 18 | }, 19 | }); 20 | User.associate = (models) => { 21 | User.hasOne(models.Cluster, { onDelete: 'CASCADE' }); 22 | }; 23 | User.findByLogin = async (login) => { 24 | let user = await User.findOne({ 25 | where: { username: login }, 26 | }); 27 | return user; 28 | }; 29 | return User; 30 | }; 31 | 32 | export default getUserModel; 33 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/containers/DashNav.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import AlertsContainer from './AlertsContainer'; 3 | import UserMenu from '../components/UserMenu'; 4 | import logo from '../../../assets/logo.png'; 5 | import { v4 as uuidv4 } from 'uuid'; 6 | 7 | const DashNav = ({ brokers, username, connectionString, handleSubmit }) => { 8 | const [menuOpen, setMenuOpen] = useState(false); 9 | const toggleMenu = () => setMenuOpen(!menuOpen); 10 | return ( 11 | 27 | ); 28 | }; 29 | 30 | export default DashNav; 31 | -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 3 | import LoginPage from './pages/loginPage/LoginPage.jsx'; 4 | import DashboardPage from './pages/dashboardPage/DashboardPage.jsx'; 5 | import LandingPage from './pages/landingPage/LandingPage.jsx'; 6 | import { library } from '@fortawesome/fontawesome-svg-core'; 7 | import { fas } from '@fortawesome/free-solid-svg-icons'; 8 | import { faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons'; 9 | library.add(fas, faGithub, faLinkedin); 10 | 11 | import '../build/styles/index.css'; 12 | import RootPage from './pages/RootPage/RootPage.jsx'; 13 | 14 | const App = () => { 15 | return ( 16 | 17 | 18 | }> 19 | } /> 20 | } /> 21 | 22 | } /> 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/src/pages/landingPage/components/Features.jsx: -------------------------------------------------------------------------------- 1 | import SingleFeature from './SingleFeature'; 2 | import { v4 as uuid } from 'uuid'; 3 | 4 | const featureObj = [ 5 | { 6 | icon: 'fa-universal-access', 7 | header: 'Accessibility', 8 | text: 'Designed with accessibility as the top priority, kafkAlerts works well with screen readers and keyboard navigation', 9 | }, 10 | { 11 | icon: 'fa-chart-line', 12 | header: 'Broker Metrics', 13 | text: 'See Kafka Cluster metrics broken down by individual broker.', 14 | }, 15 | { 16 | icon: 'fa-bell', 17 | header: 'Alerts', 18 | text: 'When an issue is detected, broker specific alerts immediately appear at the top of your page.', 19 | }, 20 | { 21 | icon: 'fa-lock', 22 | header: 'Security', 23 | text: 'User profile data stored securely.', 24 | }, 25 | { 26 | icon: 'fa-suitcase-medical', 27 | header: 'Diagnose Issues', 28 | text: 'Use alerts and metrics to determine the source of cluster health issues.', 29 | }, 30 | { 31 | icon: 'fa-file-lines', 32 | header: 'Testing', 33 | text: 'Tested for accessibility and reliability.', 34 | }, 35 | ]; 36 | 37 | const Features = () => { 38 | const features = featureObj.map((feature) => ( 39 | 40 | )); 41 | return ( 42 |
43 |

Features

44 | {features} 45 |
46 | ); 47 | }; 48 | 49 | export default Features; 50 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/components/BrokerIdForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import Button from '../../RootPage/components/Button.jsx'; 3 | import TextField from '../../RootPage/components/TextField.jsx'; 4 | const BrokerIdForm = ({ handleSubmit, menuOpen }) => { 5 | const [isExpanded, setIsExpanded] = useState(false); 6 | const [promURI, setPromURI] = useState(''); 7 | const [brokerIds, setBrokerIds] = useState(''); 8 | const toggleExpand = () => { 9 | setIsExpanded((prevExpanded) => !prevExpanded); 10 | }; 11 | return ( 12 |
13 | {isExpanded ? ( 14 | <> 15 | 22 | 29 |
30 | 33 | 34 |
35 | 36 | ) : ( 37 | 40 | )} 41 |
42 | ); 43 | }; 44 | 45 | export default BrokerIdForm; 46 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/components/Broker.jsx: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { useState, useEffect } from 'react'; 3 | import Metric from './Metric'; 4 | import Button from '../../RootPage/components/Button.jsx'; 5 | 6 | const Broker = ({ id, alerts, getBytesIn, getBytesOut, getUrp }) => { 7 | const [expandedDisplay, setExpandedDisplay] = useState(false); 8 | const [metrics, setMetrics] = useState({}); 9 | 10 | const toggleExpand = async () => { 11 | if (!expandedDisplay) { 12 | //if open, fetch and render graph 13 | //get bytesin, bytesOut, and URP 14 | setMetrics({ 15 | 'Bytes In': await getBytesIn(id), 16 | 'Bytes Out': await getBytesOut(id), 17 | URP: await getUrp(id), 18 | }); 19 | } 20 | setExpandedDisplay(!expandedDisplay); 21 | }; 22 | 23 | const brokerMetrics = []; 24 | for (const metric in metrics) { 25 | brokerMetrics.push( 26 | 27 | ); 28 | } 29 | useEffect(() => { 30 | if (alerts.length) toggleExpand(); 31 | }, []); 32 | 33 | return ( 34 |
35 |
36 |
ID: {id} |
37 | Alerts: 38 |
42 | {alerts.length} 43 |
44 |

{alerts}

45 | 48 |
49 | {expandedDisplay &&
{brokerMetrics}
} 50 |
51 | ); 52 | }; 53 | 54 | export default Broker; 55 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | output: { 7 | path: path.resolve(__dirname, 'build'), 8 | publicPath: '/', 9 | filename: 'bundle.js', 10 | }, 11 | mode: 'production', 12 | resolve: { 13 | modules: [path.join(__dirname, 'src'), 'node_modules'], 14 | alias: { 15 | react: path.join(__dirname, 'node_modules', 'react'), 16 | }, 17 | }, 18 | devServer: { 19 | historyApiFallback: true, 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(js|jsx)$/, 25 | use: [ 26 | { 27 | loader: 'babel-loader', 28 | options: { 29 | presets: [ 30 | '@babel/preset-env', 31 | ['@babel/preset-react', { runtime: 'automatic' }], 32 | ], 33 | }, 34 | }, 35 | ], 36 | exclude: /node_modules/, 37 | }, 38 | { 39 | test: /\.m?js$/, 40 | enforce: 'pre', 41 | use: ['source-map-loader'], 42 | }, 43 | { 44 | test: /\.css$/i, 45 | use: ['style-loader', 'css-loader', 'postcss-loader'], 46 | }, 47 | { 48 | test: /\.(jpg|png)$/, 49 | loader: 'file-loader', 50 | options: { 51 | name: '[path][name].[hash].[ext]', 52 | }, 53 | }, 54 | { 55 | test: /\.svg$/, 56 | use: ['@svgr/webpack'], 57 | }, 58 | ], 59 | }, 60 | resolve: { 61 | extensions: ['.js', '.jsx', '.css', '.scss'], 62 | }, 63 | plugins: [ 64 | new HtmlWebpackPlugin({ 65 | template: './src/index.html', 66 | }), 67 | // new webpack.ProvidePlugin({ 68 | // Chart: 'react-chartjs-2', 69 | // }), 70 | ], 71 | }; 72 | -------------------------------------------------------------------------------- /client/src/__tests__/AuthControllerTests.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const server = 'http://localhost:3000'; 3 | const bcrypt = require('bcrypt'); 4 | 5 | // test createAccount 6 | // describe('Account created successfully', () => { 7 | // it('Account is created', () => { 8 | 9 | // }) 10 | 11 | // it('Account is not duplicated', () => { 12 | 13 | // }) 14 | // }) 15 | 16 | describe('Bcrypting passwords', () => { 17 | 18 | it('Passwords should not be stored in plaintext', () => { 19 | request(server) 20 | .post('/signup') 21 | .send({"username": "test3", "password" : "password3"}) 22 | .type('form') 23 | .end((err, res) => { 24 | authController.createAccount({username: 'test3'}, (err, user) => { 25 | expect(user.password).to.not.eql('password3'); 26 | }); 27 | }); 28 | }); 29 | 30 | it('Passwords be bcrypted', () => { 31 | request(server) 32 | .post('/signup') 33 | .send({ username: 'test4', password: 'password4' }) 34 | .type('form') 35 | .end((err, res) => { 36 | authController.createAccount({username: 'test4'}, (err, user) => { 37 | expect(bcrypt.compareSync('password4', user.password)).to.be.true; 38 | }); 39 | }); 40 | }); 41 | 42 | it('Bcrypts passwords in SQL middleware, not in authController', () => { 43 | request(server) 44 | .post('/signup') 45 | .send({ username: 'petri', password: 'aight' }) 46 | .type('form') 47 | .end((err, res) => { 48 | authController.createAccount({ username: 'petri', password: 'aight' }, (err, user) => { 49 | expect(user.password).to.not.eql('aight'); 50 | expect(bcrypt.compareSync('aight', user.password)).to.be.true; 51 | }); 52 | }); 53 | }); 54 | }) 55 | 56 | // test verify account 57 | // describe('Verifying user') -------------------------------------------------------------------------------- /client/src/pages/landingPage/components/Team.jsx: -------------------------------------------------------------------------------- 1 | import Player from './Player'; 2 | import hazel from '../../../assets/headshots/hazel-headshot.jpg'; 3 | import ian from '../../../assets/headshots/ian-headshot.jpg'; 4 | import krystal from '../../../assets/headshots/krystal-headshot.jpg'; 5 | import annie from '../../../assets/headshots/annie-headshot.jpg'; 6 | import jeb from '../../../assets/headshots/jeb-headshot.jpg'; 7 | import { v4 as uuid } from 'uuid'; 8 | const Team = () => { 9 | const team = [ 10 | { 11 | name: 'Hazel Bolivar', 12 | src: hazel, 13 | github: 'https://github.com/hazelbolivar', 14 | linkedIn: 'https://www.linkedin.com/in/hazelbolivar/', 15 | }, 16 | { 17 | name: 'Ian Flynn', 18 | src: ian, 19 | github: 'https://github.com/ian-flynn', 20 | linkedIn: 'https://www.linkedin.com/in/ianrflynn/', 21 | }, 22 | { 23 | name: 'Krystal Fung', 24 | src: krystal, 25 | github: 'https://github.com/klfung7', 26 | linkedIn: 'https://www.linkedin.com/in/krystal-fung/', 27 | }, 28 | { 29 | name: 'Annie Rosen', 30 | src: annie, 31 | github: 'https://github.com/mezzocarattere', 32 | linkedIn: 'https://www.linkedin.com/in/rosen-annie/', 33 | }, 34 | { 35 | name: 'Jeb Stone', 36 | src: jeb, 37 | github: 'https://github.com/jeb-stone', 38 | linkedIn: 'https://www.linkedin.com/in/jeb-stone/', 39 | }, 40 | ]; 41 | 42 | const players = team.map((player) => ( 43 | 50 | )); 51 | 52 | return ( 53 |
54 |

Meet the team

55 |
{players}
56 |
57 | ); 58 | }; 59 | 60 | export default Team; 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | build -------------------------------------------------------------------------------- /client/src/assets/animation.js: -------------------------------------------------------------------------------- 1 | const animation = {}; 2 | 3 | 4 | let restart = true; 5 | const totalDuration = 50000; 6 | //const duration = (ctx) => easing(ctx.index / data.length) * totalDuration / data.length; 7 | const delay = totalDuration / data.length; //(ctx) => easing(ctx.index / data.length) * totalDuration; 8 | const previousY = (ctx) => ctx.index === 0 ? ctx.chart.scales.y.getPixelForValue(100) : ctx.chart.getDatasetMeta(ctx.datasetIndex).data[ctx.index - 1].getProps(['y'], true).y; 9 | 10 | animation.bytesIn = { 11 | x: { 12 | type: 'number', 13 | easing: 'linear', 14 | duration: delay, 15 | from: NaN, // the point is initially skipped 16 | delay(ctx) { 17 | if (ctx.type !== 'data' || ctx.xStarted) { 18 | return 0; 19 | } 20 | ctx.xStarted = true; 21 | return ctx.index * delay; 22 | } 23 | }, 24 | y: { 25 | type: 'number', 26 | easing: 'linear', 27 | duration: delay, 28 | from: previousY, 29 | delay(ctx) { 30 | if (ctx.type !== 'data' || ctx.yStarted) { 31 | return 0; 32 | } 33 | ctx.yStarted = true; 34 | return ctx.index * delay; 35 | } 36 | } 37 | }; 38 | 39 | 40 | //SAMPLE DATA GENERATOR 41 | const data = []; 42 | const data2 = []; 43 | let prev = 1000; 44 | let prev2 = 1000; 45 | for (let i = 0; i < 100; i++) { 46 | prev += 5 - Math.random() * 10; 47 | data.push({x: i, y: prev}); 48 | prev2 += 5 - Math.random() * 10; 49 | data2.push({x: i, y: prev2}); 50 | } 51 | 52 | //SAMPLE CONFIG 53 | //TODO: SEPARATE CHARTS INTO INDIVIDUAL MODULES - UPDATE CONFIG 54 | const config = { 55 | type: 'line', 56 | data: { 57 | datasets: [{ 58 | label: 'Bytes In', 59 | borderColor: 'red', 60 | borderWidth: 1, 61 | radius: 0, 62 | data: data, 63 | }, 64 | { 65 | label: 'Bytes Out', 66 | borderColor: 'blue', 67 | borderWidth: 1, 68 | radius: 0, 69 | data: data2, 70 | }] 71 | }, 72 | options: { 73 | animation, 74 | interaction: { 75 | intersect: false 76 | }, 77 | responsive: true, 78 | plugins: { 79 | legend: false, 80 | title: { 81 | display: true, 82 | text: 'My Chart' 83 | } 84 | }, 85 | scales: { 86 | x: { 87 | type: 'linear', 88 | ticks: { 89 | stepSize: 10, 90 | } 91 | } 92 | } 93 | } 94 | }; 95 | export default animation; -------------------------------------------------------------------------------- /client/src/__tests__/ServerTests.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const server = 'http://localhost:3000'; 3 | // const server = require('../../server/server.js') 4 | 5 | describe('Route integration', () => { 6 | const username = 'test' + Math.floor(Math.random()*100); 7 | const body = {username, password: 'test'} 8 | 9 | // test that index.html gets sent so that react router handles routes 10 | // rather than routes being handled in backend 11 | describe('/*', () => { 12 | describe('GET', () => { 13 | it('responds with 200 status and text/html content type', () => { 14 | request(server) 15 | .get('/*') 16 | .expect('Content-Type', /text\/html/) 17 | .expect(200); 18 | }); 19 | }); 20 | }); 21 | 22 | // login route 23 | describe('/login', () => { 24 | describe('POST', () => { 25 | it('login route responds with 200 status and application/json content type', async () => { 26 | request(server) 27 | .post('/login') 28 | .send(JSON.stringify(body)) 29 | .set('Content-Type', /application\/json/) 30 | .expect('Content-Type', /application\/json/) 31 | .expect(200); 32 | }); 33 | body.isVerified = true; 34 | body.cookieId = username; 35 | it('res.locals.isVerified and res.locals.cookieID is in the body of the response', async () => { 36 | request(server) 37 | .post('/login') 38 | .send(JSON.stringify(body)) 39 | .expect('isVerified', true) 40 | .expect('cookieID', username) 41 | .expect(200); 42 | }); 43 | }); 44 | }); 45 | 46 | // signup route 47 | describe('/signup', () => { 48 | describe('POST', () => { 49 | it('signup route responds with 200 status and application/json content type', () => { 50 | request(server) 51 | .post('/signup') 52 | .send(JSON.stringify(body)) 53 | .set('Content-Type', /application\/json/) 54 | .expect('Content-Type', /application\/json/) 55 | .expect(200); 56 | }); 57 | body.isVerified = true; 58 | body.cookieId = username; 59 | it('res.locals.isVerified and res.locals.cookieID is in the body of the response', async () => { 60 | request(server) 61 | .post('/signup') 62 | .send(JSON.stringify(body)) 63 | .expect('isVerified', true) 64 | .expect('cookieID', username) 65 | .expect(200); 66 | }); 67 | }); 68 | }); 69 | 70 | }) 71 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import cookieParser from 'cookie-parser'; 4 | import authController from './controllers/authController.js'; 5 | import cookieController from './controllers/cookieController.js'; 6 | const app = express(); 7 | 8 | import { fileURLToPath } from 'url'; 9 | import cors from 'cors'; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | import models, { sequelize } from './models/index.js'; 15 | 16 | const corsOptions = { 17 | origin: process.env.CLIENT_URL_DEV, 18 | credentials: true, 19 | methods: 'GET, POST, PUT, DELETE, OPTIONS', 20 | allowedHeaders: 'Origin, X-Requested-With, Content-Type, Accept', 21 | }; 22 | app.use(cors(corsOptions)); 23 | 24 | app.use(cookieParser()); 25 | app.use(express.urlencoded({ extended: true })); 26 | app.use(express.json()); 27 | 28 | //always send index.html at all routes so react router handles them instead of backend 29 | // app.get('/*', (req, res) => { 30 | // console.log('here in the server'); 31 | // return res.sendFile(path.join(__dirname, '../index.html'), (err) => { 32 | // if (err) return res.status(500).send(err); 33 | // }); 34 | // }); 35 | // if (process.env.NODE_ENV === 'production') { 36 | // app.use(express.static('client/build')); 37 | // app.get('*', (req, res) => 38 | // res.sendFile(path.resolve(__dirname, '..', 'client', 'build', 'index.html')) 39 | // ); 40 | // } 41 | // app.use(express.static(path.join(__dirname, '../index.html'))); 42 | 43 | // LOG IN ROUTE 44 | app.post( 45 | '/api/login', 46 | (req, res, next) => { 47 | console.log('in the login route'); 48 | return next(); 49 | }, 50 | authController.verifyUser, 51 | cookieController.setCookie, 52 | (req, res) => { 53 | return res.status(200).json(res.locals); 54 | } 55 | ); 56 | 57 | // SIGN UP ROUTE 58 | app.post( 59 | '/api/signup', 60 | authController.createAccount, 61 | cookieController.setCookie, 62 | (req, res) => { 63 | return res.status(200).json(res.locals); 64 | } 65 | ); 66 | 67 | // ADD BROKER IDS ROUTE 68 | // app.post('/api/addbrokers', authController.addBrokers, (req, res) => { 69 | // return res.status(200).json('ids added'); 70 | // }); 71 | 72 | // global error handler 73 | app.use((err, req, res, next) => { 74 | const defaultErr = { 75 | log: 'Express error handler caught unknown middleware error', 76 | status: 400, 77 | message: { err: 'An error occurred' }, 78 | }; 79 | const errObj = Object.assign({}, defaultErr, err); 80 | console.log(errObj.log); 81 | return res.status(errObj.status).json(errObj.message); 82 | }); 83 | 84 | sequelize.sync({ force: true }).then(async () => { 85 | app.listen(process.env.PORT, () => { 86 | console.log(`Server listening on port: ${process.env.PORT}`); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "build": "concurrently \"webpack --mode=production\" \"npm run build:css\"", 8 | "test": "jest --detectOpenHandles", 9 | "dev": "concurrently \"webpack-dev-server --mode=development --open --hot\" \"npm run watch:css\"", 10 | "build:css": "postcss src/styles/styles.css -o build/styles/index.css", 11 | "watch:css": "postcss src/styles/styles.css -o build/styles/index.css -w" 12 | }, 13 | "author": "Annie Rosen, Hazel Bolivar, Ian Flynn, Jeb Stone, Krystal Fung", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 17 | "@fortawesome/free-brands-svg-icons": "^6.4.0", 18 | "@fortawesome/free-regular-svg-icons": "^6.4.0", 19 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 20 | "@fortawesome/react-fontawesome": "^0.2.0", 21 | "babel-plugin-macros": "^3.1.0", 22 | "chart.js": "^4.3.0", 23 | "concurrently": "^8.2.1", 24 | "cors": "^2.8.5", 25 | "react": "^18.2.0", 26 | "react-aria": "^3.24.0", 27 | "react-chartjs-2": "^5.2.0", 28 | "react-dom": "^18.2.0", 29 | "react-router-dom": "^6.11.1", 30 | "react-scroll": "^1.8.9", 31 | "uuid": "^9.0.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.21.8", 35 | "@babel/preset-env": "^7.21.5", 36 | "@babel/preset-react": "^7.18.6", 37 | "@svgr/webpack": "^7.0.0", 38 | "@testing-library/dom": "^9.2.0", 39 | "@testing-library/jest-dom": "^5.16.5", 40 | "@testing-library/react": "^14.0.0", 41 | "babel-jest": "^29.5.0", 42 | "babel-loader": "^9.1.2", 43 | "css-loader": "^6.7.3", 44 | "cssnano": "^6.0.1", 45 | "file-loader": "^6.2.0", 46 | "html-webpack-plugin": "^5.5.1", 47 | "jest": "^29.5.0", 48 | "jest-axe": "^7.0.1", 49 | "jest-environment-jsdom": "^29.5.0", 50 | "postcss": "^8.4.28", 51 | "postcss-cli": "^10.1.0", 52 | "postcss-import": "^15.1.0", 53 | "postcss-loader": "^7.3.3", 54 | "postcss-preset-env": "^9.1.1", 55 | "sass-loader": "^13.2.2", 56 | "source-map-loader": "^4.0.1", 57 | "style-loader": "^3.3.2", 58 | "supertest": "^6.3.3", 59 | "webpack": "^5.82.0", 60 | "webpack-cli": "^5.0.2", 61 | "webpack-dev-server": "^4.13.3" 62 | }, 63 | "jest": { 64 | "verbose": true, 65 | "testEnvironment": "jest-environment-jsdom", 66 | "globalSetup": "./jest-setup.js", 67 | "globalTeardown": "./jest-teardown.js", 68 | "setupFilesAfterEnv": [ 69 | "@testing-library/jest-dom/extend-expect" 70 | ], 71 | "moduleNameMapper": { 72 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/__mocks__/fileMock.js", 73 | "\\.(scss)$": "/src/__mocks__/styleMock.js" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/components/URPChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | PointElement, 7 | LineElement, 8 | Title, 9 | Tooltip, 10 | Legend, 11 | } from 'chart.js'; 12 | import { Line } from 'react-chartjs-2'; 13 | 14 | ChartJS.register( 15 | CategoryScale, 16 | LinearScale, 17 | PointElement, 18 | LineElement, 19 | Title, 20 | Tooltip, 21 | Legend 22 | ); 23 | 24 | export default function URPChart(urp) { 25 | const totalDuration = 14440; 26 | const delay = totalDuration / urp.length; 27 | 28 | const previousY = (ctx) => 29 | ctx.index === 0 30 | ? ctx.chart.scales.y.getPixelForValue(100) 31 | : ctx.chart 32 | .getDatasetMeta(ctx.datasetIndex) 33 | .data[ctx.index - 1].getProps(['y'], true).y; 34 | 35 | const animation = { 36 | x: { 37 | type: 'number', 38 | easing: 'linear', 39 | duration: delay, 40 | from: NaN, // the point is initially skipped 41 | delay(ctx) { 42 | if (ctx.type !== 'data' || ctx.xStarted) { 43 | return 0; 44 | } 45 | ctx.xStarted = true; 46 | return ctx.index * delay; 47 | }, 48 | }, 49 | y: { 50 | type: 'number', 51 | easing: 'linear', 52 | duration: delay, 53 | from: previousY, 54 | delay(ctx) { 55 | if (ctx.type !== 'data' || ctx.yStarted) { 56 | return 0; 57 | } 58 | ctx.yStarted = true; 59 | return ctx.index * delay; 60 | }, 61 | }, 62 | }; 63 | 64 | const urpOptions = { 65 | responsive: true, 66 | maintainAspectRatio: false, 67 | 68 | plugins: { 69 | legend: { 70 | position: 'none', 71 | }, 72 | title: { 73 | display: true, 74 | text: 'Under Replicated Partitions', 75 | }, 76 | }, 77 | animation, 78 | scales: { 79 | x: { 80 | type: 'linear', 81 | display: true, 82 | title: { 83 | display: true, 84 | text: 'Time (Seconds)', 85 | }, 86 | }, 87 | y: { 88 | display: true, 89 | title: { 90 | display: true, 91 | text: 'URP', 92 | }, 93 | ticks: { 94 | stepSize: 10000, 95 | }, 96 | }, 97 | }, 98 | }; 99 | 100 | const urpY = urp?.map((tuple) => Number(tuple[1])); 101 | const startTime = urp[0][0]; 102 | const timeX = urp?.map((tuple, idx) => { 103 | return tuple[0] - startTime; 104 | }); 105 | 106 | const data = { 107 | labels: timeX, 108 | datasets: [ 109 | { 110 | label: 'URP', 111 | data: urp, 112 | borderColor: 'rgba(249, 75, 6)', 113 | backgroundColor: 'rgba(249, 75, 6, 0.5)', 114 | borderWidth: 1, 115 | radius: 1, 116 | }, 117 | ], 118 | }; 119 | 120 | return ; 121 | } 122 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/DashboardPage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import DashNav from './containers/DashNav'; 3 | import BrokersContainer from './containers/BrokersContainer'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | import Footer from '../RootPage/components/Footer'; 6 | const DashboardPage = () => { 7 | const [username, setUsername] = useState('Demo User'); 8 | const [connectionString, setConnectionString] = useState('prometheus:9090'); 9 | const [brokerIds, setBrokerIds] = useState([ 10 | '1', 11 | '2', 12 | '3', 13 | '4', 14 | '5', 15 | '6', 16 | '7', 17 | '8', 18 | '9', 19 | '10', 20 | '11', 21 | '12', 22 | ]); 23 | const [brokersAndAlerts, setBrokersAndAlerts] = useState([]); 24 | 25 | // when user submits form, ids will be added to an array 26 | const handleSubmit = async (promURI, brokerIds) => { 27 | const idsArray = brokerIds.split(',').map((id) => id.trim().toString()); 28 | // update Prometheus host to use for querying later 29 | setConnectionString(promURI); 30 | // store brokerIds in DB 31 | try { 32 | const response = await fetch('/api/addbrokers', { 33 | method: 'POST', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | }, 37 | body: JSON.stringify({ idsArray: idsArray, username: username }), 38 | }); 39 | const data = await response.json(); 40 | // update state with new array of Ids 41 | setBrokerIds(idsArray); 42 | console.log('broker IDs... ', idsArray); 43 | console.log('response from DB: ', data); 44 | e.target.reset(); 45 | } catch (error) { 46 | console.error('Error submitting brokerIDs to database: ', error); 47 | } 48 | }; 49 | 50 | useEffect(() => { 51 | // TO DO: change temp messages to three relevant messages 52 | const tempErrorMessages = [ 53 | 'Under Replicated Paritions is greater than 0', 54 | 'Specified SLO 1 is out of bounds', 55 | 'Specified SLO 4 is out of bounds', 56 | ]; 57 | Promise.all( 58 | brokerIds.map(async (brokerId) => { 59 | try { 60 | const response = await fetch('prometheus'); 61 | const jsonResponse = await response.json(); 62 | return [jsonResponse]; 63 | } catch (err) { 64 | console.log('error getting alert info for broker ', brokerId); 65 | return { 66 | brokerId: brokerId, 67 | alerts: 68 | brokerId === '2' || brokerId === '4' || brokerId === '9' 69 | ? [tempErrorMessages[(brokerId - 1) % 3]] 70 | : [], 71 | }; 72 | } 73 | }) 74 | ).then((values) => setBrokersAndAlerts(values)); 75 | }, []); 76 | 77 | return ( 78 |
79 |
80 | 87 | 88 |
89 |
90 |
91 | ); 92 | }; 93 | 94 | export default DashboardPage; 95 | -------------------------------------------------------------------------------- /client/src/pages/loginPage/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import TextField from '../RootPage/components/TextField'; 4 | import Button from '../RootPage/components/Button'; 5 | 6 | const LoginPage = () => { 7 | const [username, setUsername] = useState(''); 8 | const [usernameRulesDisplay, setUsernameRulesDisplay] = useState(false); 9 | const [passwordRulesDisplay, setPasswordRulesDisplay] = useState(false); 10 | const [usernameError, setUsernameError] = useState(''); 11 | const [passwordError, setPasswordError] = useState(''); 12 | const [password, setPassword] = useState(''); 13 | const navigate = useNavigate(); 14 | 15 | const checkUsername = (username) => { 16 | return ( 17 | /[^a-z0-9_\-.]/i.test(username) || 18 | username.length < 4 || 19 | username.length > 32 20 | ); 21 | }; 22 | const checkPassword = (password) => { 23 | return password.length < 8 || password.length > 32; 24 | }; 25 | const handleSend = async (endpoint) => { 26 | let shouldIReturn; 27 | 28 | if (checkUsername(username)) { 29 | setUsernameError( 30 | 'Must only contain letters (a-z, A-Z), numbers (0-9), dashes or underscores (no spaces), periods (.), and be between 4-32 characters long.' 31 | ); 32 | shouldIReturn = true; 33 | } else { 34 | setUsernameError(''); 35 | } 36 | if (checkPassword(password)) { 37 | setPasswordError('Must be between 8-32 characters long.'); 38 | shouldIReturn = true; 39 | } else { 40 | setPasswordError(''); 41 | } 42 | if (shouldIReturn) return; 43 | 44 | try { 45 | const url = 46 | process.env.NODE_ENV === 'development' 47 | ? `http://localhost:3000/api/${endpoint}` 48 | : `/api/${endpoint}`; 49 | const response = await fetch(url, { 50 | method: 'POST', 51 | headers: { 'Content-Type': 'application/json' }, 52 | body: JSON.stringify({ username: username, password: password }), 53 | }); 54 | console.log(response); 55 | // if (response.status === 200) navigate('/dashboard'); 56 | } catch (err) { 57 | console.log(err); 58 | } 59 | }; 60 | return ( 61 |
62 |
63 | 70 | 71 |

{usernameError}

72 | 79 |

{passwordError}

80 |
81 | 84 | 87 |
88 |
89 | 92 |
93 |
94 | ); 95 | }; 96 | export default LoginPage; 97 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/components/BytesOutChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | PointElement, 7 | LineElement, 8 | Title, 9 | Tooltip, 10 | Legend, 11 | } from 'chart.js'; 12 | import { Line } from 'react-chartjs-2'; 13 | 14 | ChartJS.register( 15 | CategoryScale, 16 | LinearScale, 17 | PointElement, 18 | LineElement, 19 | Title, 20 | Tooltip, 21 | Legend 22 | ); 23 | 24 | export default function BytesOutChart(bytesOut) { 25 | // Generates Demo Data 26 | // const demoData = []; 27 | // let prev = 1000; 28 | // for (let i = 0; i < 100; i++) { 29 | // prev += 5 - Math.random() * 10; 30 | // demoData.push({x: i, y: prev}); 31 | // } 32 | const totalDuration = 50000; 33 | //const duration = (ctx) => easing(ctx.index / data.length) * totalDuration / data.length; 34 | const delay = totalDuration / bytesOut.length; //(ctx) => easing(ctx.index / data.length) * totalDuration; 35 | const previousY = (ctx) => 36 | ctx.index === 0 37 | ? ctx.chart.scales.y.getPixelForValue(100) 38 | : ctx.chart 39 | .getDatasetMeta(ctx.datasetIndex) 40 | .data[ctx.index - 1].getProps(['y'], true).y; 41 | 42 | const animation = { 43 | x: { 44 | type: 'number', 45 | easing: 'linear', 46 | duration: delay, 47 | from: NaN, // the point is initially skipped 48 | delay(ctx) { 49 | if (ctx.type !== 'data' || ctx.xStarted) { 50 | return 0; 51 | } 52 | ctx.xStarted = true; 53 | return ctx.index * delay; 54 | }, 55 | }, 56 | y: { 57 | type: 'number', 58 | easing: 'linear', 59 | duration: delay, 60 | from: previousY, 61 | delay(ctx) { 62 | if (ctx.type !== 'data' || ctx.yStarted) { 63 | return 0; 64 | } 65 | ctx.yStarted = true; 66 | return ctx.index * delay; 67 | }, 68 | }, 69 | }; 70 | 71 | const bytesOutOptions = { 72 | responsive: true, 73 | maintainAspectRatio: false, 74 | 75 | plugins: { 76 | legend: { 77 | position: 'none', 78 | }, 79 | title: { 80 | display: true, 81 | }, 82 | }, 83 | animation, 84 | scales: { 85 | x: { 86 | type: 'linear', 87 | display: true, 88 | title: { 89 | display: true, 90 | text: 'Time (Seconds)', 91 | }, 92 | }, 93 | y: { 94 | display: true, 95 | title: { 96 | display: true, 97 | text: 'Bytes', 98 | }, 99 | ticks: { 100 | stepSize: 10000, 101 | }, 102 | }, 103 | }, 104 | }; 105 | 106 | const bytesY = bytesOut?.map((tuple) => Number(tuple[1])); 107 | const startTime = bytesOut[0][0]; 108 | const timeX = bytesOut?.map((tuple, idx) => { 109 | return tuple[0] - startTime; 110 | }); 111 | 112 | const data = { 113 | labels: timeX, 114 | datasets: [ 115 | { 116 | label: 'Bytes Out', 117 | data: bytesOut, 118 | borderColor: 'rgba(249, 75, 6)', 119 | backgroundColor: 'rgba(249, 75, 6, 0.5)', 120 | borderWidth: 1, 121 | radius: 1, 122 | }, 123 | ], 124 | }; 125 | 126 | return ; 127 | } 128 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/components/BytesInChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | CategoryScale, 5 | LinearScale, 6 | PointElement, 7 | LineElement, 8 | Title, 9 | Tooltip, 10 | Legend, 11 | } from 'chart.js'; 12 | import { Line } from 'react-chartjs-2'; 13 | 14 | ChartJS.register( 15 | CategoryScale, 16 | LinearScale, 17 | PointElement, 18 | LineElement, 19 | Title, 20 | Tooltip, 21 | Legend 22 | ); 23 | 24 | export default function BytesInChart(bytesIn) { 25 | // Generates Demo Data 26 | // const demoData = []; 27 | // let prev = 1000; 28 | // for (let i = 0; i < 100; i++) { 29 | // prev += 5 - Math.random() * 10; 30 | // demoData.push({x: i, y: prev}); 31 | // } 32 | const totalDuration = 50000; 33 | //const duration = (ctx) => easing(ctx.index / data.length) * totalDuration / data.length; 34 | const delay = totalDuration / bytesIn.length; //(ctx) => easing(ctx.index / data.length) * totalDuration; 35 | const previousY = (ctx) => 36 | ctx.index === 0 37 | ? ctx.chart.scales.y.getPixelForValue(100) 38 | : ctx.chart 39 | .getDatasetMeta(ctx.datasetIndex) 40 | .data[ctx.index - 1].getProps(['y'], true).y; 41 | 42 | const animation = { 43 | x: { 44 | type: 'number', 45 | easing: 'linear', 46 | duration: delay, 47 | from: NaN, // the point is initially skipped 48 | delay(ctx) { 49 | if (ctx.type !== 'data' || ctx.xStarted) { 50 | return 0; 51 | } 52 | ctx.xStarted = true; 53 | return ctx.index * delay; 54 | }, 55 | }, 56 | y: { 57 | type: 'number', 58 | easing: 'linear', 59 | duration: delay, 60 | from: previousY, 61 | delay(ctx) { 62 | if (ctx.type !== 'data' || ctx.yStarted) { 63 | return 0; 64 | } 65 | ctx.yStarted = true; 66 | return ctx.index * delay; 67 | }, 68 | }, 69 | }; 70 | 71 | const bytesInOptions = { 72 | responsive: true, 73 | maintainAspectRatio: false, 74 | 75 | plugins: { 76 | legend: { 77 | position: 'none', 78 | }, 79 | title: { 80 | display: true, 81 | text: 'Kafka Broker Metrics - Bytes In', 82 | }, 83 | }, 84 | animation, 85 | scales: { 86 | x: { 87 | type: 'linear', 88 | display: true, 89 | title: { 90 | display: true, 91 | text: 'Time (Seconds)', 92 | }, 93 | }, 94 | y: { 95 | display: true, 96 | title: { 97 | display: true, 98 | text: 'Bytes', 99 | }, 100 | ticks: { 101 | stepSize: 10000, 102 | }, 103 | }, 104 | }, 105 | }; 106 | // console.log('bytesIn... ', bytesIn); 107 | // get data 108 | // get bytes in/ time from bytes in array 109 | const bytesY = bytesIn?.map((tuple) => Number(tuple[1])); 110 | const startTime = bytesIn[0][0]; 111 | const timeX = bytesIn?.map((tuple, idx) => { 112 | return tuple[0] - startTime; 113 | }); 114 | 115 | const data = { 116 | labels: timeX, 117 | datasets: [ 118 | { 119 | label: 'Bytes In', 120 | data: bytesIn, 121 | borderColor: 'rgba(249, 75, 6)', 122 | backgroundColor: 'rgba(249, 75, 6, 0.5)', 123 | borderWidth: 1, 124 | radius: 1, 125 | }, 126 | ], 127 | }; 128 | 129 | return ; 130 | } 131 | -------------------------------------------------------------------------------- /server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt'; 2 | import models from '../models/index.js'; 3 | 4 | const authController = { 5 | createAccount: async (req, res, next) => { 6 | const password = await bcrypt.hash(req.body.password, 10).catch(next); 7 | await models.User.create({ 8 | username: req.body.username, 9 | password: password, 10 | }).catch((error) => { 11 | next({ 12 | log: 'Error occurred during create account.', 13 | status: 400, 14 | message: { err: 'Error occurred during create account.', error }, 15 | }); 16 | }); 17 | return next(); 18 | }, 19 | verifyUser: async (req, res, next) => { 20 | return next(); 21 | }, 22 | addBrokers: async (req, res, next) => { 23 | return next(); 24 | }, 25 | addConnectionString: async (req, res, next) => { 26 | return next(); 27 | }, 28 | }; 29 | 30 | // authController.createAccount = async (req, res, next) => { 31 | // if (req.body.username && req.body.password) { 32 | // try { 33 | // const password = await bcrypt.hash(req.body.password, 10); 34 | // const { username } = req.body; 35 | // await User.create({ 36 | // username, 37 | // password, 38 | // }); 39 | // return next(); 40 | // } catch (err) { 41 | // console.log('ERROR: ', err); 42 | // return next({ 43 | // log: 'Error occurred during create account.', 44 | // status: 400, 45 | // message: { err: 'Error occurred during create account.', err }, 46 | // }); 47 | // } 48 | // } 49 | // return next({ 50 | // log: 'No username/password provided.', 51 | // status: 400, 52 | // message: { err: 'No username/password provided.' }, 53 | // }); 54 | // }; 55 | 56 | // authController.verifyUser = async (req, res, next) => { 57 | // if (req.body.username && req.body.password) { 58 | // try { 59 | // const { username, password } = req.body; 60 | // const user = await User.findOne({ where: { username } }); 61 | // const badInput = { 62 | // log: 'Incorrect username/password combination.', 63 | // status: 400, 64 | // message: { err: 'Incorrect username/password combination.' }, 65 | // }; 66 | // if (!user) return next(badInput); 67 | // const matched = await bcrypt.compare(password, user.password); 68 | // if (!matched) return next(badInput); 69 | // return next(); 70 | // } catch (err) { 71 | // return next({ 72 | // log: 'Error verifying user', 73 | // status: 400, 74 | // message: { err: 'Error verifying user', err }, 75 | // }); 76 | // } 77 | // } 78 | // }; 79 | 80 | // authController.addBrokers = async (req, res, next) => { 81 | // // console.log('inside addBrokers'); 82 | // // try { 83 | // // const { idsArray, username } = req.body; 84 | // // console.log('req.body in addBrokers... ', req.body); 85 | // // const queryString = ` 86 | // // UPDATE users 87 | // // SET broker_ids = $1 88 | // // WHERE username = $2 89 | // // `; 90 | // // let inserted = await db.query(queryString, [idsArray, username]); 91 | // // console.log('inserted.. ', inserted); 92 | // // return next(); 93 | // // } catch (err) { 94 | // // return next({ 95 | // // log: 'Error inside add Brokers.', 96 | // // status: 401, 97 | // // message: { err: 'Unable to add brokers to database table users.', err }, 98 | // // }); 99 | // // } 100 | // return next(); 101 | // }; 102 | export default authController; 103 | // module.exports = authController; 104 | -------------------------------------------------------------------------------- /client/src/__tests__/BrokerMetrics.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import {within} from '@testing-library/dom' 5 | import BrokersContainer from '../BrokersContainer'; 6 | import Broker from '../components/Broker'; 7 | import MetricOne from '../components/MetricOne'; 8 | import Button from '../components/Button'; 9 | 10 | console.log('hello '); 11 | 12 | describe('Testing brokers and metrics', () => { 13 | let brokers = [{name: 'brokerOne', metrics: ['lag', 'backwards overflow', 'urp'], key:'123'}, 14 | {name: 'brokerTwo', metrics: ['lag', 'backwards overflow', 'urp'], key:'456'}]; 15 | let broker = {name: 'brokerOne', metrics: ['lag', 'backwards overflow', 'urp'], key:'123'} 16 | let brokerName = broker.name; 17 | let brokerMetrics = broker.metrics; 18 | let isShowing = false; 19 | const handleClick = jest.fn(isShowing => { 20 | isShowing = isShowing ? false : true; 21 | return isShowing; 22 | }); 23 | 24 | beforeAll(() => { 25 | // container = render() 26 | broker = render() 27 | }); 28 | 29 | test('broker has a button', () => { 30 | //render() 31 | const buttons = screen.getAllByRole('button'); 32 | for(let button of buttons) expect(button).toBeInTheDocument(); 33 | }); 34 | 35 | test('button says show/hide metrics', () => { 36 | render() 37 | const buttons = screen.getAllByRole('button'); 38 | for(let button of buttons) expect(button).toHaveTextContent('Show/Hide Metrics'); 39 | }); 40 | 41 | test('button is clickable', () => { 42 | render() 43 | 44 | const button = render( */} 87 | // 88 | // 89 | // ); 90 | // }; 91 | }); 92 | 93 | -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/containers/BrokersContainer.jsx: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import Broker from '../components/Broker'; 3 | import test from '../../../assets/testData.js'; 4 | 5 | const BrokersContainer = ({ brokers }) => { 6 | async function fetchWithTimeout(resource, options = {}) { 7 | const { timeout = 1000 } = options; 8 | 9 | const controller = new AbortController(); 10 | const id = setTimeout(() => controller.abort(), timeout); 11 | 12 | const response = await fetch(resource, { 13 | ...options, 14 | signal: controller.signal, 15 | }); 16 | clearTimeout(id); 17 | 18 | return response; 19 | } 20 | 21 | const getBytesIn = async (id) => { 22 | console.log('getting Bytes In'); 23 | // window of 20 minutes, getting data in 1 minute intervals 24 | const now = new Date(); 25 | const endTimestamp = Math.floor(now.getTime() / 1000); 26 | const startTimestamp = Math.floor(endTimestamp - 20 * 60); 27 | const step = 60; 28 | //fetch('http://localhost:9090/api/v1/query?query=kafka_server_brokertopicmetrics_bytesin_total') 29 | try { 30 | const response = await fetchWithTimeout( 31 | `http://localhost:9090/api/v1/query_range?query=kafka_server_brokertopicmetrics_bytesin_total&start=${startTimestamp}&end=${endTimestamp}&step=${step}s{broker_id=${id}}` 32 | ); 33 | const data = await response.json(); 34 | if (data) { 35 | return data.data.result; 36 | } 37 | } catch (err) { 38 | console.log('error in bytesIn in broker ', id); 39 | return test.bytesInAlt(); 40 | } 41 | }; 42 | 43 | const getBytesOut = async (id) => { 44 | console.log('getting Bytes Out'); 45 | // // window of 20 minutes, getting data in 1 minute intervals 46 | const now = new Date(); 47 | const endTimestamp = Math.floor(now.getTime() / 1000); 48 | const startTimestamp = Math.floor(endTimestamp - 20 * 60); 49 | const step = 60; 50 | try { 51 | const response = await fetchWithTimeout( 52 | `http://localhost:9090/api/v1/query_range?query=kafka_server_brokertopicmetrics_bytesout_total&start=${startTimestamp}&end=${endTimestamp}&step=${step}s{broker_id=${id}}` 53 | ); 54 | const data = await response.json(); 55 | if (data) { 56 | return data.data.result; 57 | } 58 | } catch (err) { 59 | console.log('error getting bytes out in broker', id); 60 | return test.bytesOutAlt(); 61 | } 62 | }; 63 | 64 | const getUrp = async (id) => { 65 | console.log('Getting URP'); 66 | const now = new Date(); 67 | const endTimestamp = Math.floor(now.getTime() / 1000); 68 | const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); 69 | const startTimestamp = Math.floor(sevenDaysAgo.getTime() / 1000); 70 | const step = 86400 / 24; 71 | const queryURL = `http://localhost:9090/api/v1/query_range?query=kafka_server_replicamanager_underreplicatedpartitions&start=${startTimestamp}&end=${endTimestamp}&step=${step}s{broker_id=${id}}`; 72 | // const oldQuery = 73 | // 'http://localhost:9090/api/v1/query?query=kafka_server_replicamanager_underreplicatedpartitions'; 74 | try { 75 | const response = await fetchWithTimeout(queryURL); 76 | const data = await response.json(); 77 | if (data) { 78 | return data.data.result; 79 | } 80 | } catch (err) { 81 | console.log('error getting URP in broker ', id); 82 | return test.urpAlt(); 83 | } 84 | }; 85 | const displayBrokers = brokers.map((broker) => { 86 | return ( 87 | 95 | ); 96 | }); 97 | return
{displayBrokers}
; 98 | }; 99 | 100 | export default BrokersContainer; 101 | -------------------------------------------------------------------------------- /client/src/assets/testData.js: -------------------------------------------------------------------------------- 1 | const test = {} 2 | 3 | test.bytesIn = { 4 | 0: [ 5 | [1682784224, "1316274"], 6 | [1682784284, "1328934"], 7 | [1682784344, "1341594"], 8 | [1682784404, "1366274"], 9 | [1682784464, "1388934"], 10 | [1682784524, "1401594"], 11 | [1682784584, "1416274"], 12 | [1682784644, "1428934"], 13 | [1682784704, "1441594"] 14 | ], 15 | 1: [ 16 | [1682784224, "1306274"], 17 | [1682784284, "1338934"], 18 | [1682784344, "1358994"], 19 | [1682784404, "1376274"], 20 | [1682784464, "1398934"], 21 | [1682784524, "1441594"], 22 | [1682784584, "1446274"], 23 | [1682784644, "1448934"], 24 | [1682784704, "1451594"] 25 | ], 26 | 2: [ 27 | [1682784224, "1260274"], 28 | [1682784284, "1289034"], 29 | [1682784344, "1295904"], 30 | [1682784404, "1316274"], 31 | [1682784464, "1328934"], 32 | [1682784524, "1341594"], 33 | [1682784584, "1386274"], 34 | [1682784644, "1428934"], 35 | [1682784704, "1441594"] 36 | ] 37 | } 38 | 39 | test.bytesOut = { 40 | 0: [ 41 | [1682784224, "12516274"], 42 | [1682784284, "12608934"], 43 | [1682784344, "13001594"], 44 | [1682784404, "13116274"], 45 | [1682784464, "13389340"], 46 | [1682784524, "13415940"], 47 | [1682784584, "13416274"], 48 | [1682784644, "13417340"], 49 | [1682784704, "13417940"] 50 | ], 51 | 1: [ 52 | [1682784224, "12216274"], 53 | [1682784284, "12389340"], 54 | [1682784344, "12415940"], 55 | [1682784404, "12516274"], 56 | [1682784464, "12689340"], 57 | [1682784524, "12715940"], 58 | [1682784584, "12816274"], 59 | [1682784644, "12989304"], 60 | [1682784704, "13115904"] 61 | ], 62 | 2: [ 63 | [1682784224, "12316274"], 64 | [1682784284, "12389354"], 65 | [1682784344, "12395954"], 66 | [1682784404, "12516274"], 67 | [1682784464, "12689304"], 68 | [1682784524, "13415994"], 69 | [1682784584, "13516274"], 70 | [1682784644, "13689347"], 71 | [1682784704, "13715949"] 72 | ] 73 | } 74 | 75 | test.urp = { 76 | 0: [ 77 | [1682784224, "0"], 78 | [1682784284, "0"], 79 | [1682784344, "0"], 80 | [1682784404, "1"], 81 | [1682784464, "2"], 82 | [1682784524, "2"], 83 | [1682784584, "2"], 84 | [1682784644, "2"], 85 | [1682784704, "2"] 86 | ], 87 | 1: [ 88 | [1682784224, "0"], 89 | [1682784284, "2"], 90 | [1682784344, "1"], 91 | [1682784404, "0"], 92 | [1682784464, "0"], 93 | [1682784524, "1"], 94 | [1682784584, "0"], 95 | [1682784644, "0"], 96 | [1682784704, "0"] 97 | ], 98 | 2: [ 99 | [1682784224, "1"], 100 | [1682784284, "2"], 101 | [1682784344, "3"], 102 | [1682784404, "0"], 103 | [1682784464, "0"], 104 | [1682784524, "0"], 105 | [1682784584, "0"], 106 | [1682784644, "0"], 107 | [1682784704, "0"] 108 | ] 109 | } 110 | 111 | test.bytesInAlt = () => { 112 | const demoData = []; 113 | let prev = 1000000; 114 | for (let i = 0; i < 100; i++) { 115 | prev += 5000 - Math.random() * 10000; 116 | demoData.push({x: i, y: prev}); 117 | } 118 | return demoData; 119 | } 120 | test.bytesOutAlt = () => { 121 | const demoData = []; 122 | let prev = 1000000; 123 | for (let i = 0; i < 100; i++) { 124 | prev += 4000 - Math.random() * 10000; 125 | demoData.push({x: i, y: prev}); 126 | } 127 | return demoData; 128 | } 129 | 130 | test.urpAlt = () => { 131 | const urpValues = []; 132 | let prev = 0; 133 | for (let i = 0; i < 100; i++){ 134 | if (i % 5 === 0) { 135 | if (prev >= 3) prev = 0; 136 | else prev += Math.floor(Math.random() * 2) 137 | } 138 | 139 | urpValues.push({x: i, y: prev}) 140 | } 141 | return urpValues; 142 | } 143 | export default test; -------------------------------------------------------------------------------- /client/src/pages/dashboardPage/components/Charts.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Chart as ChartJS, 3 | CategoryScale, 4 | LinearScale, 5 | PointElement, 6 | LineElement, 7 | Title, 8 | Tooltip, 9 | Legend, 10 | } from 'chart.js'; 11 | import { Line } from 'react-chartjs-2'; 12 | 13 | ChartJS.register( 14 | CategoryScale, 15 | LinearScale, 16 | PointElement, 17 | LineElement, 18 | Title, 19 | Tooltip, 20 | Legend 21 | ); 22 | 23 | export function BytesInChart(bytesIn) { 24 | const bytesInOptions = { 25 | responsive: true, 26 | maintainAspectRatio: false, 27 | // animation: animation.bytesIn, 28 | plugins: { 29 | legend: { 30 | position: 'none', 31 | }, 32 | title: { 33 | display: true, 34 | text: 'Kafka Broker Metrics - Bytes In', 35 | }, 36 | }, 37 | scales: { 38 | x: { 39 | type: 'linear', 40 | display: true, 41 | title: { 42 | display: true, 43 | text: 'Time (Seconds)', 44 | }, 45 | }, 46 | y: { 47 | display: true, 48 | title: { 49 | display: true, 50 | text: 'Bytes In', 51 | }, 52 | ticks: { 53 | stepSize: 10000, 54 | }, 55 | }, 56 | }, 57 | }; 58 | // console.log('bytesIn... ', bytesIn); 59 | // get data 60 | // get bytes in/ time from bytes in array 61 | const bytesY = bytesIn?.map((tuple) => Number(tuple[1])); 62 | const startTime = bytesIn[0][0]; 63 | const timeX = bytesIn?.map((tuple, idx) => { 64 | return tuple[0] - startTime; 65 | }); 66 | const data = { 67 | labels: timeX, 68 | datasets: [ 69 | { 70 | label: 'Dataset 1', 71 | data: bytesY, 72 | borderColor: 'rgb(249, 75, 6)', 73 | backgroundColor: 'rgba(249, 75, 6, 0.5)', 74 | }, 75 | ], 76 | }; 77 | 78 | return ; 79 | } 80 | 81 | export function BytesOutChart(bytesOut) { 82 | const bytesOutOptions = { 83 | responsive: true, 84 | maintainAspectRatio: false, 85 | plugins: { 86 | legend: { 87 | position: 'none', 88 | }, 89 | title: { 90 | display: true, 91 | text: 'Kafka Broker Metrics - Bytes Out', 92 | }, 93 | }, 94 | scales: { 95 | x: { 96 | type: 'linear', 97 | display: true, 98 | title: { 99 | display: true, 100 | text: 'Time (Seconds)', 101 | }, 102 | }, 103 | y: { 104 | display: true, 105 | title: { 106 | display: true, 107 | text: 'Bytes Out', 108 | }, 109 | ticks: { 110 | stepSize: 10000, 111 | }, 112 | }, 113 | }, 114 | }; 115 | // console.log('bytesOut... ', bytesOut); 116 | // get data 117 | // get bytes out / time from bytes in array 118 | const bytesY = bytesOut?.map((tuple) => Number(tuple[1])); 119 | const startTime = bytesOut[0][0]; 120 | const timeX = bytesOut?.map((tuple, idx) => { 121 | return tuple[0] - startTime; 122 | }); 123 | const data = { 124 | labels: timeX, 125 | datasets: [ 126 | { 127 | label: 'Dataset 1', 128 | data: bytesY, 129 | borderColor: 'rgb(249, 75, 6)', 130 | backgroundColor: 'rgba(249, 75, 6, 0.5)', 131 | }, 132 | ], 133 | }; 134 | 135 | return ; 136 | } 137 | 138 | export function URPChart(URPData) { 139 | const URPOptions = { 140 | responsive: true, 141 | maintainAspectRatio: false, 142 | plugins: { 143 | legend: { 144 | // position: 'bottom', 145 | position: 'none', 146 | }, 147 | title: { 148 | display: true, 149 | text: 'Under-Replicated Partitions', 150 | }, 151 | }, 152 | scales: { 153 | x: { 154 | type: 'linear', 155 | display: true, 156 | title: { 157 | display: true, 158 | text: 'Last 7 Days', 159 | }, 160 | ticks: { 161 | stepSize: 1, 162 | }, 163 | }, 164 | y: { 165 | display: true, 166 | title: { 167 | display: true, 168 | text: 'URP Count', 169 | }, 170 | ticks: { 171 | stepSize: 1, 172 | }, 173 | }, 174 | }, 175 | }; 176 | // console.log('URP... ', URPData); 177 | 178 | const urps = URPData.map((tuple) => Number(tuple[1])); 179 | const startTime = URPData[0][0]; 180 | const timeX = URPData.map((tuple, idx) => { 181 | return tuple[0] - startTime; 182 | }); 183 | const data = { 184 | labels: timeX, 185 | datasets: [ 186 | { 187 | label: 'Dataset 1', 188 | data: urps, 189 | borderColor: 'rgb(249, 75, 6)', 190 | backgroundColor: 'rgba(249, 75, 6, 0.5)', 191 | }, 192 | ], 193 | }; 194 | 195 | return ; 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # kafkAlerts 3 | 4 | 5 | kafkAlerts logo 6 | 7 | 8 |

Open source, usability driven web application for monitoring and alerting Apache Kafka® broker metrics

9 | 10 | --- 11 | 12 | ## Table of Contents 13 | 14 | - [Product Description](#product-description) 15 | - [Install and Run](#install-and-run) 16 | - [Install Locally](#install-locally) 17 | - [How to Use](#how-to-use) 18 | - [Contribute](#contribute) 19 | - [Our Team](#our-team) 20 | - [License](#license) 21 | 22 | --- 23 | 24 | ## Product Description 25 | 26 | With usability and accessibility at the forefront of all our design decisions, kafkAlerts offers a better way to visualize Kafka broker metrics and receive alerts. Only the most relevant data renders on the screen, including what brokers are alerting and appropriate metrics for those brokers. 27 | 28 | All other brokers will be listed in your feed and can be expanded to view metrics with the click or press of a button. 29 | 30 | --- 31 | 32 | ## Install and Run 33 | 34 | To begin using kafkAlerts, navigate to kafkalerts.com and create an account. 35 | 36 | - Make sure that Prometheus is running and connected to your cluster. 37 | - Click on the user menu icon in the top right. 38 | - Click on 'Connect Cluster'. 39 | - Input your Prometheus instance and broker ids. 40 | 41 | ### Install Locally 42 | 43 | If you would prefer to run kafkAlerts locally, you may fork and clone our Github repository. 44 | 45 | - In your terminal run: 46 | - `npm install` 47 | - `npm run dev` 48 | - Once the application is running, navigate to localhost:3000 and input the port of your Prometheus server and brokers you would like to monitor. 49 | 50 | --- 51 | 52 | ## How to Use 53 | 54 | Once you have successfully installed kafkAlerts and connected to your cluster, simply log in to view all of your brokers in one place. 55 | 56 | - If any brokers are alerting, you will see them listed by id in your alerts menu. 57 | - Click on that id to be taken directly to the alerting broker. Here you can find metrics that may indicate where there is an issue. 58 | - Click on non-alerting brokers to view their metrics and see how they are generally performing. 59 | - To add more brokers, simply go to the user menu at the top, click on 'Connect Cluster' and add any additional broker ids. 60 | 61 | --- 62 | 63 | ## Contribute 64 | 65 | If you would like to contribute to this product by improving current functionality or adding a feature, please fork the repository and submit a pull request. 66 | 67 | Some of our planned features for kafkAlerts include: 68 | 69 | - Additional testing 70 | - Custom metrics 71 | - User defined accessibility templates 72 | - Adding OAuth authentication 73 | - Alerting based on user defined SLIs 74 | - Expanding metric insights 75 | 76 | --- 77 | 78 | ## Our Team 79 | 80 | Questions, comments, concerns... funny memes? Reach out to us any time! 81 | 82 | 83 | 84 | 92 | 100 | 108 | 116 | 124 | 125 |
85 | Hazel Bolivar's headshot 86 |
87 | Hazel Bolivar 88 |
89 | Github 90 | LinkedIn 91 |
93 | Ian Flynn's headshot 94 |
95 | Ian Flynn 96 |
97 | Github 98 | LinkedIn 99 |
101 | Krystal Fung's headshot 102 |
103 | Krystal Fung 104 |
105 | Github 106 | LinkedIn 107 |
109 | Annie Rosen's headshot 110 |
111 | Annie Rosen 112 |
113 | Github 114 | LinkedIn 115 |
117 | Jeb Stone's headshot 118 |
119 | Jeb Stone 120 |
121 | Github 122 | LinkedIn 123 |
126 | 127 | --- 128 | 129 | # License 130 | 131 | MIT Licensed 132 | -------------------------------------------------------------------------------- /client/src/__tests__/BrokersContainer.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import '@testing-library/jest-dom'; 4 | import {within} from '@testing-library/dom' 5 | import BrokersContainer from '../BrokersContainer'; 6 | import Broker from '../components/Broker'; 7 | import MetricOne from '../components/MetricOne'; 8 | import Button from '../components/Button'; 9 | console.log('hello '); 10 | // test if broker container has a broker property 11 | // test if broker has name and metrics property 12 | // test map function - displayBrokers should be an array of brokers 13 | // brokers should be an array 14 | 15 | // test if container renders 16 | describe('Broker Container displays properly', () => { 17 | let container; 18 | let alerts = []; 19 | let brokers = [ 20 | { 21 | name: 'Alerting-Test-Broker', 22 | metrics: [ 23 | { stat: 'backward overflow', alerting: true }, 24 | { stat: 'lag', alerting: false }, 25 | { stat: 'urp', alerting: true }, 26 | ], 27 | }, 28 | { 29 | name: 'All-Good-Test-Broker', 30 | metrics: [ 31 | { stat: 'backward overflow', alerting: false }, 32 | { stat: 'lag', alerting: false }, 33 | { stat: 'urp', alerting: false }, 34 | ], 35 | }, 36 | { 37 | name: 'Second-Alerting-Test-Broker', 38 | metrics: [ 39 | { stat: 'backward overflow', alerting: false }, 40 | { stat: 'lag', alerting: false }, 41 | { stat: 'urp', alerting: true }, 42 | ], 43 | }, 44 | ]; 45 | // render(); 46 | // const { getByText } = render(); 47 | beforeAll(() => { 48 | container = render() 49 | }); 50 | 51 | test('BrokersContainer should render to DOM', () => { 52 | // render(); 53 | const heading = container.getByText('Brokers'); 54 | expect(heading).toBeInTheDocument(); 55 | }); 56 | 57 | test('BrokersContainer renders number of brokers equal to brokers.length with name of broker on page', () => { 58 | // how to get number of broker components rendered on screen? 59 | let numBrokers = 0; 60 | for(let broker of brokers){ 61 | let brokerName = broker.name; 62 | let brokerMetrics = broker.metrics; 63 | 64 | render() 65 | 66 | let renderedBroker = screen.getByText(brokerName) 67 | expect(renderedBroker).toBeInTheDocument(); 68 | numBrokers++; 69 | } 70 | // expect(getAllByText(brokers[0].metrics[0]).toEqual(brokers.length)) 71 | expect(brokers.length).toEqual(numBrokers); 72 | 73 | }); 74 | 75 | test('Brokers names are displayed on the page', () => { 76 | const broker1Name = brokers[0].name; 77 | const broker1Metrics = brokers[0].metrics; 78 | const broker2Name = brokers[1].name; 79 | const broker2Metrics = brokers[1].metrics; 80 | 81 | render() 82 | render() 83 | 84 | const broker1 = screen.getByText(broker1Name); 85 | const broker2 = screen.getByText(broker2Name); 86 | 87 | expect(broker1).toBeInTheDocument(); 88 | expect(broker1).toHaveTextContent('brokerOne'); 89 | expect(broker2).toBeInTheDocument(); 90 | expect(broker2).toHaveTextContent('brokerTwo'); 91 | }) 92 | 93 | }); 94 | 95 | // check if button has functionality 96 | describe('Broker components... ', () => { 97 | let container; 98 | let brokers = [ 99 | { 100 | name: 'Alerting-Test-Broker', 101 | metrics: [ 102 | { stat: 'backward overflow', alerting: true }, 103 | { stat: 'lag', alerting: false }, 104 | { stat: 'urp', alerting: true }, 105 | ], 106 | }, 107 | { 108 | name: 'All-Good-Test-Broker', 109 | metrics: [ 110 | { stat: 'backward overflow', alerting: false }, 111 | { stat: 'lag', alerting: false }, 112 | { stat: 'urp', alerting: false }, 113 | ], 114 | }, 115 | { 116 | name: 'Second-Alerting-Test-Broker', 117 | metrics: [ 118 | { stat: 'backward overflow', alerting: false }, 119 | { stat: 'lag', alerting: false }, 120 | { stat: 'urp', alerting: true }, 121 | ], 122 | }, 123 | ]; 124 | let broker = {name: 'brokerOne', metrics: ['lag', 'backwards overflow', 'urp'], key:'123'} 125 | let brokerName = broker.name; 126 | let brokerMetrics = broker.metrics; 127 | let isShowing = false; 128 | const handleClick = jest.fn(isShowing => !isShowing); 129 | 130 | // render(); 131 | // const { getByText } = render(); 132 | beforeAll(() => { 133 | // container = render() 134 | broker = render() 135 | }); 136 | 137 | test('broker has a button', () => { 138 | //render() 139 | const buttons = screen.getAllByRole('button'); 140 | for(let button of buttons) expect(button).toBeInTheDocument(); 141 | }); 142 | 143 | test('button says show/hide metrics', () => { 144 | render() 145 | const buttons = screen.getAllByRole('button'); 146 | for(let button of buttons) expect(button).toHaveTextContent('Show/Hide Metrics'); 147 | }); 148 | 149 | test('button is clickable', () => { 150 | render() 151 | 152 | const button = render(