├── .DS_Store
├── .dockerignore
├── .env.example
├── .gitignore
├── .prettierrc.json
├── .vscode
└── settings.json
├── Dockerfile
├── README.md
├── client
├── NavbarContext.jsx
├── app.jsx
├── components
│ ├── add-cluster-form.jsx
│ ├── add-message-form.jsx
│ ├── add-topic-form.jsx
│ ├── aggregated-chart.jsx
│ ├── authForm.jsx
│ ├── card.jsx
│ ├── chart.jsx
│ ├── cluster-item.jsx
│ ├── delete-topic-from.jsx
│ ├── drawer-side.jsx
│ ├── footer.jsx
│ ├── messages.jsx
│ ├── nav-bar.jsx
│ ├── performance-statistics.jsx
│ ├── snapshot-comparison.jsx
│ ├── switch-cluster-form.jsx
│ ├── table-data.jsx
│ └── topic-buttons.jsx
├── containers
│ ├── NotFound.jsx
│ ├── about-us.jsx
│ ├── auth.jsx
│ ├── cluster-history.jsx
│ ├── dashboard-container.jsx
│ └── landing-page-container.jsx
├── helper
│ ├── dateFormatter.js
│ └── mapChartData.js
├── index.js
├── resources
│ └── images
│ │ └── 404-dog.jpg
└── static
│ ├── DariaMordvinov.jpg
│ ├── DisonRuan.jpg
│ ├── JasonKuyper
│ ├── JasonKuyper.jpg
│ ├── KafkaCompassDashboard.jpg
│ ├── KafkaCompassDashboard2.png
│ ├── KafkaCompassDashboard3.png
│ ├── KafkaCompassPerformanceStatsDemo.gif
│ ├── KevinDooley.png
│ ├── RyanZarou.png
│ ├── Screenshot1.png
│ ├── Screenshot2.png
│ ├── Screenshot3.png
│ ├── Screenshot4.png
│ ├── cat.jpg
│ ├── clusterHistory.gif
│ ├── clusterHistoryScreenshot.png
│ ├── clusterHistoryScreenshot2.png
│ ├── consumeMessages.gif
│ ├── contentMonitoringScreenshot.png
│ ├── contentMonitoringScreenshot2.png
│ ├── favicon.ico
│ ├── homeIcon.png
│ ├── logo.png
│ ├── logo_without_text.png
│ └── styles.css
├── docker-compose.yml
├── index.html
├── package.json
├── server
├── controllers
│ ├── api-controller.js
│ ├── api-functions.js
│ ├── cloud-auth-controller.js
│ ├── metric-controller.js
│ └── user-controller.js
├── credentials.js.example
├── encryption.js.example
├── models
│ ├── cloud-cluster-model.js
│ ├── metric-model.js
│ └── user-model.js
├── routes
│ └── user-router.js
└── server.js
├── tailwind.config.js
└── webpack.config.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/.DS_Store
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | SUPER_SECRET = your_secret
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | dist/*
4 | .env
5 | encryption.js
6 | credentials.js
7 | api-functions.js
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": false
6 | }
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.defaultFormatter": "esbenp.prettier-vscode",
3 | "editor.formatOnSave": true
4 | }
5 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16.17
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY . .
6 |
7 | RUN npm install
8 |
9 | RUN npm run build
10 |
11 | CMD ["npm", "start"]
12 |
13 | EXPOSE 3000
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # KafkaCompass
2 |
3 |
4 |
5 |
6 |
7 | Welcome to **KafkaCompass Alpha**, a GUI specialized in monitoring your Confluent Cloud Kafka clusters. Get performance and content statistics, view messages in your topics, and check your cluster's history snapshots to see how you cluster's performance changed over time. We're excited to have you as a user, and we can't wait to help you navigate and optimize your Kafka clusters!
8 |
9 | Our web application will be up and running soon, for now, get started and run KafkaCompass locally.
10 |
11 | # Guide
12 |
13 | ## Getting Started 🧭
14 |
15 | - To get started with KafkaCompass, clone this repo to your local machine. Once completed, you will need to add some additional files to your project directory. These steps will help you create your own database and encryption functions so you can take advantage of all of KafkaCompass's backend functionality.
16 |
17 |
18 |
19 |
20 | ### Configuring Your Repository
21 |
22 | - **credentials.js**
23 |
24 | - Under the "server" directory, create a new file called "credentials.js." This file will allow you to connect your NoSQL database (we recommend using MongoDB). If you are not sure how to set up a database on MongoDB, check out their [guide](https://www.mongodb.com/basics/create-database).
25 | - Following the format on "credentials.js.example," copy and paste the link to your database URI. KafkaCompass is now connected to your database!
26 |
27 | - **encryption.js**
28 |
29 | - In order to encrypt and decrypt information from your database, you will need to set up functionality to do so. Using the template on "encryption.js.example," create your own "encryption.js" file.
30 | - For the security of our own application as well as yours, you will need to implement your own encryption and decryption functions. If you are not sure where to start with this, we recommend looking at some algorithms [here](https://www.labnol.org/code/encrypt-decrypt-javascript-200307).
31 |
32 | ### Running the application
33 |
34 | - Once credentials.js and encryption.js are set up, your application is now good to go! Install all dependencies with "npm install," then use "npm run dev" to run in development mode. Alternatively, use "npm run build" then "npm start" to run in production mode.
35 |
36 | ## Sign Up and Adding Clusters
37 |
38 | - Once installed, make sure you sign up on the landing page, which will appear on the initial loading of the application. Click the "Sign Up" button at the top right of the navbar, fill out the designated fields, and click 'Sign Up'
39 | - Once you’ve signed up, you will be greeted by your dashboard. It’s important to note that KafkaCompass is designed to work with Confluent Cloud Apache Kafka clusters, so to get started viewing Cluster performance statistics and content, you will need to have a Kafka cluster already running in Confluent Cloud. Follow the steps [here](https://docs.confluent.io/cloud/current/get-started/index.html#quick-start-for-ccloud) to set this up.
40 | - Once you have a running cluster, you can now link it to the application. To link your cluster, click on the “Add Cluster” button in the nav-bar at the top of the dashboard. Fill out all the fields, checking carefully for accuracy, and press submit. Your cluster information will now be stored and encrypted in your user profile for use in the app.
41 | - Having trouble finding some of your cluster information? Here is where each piece of info can be found:
42 | - **Cluster name**: Up to you! Create an alias to help you remember this specific cluster.
43 | - **API Key** and **API Secret**: On Confluent Cloud, navigate to your cluster. In the sidebar on the left, click on "API Keys," located under Cluster Overview. If you don't already have an API Key, generate a new one, making sure to securely save your key and secret - the secret will no longer be viewable after creation.
44 | - **Cloud Key** and **Cloud Secret**: In the top navbar of Confluent Cloud, click the three stacked lines on the right corner. This will open a menu, where you can click "Cloud API keys." Similar to the previous step, if you have not already generated a Cloud Key, create a new one and securely save the key and secret.
45 | - **REST Endpoint**, **Cluster ID**, and **Bootstrap Server**: Return to your cluster's page on Confluent Cloud. In the sidebar on the left, click "Cluster Settings" under "Cluster Overview." The information will be displayed in the boxes for "Identification" and "Endpoints."
46 |
47 | ## View Modes
48 |
49 | Now that you have a cluster stored in your account, you will be able to see performance and content information for the cluster. KafkaCompass has two primary viewing modes: **Performance Statistics** and **Content Monitoring**. Additionally, users have access to their **Cluster History**.
50 |
51 |
52 |
53 |
54 |
55 | ### Performance Statistics
56 |
57 | - The default mode is Performance Statistics, which initially shows a chart displaying retained bytes per topic in the first cluster of your profile. To switch charts, press the **Select Metrics** button in the nav-bar, which will open a side menu. In the side menu, you can choose the desired statistic you would like to view. If you would like to update these metrics to the most recent state of your cluster, simply press the **Update Cluster** button on the dashboard.
58 |
59 |
60 |
61 |
62 |
63 | - Once you have multiple clusters added to your profile, you can switch between these clusters by using the **Switch Cluster** button on the dashboard. Simply click on the desired cluster in the dropdown menu, and all statistics will be refreshed for this selected cluster.
64 |
65 | ### Content Monitoring
66 |
67 | - In Content Monitoring, you can consume and see the current messages in your selected cluster. To view messages, select the desired topic in your cluster via the dropdown menu, located to the left of the chart. Once you have selected a topic, simply click **Consume Messages**, and the most recent messages in the given topic of your cluster will be displayed.
68 |
69 |
70 |
71 |
72 |
73 | - Additionally, you can change the configuration of your cluster. To add and delete topics, or to add a message to a given topic, simply click on each action’s button, located on the right-hand side of the screen, and fill out their respective forms. In Content Monitoring, you will be able to see these changes nearly instantly. However, it’s worth noting that in “Performance Statistics” mode, it may take around 3-5 minutes for the addition or deletion of topics to show up, due to how Confluent Cloud updates cluster statistics.
74 | - Once you have multiple clusters added to your profile, you can switch between these clusters by using the **Switch Cluster** button, located in the menu at the top-left of the navbar. After clicking on "Switch Cluster, click on the desired cluster in the dropdown menu, and you will now be able to interact with that chosen cluster
75 |
76 | # Contributing to KafkaCompass
77 |
78 | - KafkaCompass is open source, and we would love to see contributions from the community! If you would like to contribute, submit a pull request to the "dev" branch of the original repository. Additionally, if you notice any issues with the application, please add an entry to the "issues" section of the repository.
79 |
80 | If you are looking for specific ideas on where to contribute, here are some features we would like to see being added/worked on:
81 |
82 | - Visual cluster comparison features
83 | - Additional performance metrics
84 | - Tests for front and backend functionality
85 |
86 | ## Meet the team 💻
87 |
88 |
95 |
--------------------------------------------------------------------------------
/client/NavbarContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export const NavbarContext = createContext(null);
4 |
--------------------------------------------------------------------------------
/client/app.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState, useEffect } from "react";
2 | import { Route, Routes, useNavigate, Navigate } from "react-router-dom";
3 | import { NavbarContext } from "./NavbarContext";
4 | import DashboardContainer from "./containers/dashboard-container";
5 | import LandingPage from "./containers/landing-page-container";
6 | import NotFound from "./containers/NotFound";
7 | import Navbar from "./components/nav-bar";
8 | import Auth from "./containers/auth";
9 | import Footer from "./components/footer";
10 | import "./static/styles.css";
11 |
12 | function App() {
13 | // using useNavigate hook to navigate between routes
14 | const navigate = useNavigate();
15 |
16 | // navigation bar and mode-related state
17 | const [renderDrawerButton, setRenderDrawerButton] = useState(false);
18 | const [sideBarMode, setSideBarMode] = useState("current");
19 | const [dashboardMode, setDashboardMode] = useState("performanceStatistics");
20 |
21 | // user-related state:
22 | // user data from the backend
23 | // variable to check if user is logged in
24 | // if user is not logged in, what authentification form we need to render
25 | const [user, setUser] = useState({});
26 | const [loggedIn, setLoggedIn] = useState(false);
27 | const [authMode, setAuthMode] = useState("");
28 |
29 | // metric-related state (for loading data for cluster)
30 | const [metricIndex, setMetricIndex] = useState(-1);
31 | // holder for data -> metric is the part of user state to force the re-render when user updates metrics
32 | const [metric, setMetric] = useState({});
33 | const [metricUpdated, setMetricUpdated] = useState(false);
34 |
35 | // shared navigation bar state
36 | const providerValue = {
37 | drawerButtonsState: useMemo(
38 | () => ({ renderDrawerButton, setRenderDrawerButton }),
39 | [renderDrawerButton, setRenderDrawerButton]
40 | ),
41 | loggedState: useMemo(
42 | () => ({ loggedIn, setLoggedIn }),
43 | [loggedIn, setLoggedIn]
44 | ),
45 | authModeState: useMemo(
46 | () => ({ authMode, setAuthMode }),
47 | [authMode, setAuthMode]
48 | ),
49 | userState: { user, setUser },
50 | dashboardState: useMemo(
51 | () => ({ dashboardMode, setDashboardMode }),
52 | [dashboardMode, setDashboardMode]
53 | ),
54 | sideBarState: useMemo(
55 | () => ({ sideBarMode, setSideBarMode }),
56 | [sideBarMode, setSideBarMode]
57 | ),
58 | metricState: { metric, setMetric },
59 | metricIndexState: { metricIndex, setMetricIndex },
60 | metricUpdatedState: { metricUpdated, setMetricUpdated }
61 | };
62 |
63 | const checkSession = async () => {
64 | try {
65 | const response = await fetch("/api/authenticate");
66 | if (response.ok) {
67 | const session = await response.json();
68 | if (session.auth) {
69 | setLoggedIn(true);
70 | setUser(session.user);
71 | } else {
72 | setLoggedIn(false);
73 | setUser({});
74 | }
75 | } else {
76 | setLoggedIn(false);
77 | setUser({});
78 | }
79 | } catch (err) {
80 | // If try-catch block fails Network error occurred
81 | }
82 | };
83 |
84 | const logUserOut = async () => {
85 | try {
86 | const response = await fetch("/api/logout");
87 | if (response.ok) {
88 | setUser({});
89 | setLoggedIn(false);
90 | }
91 | } catch (err) {
92 | console.log(
93 | "Network error in attempting to logout - user not logged out"
94 | );
95 | }
96 | };
97 |
98 | // if data on cluster was updated, useEffect updates user state with new metrics
99 | useEffect(() => {
100 | if (metric.created_at) {
101 | user.metric.push(metric);
102 | setUser(user);
103 | setMetricUpdated(!metricUpdated);
104 | }
105 | }, [metric]);
106 |
107 | // if user gets updated, set metric's index to -1 (we switch user to the current cluster)
108 | useEffect(() => {
109 | setMetricIndex(-1);
110 | }, [user]);
111 |
112 | useEffect(() => {
113 | checkSession();
114 | }, []);
115 |
116 | return (
117 |
118 |
119 |
120 | } />
121 |
127 |
128 |
129 | >
130 | ) : (
131 |
132 | )
133 | }
134 | />
135 | } />
136 | } />
137 |
138 |
139 |
140 |
141 | );
142 | }
143 |
144 | export default App;
145 |
--------------------------------------------------------------------------------
/client/components/add-cluster-form.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const AddClusterForm = ({ clusterAdded, setClusterAdded }) => {
4 | //keeps track of user inputs in 'Add New Cluster' form
5 | const [newAPIKeyInput, setNewAPIKeyInput] = useState("");
6 | const [newAPISecretInput, setNewAPISecretInput] = useState("");
7 | const [newCloudKeyInput, setNewCloudKeyInput] = useState("");
8 | const [newCloudSecretInput, setNewCloudSecretInput] = useState("");
9 | const [newRESTEndpointInput, setNewRESTEndpointInput] = useState("");
10 | const [newClusterIdInput, setNewClusterIdInput] = useState("");
11 | const [newBootstrapServerInput, setNewBootstrapServerInput] = useState("");
12 | const [newClusterName, setNewClusterName] = useState("");
13 |
14 | // submit new cluster
15 | async function submitNewCluster() {
16 | // create object to send to db
17 | try {
18 | const newCluster = {
19 | API_KEY: newAPIKeyInput,
20 | API_SECRET: newAPISecretInput,
21 | CLOUD_KEY: newCloudKeyInput,
22 | CLOUD_SECRET: newCloudSecretInput,
23 | clusterId: newClusterIdInput,
24 | RESTendpoint: newRESTEndpointInput,
25 | bootstrapServer: newBootstrapServerInput,
26 | cluster_name: newClusterName
27 | };
28 | // send post request to backend
29 | const data = await fetch("/api/cloud-auth", {
30 | method: "POST",
31 | headers: {
32 | "Content-Type": "application/json"
33 | },
34 | body: JSON.stringify(newCluster)
35 | });
36 | if (data.ok) {
37 | setClusterAdded(!clusterAdded);
38 | } else {
39 | console.log("error adding cluster");
40 | }
41 | } catch (err) {
42 | console.log("network error");
43 | }
44 |
45 | //add functionality here to tell user if cluster was successfully added to DB, using status code as indicator
46 | //maybe a green banner saying request was successful and red if not successful
47 |
48 | //fields will clear out if this cluster was successfully added
49 | setNewAPIKeyInput("");
50 | setNewAPISecretInput("");
51 | setNewCloudKeyInput("");
52 | setNewCloudSecretInput("");
53 | setNewRESTEndpointInput("");
54 | setNewClusterIdInput("");
55 | setNewBootstrapServerInput("");
56 | setNewClusterName("");
57 | }
58 |
59 | // Might be refactored with React useForm
60 | return (
61 | <>
62 |
63 |
64 |
65 | Input Cluster Details:
66 |
154 |
155 |
156 | >
157 | );
158 | };
159 |
160 | export default AddClusterForm;
161 |
--------------------------------------------------------------------------------
/client/components/add-message-form.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 |
3 | const AddMessage = ({ onCreate, topic }) => {
4 | // const [topic, setTopic] = useState("");
5 | const [message, setMessage] = useState("");
6 | const [topicList, setTopicList] = useState([]);
7 |
8 | //I use an identical function to get topic list as sibling button here, I'll plan on putting this higher in state later
9 | // useEffect(() => {
10 | // try {
11 | // fetch("/api/topic")
12 | // .then((res) => res.json())
13 | // .then((data) => {
14 | // console.log("data is :", data);
15 | // setTopicList(data);
16 | // })
17 | // .catch(() => {
18 | // console.log("ERROR");
19 | // });
20 | // } catch {
21 | // console.log("Could not fetch topics");
22 | // }
23 | // }, []);
24 |
25 | return (
26 | <>
27 |
28 |
29 |
30 | Write a message:
31 |
32 |
{"Current topic: " + topic}
33 | {/*
{
35 | setTopic(e.target.value);
36 | }}
37 | className="select w-full max-w-xs"
38 | >
39 |
40 | Choose a topic to insert message:
41 |
42 | {topic}
43 | */}
44 |
setMessage(e.target.value)}
48 | className="input input-bordered w-full max-w-xs"
49 | />
50 |
51 | onCreate(topic, message)}
55 | >
56 | Add Message
57 |
58 |
59 |
60 |
61 |
62 | >
63 | );
64 | };
65 |
66 | export default AddMessage;
67 |
--------------------------------------------------------------------------------
/client/components/add-topic-form.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const AddTopic = ({ onCreate }) => {
4 | const [topic, setTopic] = useState("");
5 |
6 | return (
7 | <>
8 |
9 |
10 |
11 | Topic name:
12 |
13 |
setTopic(e.target.value)}
17 | className="input input-bordered w-full max-w-xs"
18 | />
19 |
20 | onCreate(topic)}
24 | >
25 | Create topic
26 |
27 |
28 |
29 |
30 |
31 | >
32 | );
33 | };
34 |
35 | export default AddTopic;
36 |
--------------------------------------------------------------------------------
/client/components/aggregated-chart.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { NavbarContext } from "../NavbarContext";
3 | import "chart.js/auto";
4 | import { Bar, Line } from "react-chartjs-2";
5 | import date from "../helper/dateFormatter";
6 |
7 | const AggregatedChart = (props) => {
8 | const { user } = useContext(NavbarContext).userState;
9 | //object with all statistics for user clusters
10 | const rawMetric = user.metric;
11 |
12 | //stats for aggregated charts
13 | const aggStats = {
14 | active_connection_count: { label: "Active Connection Count", data: {} },
15 | partition_count: { label: "Partition Count", data: {} },
16 | successful_authentication_count: {
17 | label: "Successful Authentication Count",
18 | data: { labels: [], datasets: [] }
19 | },
20 | cluster_load_percent: { label: "Cluster Load Percent", data: {} }
21 | };
22 |
23 | const charts = [];
24 | //condition to prevent crashing due to empty data in user profile
25 | if (rawMetric.length) {
26 | //dataset creation for charts
27 | for (const stat in aggStats) {
28 | const data = { labels: [], datasets: [] };
29 | let statArr = [];
30 | const labels = [];
31 | let curr = rawMetric[0].clusterId;
32 | for (let i = 0; i < rawMetric.length; i++) {
33 | if (rawMetric[i].created_at !== undefined) {
34 | data.labels.push(date.format(new Date(rawMetric[i].created_at)));
35 | statArr.push({
36 | x: date.format(new Date(rawMetric[i].created_at)),
37 | //edge case where some of the arrays were empty
38 | y: rawMetric[i][stat].metrics.length
39 | ? rawMetric[i][stat].metrics[0].value
40 | : 0
41 | });
42 | }
43 | if (i + 1 === rawMetric.length || rawMetric[i + 1].clusterId !== curr) {
44 | const metricObj = { label: curr, data: statArr };
45 | // data.labels.sort();
46 | data.datasets.push(metricObj);
47 | if (i + 1 !== rawMetric.length) {
48 | statArr = [];
49 | curr = rawMetric[i + 1].clusterId;
50 | }
51 | }
52 | }
53 | aggStats[stat].data = data;
54 | }
55 | //creating aggregated charts
56 | for (const stat in aggStats) {
57 | const chart = (
58 |
59 |
{aggStats[stat].label}
60 |
61 |
62 |
69 |
70 | );
71 | charts.push(chart);
72 | }
73 | }
74 |
75 | return {charts}
;
76 | };
77 |
78 | export default AggregatedChart;
79 |
--------------------------------------------------------------------------------
/client/components/authForm.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import logoWithoutText from "../static/logo_without_text.png";
3 | import { Dialog } from "@headlessui/react";
4 | import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
5 |
6 | document.getElementsByTagName("html")[0].classList.add("bg-gray-50");
7 | document.getElementsByTagName("html")[0].classList.add("h-full");
8 | document.body.classList.add("h-full");
9 |
10 | const AuthForm = ({ handleSubmit, register, onSubmit, type, navigate }) => {
11 | const login = type === "Log In";
12 |
13 | return (
14 | <>
15 |
16 |
20 |
25 |
26 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
65 |
66 |
67 |
72 | {login && (
73 |
74 | Sign in to your account
75 |
76 | )}
77 | {!login && (
78 |
79 | Create new account
80 |
81 | )}
82 | {/*
83 | Or{" "}
84 |
88 | start your 14-day free trial
89 |
90 |
*/}
91 |
92 |
93 |
97 |
205 |
206 |
210 |
215 |
216 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 | >
233 | );
234 | };
235 |
236 | export default AuthForm;
237 |
--------------------------------------------------------------------------------
/client/components/card.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DariaMordvinov from "../static/DariaMordvinov.jpg";
3 | import KevinDooley from "../static/KevinDooley.png";
4 | import Cat from "../static/cat.jpg";
5 | import JasonKuyper from "../static/JasonKuyper.jpg";
6 | import RyanZarou from "../static/RyanZarou.png";
7 | import DisonRuan from "../static/DisonRuan.jpg";
8 |
9 | // const Card = ({ team }) => {
10 | // return (
11 | // <>
12 | // {team.map((person) => (
13 | //
14 | //
19 | //
20 | //
{person.name}
21 | //
{person.description}
22 | //
46 | //
47 | //
48 | // ))}
49 | // >
50 | // );
51 | // };
52 |
53 | // export default Card;
54 |
55 | import { EnvelopeIcon, PhoneIcon } from "@heroicons/react/20/solid";
56 |
57 | const people = [
58 | {
59 | name: "Daria Mordvinov",
60 | title: "Software Engineer",
61 | img: DariaMordvinov,
62 | github: "https://github.com/DariaMordvinov",
63 | linkedin: "https://www.linkedin.com/in/dariamordvinov/"
64 | },
65 | {
66 | name: "Kevin Dooley",
67 | img: KevinDooley,
68 | title: "Software Engineer",
69 | github: "https://github.com/kjdooley1",
70 | linkedin: "https://www.linkedin.com/in/kjdooley1/"
71 | },
72 | {
73 | name: "Dison Ruan",
74 | img: DisonRuan,
75 | title: "Software Engineer",
76 | github: "https://github.com/fattyduck123",
77 | linkedin: "https://www.linkedin.com/in/dison-ruan-2b484953/"
78 | },
79 | {
80 | name: "Jason Kuyper",
81 | img: JasonKuyper,
82 | title: "Software Engineer",
83 | github: "https://github.com/jasonkuyper",
84 | linkedin: "https://www.linkedin.com/in/jason-kuyper"
85 | },
86 | {
87 | name: "Ryan Zarou",
88 | img: RyanZarou,
89 | title: "Software Engineer",
90 | github: "https://github.com/rzarou",
91 | linkedin: "https://www.linkedin.com/in/rzarou/"
92 | }
93 | ];
94 |
95 | export default function AboutUsCards() {
96 | return (
97 |
102 | {people.map((person) => (
103 |
107 |
108 |
113 |
114 | {person.name}
115 |
116 |
117 | Title
118 | {person.title}
119 |
120 |
121 |
149 |
150 | ))}
151 |
152 | );
153 | }
154 |
--------------------------------------------------------------------------------
/client/components/chart.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { NavbarContext } from "../NavbarContext";
3 | import "chart.js/auto";
4 | import { Bar } from "react-chartjs-2";
5 |
6 | const Chart = (props) => {
7 | // selecting metric specific to this chart
8 | const metric = props.metricSelection;
9 | const chartData = props.chartData[metric];
10 |
11 | const data = chartData.info;
12 |
13 | const { setMetric } = useContext(NavbarContext).metricState;
14 | const { sideBarMode } = useContext(NavbarContext).sideBarState;
15 |
16 | async function updateMetrics() {
17 | const response = await fetch("/api/metric");
18 | const metric = await response.json();
19 |
20 | setMetric(metric);
21 | }
22 |
23 | return (
24 |
25 |
{chartData.colDescription}
26 |
27 |
28 |
36 |
37 | {chartData.compositeChart && (
38 | <>
39 |
40 | {chartData.totalDescription1} {chartData.totalValue1}
41 |
42 |
43 | {chartData.totalDescription2} {chartData.totalValue2}
44 |
45 | >
46 | )}
47 |
48 | {!chartData.compositeChart && (
49 |
50 | {chartData.totalDescription}
51 | {chartData.totalValue}
52 |
53 | )}
54 |
55 | );
56 | };
57 |
58 | export default Chart;
59 |
--------------------------------------------------------------------------------
/client/components/cluster-item.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { NavbarContext } from "../NavbarContext";
3 |
4 | const ClusterItem = (props) => {
5 | const { index, date, clusterId } = props;
6 | const { setMetricIndex } = useContext(NavbarContext).metricIndexState;
7 | const { setDashboardMode } = useContext(NavbarContext).dashboardState;
8 |
9 | // changing metric index to get cluster's history snapshot from the cluster history list
10 | function handleClick() {
11 | setMetricIndex(index);
12 | setDashboardMode("performanceStatistics");
13 | }
14 |
15 | return (
16 |
17 | {index}
18 | {date}
19 | {clusterId}
20 |
21 |
25 | Cluster Metrics
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default ClusterItem;
33 |
--------------------------------------------------------------------------------
/client/components/delete-topic-from.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const DeleteTopic = ({ onDelete, topicList }) => {
4 | const [topic, setTopic] = useState("");
5 | let topicNames = [];
6 | if (topicList) topicNames = topicList;
7 |
8 | return (
9 | <>
10 |
11 |
12 |
13 | Topic name:
14 |
15 |
{
17 | setTopic(e.target.value);
18 | }}
19 | className="select w-full max-w-xs"
20 | >
21 |
22 | Choose a topic to delete
23 |
24 | {topicNames.map((t) => (
25 | {t}
26 | ))}
27 |
28 |
29 |
30 | onDelete(topic)}
34 | >
35 | Delete topic
36 |
37 |
38 |
39 |
40 |
41 | >
42 | );
43 | };
44 |
45 | export default DeleteTopic;
46 |
--------------------------------------------------------------------------------
/client/components/drawer-side.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { NavbarContext } from "../NavbarContext";
3 |
4 | const DrawerSide = ({ metricSelection, updateSideDrawer }) => {
5 | const { sideBarMode, setSideBarMode } =
6 | useContext(NavbarContext).sideBarState;
7 |
8 | const { setDashboardMode } = useContext(NavbarContext).dashboardState;
9 | const { metricIndex, setMetricIndex } =
10 | useContext(NavbarContext).metricIndexState;
11 |
12 | const choices = {
13 | retained_bytes: "Retained Bytes",
14 | sent_bytes: "Sent Bytes",
15 | received_records: "Received Records",
16 | sent_records: "Sent Records",
17 | request_bytes: "Request Bytes",
18 | response_bytes: "Response Bytes",
19 | request_count: "Request Counts",
20 | req_res: "Request/Response bytes"
21 | };
22 |
23 | const listItems = Object.keys(choices).map((key) => (
24 | {
27 | updateSideDrawer(key);
28 | // if we switched to history mode but have not choosen the snapshot,
29 | // and now we whant to back to the chart on current cluster
30 | if (sideBarMode === "history" && metricIndex === -1) {
31 | setSideBarMode("current");
32 | setDashboardMode("performanceStatistics");
33 | }
34 | }}
35 | className={metricSelection === key ? "bg-blue-800 text-white" : ""}
36 | >
37 | {choices[key]}
38 |
39 | ));
40 |
41 | return (
42 |
43 |
44 |
45 | {listItems}
46 |
47 | {
50 | setSideBarMode("history");
51 | setDashboardMode("clusterHistory");
52 | }}
53 | >
54 | Cluster History
55 |
56 | {sideBarMode === "history" && (
57 | {
60 | setSideBarMode("current");
61 | setDashboardMode("performanceStatistics");
62 | setMetricIndex(-1);
63 | }}
64 | >
65 | Back to current cluster
66 |
67 | )}
68 |
69 |
70 | );
71 | };
72 |
73 | export default DrawerSide;
74 |
--------------------------------------------------------------------------------
/client/components/footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import logoWithoutText from "../static/logo_without_text.png";
3 |
4 | const Footer = () => {
5 | return (
6 | <>
7 | {/* text-neutral-content */}
8 |
81 | >
82 | );
83 | };
84 |
85 | export default Footer;
86 |
--------------------------------------------------------------------------------
/client/components/messages.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import TopicButtons from "../components/topic-buttons";
3 | import date from "../helper/dateFormatter";
4 |
5 | const Messages = ({ topic, setTopic, cluster }) => {
6 | //state for current topic
7 | const [topicList, setTopicList] = useState([]);
8 | const [messageList, setMessageList] = useState([]);
9 | const [topicDeleted, setTopicDeleted] = useState(false);
10 | const [consumedTopic, setConsumedTopic] = useState("");
11 | const [spinner, setSpinner] = useState([]);
12 | const consumeButton = useRef();
13 | let messageTable = [];
14 |
15 | useEffect(() => {
16 | setMessageList([]);
17 | setSpinner([]);
18 | }, [topic]);
19 |
20 | // const messageTable =
21 | // messageList.length === 0
22 | // ? [
23 | //
24 | // No messages
25 | //
26 | // ]
27 | // : [];
28 |
29 | // sets list of topics in the state depending on the data in the user's current cluster
30 | useEffect(() => {
31 | async function getTopicList() {
32 | try {
33 | const response = await fetch("/api/topic");
34 | const data = await response.json();
35 | if (response.ok) {
36 | setTopicList(data);
37 | } else {
38 | console.log("Could not get topics.");
39 | }
40 | } catch {
41 | console.log("Network error fetching topic list");
42 | }
43 | }
44 | getTopicList();
45 | }, [cluster, topicDeleted]);
46 |
47 | const selectTopic = (e) => {
48 | setTopic(e.target.text);
49 | };
50 |
51 | const topicMenu = [];
52 | if (topicList.length) {
53 | for (const topic of topicList) {
54 | topicMenu.push(
55 |
56 |
57 | {topic}
58 |
59 |
60 | );
61 | }
62 | }
63 |
64 | const consumeMessages = async () => {
65 | setSpinner([
66 |
67 |
74 |
78 |
82 |
83 |
Loading...
84 |
85 | ]);
86 | setConsumedTopic(topic);
87 | if (topic !== "Select a topic") {
88 | try {
89 | consumeButton.current.disabled = true;
90 | const response = await fetch(`/api/message/${topic}`);
91 | const data = await response.json();
92 | setSpinner([]);
93 | setMessageList(data);
94 | setTimeout(() => {
95 | consumeButton.current.disabled = false;
96 | }, 5000);
97 | } catch (err) {
98 | console.log(err);
99 | }
100 | } else {
101 | console.log("No topic selected");
102 | }
103 | };
104 |
105 | //check if messageList contains messages
106 |
107 | messageList.sort((a, b) => b.timestamp - a.timestamp);
108 | messageList.forEach((el) => {
109 | if (!isNaN(Number(el.timestamp))) {
110 | el.timestamp = date.format(new Date(Number(el.timestamp)));
111 | }
112 | });
113 | let i = 0;
114 |
115 | if (consumedTopic === topic) {
116 | for (const message of messageList) {
117 | messageTable.push(
118 |
122 | {/* Checkbox column: might be added in a later version */}
123 | {/*
124 |
125 |
130 |
131 | checkbox
132 |
133 |
134 | */}
135 |
139 | {message.value}
140 |
141 | {message.partition}
142 | {message.offset}
143 | {message.timestamp}
144 |
145 | );
146 | i++;
147 | }
148 | } else {
149 | messageTable.push(
150 |
151 | No messages
152 |
153 | );
154 | }
155 |
156 | return (
157 |
158 |
159 |
160 |
161 |
162 | Select Topic
163 |
164 |
170 |
171 |
172 |
173 | {topic !== "" && topic}
174 |
175 |
213 |
{spinner}
214 |
219 | Consume Messages
220 |
221 |
222 |
223 |
224 |
232 |
233 | );
234 | };
235 |
236 | export default Messages;
237 |
--------------------------------------------------------------------------------
/client/components/nav-bar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { NavbarContext } from "../NavbarContext";
3 | import logoWithoutText from "../static/logo_without_text.png";
4 |
5 | const Navbar = ({ navigate, logUserOut }) => {
6 | const { setAuthMode } = useContext(NavbarContext).authModeState;
7 | const { renderDrawerButton, setRenderDrawerButton } =
8 | useContext(NavbarContext).drawerButtonsState;
9 | const { loggedIn, setLoggedIn } = useContext(NavbarContext).loggedState;
10 | // dictates the view mode on dashbaord
11 | const { dashboardMode, setDashboardMode } =
12 | useContext(NavbarContext).dashboardState;
13 | // modifies the performance mode menu in the case of looking at historical metrics
14 | const { sideBarMode, setSideBarMode } =
15 | useContext(NavbarContext).sideBarState;
16 | // functions for switching mode of the dashboard
17 | function changeModePerformanceStatistics() {
18 | setDashboardMode("performanceStatistics");
19 | }
20 | function changeModeContentMonitoring() {
21 | setDashboardMode("contentMonitoring");
22 | }
23 | function changeModeSnapshotComparison() {
24 | setDashboardMode("snapshotComparison");
25 | }
26 |
27 | let drawerButtons = <>>;
28 |
29 | if (renderDrawerButton) {
30 | drawerButtons = (
31 |
32 |
33 |
39 |
45 |
46 |
47 |
108 |
109 | );
110 | }
111 |
112 | let logButtons = <>>;
113 | if (loggedIn === false) {
114 | logButtons = (
115 | <>
116 | {
118 | setRenderDrawerButton(false);
119 | navigate("/");
120 | }}
121 | className="btn btn-ghost btn-circle"
122 | >
123 |
129 |
135 |
136 |
137 | {
139 | setAuthMode("signup");
140 | navigate("/auth");
141 | }}
142 | className="btn btn-ghost normal-case text-l"
143 | >
144 | Sign Up
145 |
146 | {
148 | setAuthMode("login");
149 | navigate("/auth");
150 | }}
151 | className="btn btn-ghost normal-case text-l"
152 | >
153 | Login
154 |
155 | >
156 | );
157 | } else {
158 | logButtons = (
159 | <>
160 | {
162 | setRenderDrawerButton(false);
163 | navigate("/");
164 | }}
165 | className="btn btn-ghost btn-circle"
166 | >
167 |
173 |
179 |
180 |
181 | {
183 | logUserOut();
184 | setRenderDrawerButton(false);
185 | setLoggedIn(false);
186 | navigate("/");
187 | }}
188 | className="btn btn-ghost normal-case text-l"
189 | >
190 | Logout
191 |
192 | >
193 | );
194 | }
195 |
196 | return (
197 |
198 |
{drawerButtons}
199 |
{logButtons}
200 |
201 |
202 | );
203 | };
204 |
205 | export default Navbar;
206 |
--------------------------------------------------------------------------------
/client/components/performance-statistics.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from "react";
2 | import { NavbarContext } from "../NavbarContext";
3 | import TableData from "../components/table-data";
4 | import Chart from "../components/chart";
5 |
6 | const PerformanceStatistics = ({
7 | chartData,
8 | metricSelection,
9 | tableData,
10 | updateSideDrawer,
11 | clusterId,
12 | snapshotTime
13 | }) => {
14 | const { setMetric } = useContext(NavbarContext).metricState;
15 | async function updateMetrics() {
16 | const response = await fetch("/api/metric");
17 | if (response.ok) {
18 | const metric = await response.json();
19 | setMetric(metric);
20 | } else console.log("Could not update metrics");
21 | }
22 |
23 | const { sideBarMode, setSideBarMode } =
24 | useContext(NavbarContext).sideBarState;
25 | const { setDashboardMode } = useContext(NavbarContext).dashboardState;
26 | const { metricIndex, setMetricIndex } =
27 | useContext(NavbarContext).metricIndexState;
28 |
29 | const choices = {
30 | retained_bytes: "Retained Bytes",
31 | sent_bytes: "Sent Bytes",
32 | received_records: "Received Records",
33 | sent_records: "Sent Records",
34 | request_bytes: "Request Bytes",
35 | response_bytes: "Response Bytes",
36 | request_count: "Request Counts",
37 | req_res: "Request/Response bytes"
38 | };
39 |
40 | const listItems = Object.keys(choices).map((key) => (
41 | {
44 | updateSideDrawer(key);
45 | // if we switched to history mode but have not chosen the snapshot,
46 | // and now we what to back to the chart on current cluster
47 | if (sideBarMode === "history" && metricIndex === -1) {
48 | setSideBarMode("current");
49 | setDashboardMode("performanceStatistics");
50 | }
51 | }}
52 | className={metricSelection === key ? "bg-blue-800 text-white" : ""}
53 | >
54 | {choices[key]}
55 |
56 | ));
57 |
58 | return (
59 | <>
60 |
61 |
62 |
89 |
90 |
91 |
92 |
93 |
94 | Performance Statistics
95 |
96 |
97 |
98 |
99 | Cluster: {clusterId} Snapshot: {snapshotTime}
100 |
101 | {chartData && (
102 | <>
103 |
104 | >
105 | )}
106 |
107 |
110 |
111 | >
112 | );
113 | };
114 |
115 | export default PerformanceStatistics;
116 |
--------------------------------------------------------------------------------
/client/components/snapshot-comparison.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState, useRef } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import AddClusterForm from "../components/add-cluster-form";
4 | import ClusterItem from "../components/cluster-item";
5 | import AggregatedChart from "../components/aggregated-chart";
6 | import { NavbarContext } from "../NavbarContext";
7 | import date from "../helper/dateFormatter";
8 |
9 | const SnapshotComparison = ({ chartData }) => {
10 | //state toggling if displaying selection menu or comparison
11 | const [mode, setMode] = useState("select");
12 | const navigate = useNavigate();
13 | const metrics = useContext(NavbarContext).userState.user.metric;
14 | const { setRenderDrawerButton } =
15 | useContext(NavbarContext).drawerButtonsState;
16 | useEffect(() => {
17 | setRenderDrawerButton(true);
18 | });
19 | const [snapshot1State, setSnapshot1State] = useState();
20 | const [snapshot2State, setSnapshot2State] = useState();
21 |
22 | function selectSnapshot1(e) {
23 | setSnapshot1State(e.target.id);
24 | }
25 | function selectSnapshot2(e) {
26 | setSnapshot2State(e.target.id);
27 | }
28 | function submitSnapshots(e) {
29 | if (snapshot1State !== undefined && snapshot2State !== undefined) {
30 | setMode("display");
31 | } else console.log("Please select two snapshots to compare");
32 | }
33 | function backToSelectionMode() {
34 | setSnapshot1State();
35 | setSnapshot2State();
36 | setMode("select");
37 | }
38 |
39 | let render = <>>;
40 |
41 | if (mode === "select") {
42 | const snapshotList1 = [];
43 | const snapshotList2 = [];
44 | const selectItems = [];
45 | if (metrics.length) {
46 | for (const metricIndex in metrics) {
47 | snapshotList1.push(
48 |
54 |
59 | {metrics[metricIndex].clusterId} :{" "}
60 | {metrics[metricIndex].created_at !== undefined
61 | ? date.format(new Date(metrics[metricIndex].created_at))
62 | : "N/A"}
63 |
64 |
65 | );
66 | snapshotList2.push(
67 |
73 |
78 | {metrics[metricIndex].clusterId} :{" "}
79 | {metrics[metricIndex].created_at !== undefined
80 | ? date.format(new Date(metrics[metricIndex].created_at))
81 | : "N/A"}
82 |
83 |
84 | );
85 | // selectItems.push(
86 | //
87 | // {metric.clusterId} : {metric.created_at}
88 | //
89 | // );
90 | }
91 | render = (
92 |
93 |
94 |
95 |
96 | Snapshot 1
97 |
98 |
102 | {snapshotList1}
103 |
104 |
105 |
106 |
107 | Snapshot 2
108 |
109 |
113 | {snapshotList2}
114 |
115 |
116 |
120 | Compare Snapshots
121 |
122 |
123 |
124 | );
125 | }
126 | } else if (mode === "display") {
127 | const snapshot1Obj = metrics[snapshot1State];
128 | const snapshot2Obj = metrics[snapshot2State];
129 | let tableRows = [];
130 | for (const metric in snapshot1Obj) {
131 | if (
132 | metric === "created_at" ||
133 | metric === "_id" ||
134 | metric === "clusterId" ||
135 | metric === "__v"
136 | )
137 | continue;
138 | let oneRow = <>>;
139 | oneRow = (
140 |
141 | {snapshot1Obj[metric].totalValue}
142 | {metric}
143 | {snapshot2Obj[metric].totalValue}
144 |
145 | );
146 | tableRows.push(oneRow);
147 | }
148 |
149 | render = (
150 |
151 |
152 |
156 | Select New Snapshots
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | Cluster: {snapshot1Obj.clusterId} at{" "}
166 | {snapshot1Obj.created_at !== undefined
167 | ? date.format(new Date(snapshot1Obj.created_at))
168 | : "N/A"}
169 |
170 | Metric
171 |
172 | Cluster: {snapshot2Obj.clusterId} at{" "}
173 | {snapshot2Obj.created_at !== undefined
174 | ? date.format(new Date(snapshot2Obj.created_at))
175 | : "N/A"}
176 |
177 |
178 |
179 | {tableRows}
180 |
181 |
182 |
183 |
184 | );
185 | }
186 |
187 | // gets cluster items from metrics
188 | const clusterItems = metrics.map((metric, idx) => {
189 | return (
190 |
197 | );
198 | });
199 |
200 | return (
201 | <>
202 |
203 |
204 | Snapshot Comparison
205 |
206 | {render}
207 |
208 | >
209 | );
210 | };
211 |
212 | export default SnapshotComparison;
213 |
--------------------------------------------------------------------------------
/client/components/switch-cluster-form.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from "react";
2 | import { NavbarContext } from "../NavbarContext";
3 |
4 | const SwitchCluster = ({ setCluster, clusterAdded }) => {
5 | const [clusterNames, setClusterNames] = useState([]);
6 | const [clusterSelection, setClusterSelection] = useState(0);
7 |
8 | useEffect(() => {
9 | async function getClusterList() {
10 | try {
11 | const response = await fetch("/api/getClusterList");
12 | const clusterList = await response.json();
13 | setClusterNames(clusterList);
14 | } catch (err) {
15 | console.log("Network error occurred - could not get cluste list");
16 | }
17 | }
18 | getClusterList();
19 | }, [clusterAdded]);
20 |
21 | async function switchCluster() {
22 | try {
23 | const response = await fetch("/api/switchCluster", {
24 | method: "POST",
25 | headers: {
26 | "Content-Type": "application/json"
27 | },
28 | body: JSON.stringify({ cluster: clusterSelection })
29 | });
30 | if (response.ok) {
31 | setCluster(clusterSelection);
32 | console.log();
33 | }
34 | } catch (err) {
35 | console.log("Network error occurred - could not switch cluster");
36 | }
37 | }
38 |
39 | return (
40 | <>
41 |
46 |
47 |
48 | Cluster Name:
49 |
50 |
{
52 | let index = 0;
53 | for (let i = 0; i < clusterNames.length; i++) {
54 | if (e.target.value === clusterNames[i]) {
55 | index = i;
56 | break;
57 | }
58 | }
59 | setClusterSelection(index);
60 | }}
61 | className="select w-full max-w-xs"
62 | >
63 |
64 | Choose a cluster
65 |
66 | {clusterNames.map((name) => (
67 | {name}
68 | ))}
69 |
70 |
71 |
72 |
77 | Choose Cluster
78 |
79 |
80 |
81 |
82 |
83 | >
84 | );
85 | };
86 |
87 | export default SwitchCluster;
88 |
--------------------------------------------------------------------------------
/client/components/table-data.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const TableData = (props) => {
4 | let tableRows = [];
5 | let oneRow = <>>;
6 | for (let row of props.tableData) {
7 | oneRow = (
8 |
9 | {row.name}
10 | {row.value}
11 |
12 | );
13 | tableRows.push(oneRow);
14 | }
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | Metric
23 | Value
24 |
25 |
26 | {tableRows}
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default TableData;
34 |
--------------------------------------------------------------------------------
/client/components/topic-buttons.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import AddMessage from "./add-message-form";
3 | import AddTopic from "./add-topic-form";
4 | import DeleteTopic from "./delete-topic-from";
5 |
6 | const TopicButtons = ({
7 | topic,
8 | setTopic,
9 | topicList,
10 | setTopicList,
11 | topicDeleted,
12 | setTopicDeleted
13 | }) => {
14 | const handleCreateTopic = async (topic) => {
15 | try {
16 | const response = await fetch("/api/topic", {
17 | method: "POST",
18 | headers: {
19 | "Content-Type": "application/json"
20 | },
21 | body: JSON.stringify({ topic })
22 | });
23 | if (response.ok) {
24 | console.log("in ok response");
25 | setTopicList((prev) => [...prev, topic]);
26 | }
27 | } catch (err) {
28 | console.log("Unable to create topic");
29 | }
30 | };
31 |
32 | const handleDeleteTopic = async (topic) => {
33 | try {
34 | const response = await fetch("/api/topic", {
35 | method: "DELETE",
36 | headers: {
37 | "Content-Type": "application/json"
38 | },
39 | body: JSON.stringify({ topic })
40 | });
41 | if (response.ok) {
42 | setTopicDeleted(!topicDeleted);
43 | }
44 | } catch (error) {
45 | console.log("Unable to delete topic");
46 | }
47 | };
48 |
49 | //to submit message to topic
50 | const handleAddMessage = async (topic, message) => {
51 | try {
52 | const response = await fetch("/api/message", {
53 | method: "POST",
54 | headers: {
55 | "Content-Type": "application/json"
56 | },
57 | body: JSON.stringify({ topic, message })
58 | });
59 | if (response.ok) {
60 | return;
61 | } else {
62 | console.log("Could not add new message to the cluster");
63 | }
64 | } catch (err) {
65 | console.log("Network error occured");
66 | }
67 | };
68 |
69 | return (
70 | <>
71 |
72 |
73 | Create topic
74 |
75 |
76 | Delete topic
77 |
78 |
79 | Write a message
80 |
81 |
82 |
83 |
84 |
89 | >
90 | );
91 | };
92 |
93 | export default TopicButtons;
94 |
--------------------------------------------------------------------------------
/client/containers/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function NotFound({ navigate }) {
4 | return (
5 | <>
6 |
7 |
8 |
404
9 |
10 | Page not found
11 |
12 |
13 | Sorry, we couldn’t find the page you’re looking for.
14 |
15 |
32 |
33 |
34 | >
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/client/containers/about-us.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Card from "../components/card";
3 | import DariaMordvinov from "../static/DariaMordvinov.jpg";
4 | import KevinDooley from "../static/KevinDooley.png";
5 | import Cat from "../static/cat.jpg";
6 | import JasonKuyper from "../static/JasonKuyper.jpg";
7 | import RyanZarou from "../static/RyanZarou.png";
8 | import DisonRuan from "../static/DisonRuan.jpg";
9 |
10 | import { Dialog } from "@headlessui/react";
11 | import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
12 |
13 | const AboutUs = () => {
14 | const team = [
15 | {
16 | name: "Daria Mordvinov",
17 | img: DariaMordvinov,
18 | description: "Software Engineer",
19 | github: "https://github.com/DariaMordvinov",
20 | linkedin: "https://www.linkedin.com/in/dariamordvinov/"
21 | },
22 | {
23 | name: "Kevin Dooley",
24 | img: KevinDooley,
25 | description: "Software Engineer",
26 | github: "https://github.com/kjdooley1",
27 | linkedin: "https://www.linkedin.com/in/kjdooley1/"
28 | },
29 | {
30 | name: "Dison Ruan",
31 | img: DisonRuan,
32 | description: "Software Engineer",
33 | github: "https://github.com/fattyduck123",
34 | linkedin: "https://www.linkedin.com/in/dison-ruan-2b484953/"
35 | },
36 | {
37 | name: "Jason Kuyper",
38 | img: JasonKuyper,
39 | description: "Software Engineer",
40 | github: "https://github.com/jasonkuyper",
41 | linkedin: "https://www.linkedin.com/in/jason-kuyper"
42 | },
43 | {
44 | name: "Ryan Zarou",
45 | img: RyanZarou,
46 | description: "Software Engineer",
47 | github: "https://github.com/rzarou",
48 | linkedin: "https://www.linkedin.com/in/rzarou/"
49 | }
50 | ];
51 | return (
52 |
53 |
54 | Meet the team
55 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default AboutUs;
64 |
--------------------------------------------------------------------------------
/client/containers/auth.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { useForm } from "react-hook-form";
3 | import AuthForm from "../components/authForm";
4 | import { NavbarContext } from "../NavbarContext";
5 |
6 | const Auth = ({ navigate }) => {
7 | // getting sharable state from the Context
8 | const { authMode, setAuthMode } = useContext(NavbarContext).authModeState;
9 | const { setRenderDrawerButton } =
10 | useContext(NavbarContext).drawerButtonsState;
11 | const { setLoggedIn } = useContext(NavbarContext).loggedState;
12 | const { setUser } = useContext(NavbarContext).userState;
13 |
14 | // Setting document's background image back to none -> default
15 | document.body.style.backgroundImage = "none";
16 |
17 | const { register, handleSubmit } = useForm();
18 |
19 | // render either log in or sign up form
20 | const renderLogin = authMode === "login";
21 |
22 | const onSubmit = async (data) => {
23 | let endPoint = "/api/login";
24 | let errorMessage = "Login failed: invalid password or username";
25 |
26 | // getting data out of form
27 | const credentials = { username: data.username, password: data.password };
28 | if (data.email) {
29 | credentials.email = data.email;
30 | // if there is an email in the form, that means we have sign up form
31 | endPoint = "/api/signup";
32 | errorMessage = "Sign up failed: this email or username is already taken";
33 | }
34 | if (data.firstName) credentials.firstName = data.firstName;
35 | if (data.lastName) credentials.lastName = data.lastName;
36 |
37 | try {
38 | const response = await fetch(endPoint, {
39 | method: "POST",
40 | headers: {
41 | "Content-Type": "application/json"
42 | },
43 | body: JSON.stringify(credentials)
44 | });
45 | if (response.ok) {
46 | const user = await response.json();
47 | setRenderDrawerButton(true);
48 | setAuthMode("");
49 | setLoggedIn(true);
50 | setUser(user);
51 | return navigate("/dashboard");
52 | }
53 | console.log(errorMessage);
54 | } catch (err) {
55 | console.log("Network error occurred");
56 | }
57 | };
58 |
59 | return (
60 | <>
61 | {renderLogin && (
62 |
71 | )}
72 | {!renderLogin && (
73 |
82 | )}
83 | >
84 | );
85 | };
86 |
87 | export default Auth;
88 |
--------------------------------------------------------------------------------
/client/containers/cluster-history.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import AddClusterForm from "../components/add-cluster-form";
4 | import ClusterItem from "../components/cluster-item";
5 | import AggregatedChart from "../components/aggregated-chart";
6 | import { NavbarContext } from "../NavbarContext";
7 | import date from "../helper/dateFormatter";
8 |
9 | const ClusterHistory = ({ chartData }) => {
10 | const navigate = useNavigate();
11 | const metrics = useContext(NavbarContext).userState.user.metric;
12 | const { setRenderDrawerButton } =
13 | useContext(NavbarContext).drawerButtonsState;
14 |
15 | useEffect(() => {
16 | setRenderDrawerButton(true);
17 | });
18 |
19 | // gets cluster items from metrics
20 | const clusterItems = metrics.map((metric, idx) => {
21 | // console.log("time at:", metric.created_at);
22 | let metricDate = "N/A";
23 | if (metric.created_at !== undefined) {
24 | metricDate = date.format(new Date(metric.created_at));
25 | console.log(metricDate);
26 | }
27 | return (
28 |
35 | );
36 | });
37 |
38 | // //temp
39 | // const [metricSelection, setMetricSelection] = useState("retained_bytes");
40 |
41 | return (
42 | <>
43 |
44 |
45 | Cluster History
46 |
47 |
48 |
Snapshots
49 |
50 |
51 |
52 |
53 |
54 |
55 | Date
56 | Cluster Id
57 | View Metrics
58 |
59 |
60 | {clusterItems}
61 |
62 |
63 |
64 |
65 | >
66 | );
67 | };
68 |
69 | export default ClusterHistory;
70 |
--------------------------------------------------------------------------------
/client/containers/dashboard-container.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useContext } from "react";
2 | import AddClusterForm from "../components/add-cluster-form";
3 | import Chart from "../components/chart";
4 | import Messages from "../components/messages";
5 | import { NavbarContext } from "../NavbarContext";
6 | import TableData from "../components/table-data";
7 | import DrawerSide from "../components/drawer-side";
8 | import mapChartData from "../helper/mapChartData";
9 | import ClusterHistory from "./cluster-history";
10 | import SwitchCluster from "../components/switch-cluster-form";
11 | import PerformanceStatistics from "../components/performance-statistics";
12 | import SnapshotComparison from "../components/snapshot-comparison";
13 | import date from "../helper/dateFormatter";
14 |
15 | const DashboardContainer = (props) => {
16 | // state of current topic inside the Content Monitoring view
17 | const [topic, setTopic] = useState("Select a topic");
18 | const [clusterAdded, setClusterAdded] = useState(false);
19 |
20 | // cluster selection for the Content Monitoring
21 | const [cluster, setCluster] = useState(0);
22 | // current cluster information
23 | const [clusterId, setClusterId] = useState("");
24 | const [snapshotTime, setSnapshotTime] = useState("");
25 |
26 | // Setting document's background image back to none -> default
27 | document.body.style.backgroundImage = "none";
28 |
29 | // getting sharable state from the useContex
30 | const { setRenderDrawerButton } =
31 | useContext(NavbarContext).drawerButtonsState;
32 | const { sideBarMode } = useContext(NavbarContext).sideBarState;
33 | const [metricSelection, setMetricSelection] = useState("retained_bytes");
34 |
35 | // cluster data for charts and tables for the user
36 | const [chartData, setChart] = useState();
37 | const [tableData, setTableData] = useState([]);
38 | const { metricIndex } = useContext(NavbarContext).metricIndexState;
39 | const { user } = useContext(NavbarContext).userState;
40 | const { metricUpdated } = useContext(NavbarContext).metricUpdatedState;
41 |
42 | useEffect(() => {
43 | //if no clusters in user info, no charts will load
44 | try {
45 | const data = user.metric.at(metricIndex);
46 |
47 | const dataForTable = [
48 | "partition_count",
49 | "active_connection_count",
50 | "successful_authentication_count",
51 | "cluster_load_percent",
52 | "consumer_lag_offsets",
53 | "received_bytes",
54 | "received_records",
55 | "request_bytes",
56 | "request_count",
57 | "retained_bytes",
58 | "sent_bytes",
59 | "sent_records"
60 | ].map((td) => {
61 | const name = td.replace(/_/g, " ");
62 | const description = data[td].description;
63 | const value = data[td].totalValue;
64 | return {
65 | name,
66 | description,
67 | value
68 | };
69 | });
70 |
71 | setTableData(dataForTable);
72 | setChart(mapChartData(data));
73 | setClusterId(data.clusterId);
74 | setSnapshotTime(date.format(new Date(data.created_at)));
75 | } catch {
76 | console.log("No clusters in user data");
77 | }
78 | }, [metricIndex, metricUpdated]);
79 |
80 | useEffect(() => {
81 | setRenderDrawerButton(true);
82 | }, []);
83 |
84 | // dictates the view mode on dashbaord
85 | const { dashboardMode, setDashboardMode } =
86 | useContext(NavbarContext).dashboardState;
87 |
88 | // functions for switching mode of the dashboard
89 | function changeModePerformanceStatistics() {
90 | setDashboardMode("performanceStatistics");
91 | }
92 | function changeModeContentMonitoring() {
93 | setDashboardMode("contentMonitoring");
94 | }
95 |
96 | const { metric, setMetric } = useContext(NavbarContext).metricState;
97 | async function updateMetrics() {
98 | const response = await fetch("/api/metric");
99 | const metric = await response.json();
100 |
101 | setMetric(metric);
102 | }
103 |
104 | // sets current dashboard view
105 | let dashboardView = <>>;
106 | if (dashboardMode === "performanceStatistics") {
107 | dashboardView = (
108 | <>
109 |
117 | >
118 | );
119 | } else if (dashboardMode === "contentMonitoring") {
120 | dashboardView = (
121 | <>
122 |
123 | Content Monitoring
124 |
125 |
126 |
127 |
128 | >
129 | );
130 | } else if (dashboardMode === "clusterHistory") {
131 | dashboardView = ;
132 | } else if (dashboardMode === "snapshotComparison") {
133 | dashboardView = ;
134 | }
135 |
136 | // update metrics object with desired viewing metrics
137 | function updateSideDrawer(next) {
138 | setMetricSelection(next);
139 | }
140 |
141 | return (
142 | <>
143 | {dashboardView}
144 |
148 | {/* */}
152 |
153 |
158 | >
159 | );
160 | };
161 |
162 | export default DashboardContainer;
163 |
--------------------------------------------------------------------------------
/client/containers/landing-page-container.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useRef, useState } from "react";
2 | import { NavbarContext } from "../NavbarContext";
3 | import AboutUs from "./about-us.jsx";
4 | import LogoWithoutText from "../static/logo_without_text.png";
5 | import KafkaCompassPerformanceStatsDemo from "../static/KafkaCompassPerformanceStatsDemo.gif";
6 | import contentMonitoringScreenshot from "../static/contentMonitoringScreenshot.png";
7 | import "../static/styles.css";
8 | import AboutUsCards from "../components/card";
9 | import logoWithoutText from "../static/logo_without_text.png";
10 | import KafkaCompassDashboardPic from "../static/KafkaCompassDashboard3.png";
11 | import ContentMonitoringScreenshot from "../static/contentMonitoringScreenshot2.png";
12 | import ClusterHistoryScreenshot from "../static/clusterHistoryScreenshot2.png";
13 |
14 | import { Dialog } from "@headlessui/react";
15 | import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
16 |
17 | const LandingPage = ({ navigate }) => {
18 | let carouselElement = useRef(); // carouselElement has to hold the HTMLElement of the carousel
19 |
20 | function scrollCarousel(targetImageNumber) {
21 | let carouselWidth = window.innerWidth * 0.94666;
22 |
23 | console.log("carouselWidth is ", carouselWidth);
24 |
25 | // Images are numbered from 1 to 4 so thats why we substract 1
26 | let targetImage = targetImageNumber - 1;
27 | console.log("targetImage is", targetImage);
28 |
29 | let targetXPixel = carouselWidth * targetImage + 8;
30 | console.log("targetXPixel is", targetXPixel);
31 |
32 | if (carouselElement.current) {
33 | carouselElement.current.scrollTo(targetXPixel, 0);
34 | }
35 | }
36 |
37 | const { setAuthMode } = useContext(NavbarContext).authModeState;
38 | const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
39 | // const { loggedIn } = useContext(NavbarContext).loggedState;
40 |
41 | // document.body.style.backgroundImage =
42 | // "linear-gradient(to right, white, rgb(113, 165, 246))";
43 |
44 | // Using useRef hook to set smooth scrolling for the "About Us" component
45 | const myRef = useRef(null);
46 | const infoRef = useRef(null);
47 | const executeScroll = () => {
48 | myRef.current.scrollIntoView({ behavior: "smooth" });
49 | };
50 | const executeScrollToInfo = () => {
51 | infoRef.current.scrollIntoView({ behavior: "smooth" });
52 | };
53 |
54 | return (
55 |
56 |
57 |
61 |
66 |
67 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
93 |
94 | setMobileMenuOpen(true)}
98 | >
99 | Open main menu
100 |
101 |
102 |
103 |
115 |
116 |
117 |
118 |
136 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | Don't get lost navigating your Kafka clusters. Grab a
173 | compass!
174 |
175 |
176 | Welcome to KafkaCompass: an open source web application to make
177 | your Kafka monitoring experience easier. KafkaCompass will be
178 | your navigator while working with Confluent Cloud. Get
179 | performance and content statistics to monitor your Kafka
180 | cluster, read and debug message streams, and check and compare
181 | historical cluster snapshots to track performance changes over
182 | time. All you need to get started is a running Kafka cluster in
183 | your Confluent Cloud. Create an account and start monitoring!
184 |
185 |
204 |
205 | {/*
206 |
207 |
214 |
215 |
*/}
216 |
217 |
218 |
219 |
220 |
221 |
228 |
229 |
230 |
237 |
238 |
239 |
246 |
247 |
248 |
249 |
250 | scrollCarousel(1)}
252 | // href="#item1"
253 | className="btn btn-xs min-h-0 w-2 h-2 btn-circle"
254 | >
255 | scrollCarousel(2)}
257 | // href="#item2"
258 | className="btn btn-xs min-h-0 w-2 h-2 btn-circle"
259 | >
260 | scrollCarousel(3)}
262 | // href="#item3"
263 | className="btn btn-xs min-h-0 w-2 h-2 btn-circle"
264 | >
265 |
266 |
267 |
268 |
269 |
270 |
274 |
279 |
280 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 | Meet the team
298 |
299 | {/*
*/}
300 |
301 |
302 |
303 |
304 | );
305 | };
306 |
307 | export default LandingPage;
308 |
--------------------------------------------------------------------------------
/client/helper/dateFormatter.js:
--------------------------------------------------------------------------------
1 | const date = new Intl.DateTimeFormat("en-US", {
2 | weekday: "short",
3 | month: "short",
4 | day: "numeric",
5 | year: "numeric",
6 | hour: "numeric",
7 | minute: "numeric",
8 | second: "numeric",
9 | timeZoneName: "short"
10 | });
11 |
12 | export default date;
13 |
--------------------------------------------------------------------------------
/client/helper/mapChartData.js:
--------------------------------------------------------------------------------
1 | function mapChartData(data) {
2 | const metricsObj = {
3 | retained_bytes: {
4 | colDescription: "Topics in Cluster",
5 | totalDescription: "Total number of bytes retained by server: "
6 | },
7 | sent_bytes: {
8 | colDescription: "Bytes sent by server",
9 | totalDescription: "Total number of bytes sent by server: "
10 | },
11 | received_records: {
12 | colDescription: "Records received by server",
13 | totalDescription: "Total number of records received by server: "
14 | },
15 | sent_records: {
16 | colDescription: "Records sent by server",
17 | totalDescription: "Records sent by server: "
18 | },
19 | request_bytes: {
20 | colDescription: "Bytes requested from server",
21 | totalDescription: "Bytes requested from server: "
22 | },
23 | response_bytes: {
24 | colDescription: "Bytes responded by server",
25 | totalDescription: "Total number of bytes responded by server: "
26 | },
27 | request_count: {
28 | colDescription: "Number of request made to server",
29 | totalDescription: "Total number of request made to server: "
30 | }
31 | };
32 |
33 | Object.keys(metricsObj).forEach((key) => {
34 | const labels = data[key].metrics.map(
35 | (metric) => metric.type || metric.topic
36 | );
37 | const dataset = {};
38 | dataset.label = key.split("_").at(-1);
39 | dataset.data = data[key].metrics.map((metric) => metric.value);
40 | dataset.backgroundColor = "rgba(113, 165, 246, 0.5)";
41 | dataset.borderWidth = 1;
42 | metricsObj[key].totalValue = data[key].totalValue;
43 |
44 | metricsObj[key].info = {
45 | labels,
46 | datasets: [dataset]
47 | };
48 | });
49 |
50 | const newColor = { backgroundColor: "rgba(250, 73, 112, 0.5)" };
51 |
52 | metricsObj["req_res"] = {
53 | colDescription: "Server request and response bytes",
54 | totalDescription1: "Total number of request bytes: ",
55 | totalDescription2: "Total number of response bytes: ",
56 | totalValue1: metricsObj.request_bytes.totalValue,
57 | totalValue2: metricsObj.response_bytes.totalValue,
58 | info: {
59 | labels: metricsObj.request_bytes.info.labels,
60 | datasets: [
61 | metricsObj.request_bytes.info.datasets[0],
62 | Object.assign({}, metricsObj.response_bytes.info.datasets[0], newColor)
63 | ]
64 | },
65 | compositeChart: true
66 | };
67 |
68 | return metricsObj;
69 | }
70 |
71 | export default mapChartData;
72 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BrowserRouter } from "react-router-dom";
3 | import App from "./app.jsx";
4 | import ReactDOM from "react-dom/client";
5 | import styles from "./static/styles.css";
6 | import styles2 from "../dist/output.css";
7 |
8 | const root = ReactDOM.createRoot(document.getElementById("root"));
9 | root.render(
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/client/resources/images/404-dog.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/resources/images/404-dog.jpg
--------------------------------------------------------------------------------
/client/static/DariaMordvinov.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/DariaMordvinov.jpg
--------------------------------------------------------------------------------
/client/static/DisonRuan.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/DisonRuan.jpg
--------------------------------------------------------------------------------
/client/static/JasonKuyper:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/JasonKuyper
--------------------------------------------------------------------------------
/client/static/JasonKuyper.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/JasonKuyper.jpg
--------------------------------------------------------------------------------
/client/static/KafkaCompassDashboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/KafkaCompassDashboard.jpg
--------------------------------------------------------------------------------
/client/static/KafkaCompassDashboard2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/KafkaCompassDashboard2.png
--------------------------------------------------------------------------------
/client/static/KafkaCompassDashboard3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/KafkaCompassDashboard3.png
--------------------------------------------------------------------------------
/client/static/KafkaCompassPerformanceStatsDemo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/KafkaCompassPerformanceStatsDemo.gif
--------------------------------------------------------------------------------
/client/static/KevinDooley.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/KevinDooley.png
--------------------------------------------------------------------------------
/client/static/RyanZarou.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/RyanZarou.png
--------------------------------------------------------------------------------
/client/static/Screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/Screenshot1.png
--------------------------------------------------------------------------------
/client/static/Screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/Screenshot2.png
--------------------------------------------------------------------------------
/client/static/Screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/Screenshot3.png
--------------------------------------------------------------------------------
/client/static/Screenshot4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/Screenshot4.png
--------------------------------------------------------------------------------
/client/static/cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/cat.jpg
--------------------------------------------------------------------------------
/client/static/clusterHistory.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/clusterHistory.gif
--------------------------------------------------------------------------------
/client/static/clusterHistoryScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/clusterHistoryScreenshot.png
--------------------------------------------------------------------------------
/client/static/clusterHistoryScreenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/clusterHistoryScreenshot2.png
--------------------------------------------------------------------------------
/client/static/consumeMessages.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/consumeMessages.gif
--------------------------------------------------------------------------------
/client/static/contentMonitoringScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/contentMonitoringScreenshot.png
--------------------------------------------------------------------------------
/client/static/contentMonitoringScreenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/contentMonitoringScreenshot2.png
--------------------------------------------------------------------------------
/client/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/favicon.ico
--------------------------------------------------------------------------------
/client/static/homeIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/homeIcon.png
--------------------------------------------------------------------------------
/client/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/logo.png
--------------------------------------------------------------------------------
/client/static/logo_without_text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/client/static/logo_without_text.png
--------------------------------------------------------------------------------
/client/static/styles.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer components {
6 | .btn-main {
7 | @apply bg-sky-600 hover:bg-sky-700 px-5 py-3 text-white rounded-lg;
8 | }
9 | }
10 |
11 | /* .dashboard-container { display: grid;
12 | grid-template-columns: 1fr 1fr 1.2fr 0.8fr;
13 | grid-template-rows: 0.5fr 1fr 1fr 1fr 1fr;
14 | gap: 0px 0px;
15 | grid-auto-flow: row;
16 | grid-template-areas:
17 | "nav-bar nav-bar nav-bar nav-bar"
18 | "tool-bar view-container view-container view-container"
19 | "tool-bar view-container view-container view-container"
20 | "tool-bar view-container view-container view-container"
21 | "tool-bar view-container view-container view-container";
22 | }
23 |
24 | .nav-bar { grid-area: nav-bar; }
25 |
26 | .tool-bar { grid-area: tool-bar; }
27 |
28 | .view-container { grid-area: view-container; } */
29 |
30 | article {
31 | display: flex;
32 | flex-direction: column;
33 | align-items: center;
34 | }
35 |
36 | .page-title {
37 | margin-bottom: 20px;
38 | text-align: center;
39 | font-size: 26px;
40 | }
41 |
42 | .icon-logo {
43 | width: 70px;
44 | }
45 |
46 | .navbar {
47 | border-bottom: 1 px solid lightgray;
48 | display: flex;
49 | }
50 |
51 | .auth-form {
52 | height: 500px;
53 | display: flex;
54 | flex-direction: column;
55 | justify-content: flex-end;
56 | gap: 30px;
57 | margin-top: 20px;
58 | align-items: center;
59 | background-color: white;
60 | }
61 |
62 | .wrapper {
63 | display: flex;
64 | flex-direction: column;
65 | justify-content: center;
66 | align-items: center;
67 | background-color: rgb(46, 52, 64);
68 | border-radius: 20px;
69 | width: 30vw;
70 | height: 400px;
71 | padding: 20px;
72 | }
73 |
74 | .auth-form-enter {
75 | opacity: 0;
76 | transform: scale(0.9);
77 | }
78 |
79 | .auth-form-enter-active {
80 | opacity: 1;
81 | transform: translateX(0);
82 | transition: opacity 300ms, transform 300ms;
83 | }
84 |
85 | .wrapper input {
86 | width: 60%;
87 | }
88 |
89 | .landing-container article h1 {
90 | text-align: center;
91 | margin-bottom: 4rem;
92 | }
93 |
94 | .cluster-container {
95 | display: grid;
96 | grid-template-columns: 1fr 1fr auto 1fr 1fr;
97 | grid-template-rows: 1fr 1fr;
98 | }
99 |
100 | .buttons-container {
101 | grid-column-start: 4;
102 | display: flex;
103 | justify-content: flex-end;
104 | align-items: center;
105 | }
106 |
107 | .buttons-container button,
108 | .buttons-container label {
109 | width: 120px;
110 | height: 35px;
111 | font-size: 12px;
112 | }
113 |
114 | .chart-container {
115 | /* grid-column-start: 3;
116 | display: flex;
117 | flex-direction: column; */
118 | align-items: center;
119 | text-align: center;
120 | height: 40vh;
121 | width: 45vw;
122 | }
123 |
124 | .chart-total {
125 | color: rgb(113, 165, 246);
126 | }
127 |
128 | .chart-total span {
129 | font-weight: 800;
130 | }
131 |
132 | div.scrollable {
133 | max-width: 40vw;
134 | height: 100%;
135 | margin: 0;
136 | padding: 0;
137 | overflow: auto;
138 | }
139 |
140 | .table-stats {
141 | width: 90vw;
142 | display: flex;
143 | justify-content: center;
144 | margin-bottom: 4rem;
145 | }
146 |
147 | .landing-container {
148 | display: flex;
149 | flex-direction: column;
150 | justify-content: center;
151 | align-items: center;
152 | width: 60vw;
153 | margin: auto;
154 | margin-top: 3rem;
155 | }
156 |
157 | .first-page {
158 | height: 100vh;
159 | }
160 |
161 | .about-header {
162 | text-align: center;
163 | font-size: 3rem;
164 | font-family: Verdana, Geneva, Tahoma, sans-serif;
165 | }
166 |
167 | .about-us {
168 | height: 100vh;
169 | width: 100vw;
170 | }
171 |
172 | .about-body {
173 | display: flex;
174 | flex-direction: row;
175 | }
176 |
177 | .bio-cardd {
178 | /* height: 34rem; */
179 | margin: 1rem;
180 | }
181 |
182 | .card-actions form button {
183 | margin: 1rem;
184 | }
185 |
186 | .bio-pic {
187 | height: 25vh;
188 | margin: 20px;
189 | }
190 |
191 | .about-links {
192 | display: flex;
193 | flex-direction: row;
194 | margin: 1rem;
195 | }
196 |
197 | .landing-buttons button {
198 | margin: 1rem;
199 | width: 15rem;
200 | }
201 |
202 | .table-history {
203 | display: flex;
204 | justify-content: center;
205 | align-items: center;
206 | }
207 |
208 | .history-table {
209 | width: 50rem;
210 | }
211 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | dev:
3 | image: kafkacompass/prod
4 | container_name: kcompass-prod
5 | ports:
6 | - 80:3000
7 | command: npm start
8 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | KafkaCompass
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kafkacompass",
3 | "version": "0.0.1",
4 | "description": "A place to monitor and optimize your Kafka clusters",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "NODE_ENV=production webpack",
9 | "start": "nodemon server/server.js",
10 | "watch-css": "npx tailwindcss -i ./client/static/styles.css -o ./dist/output.css --watch",
11 | "dev": " concurrently \"nodemon server/server.js\" \"npm run watch-css\" \"NODE_ENV=development webpack-dev-server --open --hot --progress --color\""
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/oslabs-beta/KafkaCompass.git"
16 | },
17 | "author": "Kevin Dooley, Dison Ruan, Ryan Zarou, Daria Mordvinov, Jason Kuyper",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/oslabs-beta/KafkaCompass/issues"
21 | },
22 | "homepage": "https://github.com/oslabs-beta/KafkaCompass#readme",
23 | "dependencies": {
24 | "@headlessui/react": "^1.7.13",
25 | "@heroicons/react": "^2.0.16",
26 | "axios": "^1.1.3",
27 | "bcrypt": "^5.1.0",
28 | "chart.js": "^4.1.1",
29 | "classnames": "^2.3.2",
30 | "concurrently": "^7.6.0",
31 | "cookie-parser": "^1.4.6",
32 | "daisyui": "^2.45.0",
33 | "dotenv": "^16.0.3",
34 | "express": "^4.18.2",
35 | "express-session": "^1.17.3",
36 | "jsonwebtoken": "^9.0.0",
37 | "kafkajs": "^2.2.3",
38 | "mongoose": "^6.8.0",
39 | "nodemon": "^2.0.20",
40 | "parse-prometheus-text-format": "^1.1.1",
41 | "react": "^18.2.0",
42 | "react-chartjs-2": "^5.1.0",
43 | "react-dom": "^18.2.0",
44 | "react-hook-form": "^7.40.0",
45 | "react-router-dom": "^6.4.5",
46 | "react-transition-group": "^4.4.5"
47 | },
48 | "devDependencies": {
49 | "@babel/core": "^7.20.5",
50 | "@babel/preset-env": "^7.20.2",
51 | "@babel/preset-react": "^7.18.6",
52 | "@tailwindcss/forms": "^0.5.3",
53 | "babel-loader": "^9.1.0",
54 | "css-loader": "^6.7.3",
55 | "global-jsdom": "^8.6.0",
56 | "html-webpack-plugin": "^5.5.0",
57 | "jsdom": "^20.0.3",
58 | "prettier": "2.8.1",
59 | "style-loader": "^3.3.1",
60 | "tailwindcss": "^3.2.4",
61 | "webpack": "^5.75.0",
62 | "webpack-cli": "^5.0.1",
63 | "webpack-dev-server": "^4.11.1"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/server/controllers/api-controller.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const axios = require("axios");
3 | const User = require("../models/user-model");
4 | const CloudCluster = require("../models/cloud-cluster-model");
5 | const { decrypt } = require("../encryption");
6 | const { Kafka } = require("kafkajs");
7 |
8 | const apiController = {};
9 |
10 | apiController.getClusterInfo = async (req, res, next) => {
11 | if (!req.session.user)
12 | return next({
13 | log: "apiController.getClusterInfo: ERROR: Unauthorized",
14 | message: {
15 | err: "Unauthorized"
16 | }
17 | });
18 |
19 | try {
20 | const { cloudCluster } = req.session.user;
21 | let rawCluster;
22 | if (!req.session.currentCluster) rawCluster = cloudCluster[0];
23 | else rawCluster = cloudCluster[req.session.currentCluster];
24 |
25 | //creating a deep copy of the cluster in the session to avoid mutating the session cluster
26 | const cluster = {};
27 | for (const key in rawCluster) {
28 | cluster[key] = rawCluster[key];
29 | }
30 |
31 | for (const key in cluster) {
32 | if (typeof cluster[key] !== "string") continue;
33 | else {
34 | cluster[key] = decrypt(cluster[key]);
35 | }
36 | }
37 | res.locals.cluster = cluster;
38 | next();
39 | } catch {
40 | next({ log: "error in getClusterInfo" });
41 | }
42 | };
43 |
44 | apiController.getClusterList = (req, res, next) => {
45 | if (!req.session.user)
46 | return next({
47 | log: "apiController.getClusterList: ERROR: Unauthorized",
48 | message: {
49 | err: "Unauthorized"
50 | }
51 | });
52 |
53 | const { cloudCluster } = req.session.user;
54 |
55 | try {
56 | const clusterList = [];
57 | for (let i = 0; i < cloudCluster.length; i++) {
58 | clusterList.push(decrypt(cloudCluster[i].clusterId));
59 | }
60 | res.locals.clusterList = clusterList;
61 | next();
62 | } catch (err) {
63 | next({
64 | log: "apiController.getClusterList: ERROR: Could not create clusterList",
65 | message: { error: err } //"Could not create cluster list" }
66 | });
67 | }
68 | };
69 |
70 | apiController.getTopics = async (req, res, next) => {
71 | const { cluster } = res.locals;
72 | const { RESTendpoint, clusterId, API_KEY, API_SECRET } = cluster;
73 | const token = Buffer.from(`${API_KEY}:${API_SECRET}`, "utf8").toString(
74 | "base64"
75 | );
76 | const headers = { Authorization: "Basic " + token };
77 |
78 | try {
79 | const response = await axios({
80 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/topics`,
81 | headers
82 | });
83 | // this will access the array of topics, feel free to read just the response to see all available keys
84 | const data = response.data.data;
85 | const topicList = [];
86 | data.forEach((el) => topicList.push(el.topic_name));
87 | res.locals.topicList = topicList;
88 | next();
89 | } catch (err) {
90 | next({ log: "error in getTopics" });
91 | }
92 | };
93 |
94 | // this API call will create new topic in your cluster
95 | // it will also add default number of partitions to the topic - 6
96 | // use cluster headers here
97 | apiController.addTopic = async (req, res, next) => {
98 | const { cluster } = res.locals;
99 | const { RESTendpoint, clusterId, API_KEY, API_SECRET } = cluster;
100 | const token = Buffer.from(`${API_KEY}:${API_SECRET}`, "utf8").toString(
101 | "base64"
102 | );
103 | const headers = { Authorization: "Basic " + token };
104 |
105 | const { topic } = req.body;
106 | //format topic to remove any spaces in the name with an underscore
107 | const formattedTopic = topic.replace(/ /g, "_");
108 |
109 | try {
110 | const response = axios({
111 | method: "post",
112 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/topics`,
113 | data: {
114 | topic_name: `${formattedTopic}`
115 | },
116 | headers
117 | });
118 | const data = await response;
119 | // console.log(data);
120 | next();
121 | } catch (err) {
122 | next({
123 | log: "error in addTopic",
124 | message: "could not add topic to cluster"
125 | });
126 | }
127 | };
128 |
129 | apiController.deleteTopic = async (req, res, next) => {
130 | const { cluster } = res.locals;
131 | const { RESTendpoint, clusterId, API_KEY, API_SECRET } = cluster;
132 | const token = Buffer.from(`${API_KEY}:${API_SECRET}`, "utf8").toString(
133 | "base64"
134 | );
135 | const headers = { Authorization: "Basic " + token };
136 |
137 | // name for the new topic
138 | const { topic } = req.body;
139 |
140 | try {
141 | const response = await axios({
142 | method: "delete",
143 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/topics/${topic}`,
144 | headers
145 | });
146 | next();
147 | } catch (err) {
148 | next({
149 | log: "error in deleteTopic",
150 | message: "could not delete topic in cluster"
151 | });
152 | }
153 | };
154 |
155 | apiController.getMessages = async (req, res, next) => {
156 | //general plan for this one would be to make our own consumer which would just give the latest stream of messages from the cluster
157 | //thinking we could use kafka.js for
158 | try {
159 | const { topic } = req.params;
160 | const { cluster } = res.locals;
161 | const { API_KEY, API_SECRET, clusterId, bootstrapServer } = cluster;
162 | const kafka = new Kafka({
163 | brokers: [bootstrapServer],
164 | clientId: clusterId,
165 | ssl: true,
166 | sasl: {
167 | mechanism: "plain",
168 | password: API_SECRET,
169 | username: API_KEY
170 | }
171 | });
172 | const groupId = "kafka-group";
173 | const consumer = kafka.consumer({ groupId });
174 | const admin = kafka.admin();
175 |
176 | const receiveMessages = async () => {
177 | await admin.connect();
178 | await admin.resetOffsets({ groupId, topic });
179 | await admin.disconnect();
180 | await consumer.connect();
181 | await consumer.subscribe({ topic: topic, fromBeginning: true });
182 | res.locals.messageList = [];
183 | await consumer.run({
184 | eachMessage: async ({ topic, partition, message }) => {
185 | const kafkaMessage = {
186 | topic,
187 | partition,
188 | timestamp: message.timestamp,
189 | offset: message.offset,
190 | value: message.value.toString()
191 | };
192 | console.log("Received message:", kafkaMessage);
193 | res.locals.messageList.push(kafkaMessage);
194 | }
195 | });
196 | setTimeout(() => {
197 | consumer.disconnect();
198 | next();
199 | }, 2000);
200 | };
201 | receiveMessages();
202 | } catch (err) {
203 | next({
204 | log: "error in getMessages",
205 | message: "Could not get messages from cluster"
206 | });
207 | }
208 | };
209 |
210 | apiController.addMessage = async (req, res, next) => {
211 | const { cluster } = res.locals;
212 | const { RESTendpoint, clusterId, API_KEY, API_SECRET } = cluster;
213 | const token = Buffer.from(`${API_KEY}:${API_SECRET}`, "utf8").toString(
214 | "base64"
215 | );
216 | const headers = { Authorization: "Basic " + token };
217 |
218 | const { topic, message } = req.body;
219 |
220 | try {
221 | const response = await axios({
222 | method: "post",
223 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/topics/${topic}/records`,
224 | data: { partition_id: null, value: { type: "JSON", data: `${message}` } },
225 | headers
226 | });
227 | const data = response.data;
228 | console.log("data in addMessage: ", data);
229 | next();
230 | } catch (err) {
231 | next({
232 | log: "error in addMessage",
233 | message: "could not add message to cluster"
234 | });
235 | }
236 | };
237 |
238 | module.exports = apiController;
239 |
--------------------------------------------------------------------------------
/server/controllers/api-functions.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const axios = require("axios");
3 |
4 | // we use it to parse prometheus to java script object
5 | const parsePrometheusTextFormat = require("parse-prometheus-text-format");
6 |
7 | // cluster key and secret
8 | const API_KEY = process.env.API_KEY;
9 | const API_SECRET = process.env.API_SECRET;
10 |
11 | // cloud key and secret
12 | const CLOUD_KEY = process.env.CLOUD_KEY;
13 | const CLOUD_SECRET = process.env.CLOUD_SECRET;
14 |
15 | // both - cloud and cluster key - can be found in your confluent account
16 | // find API keys in the left list of options in your dashboard (create new one with global access)
17 | // find cloud API keys in the right list of options (under billing)
18 | // if any access errors occures, check your accounts & access settings. Try to grant roles with more rights to your account
19 |
20 | // get this in your cloud account
21 | // OR by typing in CLI 'confluent kafka cluster decribe' OR './bin/confluent kafka cluster describe'
22 | const clusterId = process.env.CLUSTER;
23 |
24 | // get this in your cloud account
25 | // OR by typing in CLI 'confluent kafka cluster decribe' OR './bin/confluent kafka cluster describe'
26 | const RESTendpoint = process.env.REST;
27 |
28 | // to send your key and secret you need to encode it first
29 | // then set your authorization header with the encoded credentials
30 | const token = Buffer.from(`${API_KEY}:${API_SECRET}`, "utf8").toString(
31 | "base64"
32 | );
33 | const headers = { Authorization: "Basic " + token };
34 |
35 | // same process but for cloud credentials
36 | const cloudToken = Buffer.from(`${CLOUD_KEY}:${CLOUD_SECRET}`, "utf8").toString(
37 | "base64"
38 | );
39 | const cloudHeaders = { Authorization: "Basic " + cloudToken };
40 |
41 | // basic API endpoint for getting metrics list
42 | // we use cloudHeaders here
43 | // you can get number of partitions and bytes per topic among the other things
44 | // (confluent_kafka_server_response_bytes, confluent_kafka_server_active_connection_count, confluent_kafka_server_request_count, etc)
45 | const getMetricsList = async () => {
46 | const response = await axios({
47 | url: `https://api.telemetry.confluent.cloud/v2/metrics/cloud/export?resource.kafka.id=${clusterId}`,
48 | headers: cloudHeaders
49 | });
50 | const data = response.data;
51 | console.log(parsePrometheusTextFormat(data));
52 | parsePrometheusTextFormat(data).forEach((obj) => {
53 | if (obj.name === "confluent_kafka_server_request_count") {
54 | console.log(obj.metrics);
55 | }
56 | });
57 | };
58 |
59 | getMetricsList();
60 |
61 | // Returns a list of configuration parameters for the specified Kafka cluster.
62 | // It's basically list of endpoints to check for specific things in your cluster
63 | // use cluster headers here
64 | const getClusterConfigs = async () => {
65 | const response = await axios({
66 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}`,
67 | headers
68 | });
69 | const data = response.data;
70 | console.log(data);
71 | };
72 |
73 | // getClusterConfigs();
74 |
75 | // Returns list of topic objects in the cluster with information on replication factor, partitions and names of the topics
76 | // Within each topic you can also find API endpoint to this particular topic and its configuration
77 | // use cluster headers here
78 | const getListOfTopics = async () => {
79 | const response = await axios({
80 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/topics`,
81 | headers
82 | });
83 | // this will access the array of topics, feel free to read just the response to see all available keys
84 | const data = response.data.data;
85 | console.log(data);
86 | };
87 |
88 | // getListOfTopics();
89 |
90 | // Returns a list of configuration parameters for the specified Kafka cluster.
91 | // I don't know, the data array is empty in my case
92 | // use cluster headers here
93 | const getBrokers = async () => {
94 | const response = await axios({
95 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/broker-configs`,
96 | headers
97 | });
98 | const data = response.data;
99 | console.log(data);
100 | };
101 |
102 | // getBrokers();
103 |
104 | // this API call will create new topic in your cluster
105 | // it will also add default number of partitions to the topic - 6
106 | // use cluster headers here
107 | const createTopic = async () => {
108 | // name for the new topic
109 | const new_topic = "songs";
110 |
111 | const response = axios({
112 | method: "post",
113 | url: `https://pkc-lzvrd.us-west4.gcp.confluent.cloud:443/kafka/v3/clusters/lkc-j33yz8/topics`,
114 | data: {
115 | topic_name: `${new_topic}`
116 | },
117 | headers
118 | });
119 | const data = await response;
120 | console.log(data);
121 | };
122 |
123 | // createTopic();
124 |
125 | // this API call will delete specified topic in the cluster
126 | // use cluster headers here
127 | const deleteTopic = async () => {
128 | const topic_name = "songs";
129 |
130 | const response = await axios({
131 | method: "delete",
132 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/topics/${topic_name}`,
133 | headers
134 | });
135 |
136 | const data = response.data;
137 | console.log(data);
138 | };
139 |
140 | // deleteTopic();
141 |
142 | // This API call will produce records to the specified topic
143 | // use cluster headers
144 | // to produce several messages with one call
145 | const produceRecords = async () => {
146 | const topic_name = "songs";
147 | const response = await axios({
148 | method: "post",
149 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/topics/${topic_name}/records`,
150 | data: {
151 | partition_id: "1",
152 | value: { type: "JSON", data: "Another one (bites the dust)" }
153 | },
154 | headers
155 | });
156 | const data = response.data;
157 | console.log(data);
158 | };
159 |
160 | // produceRecords();
161 |
162 | // gets the list of partitions for specific topic
163 | // use cluster header
164 | const listOfPartitions = async () => {
165 | const topic_name = "poems";
166 | const response = await axios({
167 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/topics/${topic_name}/partitions`,
168 | headers
169 | });
170 | const data = response.data;
171 | console.log(data);
172 | };
173 |
174 | // listOfPartitions();
175 |
176 | // get one specific partition
177 | const getPartition = async () => {
178 | const topic_name = "poems";
179 | const partition_id = 1;
180 | const response = await axios({
181 | url: `${RESTendpoint}/kafka/v3/clusters/${clusterId}/topics/${topic_name}/partitions/${partition_id}`,
182 | headers
183 | });
184 | const data = response.data;
185 | console.log(data);
186 | };
187 |
188 | // getPartition();
189 |
--------------------------------------------------------------------------------
/server/controllers/cloud-auth-controller.js:
--------------------------------------------------------------------------------
1 | const { encrypt, decrypt } = require("../encryption");
2 |
3 | const cloudAuthController = {};
4 |
5 | cloudAuthController.encryptCredentials = (req, res, next) => {
6 | const {
7 | API_KEY,
8 | API_SECRET,
9 | CLOUD_KEY,
10 | CLOUD_SECRET,
11 | clusterId,
12 | RESTendpoint,
13 | bootstrapServer,
14 | cluster_name
15 | } = req.body;
16 |
17 | const credentials = {
18 | API_KEY,
19 | API_SECRET,
20 | CLOUD_KEY,
21 | CLOUD_SECRET,
22 | clusterId,
23 | RESTendpoint,
24 | bootstrapServer,
25 | cluster_name
26 | };
27 |
28 | for (const key in credentials) {
29 | credentials[key] = encrypt(credentials[key]);
30 | }
31 |
32 | res.locals.credentials = credentials;
33 | return next();
34 | };
35 |
36 | cloudAuthController.decryptCredentials = (req, res, next) => {};
37 |
38 | module.exports = cloudAuthController;
39 |
--------------------------------------------------------------------------------
/server/controllers/metric-controller.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/user-model");
2 | const axios = require("axios");
3 | const parsePrometheusTextFormat = require("parse-prometheus-text-format");
4 |
5 | const { decrypt } = require("../encryption");
6 |
7 | const metricController = {};
8 |
9 | metricController.fetchData = async (req, res, next) => {
10 | const { CLOUD_KEY, CLOUD_SECRET, clusterId } = res.locals.credentials;
11 | try {
12 | const url = `https://api.telemetry.confluent.cloud/v2/metrics/cloud/export?resource.kafka.id=${clusterId}`;
13 | const cloudToken = Buffer.from(
14 | `${CLOUD_KEY}:${CLOUD_SECRET}`,
15 | "utf8"
16 | ).toString("base64");
17 | const cloudHeaders = { Authorization: "Basic " + cloudToken };
18 |
19 | const response = await axios({
20 | url,
21 | headers: cloudHeaders
22 | });
23 |
24 | const data = parsePrometheusTextFormat(response.data);
25 | const metricsData = await formatMetrics(data);
26 |
27 | res.locals.metricsData = metricsData;
28 | return next();
29 | } catch {
30 | return next({
31 | log: "error in metricController.fetchData",
32 | message: "could not update metrics"
33 | });
34 | }
35 | };
36 |
37 | metricController.decryptKeys = (req, res, next) => {
38 | if (!req.session.user) {
39 | return next({
40 | log: "metricController.decrpytKeys: ERROR: Unauthorized",
41 | message: {
42 | err: "Unauthorized"
43 | }
44 | });
45 | }
46 | const { cloudCluster } = req.session.user;
47 | let rawCluster;
48 | if (!req.session.currentCluster) rawCluster = cloudCluster[0];
49 | else rawCluster = cloudCluster[req.session.currentCluster];
50 |
51 | const { CLOUD_KEY, CLOUD_SECRET, clusterId } = rawCluster;
52 |
53 | const credentials = { CLOUD_KEY, CLOUD_SECRET, clusterId };
54 |
55 | for (let key in credentials) {
56 | credentials[key] = decrypt(credentials[key]);
57 | }
58 |
59 | res.locals.credentials = credentials;
60 | res.locals.userId = req.session.user.id;
61 | res.locals.clusterId = rawCluster.clusterId;
62 |
63 | return next();
64 | };
65 |
66 | const formatMetrics = async (metrics) => {
67 | const nameMap = metrics.reduce((map, obj, i) => {
68 | if (!obj.name.includes("link")) map[obj.name.substring(23)] = i;
69 | return map;
70 | }, {});
71 |
72 | return createMetricsObject(metrics, nameMap);
73 | };
74 |
75 | const createMetricsObject = (dataset, nameMap) => {
76 | const output = {};
77 |
78 | const type = ["request_bytes", "response_bytes", "request_count"];
79 | const topic = [
80 | "sent_bytes",
81 | "received_records",
82 | "sent_records",
83 | "retained_bytes"
84 | ];
85 |
86 | for (let name in nameMap) {
87 | const data = dataset.at(nameMap[name]);
88 | if (name === "consumer_lag_offsets") {
89 | output[name] = mapper(data, "consumer_lag_offsets");
90 | } else if (type.includes(name)) {
91 | output[name] = mapper(data, "type");
92 | } else if (topic.includes(name)) {
93 | output[name] = mapper(data, "topic");
94 | } else {
95 | output[name] = mapper(data);
96 | }
97 | }
98 |
99 | return output;
100 | };
101 |
102 | function mapper(data, attribute) {
103 | const output = {};
104 | output.name = data.name;
105 | output.description = data.help;
106 | let totalValue = 0;
107 | const metrics = data.metrics.map(({ value, labels }) => {
108 | totalValue += Number(value);
109 | const consumer_lag = attribute === "consumer_lag_offsets";
110 | const output = consumer_lag
111 | ? createConsumerLagObj(value, labels)
112 | : createOtherObj(value, labels, attribute);
113 | return output;
114 | });
115 | output.totalValue = totalValue;
116 | output.metrics = metrics;
117 |
118 | return output;
119 | }
120 |
121 | function createOtherObj(value, labels, attribute) {
122 | const output = { value };
123 | if (attribute !== undefined) output[attribute] = labels[attribute];
124 | return output;
125 | }
126 |
127 | function createConsumerLagObj(value, labels) {
128 | return {
129 | value,
130 | consumer_group_id: labels.consumer_group_id,
131 | topic: labels.topic
132 | };
133 | }
134 |
135 | module.exports = metricController;
136 |
--------------------------------------------------------------------------------
/server/controllers/user-controller.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/user-model");
2 | const CloudCluster = require("../models/cloud-cluster-model");
3 | const Metric = require("../models/metric-model");
4 | const { Session } = require("express-session");
5 | const bcrypt = require("bcrypt");
6 | const { decrypt } = require("../encryption");
7 | const jwt = require("jsonwebtoken");
8 | require("dotenv").config();
9 |
10 | const userController = {};
11 | const superSecret = process.env.SUPER_SECRET;
12 |
13 | userController.verifyUser = async (req, res, next) => {
14 | const { username, password } = req.body;
15 |
16 | if (username === undefined || password === undefined) {
17 | return next({
18 | log: "userController.verifyUser: ERROR: Missing info",
19 | message: {
20 | err: "missing info"
21 | }
22 | });
23 | }
24 |
25 | try {
26 | const user = await User.findOne({ username })
27 | .populate("cloudCluster")
28 | .populate("metric");
29 |
30 | //Using bcrypt to compare password with its hashed version
31 | if (!(await bcrypt.compare(password, user.password))) throw new Error();
32 |
33 | res.locals.user = user;
34 | req.session.user = user;
35 | req.session.save();
36 | return next();
37 | } catch (error) {
38 | return next({
39 | log: "userController.verifyUser: ERROR: wrong info",
40 | message: {
41 | err: "wrong info"
42 | }
43 | });
44 | }
45 | };
46 |
47 | userController.createUser = async (req, res, next) => {
48 | const { email, username, password, firstName, lastName } = req.body;
49 |
50 | const credentials = { email, username, password };
51 |
52 | if (Object.keys(credentials).some((key) => credentials[key] === undefined)) {
53 | return next({
54 | log: "userController.createUser: ERROR: Missing essential info",
55 | message: {
56 | err: "missing essential info"
57 | }
58 | });
59 | }
60 |
61 | credentials.firstName = firstName;
62 | credentials.lastName = lastName;
63 |
64 | try {
65 | //Using bcrypt to hash password
66 | const hashedPassword = await bcrypt.hash(credentials.password, 10);
67 | credentials.password = hashedPassword;
68 | const user = await User.create(credentials);
69 | res.locals.user = user;
70 | req.session.user = user;
71 | req.session.save();
72 | return next();
73 | } catch (error) {
74 | return next({ log: "error in userController.createrUser" });
75 | }
76 | };
77 |
78 | userController.logOut = (req, res, next) => {
79 | req.session.destroy();
80 | res.clearCookie("token");
81 | return next();
82 | };
83 |
84 | // Authentificates user with unique token for 2 hours and stores JWT token in the cookie
85 | userController.setUserAuth = (req, res, next) => {
86 | const user = res.locals.user._id;
87 | const token = jwt.sign({ user }, superSecret, { expiresIn: 60 * 60 * 2 });
88 | res.cookie("token", token, { httpOnly: true });
89 | return next();
90 | };
91 |
92 | userController.checkUserAuth = (req, res, next) => {
93 | try {
94 | const token = req.cookies.token;
95 | const user = jwt.verify(token, superSecret);
96 | if (req.session.user && user.user === req.session.user._id) {
97 | req.session.auth = true;
98 | return next();
99 | } else {
100 | req.session.auth = false;
101 | return next();
102 | }
103 | } catch (err) {
104 | return next({
105 | log: "userController.checkUserAuth: ERROR: Unauthorized User",
106 | status: 401,
107 | message: {
108 | err: err
109 | }
110 | });
111 | }
112 | };
113 |
114 | userController.addCloudCluster = async (req, res, next) => {
115 | if (!req.session.user)
116 | return next({
117 | log: "userController.addCloudCluster: ERROR: Unauthorized",
118 | message: {
119 | err: "Unauthorized"
120 | }
121 | });
122 |
123 | const { _id } = req.session.user;
124 |
125 | const {
126 | API_KEY,
127 | API_SECRET,
128 | CLOUD_KEY,
129 | CLOUD_SECRET,
130 | clusterId,
131 | RESTendpoint,
132 | bootstrapServer,
133 | cluster_name
134 | } = res.locals.credentials;
135 |
136 | let user;
137 |
138 | try {
139 | user = await User.findById(_id);
140 | } catch (error) {
141 | return next({
142 | log: "userController.addCloudCluster: ERROR: unknown user",
143 | message: {
144 | err: "unknown user"
145 | }
146 | });
147 | }
148 |
149 | const clusterInfo = {
150 | API_KEY,
151 | API_SECRET,
152 | CLOUD_KEY,
153 | CLOUD_SECRET,
154 | clusterId,
155 | RESTendpoint,
156 | bootstrapServer,
157 | cluster_name
158 | };
159 |
160 | try {
161 | const cluster = await CloudCluster.create(clusterInfo);
162 |
163 | user.cloudCluster.push(cluster);
164 | user.save();
165 | req.session.user.cloudCluster.push(cluster);
166 | } catch (error) {
167 | return next({
168 | log: "userController.addCloudCluster: ERROR: failed to create cluster",
169 | message: {
170 | err: "failed to create cluster"
171 | }
172 | });
173 | }
174 |
175 | return next();
176 | };
177 |
178 | userController.addMetrics = async (req, res, next) => {
179 | const clusterId = decrypt(res.locals.clusterId);
180 | const user = await User.findById(req.session.user._id);
181 | const metricsData = res.locals.metricsData;
182 | metricsData.clusterId = clusterId;
183 | metricsData.created_at = Date.now();
184 | try {
185 | const metric = await Metric.create(metricsData);
186 | user.metric.push(metric);
187 | user.save();
188 | res.locals.metric = metric;
189 | } catch (error) {
190 | return next({
191 | log: "userController.addMetric: ERROR: failed to create metric",
192 | message: {
193 | err: "failed to create metric"
194 | }
195 | });
196 | }
197 | return next();
198 | };
199 |
200 | userController.switchCluster = async (req, res, next) => {
201 | const { cluster } = req.body;
202 | try {
203 | req.session.currentCluster = cluster;
204 | next();
205 | } catch {
206 | next({
207 | log: "userController.switchCluster: ERROR: failed to switch cluster",
208 | message: { err: "could not switch cluster" }
209 | });
210 | }
211 | };
212 |
213 | module.exports = userController;
214 |
--------------------------------------------------------------------------------
/server/credentials.js.example:
--------------------------------------------------------------------------------
1 | const MONGO_URI = your Mongo DB URI;
2 |
3 | module.exports = MONGO_URI;
--------------------------------------------------------------------------------
/server/encryption.js.example:
--------------------------------------------------------------------------------
1 | function encrypt(message){
2 | //your code here
3 | }
4 |
5 | function decrypt(message) {
6 | //your code here
7 | }
8 |
9 | module.exports = { encrypt, decrypt };
--------------------------------------------------------------------------------
/server/models/cloud-cluster-model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const { Schema, model } = mongoose;
4 |
5 | const cloudClusterSchema = new Schema({
6 | cluster_name: { type: String, required: true },
7 | API_KEY: { type: String, required: true },
8 | API_SECRET: { type: String, required: true },
9 | CLOUD_KEY: { type: String, required: true },
10 | CLOUD_SECRET: { type: String, required: true },
11 | clusterId: { type: String, required: true },
12 | RESTendpoint: { type: String, required: true },
13 | bootstrapServer: { type: String, required: true }
14 | });
15 |
16 | module.exports = new model("CloudCluster", cloudClusterSchema);
17 |
--------------------------------------------------------------------------------
/server/models/metric-model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const { Schema, model } = mongoose;
4 |
5 | const metricSchema = new Schema({
6 | clusterId: String,
7 | request_bytes: Object,
8 | response_bytes: Object,
9 | received_bytes: Object,
10 | sent_bytes: Object,
11 | received_records: Object,
12 | sent_records: Object,
13 | retained_bytes: Object,
14 | active_connection_count: Object,
15 | request_count: Object,
16 | partition_count: Object,
17 | successful_authentication_count: Object,
18 | consumer_lag_offsets: Object,
19 | cluster_load_percent: Object,
20 | created_at: Date
21 | });
22 |
23 | module.exports = new model("Metric", metricSchema);
24 |
--------------------------------------------------------------------------------
/server/models/user-model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const { Schema, model } = mongoose;
4 |
5 | const userSchema = new Schema({
6 | username: { type: String, required: true, unique: true },
7 | email: { type: String, required: true, unique: true },
8 | password: { type: String, required: true },
9 | firstName: String,
10 | lastName: String,
11 | cloudCluster: [
12 | {
13 | type: Schema.Types.ObjectId,
14 | ref: "CloudCluster"
15 | }
16 | ],
17 | metric: [
18 | {
19 | type: Schema.Types.ObjectId,
20 | ref: "Metric"
21 | }
22 | ]
23 | });
24 |
25 | module.exports = new model("User", userSchema);
26 |
--------------------------------------------------------------------------------
/server/routes/user-router.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/KafkaCompass/e0ce76c20a99c4de2d342a8df839773f070ece2a/server/routes/user-router.js
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const session = require("express-session");
3 | const cookieParser = require("cookie-parser");
4 |
5 | const path = require("path");
6 | const PORT = 3000;
7 |
8 | const mongodb = require("mongoose");
9 | const MONGO_URI = require("./credentials");
10 |
11 | const cloudAuthController = require("./controllers/cloud-auth-controller");
12 | const userController = require("./controllers/user-controller");
13 | const apiController = require("./controllers/api-controller");
14 | const metricController = require("./controllers/metric-controller");
15 |
16 | const app = express();
17 |
18 | mongodb.connect(MONGO_URI);
19 | // to parse incoming json objects and cookies
20 | app.use(express.json());
21 | app.use(cookieParser());
22 | app.use(express.urlencoded({ extended: true }));
23 |
24 | app.use(express.static(path.resolve(__dirname, "../dist")));
25 |
26 | const min = 60 * 1000;
27 | const hour = 60 * min;
28 | app.use(
29 | session({
30 | secret: "test",
31 | saveUninitialized: true,
32 | cookie: { maxAge: 50000000 },
33 | resave: false
34 | })
35 | );
36 |
37 | app.use(
38 | "/api/login",
39 | userController.verifyUser,
40 | userController.setUserAuth,
41 | (req, res, next) => {
42 | res.status(200).json(res.locals.user);
43 | }
44 | );
45 |
46 | app.get("/api/authenticate", userController.checkUserAuth, (req, res, next) => {
47 | res.status(200).json(req.session);
48 | });
49 |
50 | app.get("/api/logout", userController.logOut, (req, res, next) => {
51 | console.log("user logged out");
52 | res.status(200).json();
53 | });
54 |
55 | // testing endpoint for sign up
56 | app.use(
57 | "/api/signup",
58 | userController.createUser,
59 | userController.setUserAuth,
60 | (req, res, next) => {
61 | res.status(200).json(res.locals.user);
62 | }
63 | );
64 |
65 | // app.get('/', (req, res) => {
66 | // console.log('WE ARE HERE');
67 | // res.sendFile(path.resolve(__dirname, '../dist/index.html'));
68 | // });
69 | //requests to server go here
70 | app.post(
71 | "/api/cloud-auth",
72 | userController.checkUserAuth,
73 | cloudAuthController.encryptCredentials,
74 | userController.addCloudCluster,
75 | (req, res) => {
76 | return res.json(res.locals.credentials);
77 | }
78 | );
79 |
80 | //endpoint to switch current cluster in session
81 | app.get(
82 | "/api/getClusterList",
83 | userController.checkUserAuth,
84 | apiController.getClusterList,
85 | (req, res) => {
86 | return res.status(201).json(res.locals.clusterList);
87 | }
88 | );
89 |
90 | app.post(
91 | "/api/switchCluster",
92 | userController.checkUserAuth,
93 | userController.switchCluster,
94 | (req, res) => {
95 | return res.status(201).json("cluster switched");
96 | }
97 | );
98 |
99 | //endpoints to get and modify various elements of the cluster
100 |
101 | //message-related endpoints
102 | //get all messages in a topic
103 | app.get(
104 | "/api/message/:topic",
105 | apiController.getClusterInfo,
106 | apiController.getMessages,
107 | async (req, res) => {
108 | return res.status(200).json(res.locals.messageList);
109 | }
110 | );
111 | //add a message to a topic
112 | app.post(
113 | "/api/message",
114 | userController.checkUserAuth,
115 | apiController.getClusterInfo,
116 | apiController.addMessage,
117 | (req, res) => {
118 | return res.status(201).json("message added");
119 | }
120 | );
121 |
122 | //topic-related endpoints
123 | //get all topics in a cluster
124 | app.get(
125 | "/api/topic",
126 | userController.checkUserAuth,
127 | apiController.getClusterInfo,
128 | apiController.getTopics,
129 | (req, res) => {
130 | return res.status(200).json(res.locals.topicList);
131 | }
132 | );
133 | //add a topic to a cluster
134 | app.post(
135 | "/api/topic",
136 | userController.checkUserAuth,
137 | apiController.getClusterInfo,
138 | apiController.addTopic,
139 | (req, res) => {
140 | return res.status(201).json("topic added");
141 | }
142 | );
143 | //remove a topic from a cluster
144 | app.delete(
145 | "/api/topic",
146 | userController.checkUserAuth,
147 | apiController.getClusterInfo,
148 | apiController.deleteTopic,
149 | (req, res) => {
150 | return res.status(202).json("topic deleted");
151 | }
152 | );
153 |
154 | app.get(
155 | "/api/metric",
156 | userController.checkUserAuth,
157 | metricController.decryptKeys,
158 | metricController.fetchData,
159 | userController.addMetrics,
160 | (req, res) => {
161 | return res.json(res.locals.metric);
162 | }
163 | );
164 |
165 | app.get("/index.js", (req, res) => {
166 | res.status(200).sendFile(path.resolve(__dirname, "../dist/index.html"));
167 | });
168 |
169 | //catch-all that sends index.html file to client-side
170 | app.get("*", (req, res) => {
171 | res.status(200).sendFile(path.resolve(__dirname, "../dist/index.html"));
172 | });
173 |
174 | // global error handler
175 | app.use((err, req, res, next) => {
176 | const defaultErr = {
177 | log: "Express error handler caught unknown middleware error",
178 | status: 400,
179 | message: { err: "Unknown error occurred" }
180 | };
181 | const errorObj = Object.assign(defaultErr, err);
182 | console.log("Global error handler caught: ", errorObj.log);
183 | return res.status(errorObj.status).json(errorObj.message);
184 | });
185 |
186 | app.listen(PORT, () => {
187 | console.log(`Server listening on port: ${PORT}`);
188 | });
189 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./client/**/*.{js,jsx,ts,tsx}",
5 | "./client/containers/landing-page-container.jsx",
6 | "./client/components/**/*.{js,jsx,ts,tsx}"
7 | ],
8 | mode: "jit",
9 | purge: ["./dist/*.html", "./client/**/*.{js,jsx,ts,tsx,vue}"],
10 | theme: {
11 | screens: {
12 | xsm: { min: "480px" },
13 | sm: { min: "640px" },
14 | // => @media (min-width: 640px) { ... }
15 | md: { min: "768px" },
16 | // => @media (min-width: 768px) { ... }
17 | lgmax: { min: "1024px" },
18 | lg: "1024px",
19 | // => @media (min-width: 1024px) { ... }
20 | xl: "1280px",
21 | // => @media (min-width: 1280px) { ... }
22 | "2xl": "1536px"
23 | // => @media (min-width: 1536px) { ... }
24 | },
25 | extend: {
26 | fontFamily: { sans: ["Inter var"] }
27 | }
28 | },
29 | plugins: [require("daisyui"), require("@tailwindcss/forms")],
30 |
31 | daisyui: {
32 | styled: true,
33 | themes: false,
34 | base: true,
35 | utils: true,
36 | logs: true,
37 | rtl: false,
38 | prefix: "",
39 | darkTheme: "dark"
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const HtmlWebpackPlugin = require("html-webpack-plugin");
3 |
4 | module.exports = {
5 | mode: process.env.NODE_ENV || "production",
6 | entry: "./client/index.js",
7 | output: {
8 | path: path.resolve(__dirname, "./dist"),
9 | filename: "bundle.js"
10 | },
11 | devServer: {
12 | historyApiFallback: true,
13 | hot: true,
14 | host: "localhost",
15 | port: 8080,
16 | static: {
17 | directory: path.resolve(__dirname, "dist"),
18 | publicPath: "/"
19 | },
20 | proxy: {
21 | "/api": "http://localhost:3000"
22 | }
23 | },
24 | plugins: [
25 | new HtmlWebpackPlugin({
26 | template: "index.html",
27 | favicon: "./client/static/favicon.ico"
28 | })
29 | ],
30 | module: {
31 | rules: [
32 | {
33 | test: /\.jsx?/,
34 | exclude: /(node_modules|bower_components)/,
35 | use: {
36 | loader: "babel-loader",
37 | options: {
38 | presets: ["@babel/preset-env", "@babel/preset-react"]
39 | }
40 | }
41 | },
42 | {
43 | test: /\.css$/i,
44 | exclude: /node_modules/,
45 | use: ["style-loader", "css-loader"]
46 | },
47 | {
48 | test: /\.(jpe?g|png|gif|svg)$/i,
49 | type: "asset"
50 | }
51 | ]
52 | },
53 | resolve: {
54 | extensions: ["", ".js", ".jsx"]
55 | }
56 | };
57 |
--------------------------------------------------------------------------------