├── .gitignore ├── README.md ├── client ├── App.jsx ├── actions │ ├── actionTypes.js │ └── actions.js ├── components │ ├── BrokerDisplay.jsx │ ├── ConnectCluster.jsx │ ├── ConsumerDisplay.jsx │ ├── DisconnectCluster.jsx │ ├── NetworkDisplay.jsx │ ├── PortAlert.jsx │ ├── ProducerDisplay.jsx │ ├── Sidebar.jsx │ ├── UnderConstruction.jsx │ ├── charts │ │ ├── BarChart.jsx │ │ ├── LineChart.jsx │ │ ├── MultipleLineChart.jsx │ │ └── ScoreCard.jsx │ └── timeFunction.js ├── images │ └── monokl_white.svg ├── index.js ├── reducers │ ├── index.js │ └── mainReducer.js ├── store.js └── styles.css ├── icon └── icon.png ├── index.html ├── main.js ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | build 4 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 |

An open source monitoring tool for Apache Kafka 4 |

5 | 6 | 7 | 8 | 9 |

10 | 11 | 12 | 13 | last commit 14 | Repo stars 15 |

16 | 17 | 18 | ## Table of Contents 19 | 20 | * [Features](#features) 21 | * [Demo](#demo) 22 | * [Installation](#installation) 23 | * [Engineering Team](#monokl-engineering-team) 24 | 27 | ## Features 28 | * ### User-friendly GUI 29 | * ### Insights into cluster brokers, producers, consumers, and network metrics 30 | * ### Graphical displays of key performance metrics 31 | 32 | 33 | ## Demo 34 |
35 |

Enter the Prometheus port where your Kafka instance is running

36 |

37 | Enter Ports 38 |

39 |
40 |

Upon successful submission, critical information about your brokers, topics, and consumer groups becomes immediately available

41 |

42 | Cluster Overview 43 |

44 |
45 | 46 | 47 | ## Installation 48 | - To download our desktop application, please visit the releases and downlaod the appropriate file for your OS. (*Note: If you are using the desktop application, you may need to configure your privacy or security settings to allow the application to open without being verified.) 49 | - You can also clone the repository directly from GitHub and run these commands in your terminal: 50 | 1. npm install 51 | 2. npm run watch 52 | 3. npm start (in a separate terminal) 53 | - Monokl requires a Kafka Cluster configured with JMX and Prometheus to capture advanced metrics. 54 | - Enter the port number for Prometheus to verify connection 55 | - Select through various Displays for up to date chart information 56 | 57 |

58 | 59 |

60 | 61 | ## Monokl Engineering Team 62 | [Wesley Mungal](https://github.com/lagnum22) 63 | | [Savitri Beaver](https://github.com/savybeav) 64 | | [Tyler Holt](https://github.com/tylerprestonholt) 65 | 66 | We welcome contributions, so please feel free to fork, clone, and help monokl grow! Remember to leave a [![GitHub stars](https://img.shields.io/github/stars/oslabs-beta/monokl?style=social&label=Star&)](https://github.com/oslabs-beta/monokl/stargazers) if you'd like to support our work! 67 | 71 | 72 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Sidebar from './components/Sidebar.jsx'; 3 | 4 | function App() { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /client/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_PORT = "ADD_PORT"; 2 | export const REMOVE_PORT = "REMOVE_PORT"; 3 | export const ADD_CONNECTION_TIME = 'ADD_CONNECTION_TIME'; 4 | 5 | -------------------------------------------------------------------------------- /client/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from "./actionTypes.js"; 2 | 3 | //action creators below 4 | export const addPortAction = (userPort) => { 5 | return { 6 | type: types.ADD_PORT, 7 | payload: userPort, 8 | }; 9 | }; 10 | 11 | export const removePortAction = () => { 12 | return { 13 | type: types.REMOVE_PORT, 14 | payload: "", 15 | }; 16 | }; 17 | 18 | export const addConnectionTimeAction = (timestamp) => { 19 | return { 20 | type: types.ADD_CONNECTION_TIME, 21 | payload: timestamp, 22 | }; 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /client/components/BrokerDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import { connect } from "react-redux"; 4 | //Material UI - Core 5 | import {Paper, Grid } from "@material-ui/core" 6 | //Chart Components 7 | import LineChart from "./charts/LineChart.jsx"; 8 | import ScoreCard from "./charts/ScoreCard.jsx"; 9 | //Time Function 10 | import { timeFunction } from "./timeFunction.js"; 11 | 12 | const useStyles = makeStyles((theme) => ({ 13 | root: { 14 | display: "flex", 15 | flexDirection: 'column', 16 | flexGrow: "1", 17 | alignItems: "center", 18 | }, 19 | title: { 20 | color: "rgba(0, 0, 0, 0.54)", 21 | fontSize: "42px", 22 | fontWeight: "bold", 23 | }, 24 | paper: { 25 | padding: theme.spacing(2), 26 | textAlign: "center", 27 | color: theme.palette.text.secondary, 28 | minHeight: "180px" 29 | }, 30 | })); 31 | 32 | const mapStateToProps = (state) => { 33 | return { 34 | port: state.mainReducer.port, 35 | connectionTime: state.mainReducer.connectionTime 36 | }; 37 | }; 38 | 39 | function BrokerDisplay(props) { 40 | const classes = useStyles(); 41 | // component state properties for each charts x and y axis 42 | const [underReplicatedPartitions, setURP] = useState([]); 43 | const [activeControllerCount, setActiveController] = useState([]); 44 | const [offlinePartitions, setOfflinePartitions] = useState([]); 45 | const [xArrayLeader, setXArrayLeader] = useState([]); 46 | const [yArrayLeader, setYArrayLeader] = useState([]); 47 | const [xArrayTTFetch, setXArrayTTFetch] = useState([]); 48 | const [yArrayTTFetch, setYArrayTTFetch] = useState([]); 49 | const [xArrayPurg, setXArrayPurg] = useState([]); 50 | const [yArrayPurg, setYArrayPurg] = useState([]); 51 | const [xArrayIn, setXArrayIn] = useState([]); 52 | const [yArrayIn, setYArrayIn] = useState([]); 53 | const [xArrayOut, setXArrayOut] = useState([]); 54 | const [yArrayOut, setYArrayOut] = useState([]); 55 | 56 | //calculate the interval 57 | let interval = timeFunction(props.connectionTime); 58 | 59 | useEffect(() => { 60 | //1. Under Replicated Partitions Count (Score Card) 61 | let underReplicated = fetch( 62 | `http://localhost:${props.port}/api/v1/query?query=kafka_server_replicamanager_underreplicatedpartitions` 63 | ).then((respose) => respose.json()); 64 | 65 | //2. Active Controller Count (Score Card) 66 | let activeController = fetch( 67 | `http://localhost:${props.port}/api/v1/query?query=kafka_controller_kafkacontroller_activecontrollercount` 68 | ).then((respose) => respose.json()); 69 | 70 | //3. Offline Partitions Count (Score Card) 71 | let offlinePartitions = fetch( 72 | `http://localhost:${props.port}/api/v1/query?query=kafka_controller_kafkacontroller_offlinepartitionscount` 73 | ).then((respose) => respose.json()); 74 | 75 | //4. Leader Election Rate and Time Ms 76 | let leaderElectionRateMs = fetch( 77 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_controller_controllerstats_leaderelectionrateandtimems_count&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 78 | ).then((respose) => respose.json()); 79 | 80 | //5. Fetch Consumer Total Time 81 | let fetchConsumerTime = fetch( 82 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_network_requestmetrics_totaltimems{request="FetchConsumer"}&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 83 | ).then((respose) => respose.json()); 84 | 85 | //6. Purgatory Size 86 | let purgatorySizeFetch = fetch( 87 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_delayedoperationpurgatory_purgatorysize{delayedOperation="Fetch"}&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 88 | ).then((respose) => respose.json()); 89 | 90 | //7. Bytes In Total (Range) 91 | let bytesIn = fetch( 92 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_brokertopicmetrics_bytesin_total&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 93 | ).then((respose) => respose.json()); 94 | //8. Bytes Out Total (Range) 95 | let bytesOut = fetch( 96 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_brokertopicmetrics_bytesout_total&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 97 | ).then((respose) => respose.json()); 98 | 99 | Promise.all([underReplicated, activeController, offlinePartitions, leaderElectionRateMs, fetchConsumerTime, purgatorySizeFetch, bytesIn, bytesOut]) 100 | .then((allData) => { 101 | //1. Under Replicated Partitions Chart 102 | setURP(allData[0].data.result[0].value[1]) 103 | 104 | //2. Active Controller Count Chart 105 | setActiveController(allData[1].data.result[0].value[1]) 106 | 107 | //3. Offline Partitions Count Chart 108 | setOfflinePartitions(allData[2].data.result[0].value[1]) 109 | 110 | //4. Leader Election Rate Ms Chart 111 | setXArrayLeader(allData[3].data.result[0].values.map((data) => { 112 | let date = new Date(data[0] * 1000); 113 | return date.toLocaleTimeString('en-GB'); 114 | })) 115 | setYArrayLeader(allData[3].data.result[0].values.map((data) => Number(data[1]))) 116 | 117 | //5. Fetch Consumer Time Chart 118 | setXArrayTTFetch(allData[4].data.result[0].values.map((data) => { 119 | let date = new Date(data[0] * 1000); 120 | return date.toLocaleTimeString('en-GB'); 121 | })); 122 | setYArrayTTFetch(allData[4].data.result[0].values.map((data) => Number(data[1]))); 123 | 124 | //6. Purgatory Size Chart 125 | setXArrayPurg(allData[5].data.result[0].values.map((data) => { 126 | let date = new Date(data[0] * 1000); 127 | return date.toLocaleTimeString('en-GB'); 128 | })); 129 | setYArrayPurg(allData[5].data.result[0].values.map((data) => Number(data[1]))); 130 | 131 | //7. Bytes In Chart 132 | setXArrayIn(allData[6].data.result[0].values.map((data) => { 133 | let date = new Date(data[0] * 1000); 134 | return date.toLocaleTimeString('en-GB'); 135 | })); 136 | setYArrayIn(allData[6].data.result[0].values.map((data) => Number(data[1]))); 137 | 138 | //8. Bytes Out Chart 139 | setXArrayOut(allData[7].data.result[0].values.map((data) => { 140 | let date = new Date(data[0] * 1000); 141 | return date.toLocaleTimeString('en-GB'); 142 | })); 143 | setYArrayOut(allData[7].data.result[0].values.map((data) => Number(data[1]))); 144 | }) 145 | .catch(err => console.log(err)) 146 | }, []) 147 | 148 | return ( 149 | <> 150 |
151 |

Broker Metrics

152 | 153 | {/* 1. Under Replicated Partitions */} 154 | 155 | 156 | 160 | 161 | 162 | {/* 2. Active Controller Count */} 163 | 164 | 165 | 169 | 170 | 171 | {/* 3. Offline Partition Count */} 172 | 173 | 174 | 178 | 179 | 180 | {/* 4. Leader Election Rate and Time Ms */} 181 | 182 | 183 | 189 | 190 | 191 | {/* 5. Fetch Consumer Time - 50th quantile*/} 192 | 193 | 194 | 200 | 201 | 202 | {/* 6. Purgatory Size */} 203 | 204 | 205 | 211 | 212 | 213 | {/* 7. Bytes In */} 214 | 215 | 216 | 222 | 223 | 224 | {/* 8. Bytes Out */} 225 | 226 | 227 | 233 | 234 | 235 | 236 |
237 | 238 | ); 239 | } 240 | 241 | export default connect(mapStateToProps, null)(BrokerDisplay); 242 | -------------------------------------------------------------------------------- /client/components/ConnectCluster.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { addPortAction, addConnectionTimeAction } from "../actions/actions"; 3 | import { connect } from "react-redux"; 4 | //Material UI - Core 5 | import {TextField,Button } from "@material-ui/core" 6 | //Material UI- Styles 7 | import { makeStyles } from "@material-ui/core/styles"; 8 | //Application Components 9 | import PortAlert from './PortAlert.jsx'; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | root: { 13 | display: "flex", 14 | flexDirection: "column", 15 | justifyContent: "center", 16 | alignItems: "center", 17 | marginTop: "185px", 18 | "& .MuiTextField-root": { 19 | margin: theme.spacing(1), 20 | width: "35ch", 21 | }, 22 | }, 23 | connect: { 24 | "& > *": { 25 | margin: theme.spacing(1), 26 | color: "#537791", 27 | }, 28 | }, 29 | })); 30 | 31 | const mapStateToProps = (state) => { 32 | return { 33 | port: state.mainReducer.port, 34 | }; 35 | }; 36 | 37 | const mapDispatchToProps = (dispatch) => { 38 | return { 39 | addPortAction: (userInput) => { 40 | dispatch(addPortAction(userInput)); 41 | }, 42 | addConnectionTimeAction: (timestamp) => { 43 | dispatch(addConnectionTimeAction(timestamp)); 44 | } 45 | }; 46 | }; 47 | 48 | const verifyPort = async (port) => { 49 | let valid = false; 50 | const url = `http://localhost:${port}/api/v1/query?query=up`; 51 | await fetch(url) 52 | .then(res => res.json()) 53 | .then(data => { 54 | if (data.status === 'success') valid = true; 55 | }) 56 | .catch(err => console.log(err)); 57 | return valid; 58 | } 59 | 60 | function ConnectCluster(props) { 61 | const classes = useStyles(); 62 | const [attempts, addAttempt] = useState(0); 63 | 64 | return ( 65 | <> 66 |
67 | {attempts > 0 ? : <> } 68 | 74 | 93 | 94 | 95 | ); 96 | } 97 | 98 | export default connect(mapStateToProps, mapDispatchToProps)(ConnectCluster); 99 | -------------------------------------------------------------------------------- /client/components/ConsumerDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import { connect } from "react-redux"; 4 | //Material UI - Core 5 | import {Paper, Grid } from "@material-ui/core" 6 | //App Chart Components 7 | import MultipleLineChart from "./charts/MultipleLineChart.jsx"; 8 | import { timeFunction } from "./timeFunction.js"; 9 | 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | root: { 13 | display: "flex", 14 | flexDirection: 'column', 15 | flexGrow: "1", 16 | alignItems: "center", 17 | }, 18 | title: { 19 | color: "rgba(0, 0, 0, 0.54)", 20 | fontSize: "42px", 21 | fontWeight: "bold", 22 | }, 23 | paper: { 24 | padding: theme.spacing(2), 25 | textAlign: "center", 26 | color: theme.palette.text.secondary, 27 | }, 28 | })); 29 | 30 | const mapStateToProps = (state) => { 31 | return { 32 | port: state.mainReducer.port, 33 | connectionTime: state.mainReducer.connectionTime 34 | }; 35 | }; 36 | 37 | function ConsumerDisplay(props) { 38 | const classes = useStyles(); 39 | // component state properties for charts x and y axes 40 | const [xArray50, setXArray] = useState([]); 41 | const [yArray50, setYArray50] = useState([]); 42 | const [yArray75, setXArray75] = useState([]); 43 | const [yArray95, setXArray95] = useState([]); 44 | const [yArray99, setXArray99] = useState([]); 45 | 46 | //calculate the interval 47 | let interval = timeFunction(props.connectionTime); 48 | 49 | useEffect(() => { 50 | fetch( 51 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_network_requestmetrics_totaltimems{request="FetchConsumer"}&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 52 | ) 53 | .then((respose) => respose.json()) 54 | .then((res) => { 55 | // 1. Network Request Metrics Time 56 | setXArray(res.data.result[0].values.map((data) => { 57 | let date = new Date(data[0] * 1000); 58 | return date.toLocaleTimeString('en-GB'); 59 | })) 60 | setYArray50(res.data.result[0].values.map((data) => Number(data[1]))) 61 | setXArray75(res.data.result[1].values.map((data) => Number(data[1]))) 62 | setXArray95(res.data.result[2].values.map((data) => Number(data[1]))) 63 | setXArray99(res.data.result[4].values.map((data) => Number(data[1]))) 64 | }) 65 | .catch(err => console.log(err)); 66 | }, []); 67 | 68 | return ( 69 |
70 |

Consumer Metrics

71 | 72 | {/*1. Network Request Metrics Time */} 73 | 74 | 75 | 89 | 90 | 91 | 92 |
93 | ); 94 | } 95 | 96 | export default connect(mapStateToProps, null)(ConsumerDisplay); 97 | -------------------------------------------------------------------------------- /client/components/DisconnectCluster.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { removePortAction } from "../actions/actions"; 3 | import { connect } from "react-redux"; 4 | //Material UI - Core 5 | import {Button, Paper} from "@material-ui/core" 6 | //Material UI - Styles 7 | import { makeStyles } from "@material-ui/core/styles"; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | root: { 11 | display: "flex", 12 | flexDirection: "column", 13 | justifyContent: "center", 14 | flexGrow: "1", 15 | marginTop: "140px", 16 | alignItems: "center", 17 | }, 18 | paper: { 19 | padding: theme.spacing(3), 20 | marginBottom: '8px', 21 | textAlign: "center", 22 | color: theme.palette.text.secondary, 23 | }, 24 | 25 | connect: { 26 | "& > *": { 27 | margin: theme.spacing(1), 28 | color: "#537791", 29 | }, 30 | }, 31 | })); 32 | 33 | const mapStateToProps = (state) => { 34 | return { 35 | port: state.mainReducer.port, 36 | connectionTime: state.mainReducer.connectionTime 37 | }; 38 | }; 39 | 40 | const mapDispatchToProps = (dispatch) => { 41 | return { 42 | removePortAction: () => { 43 | dispatch(removePortAction()); 44 | }, 45 | }; 46 | }; 47 | 48 | function DisconnectCluster(props) { 49 | const classes = useStyles(); 50 | 51 | return ( 52 |
53 | 54 |

Successfully connected to Prometheus on port: {props.port}

55 |
56 | 67 |
68 | ); 69 | } 70 | 71 | export default connect(mapStateToProps, mapDispatchToProps)(DisconnectCluster); 72 | -------------------------------------------------------------------------------- /client/components/NetworkDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { connect } from "react-redux"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | //Material UI - Core 5 | import {Paper, Grid } from "@material-ui/core" 6 | //Application Chart Components 7 | import LineChart from "./charts/LineChart.jsx"; 8 | //Time Function 9 | import { timeFunction } from "./timeFunction.js"; 10 | 11 | 12 | 13 | const useStyles = makeStyles((theme) => ({ 14 | root: { 15 | display: "flex", 16 | flexDirection: 'column', 17 | flexGrow: "1", 18 | alignItems: "center", 19 | }, 20 | title: { 21 | color: "rgba(0, 0, 0, 0.54)", 22 | fontSize: "42px", 23 | fontWeight: "bold", 24 | }, 25 | paper: { 26 | padding: theme.spacing(2), 27 | textAlign: "center", 28 | color: theme.palette.text.secondary, 29 | }, 30 | })); 31 | 32 | const mapStateToProps = (state) => { 33 | return { 34 | port: state.mainReducer.port, 35 | connectionTime: state.mainReducer.connectionTime 36 | }; 37 | }; 38 | 39 | const mapDispatchToProps = (dispatch) => { 40 | return { 41 | fetchNetworkMetrics: () => { 42 | dispatch(fetchNetworkMetrics()); 43 | }, 44 | }; 45 | }; 46 | 47 | function NetworkDisplay(props) { 48 | const classes = useStyles(); 49 | // component state properties chart x and y axis 50 | const [xArrayIdle, setXArray] = useState([]); 51 | const [yArrayIdle, setYArray] = useState([]); 52 | 53 | //calculate the interval 54 | let interval = timeFunction(props.connectionTime); 55 | 56 | useEffect(() => { 57 | fetch( 58 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_network_socketserver_networkprocessoravgidlepercent&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 59 | ) 60 | .then((response) => response.json()) 61 | .then((res)=>{ 62 | //1. Network Processor Avg Idle Percentage 63 | setXArray(res.data.result[0].values.map((data) => { 64 | let date = new Date(data[0] * 1000); 65 | return date.toLocaleTimeString('en-GB'); 66 | })) 67 | setYArray(res.data.result[0].values.map((data) => Number(data[1]))) 68 | console.log('this is x and y array: ', setXArray, setYArray) 69 | }) 70 | .catch(err => console.log(err)) 71 | }, []); 72 | 73 | return ( 74 |
75 |

Network Metrics

76 | 77 | {/* 1. network Processor Avg Idle Percentage */} 78 | 79 | 80 | 86 | 87 | 88 | 89 |
90 | ); 91 | } 92 | 93 | export default connect(mapStateToProps, mapDispatchToProps)(NetworkDisplay); 94 | -------------------------------------------------------------------------------- /client/components/PortAlert.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | //Material UI - Material 3 | import {Alert,Stack } from "@mui/material" 4 | 5 | 6 | export default function PortAlert() { 7 | return ( 8 | 9 | 10 | Invalid port number — please try again. 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/components/ProducerDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { connect } from "react-redux"; 3 | //Material UI - Core 4 | import {Paper, Grid } from "@material-ui/core" 5 | //Material UI - Styles 6 | import { makeStyles } from "@material-ui/core/styles"; 7 | //Application Chart Components 8 | import LineChart from "./charts/LineChart.jsx"; 9 | import MultipleLineChart from "./charts/MultipleLineChart.jsx"; 10 | import ScoreCard from "./charts/ScoreCard.jsx"; 11 | //Time Function 12 | import { timeFunction } from "./timeFunction.js"; 13 | 14 | const useStyles = makeStyles((theme) => ({ 15 | root: { 16 | display: "flex", 17 | flexGrow: "1", 18 | flexDirection: "column", 19 | alignItems: "center", 20 | }, 21 | title: { 22 | color: "rgba(0, 0, 0, 0.54)", 23 | fontSize: "42px", 24 | fontWeight: "bold", 25 | }, 26 | paper: { 27 | padding: theme.spacing(2), 28 | textAlign: "center", 29 | color: theme.palette.text.secondary, 30 | }, 31 | })); 32 | 33 | //use a mapstatetoprops to pass data to both line charts 34 | const mapStateToProps = (state) => { 35 | return { 36 | port: state.mainReducer.port, 37 | connectionTime: state.mainReducer.connectionTime 38 | }; 39 | }; 40 | 41 | function ProducerDisplay(props) { 42 | const classes = useStyles(); 43 | // component state properties for each charts x and y axis 44 | const [xArray50, setXArray] = useState([]); 45 | const [yArray50, setYArray50] = useState([]); 46 | const [yArray75, setYArray75] = useState([]); 47 | const [yArray95, setYArray95] = useState([]); 48 | const [yArray99, setYArray99] = useState([]); 49 | const [xArrayTotalProducer, setXArrayTotalProducer] = useState([]); 50 | const [yArrayTotalProducer, setYArrayTotalProducer] = useState([]); 51 | const [xFailedProducerRequest, setXArrayFailedProducerRequest] = useState([]); 52 | const [yFailedProducerRequest, setYArrayFailedProducerRequest] = useState([]); 53 | 54 | //calculate the interval 55 | let interval = timeFunction(props.connectionTime); 56 | 57 | useEffect(() => { 58 | //1. Total Time for Producer Requests 59 | let totalTimeMs = fetch( 60 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_network_requestmetrics_totaltimems{request="Produce"}&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 61 | ).then((respose) => respose.json()); 62 | 63 | //2. Total Producer Requests= (Aggregate) 64 | let producerReqsTotal = fetch( 65 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_brokertopicmetrics_totalproducerequests_total&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 66 | ).then((respose) => respose.json()); 67 | 68 | //3. Failed Producer Requests (Aggregate) 69 | let failedProducerReqs = fetch( 70 | `http://localhost:${props.port}/api/v1/query_range?query=kafka_server_brokertopicmetrics_failedproducerequests_total&start=${props.connectionTime}&end=${new Date().toISOString()}&step=${interval.toString()}s` 71 | ).then((respose) => respose.json()); 72 | 73 | Promise.all([totalTimeMs, producerReqsTotal, failedProducerReqs]) 74 | .then((allData) => { 75 | //1. Total Time in Ms for Producer Requests 76 | setXArray(allData[0].data.result[0].values.map((data) => { 77 | let date = new Date(data[0] * 1000); 78 | return date.toLocaleTimeString('en-GB'); 79 | })) 80 | setYArray50(allData[0].data.result[0].values.map((data) => Number(data[1]))) 81 | setYArray75(allData[0].data.result[1].values.map((data) => Number(data[1]))) 82 | setYArray95(allData[0].data.result[2].values.map((data) => Number(data[1]))) 83 | setYArray99(allData[0].data.result[4].values.map((data) => Number(data[1]))) 84 | //2. Total Producer Requests Chart 85 | setXArrayTotalProducer(allData[1].data.result[0].values.map((data) => { 86 | let date = new Date(data[0] * 1000); 87 | return date.toLocaleTimeString('en-GB'); 88 | })) 89 | setYArrayTotalProducer(allData[1].data.result[0].values.map((data)=> Number(data[1]))) 90 | //3. Failed Producer Requests Chart 91 | setXArrayFailedProducerRequest(allData[2].data.result[0].values.map((data) => { 92 | let date = new Date(data[0] * 1000); 93 | return date.toLocaleTimeString('en-GB'); 94 | })) 95 | setYArrayFailedProducerRequest(allData[2].data.result[0].values.map((data) => Number(data[1]))) 96 | }) 97 | .catch(err => console.log(err)); 98 | }, []); 99 | 100 | return ( 101 |
102 |

Producer Metrics

103 | 104 | {/* 1. Total Time for Producer Requests */} 105 | 106 | 107 | 121 | 122 | 123 | {/* Total Producer Requests */} 124 | 125 | 126 | 132 | 133 | 134 | {/* Failed Producer Requests */} 135 | 136 | 137 | 143 | 144 | 145 | 146 |
147 | ); 148 | } 149 | 150 | export default connect(mapStateToProps, null)(ProducerDisplay); 151 | -------------------------------------------------------------------------------- /client/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | //React Router 4 | import { HashRouter, Switch, Route, Redirect, Link } from "react-router-dom"; 5 | //Material UI 6 | import clsx from "clsx"; 7 | //Material UI Components - Core 8 | import { Drawer, AppBar, Toolbar, List, CssBaseline, Typography, Divider, IconButton, ListItem, ListItemIcon, ListItemText } from "@material-ui/core" 9 | //Material UI Icons 10 | import MenuIcon from "@material-ui/icons/Menu"; 11 | import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; 12 | import ChevronRightIcon from "@material-ui/icons/ChevronRight"; 13 | import ConnectIcon from "@material-ui/icons/SettingsEthernetOutlined"; 14 | import AlertIcon from "@material-ui/icons/ErrorOutlineOutlined"; 15 | import BrokerIcon from '@mui/icons-material/OpenWithOutlined'; 16 | import ProducerIcon from '@mui/icons-material/StoreOutlined'; 17 | import ConsumerIcon from '@mui/icons-material/ShoppingBagOutlined'; 18 | import NetworkIcon from "@material-ui/icons/NetworkCheckOutlined"; 19 | import AlertSettingsIcon from "@material-ui/icons/SettingsApplicationsOutlined"; 20 | //Material UI - Styles 21 | import { makeStyles, useTheme } from "@material-ui/core/styles"; 22 | //Application Components 23 | import ConnectCluster from "./ConnectCluster.jsx"; 24 | import DisconnectCluster from "./DisconnectCluster.jsx"; 25 | import BrokerDisplay from "./BrokerDisplay.jsx"; 26 | import ProducerDisplay from "./ProducerDisplay.jsx"; 27 | import ConsumerDisplay from "./ConsumerDisplay.jsx"; 28 | import NetworkDisplay from "./NetworkDisplay.jsx"; 29 | import UnderConstruction from "./UnderConstruction.jsx"; 30 | //Logo 31 | import Logo from "../images/monokl_white.svg"; 32 | 33 | const drawerWidth = 240; 34 | 35 | const useStyles = makeStyles((theme) => ({ 36 | root: { 37 | display: "flex", 38 | minHeight: "100vh", 39 | }, 40 | appBar: { 41 | backgroundColor: "#537791", 42 | zIndex: theme.zIndex.drawer + 1, 43 | transition: theme.transitions.create(["width", "margin"], { 44 | easing: theme.transitions.easing.sharp, 45 | duration: theme.transitions.duration.leavingScreen, 46 | }), 47 | }, 48 | appBarShift: { 49 | marginLeft: drawerWidth, 50 | width: `calc(100% - ${drawerWidth}px)`, 51 | transition: theme.transitions.create(["width", "margin"], { 52 | easing: theme.transitions.easing.sharp, 53 | duration: theme.transitions.duration.enteringScreen, 54 | }), 55 | }, 56 | menuButton: { 57 | marginRight: 36, 58 | }, 59 | hide: { 60 | display: "none", 61 | }, 62 | drawer: { 63 | width: drawerWidth, 64 | flexShrink: 0, 65 | whiteSpace: "nowrap", 66 | }, 67 | drawerOpen: { 68 | width: drawerWidth, 69 | transition: theme.transitions.create("width", { 70 | easing: theme.transitions.easing.sharp, 71 | duration: theme.transitions.duration.enteringScreen, 72 | }), 73 | }, 74 | drawerClose: { 75 | transition: theme.transitions.create("width", { 76 | easing: theme.transitions.easing.sharp, 77 | duration: theme.transitions.duration.leavingScreen, 78 | }), 79 | overflowX: "hidden", 80 | width: theme.spacing(7) + 1, 81 | [theme.breakpoints.up("sm")]: { 82 | width: theme.spacing(9) + 1, 83 | }, 84 | }, 85 | toolbar: { 86 | backgroundColor: "#537791", 87 | display: "flex", 88 | alignItems: "center", 89 | justifyContent: "flex-end", 90 | padding: theme.spacing(0, 1), 91 | // necessary for content to be below app bar 92 | ...theme.mixins.toolbar, 93 | }, 94 | content: { 95 | flexGrow: "1", 96 | padding: theme.spacing(3), 97 | marginTop: "50px", 98 | minWidth: "380px", 99 | }, 100 | logo: { 101 | backgroundSize: "20px 30px", 102 | marginTop: "8px" 103 | } 104 | })); 105 | 106 | const mapStateToProps = (state) => ({ 107 | port: state.mainReducer.port, 108 | }); 109 | 110 | function Sidebar(props) { 111 | const classes = useStyles(); 112 | const theme = useTheme(); 113 | const [open, setOpen] = React.useState(false); 114 | 115 | const handleDrawerOpen = () => { 116 | setOpen(true); 117 | }; 118 | 119 | const handleDrawerClose = () => { 120 | setOpen(false); 121 | }; 122 | 123 | return ( 124 |
125 | 126 | 132 | 133 | 142 | 143 | 144 | 145 | {/* */} 146 | 147 | 148 | 149 | 162 |
163 | 164 | {theme.direction === "rtl" ? ( 165 | 166 | ) : ( 167 | 168 | )} 169 | 170 |
171 | 172 | 173 | 174 | {} 175 | 176 | 177 | {(() => { 178 | if (props.port) { 179 | return ( 180 | <> 181 | 187 | {} 188 | 189 | 190 | 196 | {} 197 | 198 | 199 | 205 | {} 206 | 207 | 208 | 214 | {} 215 | 216 | 217 | 223 | {} 224 | 225 | 226 | 227 | ); 228 | } 229 | })()} 230 | 231 | 232 | {(() => { 233 | if (props.port) { 234 | return ( 235 | <> 236 | 237 | 243 | {} 244 | 245 | 246 | 247 | 248 | ); 249 | } 250 | })()} 251 |
252 |
253 | 254 | 258 | } 261 | /> 262 | } 265 | /> 266 | } 269 | /> 270 | } 273 | /> 274 | } 277 | /> 278 | } 281 | /> 282 | 283 |
284 |
285 |
286 | ); 287 | } 288 | 289 | export default connect(mapStateToProps, null)(Sidebar); 290 | -------------------------------------------------------------------------------- /client/components/UnderConstruction.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | root: { 6 | display: "flex", 7 | flexDirection: "column", 8 | justifyContent: "center", 9 | alignItems: "center", 10 | color: "rgba(0, 0, 0, 0.54)", 11 | fontSize: "42px", 12 | fontWeight: "bold", 13 | marginTop: "185px", 14 | "& .MuiTextField-root": { 15 | margin: theme.spacing(1), 16 | width: "35ch", 17 | }, 18 | } 19 | })); 20 | 21 | 22 | export default function UnderConstruction() { 23 | const classes = useStyles(); 24 | 25 | return ( 26 |
27 |

Coming soon!

28 |
29 | ); 30 | }; 31 | 32 | 33 | -------------------------------------------------------------------------------- /client/components/charts/BarChart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Bar } from "react-chartjs-2"; 3 | 4 | const data = { 5 | labels: ["10:00", "11:00", "12:00", "13:00", "14:00", "15:00"], 6 | datasets: [ 7 | { 8 | label: "# of Connections", 9 | data: [825, 400, 865, 740, 600, 890, 540], 10 | backgroundColor: [ 11 | "rgba(255, 99, 132, 0.2)", 12 | "rgba(54, 162, 235, 0.2)", 13 | "rgba(255, 206, 86, 0.2)", 14 | "rgba(75, 192, 192, 0.2)", 15 | "rgba(153, 102, 255, 0.2)", 16 | "rgba(255, 159, 64, 0.2)", 17 | ], 18 | borderColor: [ 19 | "black", 20 | "black", 21 | "black", 22 | "black", 23 | "black", 24 | "black", 25 | ], 26 | borderWidth: 1, 27 | }, 28 | ] 29 | } 30 | 31 | const options = { 32 | maintainAspectRatio: false 33 | } 34 | 35 | const BarChart = (props) => { 36 | return ( 37 | <> 38 |

{props.metricName}

39 |
40 | 44 |
45 | 46 | ); 47 | } 48 | 49 | export default BarChart; -------------------------------------------------------------------------------- /client/components/charts/LineChart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Line } from "react-chartjs-2"; 3 | 4 | const LineChart = (props) => { 5 | const data = { 6 | labels: props.x, 7 | datasets: [ 8 | { 9 | label: props.label, 10 | data: props.y, 11 | fill: false, 12 | backgroundColor: "#018790", 13 | borderColor: "rgba(75, 192, 192, 0.2)", 14 | }, 15 | ], 16 | }; 17 | 18 | const options = { 19 | scales: { 20 | yAxes: [ 21 | { 22 | ticks: { 23 | beginAtZero: true, 24 | }, 25 | }, 26 | ], 27 | }, 28 | }; 29 | 30 | return ( 31 | <> 32 |

{props.metricName}

33 |
34 | 35 |
36 | 37 | ); 38 | }; 39 | 40 | export default LineChart; 41 | -------------------------------------------------------------------------------- /client/components/charts/MultipleLineChart.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Line } from "react-chartjs-2"; 3 | 4 | const MultipleLineChart = (props) => { 5 | const data = { 6 | labels: props.xtime, 7 | datasets: [ 8 | { 9 | label: props.label[0], 10 | data: props.y50, 11 | fill: false, 12 | backgroundColor: "#018790", 13 | borderColor: "rgba(75, 192, 192, 0.2)", 14 | }, 15 | { 16 | label: props.label[1], 17 | data: props.y75, 18 | fill: false, 19 | backgroundColor: "#db4c96", 20 | borderColor: "rgba(219, 76, 150, 0.2)", 21 | }, 22 | { 23 | label: props.label[2], 24 | data: props.y95, 25 | fill: false, 26 | backgroundColor: "#4c61db", 27 | borderColor: "rgba(76, 97, 219, 0.2)", 28 | }, 29 | { 30 | label: props.label[3], 31 | data: props.y99, 32 | fill: false, 33 | backgroundColor: "#f79a08", 34 | borderColor: "rgba(247, 154, 8, 0.2)", 35 | }, 36 | ], 37 | }; 38 | 39 | const options = { 40 | scales: { 41 | yAxes: [ 42 | { 43 | ticks: { 44 | beginAtZero: true, 45 | }, 46 | }, 47 | ], 48 | }, 49 | }; 50 | 51 | return ( 52 | <> 53 |

{props.metricName}

54 |
55 | 56 |
57 | 58 | ); 59 | }; 60 | 61 | export default MultipleLineChart; 62 | -------------------------------------------------------------------------------- /client/components/charts/ScoreCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ScoreCard = (props) => ( 4 | <> 5 |

{props.metricName}

6 |
7 | {props.data} 8 |
9 | 10 | ); 11 | 12 | export default ScoreCard; -------------------------------------------------------------------------------- /client/components/timeFunction.js: -------------------------------------------------------------------------------- 1 | export function timeFunction(connectionTIME) { 2 | let interval = 60; 3 | let timeDifference = new Date() - new Date(connectionTIME); 4 | 5 | //Less than 1 minute(60,000 ms) 6 | if (timeDifference < 60000) { 7 | //every 1 second 8 | return (interval = 1); 9 | } 10 | //More than 1 min, Less than 5 minutes 11 | if (timeDifference > 60000 && timeDifference < 300000) { 12 | //every 1 minute 13 | return (interval = 60); 14 | } 15 | //more than 1 hour but less than 2 hours 16 | if (timeDifference > 3600000 && timeDifference < 7200000) { 17 | //every 5 minutes= 300 seconds 18 | return (interval = 300); 19 | } 20 | //More than 2 hours 21 | if (timeDifference > 14400000) { 22 | //every 30 minutes = 1800 seconds 23 | return (interval = 1800); 24 | } 25 | return interval; 26 | } 27 | -------------------------------------------------------------------------------- /client/images/monokl_white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import store from './store.js'; 5 | import App from './App.jsx'; 6 | import './styles.css'; 7 | 8 | render( 9 | 10 | 11 | , 12 | document.getElementById('root') 13 | ); 14 | -------------------------------------------------------------------------------- /client/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import mainReducers from './mainReducer.js'; 3 | 4 | export default combineReducers({ mainReducer: mainReducers }); 5 | 6 | -------------------------------------------------------------------------------- /client/reducers/mainReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from "../actions/actionTypes.js"; 2 | 3 | const initialState = { 4 | connectionTime: '', 5 | port: "", 6 | data: [], 7 | }; 8 | 9 | const mainReducer = (state = initialState, action) => { 10 | switch (action.type) { 11 | case types.ADD_PORT: 12 | return { 13 | ...state, 14 | port: action.payload, 15 | }; 16 | case types.REMOVE_PORT: 17 | return { 18 | ...state, 19 | port: action.payload, 20 | }; 21 | case types.ADD_CONNECTION_TIME: 22 | return { 23 | ...state, 24 | connectionTime: action.payload, 25 | }; 26 | default: 27 | return state; 28 | } 29 | }; 30 | 31 | export default mainReducer; 32 | 33 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import reducers from "./reducers/index.js"; 4 | 5 | const store = createStore(reducers, applyMiddleware(thunk)); 6 | 7 | export default store; 8 | 9 | -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/monokl/dc3c6b3c5f51a3e12d203e76674e1ca85e887f47/client/styles.css -------------------------------------------------------------------------------- /icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/monokl/dc3c6b3c5f51a3e12d203e76674e1ca85e887f47/icon/icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Monokl 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require("electron"); 2 | const path = require("path"); 3 | 4 | function createWindow() { 5 | const win = new BrowserWindow({ 6 | width: 1200, 7 | height: 800, 8 | backgroundColor: "white", 9 | webPreferences: { 10 | nodeIntegration: false, 11 | worldSafeExecuteJavaScript: true, 12 | contextIsolation: true, 13 | }, 14 | }); 15 | win.loadFile("index.html"); 16 | } 17 | //requried for developer environment, comment this require fuction out before packaging in Electron 18 | require("electron-reload")(__dirname, { 19 | electron: path.join(__dirname, "node_modules", ".bin", "electron"), 20 | }); 21 | 22 | app.whenReady().then(createWindow); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monokl", 3 | "version": "1.0.0", 4 | "description": "Monitoring and visualization tool for Apache Kafka", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "electron .", 9 | "watch": "webpack --config webpack.config.js --watch", 10 | "pack": "electron-builder -mwl" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@emotion/react": "^11.4.1", 17 | "@emotion/styled": "^11.3.0", 18 | "@material-ui/core": "^4.12.3", 19 | "@material-ui/icons": "^4.11.2", 20 | "@mui/icons-material": "^5.0.0", 21 | "@mui/material": "^5.0.0", 22 | "chart.js": "^3.5.1", 23 | "clsx": "^1.1.1", 24 | "react": "^17.0.2", 25 | "react-chartjs-2": "^3.0.4", 26 | "react-dom": "^17.0.2", 27 | "react-redux": "^7.2.5", 28 | "react-router-dom": "^5.3.0", 29 | "redux": "^4.1.1", 30 | "redux-thunk": "^2.3.0", 31 | "save": "^2.4.0" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.15.0", 35 | "@babel/plugin-transform-runtime": "^7.15.0", 36 | "@babel/preset-env": "^7.15.0", 37 | "@babel/preset-react": "^7.14.5", 38 | "@svgr/webpack": "^5.5.0", 39 | "babel-loader": "^8.2.2", 40 | "css-loader": "^6.2.0", 41 | "electron": "^15.0.0", 42 | "electron-builder": "^22.11.7", 43 | "electron-reload": "^2.0.0-alpha.1", 44 | "sass": "^1.38.2", 45 | "sass-loader": "^12.1.0", 46 | "style-loader": "^3.2.1", 47 | "webpack": "^5.51.1", 48 | "webpack-cli": "^4.8.0" 49 | }, 50 | "build": { 51 | "appId": "com.oslabs.monokl", 52 | "productName": "monokl", 53 | "copyright": "wes tyler savitri", 54 | "directories": { 55 | "app": ".", 56 | "output": "dist", 57 | "buildResources": "icon" 58 | }, 59 | "files": [ 60 | "package.json", 61 | "**/*", 62 | "node_modules", 63 | "!README.md", 64 | "!.gitignore" 65 | ], 66 | "dmg": { 67 | "background": null, 68 | "backgroundColor": "#ffffff", 69 | "window": { 70 | "width": "400", 71 | "height": "300" 72 | }, 73 | "contents": [ 74 | { 75 | "x": 100, 76 | "y": 100 77 | }, 78 | { 79 | "x": 300, 80 | "y": 100, 81 | "type": "link", 82 | "path": "/Applications" 83 | } 84 | ] 85 | }, 86 | "mac": { 87 | "target": "dmg", 88 | "category": "public.app-category.utilities" 89 | }, 90 | "win": { 91 | "target": "nsis" 92 | }, 93 | "linux": { 94 | "target": "AppImage", 95 | "category": "Utility" 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | mode: "development", 5 | entry: "./client/index.js", 6 | devtool: "inline-source-map", 7 | target: "electron-renderer", 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.jsx?/, 12 | exclude: /node_modules/, 13 | use: { 14 | loader: "babel-loader", 15 | options: { 16 | presets: ["@babel/preset-react", "@babel/preset-env"], 17 | plugins: ["@babel/plugin-transform-runtime"] 18 | }, //end of options 19 | }, 20 | }, 21 | { 22 | test: [/\.s[ac]ss$/i, /\.css$/i], 23 | use: [ 24 | // Creates `style` nodes from JS strings 25 | "style-loader", 26 | // Translates CSS into CommonJS 27 | "css-loader", 28 | // Compiles Sass to CSS 29 | "sass-loader", 30 | ], 31 | }, 32 | { 33 | test: /\.svg$/, 34 | use: ['@svgr/webpack'], 35 | }, 36 | ], 37 | }, 38 | resolve: { 39 | extensions: [".js"], 40 | }, 41 | output: { 42 | filename: "app.js", 43 | path: path.resolve(__dirname, "build"), 44 | }, 45 | }; 46 | 47 | // presets: [ 48 | // [ 49 | // "@babel/preset-env", 50 | // { 51 | // targets: { 52 | // esmodules: true, 53 | // }, 54 | // }, 55 | // ], 56 | // "@babel/preset-react", 57 | // ], //end of presets 58 | --------------------------------------------------------------------------------