├── 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 |
11 |
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 |
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 |
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(