├── .dockerignore ├── .env.example ├── .gitignore ├── .vscode └── extensions.json ├── Dockerfile-server ├── Dockerfile-ui ├── Dockerfile-ui-dev ├── LICENSE ├── NextAuth ├── auth.js └── env.local ├── README.md ├── configs ├── next.config.js ├── postcss.config.js └── tailwind.config.js ├── consumer.js ├── dashboard.json ├── docker-compose.yml ├── docs ├── SETUP_GUIDE.md └── SLACK_SETUP_GUIDE.md ├── grafana-data ├── alerting │ └── 1 │ │ └── __default__.tmpl └── grafana.db ├── grafana.ini ├── grafana ├── dashboard.json ├── datasource.yml └── provisioning │ ├── dashboards │ ├── default.yaml │ ├── default.yml │ └── home.json │ └── datasources │ └── datasource.yml ├── kafka-cluster ├── .DS_Store ├── docker-compose.yml ├── dockerfile ├── instruction.txt ├── jmx-exporter │ └── jmx_prometheus_javaagent-0.19.0.jar ├── kafka.yml └── prometheus.yml ├── loginForm.jsx ├── next.config.js ├── package-lock.json ├── package.json ├── producer.js ├── prometheus.yml ├── public ├── Logo-Dark-Tranparent-150x150.png ├── Logo-Dark-Tranparent-320x320.png ├── Logo-Dark-Tranparent-Original.png ├── Logo-Light-Transparent- 320x320.png ├── Logo-Light-Transparent-150x150.png ├── Logo-Light-Transparent-Original.png ├── kafka-kare-background-v1.png ├── kafka-kare-background-v2.jpg ├── kafka-kare-logo-v2.png ├── kafka-kare-logo-v3-dark.png ├── kafka-kare-logo-v3.png ├── kafka-kare-logo.png ├── kafka-kare-meerkat-background-v2.png ├── kafka-kare-meerkat-background.png ├── kafkaKare-creatingEnvfile.gif ├── kafkakare-dockercomposeup-d.gif ├── kafkakareAddingcluster.gif └── kafkakareSignup.gif ├── server ├── README.md ├── controllers │ ├── clusterController.js │ ├── connectionStringController.js │ ├── grafanaApiController.js │ ├── iFrameController.js │ ├── metricsController.js │ ├── oAuthController.js │ ├── settingsController.js │ ├── slackController.js │ ├── testingController.js │ ├── tokenController.js │ └── userController.js ├── models │ ├── clusterModel.js │ └── userModel.js ├── routes │ ├── authRoutes.js │ ├── clustersRoutes.js │ ├── grafanaApiRoutes.js │ ├── iFrameRoutes.js │ ├── metricsRoutes.js │ ├── oAuthRoutes.js │ ├── settingsRoutes.js │ ├── slackRoutes.js │ └── testingRoutes.js └── server.js ├── src ├── app │ ├── 404 │ │ └── page.jsx │ ├── _app.jsx │ ├── about │ │ └── page.jsx │ ├── alerts │ │ └── page.jsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.js │ ├── cluster-health │ │ └── page.jsx │ ├── clusters │ │ └── page.jsx │ ├── consumer-lag │ │ └── page.jsx │ ├── dashboard │ │ └── page.jsx │ ├── layout.jsx │ ├── login │ │ └── page.jsx │ ├── page.jsx │ ├── providers.jsx │ ├── secret.js │ └── signup │ │ └── page.jsx ├── components │ ├── alerts │ │ ├── alertHistoryGraph.jsx │ │ ├── alertsByMetricGraph.jsx │ │ └── configureCustom.jsx │ ├── clusters │ │ ├── addClusterModal.jsx │ │ ├── clusterCard.jsx │ │ ├── deleteClusterModal.jsx │ │ ├── editClusterModal.jsx │ │ ├── logoutModal.jsx │ │ ├── mainContainer.jsx │ │ ├── mainContainer │ │ │ ├── clusterCard.jsx │ │ │ ├── deleteClusterModal.jsx │ │ │ └── editClusterModal.jsx │ │ ├── menuDrawer.jsx │ │ ├── navbar.jsx │ │ └── navbar │ │ │ ├── accountMenu.jsx │ │ │ ├── accountMenu │ │ │ ├── changePasswordModal.jsx │ │ │ ├── deleteAccountModal.jsx │ │ │ └── logoutModal.jsx │ │ │ ├── addClusterModal.jsx │ │ │ ├── menuDrawer.jsx │ │ │ └── searchInput.jsx │ ├── colorModeButton.jsx │ ├── graphs │ │ └── graph.jsx │ ├── index │ │ └── navbar.jsx │ ├── loadingModal.jsx │ ├── login │ │ └── loginForm.jsx │ ├── loginForm.jsx │ ├── navbar.jsx │ ├── signup │ │ └── signupForm.jsx │ └── signupForm.jsx ├── index.js ├── store │ ├── clusters.js │ └── user.js ├── styles │ ├── cn.js │ ├── cn.ts │ ├── globals.css │ └── theme.js ├── ui │ ├── home-background-animation.jsx │ └── navbar-menu.jsx └── utils │ ├── clustersHandler.js │ ├── dashboardHandler.js │ └── userHandler.js └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | Dockerfile 4 | Dockerfile-ui 5 | .dockerignore 6 | docker-compose.yml 7 | .next -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Secret key for JWT 2 | SECRET_KEY= 3 | 4 | # MongoDB contrainerized with docker 5 | MONGO_DB_USERNAME= 6 | MONGO_DB_PWD= 7 | 8 | # Slack Webhooks URL 9 | SLACK_WEBHOOK_URL= 10 | 11 | # MongoDB locally hosted 12 | # MONGODB_URI= 13 | 14 | # fill in id and secret from the oauth setup for each provider 15 | AUTH_GITHUB_ID= 16 | AUTH_GITHUB_SECRET= 17 | 18 | # google id and secret 19 | AUTH_GOOGLE_ID= 20 | AUTH_GOOGLE_SECRET= 21 | 22 | NEXTAUTH_URL=http://localhost:3000/ 23 | 24 | #run this command to generate a secret key 25 | #openssl rand -base64 32 26 | NEXTAUTH_SECRET= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | kafka-cluster/prometheus 45 | kafka/ 46 | kafka-cluster/prometheus 47 | kafka/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js build output 91 | .next 92 | out 93 | 94 | # Nuxt.js build / generate output 95 | .nuxt 96 | dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | .cache 110 | 111 | # Docusaurus cache and generated files 112 | .docusaurus 113 | 114 | # Serverless directories 115 | .serverless/ 116 | 117 | # FuseBox cache 118 | .fusebox/ 119 | 120 | # DynamoDB Local files 121 | .dynamodb/ 122 | 123 | # TernJS port file 124 | .tern-port 125 | 126 | # Stores VSCode versions used for testing VSCode extensions 127 | .vscode-test 128 | 129 | # yarn v2 130 | .yarn/cache 131 | .yarn/unplugged 132 | .yarn/build-state.yml 133 | .yarn/install-state.gz 134 | .pnp.* -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [] 3 | } -------------------------------------------------------------------------------- /Dockerfile-server: -------------------------------------------------------------------------------- 1 | FROM node:21-alpine 2 | 3 | WORKDIR /app/server 4 | COPY package*.json ./ 5 | COPY . . 6 | RUN npm install 7 | EXPOSE 3001 8 | #will start server in dev mode with nodemon. Not recommended for production 9 | CMD [ "npm","run", "server" ] -------------------------------------------------------------------------------- /Dockerfile-ui: -------------------------------------------------------------------------------- 1 | # Front UI dockerfile 2 | FROM node:21-alpine 3 | 4 | WORKDIR /usr/src/app 5 | # Copy package.json, package-lock.json, Next.js, Tailwind CSS configuration files 6 | COPY package.json package-lock.json ./ 7 | COPY configs/next.config.js ./ 8 | COPY configs/postcss.config.js ./ 9 | COPY configs/tailwind.config.js ./ 10 | 11 | RUN npm install 12 | 13 | COPY . . 14 | 15 | RUN npm run build 16 | 17 | 18 | EXPOSE 3000 19 | 20 | 21 | # switch back to this if run dev 22 | #CMD ["npm", "start"] 23 | CMD ["npm", "run", "dev"] 24 | 25 | 26 | -------------------------------------------------------------------------------- /Dockerfile-ui-dev: -------------------------------------------------------------------------------- 1 | # Front UI dockerfile 2 | FROM node:21-alpine 3 | 4 | # WORKDIR /app/ui 5 | #from the docerfile tutorial 6 | WORKDIR /usr/src/app 7 | # Copy package.json, package-lock.json, Next.js, Tailwind CSS configuration files 8 | # Necessary and differs from the backend for several reasons related to how Next.js and Tailwind CSS applications are built and run 9 | COPY package*.json ./ 10 | COPY configs/next.config.js ./ 11 | COPY configs/postcss.config.js ./ 12 | COPY configs/tailwind.config.js ./ 13 | 14 | COPY . . 15 | RUN npm install 16 | #for hot reloading per muticontainer tutorial/ idk if npm install already does this 17 | RUN npm install -g nodemon 18 | #added from multi container todo tutorial/ Runs app as a non-root user 19 | RUN chown -R node /usr/src/app 20 | USER node 21 | 22 | EXPOSE 4000 23 | 24 | #CMD ["npm", "start"] 25 | #testing this from programming with UMA 26 | CMD ["npm", "run", "dev"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Open Source Labs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NextAuth/auth.js: -------------------------------------------------------------------------------- 1 | import { NextAuthConfig } from 'next-auth'; //not sure if we need this one? 2 | import NextAuth from 'next-auth'; 3 | import CredentialsProvider from 'next-auth/providers/credentials'; 4 | 5 | import Google from 'next-auth/providers/google'; 6 | import GitHub from 'next-auth/providers/github'; 7 | 8 | const credentialsConfig = CredentialsProvider({ 9 | name: "Credentials", 10 | credentials: { 11 | username: { 12 | label: "Username", 13 | type: "text", 14 | }, 15 | password: { 16 | label: "Passpord", 17 | type: "password", 18 | }, 19 | }, 20 | async authorize(credentials) { 21 | if (credentials.username === '' && credentials.password === '') 22 | return { 23 | name: 'success', 24 | }; 25 | else return console.log('error authorizing credentials') 26 | }, 27 | }); 28 | 29 | 30 | const config = { 31 | providers: [Google, credentialsConfig], 32 | callbacks: { 33 | authorized( { request, auth }) { 34 | const { pathname } = request.nextUrl; 35 | if (pathname === 'middlewareProtected') return !!auth; 36 | return true; 37 | }, 38 | }, 39 | }; 40 | 41 | 42 | export const { handlers, auth, signIn, signOut } = NextAuth(config); -------------------------------------------------------------------------------- /NextAuth/env.local: -------------------------------------------------------------------------------- 1 | //fill in id and secret from the oauth setup for each provider 2 | 3 | //google id and secret 4 | AUTH_GOOGLE_ID=950219203152-vcngmgs4k2i9pn0g9k0ta8kn7a4hk7i5.apps.googleusercontent.com 5 | AUTH_GOOGLE_SECRET=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 6 | 7 | //github if we want to add it 8 | AUTH_GITHUB_ID= 9 | 10 | AUTH_GITHUB_SECRET= -------------------------------------------------------------------------------- /configs/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | //may or may not need all of these options 3 | webpack: ( 4 | config, 5 | { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack } 6 | ) => { 7 | return config 8 | } 9 | } -------------------------------------------------------------------------------- /configs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /configs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | //can modify the file extensions as needed 4 | content: ['../src/**/*.{js, ts, jsx, tsx}'], 5 | darkMode: "class", 6 | theme: { 7 | extend: { 8 | animation: { 9 | first: "moveVertical 30s ease infinite", 10 | second: "moveInCircle 20s reverse infinite", 11 | third: "moveInCircle 40s linear infinite", 12 | fourth: "moveHorizontal 40s ease infinite", 13 | fifth: "moveInCircle 20s ease infinite", 14 | }, 15 | keyframes: { 16 | moveHorizontal: { 17 | "0%": { 18 | transform: "translateX(-50%) translateY(-10%)", 19 | }, 20 | "50%": { 21 | transform: "translateX(50%) translateY(10%)", 22 | }, 23 | "100%": { 24 | transform: "translateX(-50%) translateY(-10%)", 25 | }, 26 | }, 27 | moveInCircle: { 28 | "0%": { 29 | transform: "rotate(0deg)", 30 | }, 31 | "50%": { 32 | transform: "rotate(180deg)", 33 | }, 34 | "100%": { 35 | transform: "rotate(360deg)", 36 | }, 37 | }, 38 | moveVertical: { 39 | "0%": { 40 | transform: "translateY(-50%)", 41 | }, 42 | "50%": { 43 | transform: "translateY(50%)", 44 | }, 45 | "100%": { 46 | transform: "translateY(-50%)", 47 | }, 48 | }, 49 | }, 50 | }, 51 | }, 52 | plugins: [], 53 | }; 54 | -------------------------------------------------------------------------------- /consumer.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require("kafkajs"); 2 | 3 | const kafka = new Kafka({ 4 | clientId: "my-consumer", 5 | brokers: ["localhost:9093"], 6 | }); 7 | 8 | const topic = "test-topic"; 9 | const consumer = kafka.consumer({ groupId: "test-group" }); 10 | 11 | const consumeMessages = async () => { 12 | await consumer.connect(); 13 | await consumer.subscribe({ topic, fromBeginning: true }); 14 | 15 | await consumer.run({ 16 | eachMessage: async ({ topic, partition, message }) => { 17 | console.log({ 18 | partition, 19 | offset: message.offset, 20 | value: message.value.toString(), 21 | }); 22 | }, 23 | }); 24 | }; 25 | 26 | consumeMessages().catch((error) => { 27 | console.error("Error in consumer script", error); 28 | }); 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | # This container includes 5 services - application-server, application-ui, mongo, mongo-express. and grafana 4 | services: 5 | kafka-kare-server: 6 | build: 7 | context: . 8 | dockerfile: Dockerfile-server 9 | ports: 10 | - "3001:3001" 11 | #for hot reloading in server, not working now 12 | # volumes: 13 | # - ./server:/app/server 14 | # - /app/node_modules 15 | #overides CMD in Dockerfile-server. This will start the server in dev mode(using nodemon) 16 | command: npm run prod 17 | #npm run server 18 | depends_on: 19 | - mongo 20 | environment: 21 | - MONGO_DB_USERNAME=admin # Same username and password. MUST MATCH. 22 | - MONGO_DB_PWD=supersecret 23 | networks: 24 | - kafka-kare-network 25 | # develop: 26 | # watch: 27 | # # - path: ./server/server.js 28 | # # target: /app/server 29 | # # action: sync 30 | # - path: /package.json 31 | # action: rebuild 32 | kafka-kare-ui: 33 | build: 34 | context: . 35 | dockerfile: Dockerfile-ui 36 | ports: 37 | - 3000:3000 #Frontend on localhost:3000 38 | depends_on: 39 | - kafka-kare-server 40 | networks: 41 | - kafka-kare-network 42 | #volumes are necessary for hot reloading 43 | volumes: 44 | - .:/usr/src/app 45 | - /usr/src/app/node_modules 46 | # watch isn't working perfectly. Need to figure out how to sync files correctly. So hot reloading in dev mode works. 47 | # develop: 48 | # watch: 49 | # - path: ./app/package.json 50 | # action: rebuild 51 | # - path: ./app/src/components 52 | # target: /usr/src/app 53 | # action: sync 54 | #does not work if you change name from mongo 55 | mongo: 56 | image: mongo 57 | ports: 58 | - 27017:27017 59 | environment: 60 | - MONGO_INITDB_ROOT_USERNAME=admin # Same username and password. MUST MATCH. 61 | - MONGO_INITDB_ROOT_PASSWORD=supersecret 62 | networks: 63 | - kafka-kare-network 64 | volumes: 65 | - mongo-data:/data/db 66 | 67 | mongo-express: 68 | image: mongo-express 69 | restart: always 70 | ports: 71 | - 8081:8081 72 | environment: 73 | - ME_CONFIG_MONGODB_ADMINUSERNAME=admin # Same username and password. MUST MATCH. 74 | - ME_CONFIG_MONGODB_ADMINPASSWORD=supersecret 75 | - ME_CONFIG_MONGODB_URL=mongodb://admin:supersecret@mongo 76 | #new way is to use mongo URL 77 | # - ME_CONFIG_MONGODB_SERVER=mongo 78 | networks: 79 | - kafka-kare-network 80 | depends_on: 81 | - mongo 82 | 83 | grafana: 84 | image: grafana/grafana:latest 85 | ports: 86 | - "3002:3000" # running on localhost:3002 87 | environment: 88 | GF_SECURITY_ADMIN_PASSWORD: "kafkakarepw" #username is admin 89 | GF_USERS_ALLOW_SIGN_UP: "false" 90 | volumes: 91 | - ./grafana-data:/var/lib/grafana 92 | - ./grafana.ini:/etc/grafana/grafana.ini 93 | - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources #needed for provisioning 94 | depends_on: 95 | - kafka-kare-server 96 | networks: 97 | - kafka-kare-network 98 | networks: 99 | kafka-kare-network: 100 | driver: bridge 101 | volumes: 102 | mongo-data: 103 | grafana-data: -------------------------------------------------------------------------------- /docs/SETUP_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Setup Guide 2 | 3 | 4 | ## Instructions to start the application 5 | 1. Build frontend image 6 | ``` 7 | docker build -f Dockerfile-ui -t kafka-kare-ui . 8 | ``` 9 | 10 | 2. Build backend image 11 | ``` 12 | docker build -f Dockerfile-server -t kafka-kare-server . 13 | ``` 14 | 15 | 3. Spin up application container 16 | ``` 17 | docker compose up -d 18 | ``` 19 | 20 | 4. Change directory to /kafka-cluster 21 | ``` 22 | cd kafka-cluster 23 | ``` 24 | 25 | 5. (First time running the application) Build kafka cluster image 26 | ``` 27 | docker build -t dockerpromkafka:latest . 28 | ``` 29 | 30 | 6. Spin up demo kafka-cluster container (demo Kafka-cluster container must be spun up after application container) 31 | ``` 32 | docker compose up -d 33 | ``` 34 | 35 | 7. Run the consumer followed by producer script 36 | ``` 37 | node consumer.js 38 | node producer.js 39 | ``` 40 | 41 | 8. Log into Grafana account at locahost:3002 42 | - Sign in with credentials admin/kafkakarepw 43 | 44 | 9. Visit application frontend at localhost:3000 45 | - Enjoy 46 | 47 | 48 | ## Instructions to stop the application 49 | 1. Spin down application container 50 | ``` 51 | docker compose down 52 | ``` 53 | 54 | 2. Change directory to /kafka-cluster 55 | ``` 56 | cd kafka-cluster 57 | ``` 58 | 59 | 3. Spin down demo kafka-cluster container 60 | ``` 61 | docker compose down 62 | ``` 63 | 64 | ## Instructions to connect Slack Notifications 65 | - For detailed instructions on how to set up and connect Slack notifications, please refer to our [Slack Setup Guide](./docs/SLACK_SETUP_GUIDE.md). -------------------------------------------------------------------------------- /docs/SLACK_SETUP_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Slack Setup Guide 2 | 3 | ## Instructions to enable Slack notifications 4 | 1. Access your Slack app at `api.slack.com` 5 | 2. Top right, under `Your Apps`, find and select the app you're working with. 6 | 3. In the sidebar menu of your app's settings page, click on `Incoming Webhooks` 7 | 4. Toggle `Active Incoming Webhooks` 8 | 5. At the bottom, click `Add New Webhook to Workspace` 9 | 6. This is your `SLACK_WEBHOOK_URL` that you will need to enter in metricsController.js -------------------------------------------------------------------------------- /grafana-data/alerting/1/__default__.tmpl: -------------------------------------------------------------------------------- 1 | 2 | {{ define "__subject" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ if gt (.Alerts.Resolved | len) 0 }}, RESOLVED:{{ .Alerts.Resolved | len }}{{ end }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}{{ end }} 3 | 4 | {{ define "__text_values_list" }}{{ if len .Values }}{{ $first := true }}{{ range $refID, $value := .Values -}} 5 | {{ if $first }}{{ $first = false }}{{ else }}, {{ end }}{{ $refID }}={{ $value }}{{ end -}} 6 | {{ else }}[no value]{{ end }}{{ end }} 7 | 8 | {{ define "__text_alert_list" }}{{ range . }} 9 | Value: {{ template "__text_values_list" . }} 10 | Labels: 11 | {{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }} 12 | {{ end }}Annotations: 13 | {{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }} 14 | {{ end }}{{ if gt (len .GeneratorURL) 0 }}Source: {{ .GeneratorURL }} 15 | {{ end }}{{ if gt (len .SilenceURL) 0 }}Silence: {{ .SilenceURL }} 16 | {{ end }}{{ if gt (len .DashboardURL) 0 }}Dashboard: {{ .DashboardURL }} 17 | {{ end }}{{ if gt (len .PanelURL) 0 }}Panel: {{ .PanelURL }} 18 | {{ end }}{{ end }}{{ end }} 19 | 20 | {{ define "default.title" }}{{ template "__subject" . }}{{ end }} 21 | 22 | {{ define "default.message" }}{{ if gt (len .Alerts.Firing) 0 }}**Firing** 23 | {{ template "__text_alert_list" .Alerts.Firing }}{{ if gt (len .Alerts.Resolved) 0 }} 24 | 25 | {{ end }}{{ end }}{{ if gt (len .Alerts.Resolved) 0 }}**Resolved** 26 | {{ template "__text_alert_list" .Alerts.Resolved }}{{ end }}{{ end }} 27 | 28 | 29 | {{ define "__teams_text_alert_list" }}{{ range . }} 30 | Value: {{ template "__text_values_list" . }} 31 | Labels: 32 | {{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }} 33 | {{ end }} 34 | Annotations: 35 | {{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }} 36 | {{ end }} 37 | {{ if gt (len .GeneratorURL) 0 }}Source: [{{ .GeneratorURL }}]({{ .GeneratorURL }}) 38 | 39 | {{ end }}{{ if gt (len .SilenceURL) 0 }}Silence: [{{ .SilenceURL }}]({{ .SilenceURL }}) 40 | 41 | {{ end }}{{ if gt (len .DashboardURL) 0 }}Dashboard: [{{ .DashboardURL }}]({{ .DashboardURL }}) 42 | 43 | {{ end }}{{ if gt (len .PanelURL) 0 }}Panel: [{{ .PanelURL }}]({{ .PanelURL }}) 44 | 45 | {{ end }} 46 | {{ end }}{{ end }} 47 | 48 | 49 | {{ define "teams.default.message" }}{{ if gt (len .Alerts.Firing) 0 }}**Firing** 50 | {{ template "__teams_text_alert_list" .Alerts.Firing }}{{ if gt (len .Alerts.Resolved) 0 }} 51 | 52 | {{ end }}{{ end }}{{ if gt (len .Alerts.Resolved) 0 }}**Resolved** 53 | {{ template "__teams_text_alert_list" .Alerts.Resolved }}{{ end }}{{ end }} 54 | -------------------------------------------------------------------------------- /grafana-data/grafana.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/grafana-data/grafana.db -------------------------------------------------------------------------------- /grafana.ini: -------------------------------------------------------------------------------- 1 | ; [paths] 2 | ; logs = /var/lib/grafana/logs 3 | ; plugins = /var/lib/grafana/plugins 4 | ; provisioning = /etc/grafana/provisioning 5 | 6 | ; [server] 7 | ; domain = localhost 8 | ; root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/ 9 | 10 | [security] 11 | allow_embedding = true 12 | # x_frame_options is deprecated and not necessary if allow_embedding is true 13 | 14 | 15 | # Set header value for X-Frame-Options, used to prevent clickjacking attacks. 16 | # Recommended values: "deny" (default), "sameorigin", or "allow-from https://example.com/" 17 | ; x_frame_options = "allow-from http://localhost:3000" 18 | 19 | -------------------------------------------------------------------------------- /grafana/datasource.yml: -------------------------------------------------------------------------------- 1 | # config file version 2 | apiVersion: 1 3 | 4 | # list of datasources that should be deleted from the database 5 | deleteDatasources: 6 | - name: Prometheus 7 | orgId: 1 8 | 9 | # list of datasources to insert/update depending 10 | # what's available in the database 11 | datasources: 12 | # name of the datasource. Required 13 | - name: Prometheus 14 | # datasource type. Required 15 | type: prometheus 16 | # access mode. proxy or direct (Server or Browser in the UI). Required 17 | access: proxy 18 | # org id. will default to orgId 1 if not specified 19 | orgId: 1 20 | # url 21 | url: http://prometheus:9090 22 | # database password, if used 23 | password: 24 | # database user, if used 25 | user: 26 | # database name, if used 27 | database: 28 | # enable/disable basic auth 29 | basicAuth: false 30 | # basic auth username 31 | # basicAuthUser: admin 32 | # basic auth password 33 | # basicAuthPassword: kafka 34 | # enable/disable with credentials headers 35 | withCredentials: true 36 | # mark as default datasource. Max one per org 37 | isDefault: true 38 | # fields that will be converted to json and stored in json_data 39 | # jsonData: 40 | # graphiteVersion: '1.1' 41 | # tlsAuth: false 42 | # tlsAuthWithCACert: false 43 | # # json object of data that will be encrypted. 44 | # secureJsonData: 45 | # tlsCACert: '...' 46 | # tlsClientCert: '...' 47 | # tlsClientKey: '...' 48 | # version: 1 49 | # # allow users to edit datasources from the UI. 50 | # editable: true -------------------------------------------------------------------------------- /grafana/provisioning/dashboards/default.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'default' 5 | type: file 6 | options: 7 | path: /etc/grafana/provisioning/dashboards -------------------------------------------------------------------------------- /grafana/provisioning/dashboards/default.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'default' 5 | type: file 6 | options: 7 | path: /etc/grafana/provisioning/dashboards -------------------------------------------------------------------------------- /grafana/provisioning/dashboards/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "id": 3, 22 | "links": [], 23 | "panels": [ 24 | { 25 | "aliasColors": {}, 26 | "bars": false, 27 | "dashLength": 10, 28 | "dashes": false, 29 | "datasource": { 30 | "type": "prometheus", 31 | "uid": "bdgw3tsrgfk74e" 32 | }, 33 | "fill": 1, 34 | "fillGradient": 0, 35 | "gridPos": { 36 | "h": 9, 37 | "w": 24, 38 | "x": 0, 39 | "y": 0 40 | }, 41 | "hiddenSeries": false, 42 | "id": 1, 43 | "legend": { 44 | "avg": false, 45 | "current": false, 46 | "max": false, 47 | "min": false, 48 | "show": true, 49 | "total": false, 50 | "values": false 51 | }, 52 | "lines": true, 53 | "linewidth": 1, 54 | "nullPointMode": "null", 55 | "options": { 56 | "alertThreshold": true 57 | }, 58 | "percentage": false, 59 | "pluginVersion": "10.4.1", 60 | "pointradius": 2, 61 | "points": false, 62 | "renderer": "flot", 63 | "seriesOverrides": [], 64 | "spaceLength": 10, 65 | "stack": false, 66 | "steppedLine": false, 67 | "targets": [ 68 | { 69 | "datasource": { 70 | "type": "prometheus", 71 | "uid": "bdgw3tsrgfk74e" 72 | }, 73 | "expr": "rate(kafka_server_brokertopicmetrics_messagesin_total{topic=\"test-topic\"}[1m])", 74 | "refId": "A" 75 | } 76 | ], 77 | "thresholds": [], 78 | "timeRegions": [], 79 | "title": "New Throughput Graph", 80 | "tooltip": { 81 | "shared": true, 82 | "sort": 0, 83 | "value_type": "individual" 84 | }, 85 | "type": "graph", 86 | "xaxis": { 87 | "mode": "time", 88 | "show": true, 89 | "values": [] 90 | }, 91 | "yaxes": [ 92 | { 93 | "format": "short", 94 | "logBase": 1, 95 | "show": true 96 | }, 97 | { 98 | "format": "short", 99 | "logBase": 1, 100 | "show": true 101 | } 102 | ], 103 | "yaxis": { 104 | "align": false 105 | } 106 | } 107 | ], 108 | "schemaVersion": 39, 109 | "tags": [], 110 | "templating": { 111 | "list": [] 112 | }, 113 | "time": { 114 | "from": "now-5m", 115 | "to": "now" 116 | }, 117 | "timepicker": {}, 118 | "timezone": "", 119 | "title": "New Dashboard", 120 | "uid": "cdgweyqwsjif4e", 121 | "version": 2, 122 | "weekStart": "" 123 | } -------------------------------------------------------------------------------- /grafana/provisioning/datasources/datasource.yml: -------------------------------------------------------------------------------- 1 | # # config file version 2 | # apiVersion: 1 3 | 4 | # # list of datasources that should be deleted from the database 5 | # deleteDatasources: 6 | # - name: Prometheus 7 | # orgId: 1 8 | 9 | # # list of datasources to insert/update depending 10 | # # what's available in the database 11 | # datasources: 12 | # # name of the datasource. Required 13 | # - name: Prometheus 14 | # # datasource type. Required 15 | # type: prometheus 16 | # # access mode. proxy or direct (Server or Browser in the UI). Required 17 | # access: proxy 18 | # # org id. will default to orgId 1 if not specified 19 | # orgId: 1 20 | # # url 21 | # url: http://prometheus:9090 22 | # # database password, if used 23 | # password: 24 | # # database user, if used 25 | # user: 26 | # # database name, if used 27 | # database: 28 | # # enable/disable basic auth 29 | # basicAuth: false 30 | # # basic auth username 31 | # # basicAuthUser: admin 32 | # # basic auth password 33 | # # basicAuthPassword: kafka 34 | # # enable/disable with credentials headers 35 | # withCredentials: true 36 | # # mark as default datasource. Max one per org 37 | # isDefault: true 38 | # # fields that will be converted to json and stored in json_data 39 | # # jsonData: 40 | # # graphiteVersion: '1.1' 41 | # # tlsAuth: false 42 | # # tlsAuthWithCACert: false 43 | # # # json object of data that will be encrypted. 44 | # # secureJsonData: 45 | # # tlsCACert: '...' 46 | # # tlsClientCert: '...' 47 | # # tlsClientKey: '...' 48 | # # version: 1 49 | # # # allow users to edit datasources from the UI. 50 | # editable: true 51 | 52 | apiVersion: 1 53 | 54 | datasources: 55 | - name: Prometheus 56 | type: prometheus 57 | access: proxy 58 | url: http://prometheus:9090 59 | jsonData: 60 | httpMethod: POST 61 | secureJsonData: 62 | {} 63 | version: 1 64 | editable: true 65 | -------------------------------------------------------------------------------- /kafka-cluster/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/kafka-cluster/.DS_Store -------------------------------------------------------------------------------- /kafka-cluster/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | # This container includes 3 services - zookeeper, prometheus, and kafka 4 | services: 5 | # zookeeper: 6 | # image: wurstmeister/zookeeper 7 | # container_name: zookeeper 8 | # ports: 9 | # - '2181:2181' 10 | # networks: 11 | # - local-cluster-prom-network 12 | zookeeper: 13 | image: docker.io/bitnami/zookeeper:3.8 14 | restart: always 15 | hostname: zookeeper 16 | container_name: zookeeper 17 | ports: 18 | - "2181:2181" 19 | environment: 20 | ZOOKEEPER_CLIENT_PORT: 2181 21 | ZOOKEEPER_TICK_TIME: 2000 22 | ALLOW_ANONYMOUS_LOGIN: yes 23 | networks: 24 | - local-cluster-prom-network 25 | 26 | prometheus: 27 | image: prom/prometheus 28 | volumes: 29 | - './prometheus.yml:/etc/prometheus/prometheus.yml' 30 | networks: 31 | - local-cluster-prom-network 32 | - kafka-kare_kafka-kare-network # Need to connect to external network to allow Grafana access to Prometheus 33 | ports: 34 | - 9090:9090 35 | 36 | kafka: 37 | image: dockerpromkafka:latest 38 | container_name: kafka 39 | ports: 40 | - '9093:9093' # Accessible to client on Port 9092 41 | - '7070:7070' # Exposes Port 7070 for JMX Exporter, which Prometheus scrapes 42 | depends_on: 43 | - zookeeper 44 | - prometheus 45 | environment: 46 | KAFKA_ADVERTISED_HOST_NAME: localhost 47 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 48 | # EXTRA_ARGS: adds the JMX Exporter Java agent to Kafka, specifying the port (7070) and the configuration file (kafka.yml) that determines which metrics to expose. 49 | EXTRA_ARGS: -javaagent:/opt/kafka_2.13-2.8.1/libs/jmx_prometheus_javaagent.jar=7070:/opt/kafka_2.13-2.8.1/libs/kafka.yml 50 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' 51 | KAFKA_DELETE_TOPIC_ENABLE: 'true' 52 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT 53 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9093 54 | KAFKA_LISTENERS: PLAINTEXT://:9093 55 | KAFKA_BROKER_ID: 2 56 | # KAFKA_ADVERTISED_PORT: 9093 57 | JMX_PORT: 1099 # This is separate from JMX Exporter 58 | KAFKA_JMX_OPTS: '-Dcom.sun.management.jmxremote=true 59 | -Dcom.sun.management.jmxremote.authenticate=false 60 | -Dcom.sun.management.jmxremote.ssl=false 61 | -Djava.rmi.server.hostname=localhost 62 | -Dcom.sun.management.jmxremote.host=localhost 63 | -Dcom.sun.management.jmxremote.port=9999 64 | -Dcom.sun.management.jmxremote.rmi.port=9999 65 | -Djava.net.preferIPv4Stack=true' 66 | volumes: 67 | - /var/run/docker.sock:/var/run/docker.sock 68 | networks: 69 | - local-cluster-prom-network 70 | 71 | networks: 72 | local-cluster-prom-network: 73 | driver: bridge 74 | kafka-kare_kafka-kare-network: # Need to connect to external network to allow Grafana access to Prometheus 75 | external: true -------------------------------------------------------------------------------- /kafka-cluster/dockerfile: -------------------------------------------------------------------------------- 1 | # Builds a custom Kafka Docker image that includes the JMX Prometheus Java agent (jmx_prometheus_javaagent-0.19.0.jar) and a Kafka configuration file (kafka.yml for the JMX Exporter). 2 | FROM wurstmeister/kafka:latest 3 | ADD kafka.yml /opt/kafka/libs/kafka.yml 4 | 5 | RUN wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.19.0/jmx_prometheus_javaagent-0.19.0.jar 6 | 7 | RUN cp jmx_prometheus_javaagent-0.19.0.jar /opt/kafka/libs/jmx_prometheus_javaagent.jar 8 | RUN chmod +r /opt/kafka/libs/jmx_prometheus_javaagent.jar 9 | 10 | EXPOSE 7070 11 | # Exposing port 7070 of the kafka cluster for JMX Exporter -------------------------------------------------------------------------------- /kafka-cluster/instruction.txt: -------------------------------------------------------------------------------- 1 | 1. from your terminal navigate to kafka-cluster folder 2 | 2. build kafka cluster image with 3 | - docker build -t dockerpromkafka:latest . 4 | 3. run docker-compose file with 5 | - docker-compose -f docker-compose.yml up -d -------------------------------------------------------------------------------- /kafka-cluster/jmx-exporter/jmx_prometheus_javaagent-0.19.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/kafka-cluster/jmx-exporter/jmx_prometheus_javaagent-0.19.0.jar -------------------------------------------------------------------------------- /kafka-cluster/kafka.yml: -------------------------------------------------------------------------------- 1 | lowercaseOutputName: true 2 | 3 | rules: 4 | # Special cases and very specific rules 5 | - pattern: kafka.server<>Value 6 | name: kafka_server_$1_$2 7 | type: GAUGE 8 | labels: 9 | clientId: '$3' 10 | topic: '$4' 11 | partition: '$5' 12 | - pattern: kafka.server<>Value 13 | name: kafka_server_$1_$2 14 | type: GAUGE 15 | labels: 16 | clientId: '$3' 17 | broker: '$4:$5' 18 | - pattern: kafka.coordinator.(\w+)<>Value 19 | name: kafka_coordinator_$1_$2_$3 20 | type: GAUGE 21 | 22 | # Generic per-second counters with 0-2 key/value pairs 23 | - pattern: kafka.(\w+)<>Count 24 | name: kafka_$1_$2_$3_total 25 | type: COUNTER 26 | labels: 27 | '$4': '$5' 28 | '$6': '$7' 29 | - pattern: kafka.(\w+)<>Count 30 | name: kafka_$1_$2_$3_total 31 | type: COUNTER 32 | labels: 33 | '$4': '$5' 34 | - pattern: kafka.(\w+)<>Count 35 | name: kafka_$1_$2_$3_total 36 | type: COUNTER 37 | 38 | # Quota specific rules 39 | - pattern: kafka.server<>([a-z-]+) 40 | name: kafka_server_quota_$4 41 | type: GAUGE 42 | labels: 43 | resource: '$1' 44 | user: '$2' 45 | clientId: '$3' 46 | - pattern: kafka.server<>([a-z-]+) 47 | name: kafka_server_quota_$3 48 | type: GAUGE 49 | labels: 50 | resource: '$1' 51 | clientId: '$2' 52 | - pattern: kafka.server<>([a-z-]+) 53 | name: kafka_server_quota_$3 54 | type: GAUGE 55 | labels: 56 | resource: '$1' 57 | user: '$2' 58 | 59 | # Generic gauges with 0-2 key/value pairs 60 | - pattern: kafka.(\w+)<>Value 61 | name: kafka_$1_$2_$3 62 | type: GAUGE 63 | labels: 64 | '$4': '$5' 65 | '$6': '$7' 66 | - pattern: kafka.(\w+)<>Value 67 | name: kafka_$1_$2_$3 68 | type: GAUGE 69 | labels: 70 | '$4': '$5' 71 | - pattern: kafka.(\w+)<>Value 72 | name: kafka_$1_$2_$3 73 | type: GAUGE 74 | 75 | # Emulate Prometheus 'Summary' metrics for the exported 'Histogram's. 76 | # 77 | # Note that these are missing the '_sum' metric! 78 | - pattern: kafka.(\w+)<>Count 79 | name: kafka_$1_$2_$3_count 80 | type: COUNTER 81 | labels: 82 | '$4': '$5' 83 | '$6': '$7' 84 | - pattern: kafka.(\w+)<>(\d+)thPercentile 85 | name: kafka_$1_$2_$3 86 | type: GAUGE 87 | labels: 88 | '$4': '$5' 89 | '$6': '$7' 90 | quantile: '0.$8' 91 | - pattern: kafka.(\w+)<>Count 92 | name: kafka_$1_$2_$3_count 93 | type: COUNTER 94 | labels: 95 | '$4': '$5' 96 | - pattern: kafka.(\w+)<>(\d+)thPercentile 97 | name: kafka_$1_$2_$3 98 | type: GAUGE 99 | labels: 100 | '$4': '$5' 101 | quantile: '0.$6' 102 | - pattern: kafka.(\w+)<>Count 103 | name: kafka_$1_$2_$3_count 104 | type: COUNTER 105 | - pattern: kafka.(\w+)<>(\d+)thPercentile 106 | name: kafka_$1_$2_$3 107 | type: GAUGE 108 | labels: 109 | quantile: '0.$4' 110 | 111 | # Generic gauges for MeanRate Percent 112 | # Ex) kafka.server<>MeanRate 113 | - pattern: kafka.(\w+)<>MeanRate 114 | name: kafka_$1_$2_$3_percent 115 | type: GAUGE 116 | - pattern: kafka.(\w+)<>Value 117 | name: kafka_$1_$2_$3_percent 118 | type: GAUGE 119 | - pattern: kafka.(\w+)<>Value 120 | name: kafka_$1_$2_$3_percent 121 | type: GAUGE 122 | labels: 123 | '$4': '$5' -------------------------------------------------------------------------------- /kafka-cluster/prometheus.yml: -------------------------------------------------------------------------------- 1 | # Configuration file specifying how Prometheus should scrape metrics 2 | global: 3 | scrape_interval: 1s 4 | evaluation_interval: 1s 5 | scrape_configs: 6 | - job_name: 'kafka' # Scrape job for Kafka 7 | scheme: http 8 | static_configs: 9 | - targets: ['kafka:7070'] # Pointing to the target Port 7070, which is the port exposed by the JMX Exporter -------------------------------------------------------------------------------- /loginForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import axios from 'axios'; 4 | // import { auth, signIn } from '../../NextAuth/auth.js'; 5 | import { 6 | Input, 7 | Box, 8 | Heading, 9 | Flex, 10 | Image, 11 | Button, 12 | InputGroup, 13 | Stack, 14 | Link, 15 | Text, 16 | InputLeftElement, 17 | chakra, 18 | FormControl, 19 | FormHelperText, 20 | InputRightElement, 21 | } from '@chakra-ui/react'; 22 | import { FaUserAlt, FaLock } from "react-icons/fa"; 23 | 24 | 25 | const LoginForm = () => { 26 | const [formData, setFormData] = useState({ username: '', password: '' }); 27 | const [showPassword, setShowPassword] = useState(false); 28 | const [errorMessage, setErrorMessage] = useState(''); 29 | const router = useRouter(); 30 | 31 | // Function to handle form input changes 32 | const handleChange = (e) => { 33 | const { name, value } = e.target; 34 | setFormData(prevState => ({ ...prevState, [name]: value })); 35 | }; 36 | 37 | // Function to toggle password visibility 38 | const handleShowClick = () => setShowPassword(!showPassword); 39 | 40 | //handles click of submit and calls handleLogin 41 | const handleSubmit = async (e) => { 42 | e.preventDefault(); 43 | // await signIn(); 44 | handleLogin(); 45 | }; 46 | 47 | const { username, password } = formData; 48 | 49 | const handleLogin = async () => { 50 | console.log('username: ', username); 51 | console.log('password: ', password); 52 | try { 53 | // Send a POST request to the backend endpoint '/auth/login' 54 | const response = await axios.post( 55 | 'http://localhost:3001/auth/login', 56 | { username, password }, 57 | {withCredentials: true} 58 | ); // Convert data to JSON format and send it in the request body 59 | console.log('response: ', response.data); 60 | 61 | // Parse the response data as JSON 62 | // const data = await response.json(); 63 | 64 | // If the response is successful (status code 2xx), navigate to the clusters page 65 | if (response.status === 200) { 66 | console.log('successful login') 67 | router.replace('/clusters'); 68 | } else { 69 | // If there's an error response, log the error message to the console 70 | // console.error(data.error); 71 | router.push('/signup'); 72 | } 73 | } catch (error) { 74 | // Handle any errors that occur during the fetch request 75 | console.log('Error:', error); 76 | router.push('/signup'); 77 | } 78 | }; 79 | 80 | const handleSignup = () => { 81 | router.push('/signup'); 82 | } 83 | 84 | return ( 85 | // Form component to handle form submission 86 | 87 |
88 | 89 | {/* Logo and heading */} 90 | 91 | 92 | {/* Kafka Kare 93 | Becuase we Kare. */} 94 | 95 | {/* Username input field */} 96 | 97 | 98 | }/> 99 | 100 | 101 | 102 | {/* Password input field */} 103 | 104 | 105 | }/> 106 | 107 | 108 | 109 | 110 | 111 | 112 | {errorMessage && {errorMessage}} 113 | 114 | 115 | 116 | {/* Link to navigate to the signup page */} 117 | 118 | New to us?{' '} 119 | 120 | Sign ?Up 121 | 122 | 123 | 124 |
125 |
126 | ); 127 | }; 128 | 129 | export default LoginForm; -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | //may or may not need all of these options 3 | webpack: ( 4 | config, 5 | { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack } 6 | ) => { 7 | return config 8 | } 9 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kafka-kare", 3 | "version": "1.0.0", 4 | "description": "A Kafka error handling visualizer", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "next dev", 9 | "build": "next build", 10 | "prod": "set \"NODE_ENV=production\" && node server/server.js", 11 | "start": "next start", 12 | "server": "nodemon server/server.js", 13 | "watch": "nodemon --watch server --ext js " 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@chakra-ui/icons": "^2.1.1", 20 | "@chakra-ui/next-js": "^2.2.0", 21 | "@chakra-ui/react": "^2.8.2", 22 | "@dnd-kit/core": "^6.1.0", 23 | "@emotion/react": "^11.11.4", 24 | "@emotion/styled": "^11.11.0", 25 | "axios": "^1.6.7", 26 | "bcryptjs": "^2.4.3", 27 | "chart.js": "^4.4.2", 28 | "chartjs-adapter-date-fns": "^3.0.0", 29 | "clsx": "^2.1.0", 30 | "connect-mongo": "^5.1.0", 31 | "cookie-parser": "^1.4.6", 32 | "cors": "^2.8.5", 33 | "dotenv": "^16.4.5", 34 | "express": "^4.18.3", 35 | "express-session": "^1.18.0", 36 | "framer-motion": "^11.0.8", 37 | "jest": "^29.7.0", 38 | "js-yaml": "^4.1.0", 39 | "jsonwebtoken": "^9.0.2", 40 | "kafkajs": "^2.2.4", 41 | "mongodb": "^6.4.0", 42 | "mongoose": "^8.2.0", 43 | "next-auth": "^4.24.7", 44 | "prom-client": "^15.1.0", 45 | "react": "^18.2.0", 46 | "react-dnd": "^16.0.1", 47 | "react-dnd-html5-backend": "^16.0.1", 48 | "react-dom": "^18.2.0", 49 | "react-icons": "^5.0.1", 50 | "socket.io": "^4.7.5", 51 | "supertest": "^6.3.4", 52 | "tailwind-merge": "^2.2.1", 53 | "zustand": "^4.5.2" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "^7.24.0", 57 | "@babel/preset-env": "^7.24.0", 58 | "@babel/preset-react": "^7.23.3", 59 | "@testing-library/jest-dom": "^6.4.2", 60 | "@testing-library/react": "^14.2.1", 61 | "autoprefixer": "^10.4.18", 62 | "babel-loader": "^9.1.3", 63 | "css-loader": "^6.10.0", 64 | "html-webpack-plugin": "^5.6.0", 65 | "jest-environment-jsdom": "^29.7.0", 66 | "nodemon": "^3.1.0", 67 | "postcss": "^8.4.35", 68 | "sass": "^1.71.1", 69 | "sass-loader": "^14.1.1", 70 | "style-loader": "^3.3.4", 71 | "tailwindcss": "^3.4.1", 72 | "webpack": "^5.90.3", 73 | "webpack-cli": "^5.1.4", 74 | "webpack-dev-server": "^5.0.2" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /producer.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require("kafkajs"); 2 | const CHANCE_MESSAGE_SEND = 0.6; 3 | 4 | const kafka = new Kafka({ 5 | clientId: "my-producer", 6 | brokers: ["localhost:9093"], 7 | }); 8 | 9 | const topic = "test-topic"; 10 | const producer = kafka.producer(); 11 | 12 | const produceMessages = async () => { 13 | await producer.connect(); 14 | setInterval(async () => { 15 | try { 16 | if (Math.random() < CHANCE_MESSAGE_SEND) { 17 | const message = { value: `Message from producer at ${new Date().toISOString()}` }; 18 | console.log(`Sending message: ${message.value}`); 19 | await producer.send({ 20 | topic, 21 | messages: [message], 22 | }); 23 | } 24 | } catch (error) { 25 | console.error("Error producing message", error); 26 | } 27 | }, 1000); // Send a message every second 28 | }; 29 | 30 | produceMessages().catch((error) => { 31 | console.error("Error in producer script", error); 32 | }); 33 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interva: 15s 4 | 5 | scrape_configs: 6 | - job_name: 'kafka-kare' 7 | static_configs: 8 | - targets: ['kafka-kare-server:3001'] 9 | 10 | - job_name: 'kafka-demo' 11 | static_configs: 12 | - targets: ['kafka-demo:9092'] 13 | 14 | # below is what is required with Grafana to write data to it 15 | remote_write: 16 | - url: 17 | basic_auth: 18 | username: 19 | password: -------------------------------------------------------------------------------- /public/Logo-Dark-Tranparent-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/Logo-Dark-Tranparent-150x150.png -------------------------------------------------------------------------------- /public/Logo-Dark-Tranparent-320x320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/Logo-Dark-Tranparent-320x320.png -------------------------------------------------------------------------------- /public/Logo-Dark-Tranparent-Original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/Logo-Dark-Tranparent-Original.png -------------------------------------------------------------------------------- /public/Logo-Light-Transparent- 320x320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/Logo-Light-Transparent- 320x320.png -------------------------------------------------------------------------------- /public/Logo-Light-Transparent-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/Logo-Light-Transparent-150x150.png -------------------------------------------------------------------------------- /public/Logo-Light-Transparent-Original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/Logo-Light-Transparent-Original.png -------------------------------------------------------------------------------- /public/kafka-kare-background-v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafka-kare-background-v1.png -------------------------------------------------------------------------------- /public/kafka-kare-background-v2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafka-kare-background-v2.jpg -------------------------------------------------------------------------------- /public/kafka-kare-logo-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafka-kare-logo-v2.png -------------------------------------------------------------------------------- /public/kafka-kare-logo-v3-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafka-kare-logo-v3-dark.png -------------------------------------------------------------------------------- /public/kafka-kare-logo-v3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafka-kare-logo-v3.png -------------------------------------------------------------------------------- /public/kafka-kare-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafka-kare-logo.png -------------------------------------------------------------------------------- /public/kafka-kare-meerkat-background-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafka-kare-meerkat-background-v2.png -------------------------------------------------------------------------------- /public/kafka-kare-meerkat-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafka-kare-meerkat-background.png -------------------------------------------------------------------------------- /public/kafkaKare-creatingEnvfile.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafkaKare-creatingEnvfile.gif -------------------------------------------------------------------------------- /public/kafkakare-dockercomposeup-d.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafkakare-dockercomposeup-d.gif -------------------------------------------------------------------------------- /public/kafkakareAddingcluster.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafkakareAddingcluster.gif -------------------------------------------------------------------------------- /public/kafkakareSignup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Kare/ee7e1a331146937299d07cbc94c617da1c4ae76c/public/kafkakareSignup.gif -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Server Documentation 2 | 3 | ## Setup Instructions 4 | 5 | 1. You will need a `.env` file. Look at the `.env.example` file for inspiration. 6 | 2. Install depenendencies: `npm install` 7 | 3. Start the server: `npm run server` 8 | 9 | ## Docker Setup 10 | 11 | - Note: This server application was designed to run in Docker 12 | - Please visit the README.md file in the project root directory for Docker setup instructions 13 | 14 | ## Endpoints 15 | 16 | /auth 17 | - **POST /auth/signup**: Route for user signup 18 | - **POST /auth/login**: Route for user login 19 | - **GET /auth/logout**: Route for user logout 20 | - **UPDATE /auth/password/update**: Route for updating user password 21 | - **DELETE /account/delete**: Route for deleting a user account 22 | 23 | /clusters 24 | - **GET /clusters/userClusters**: Route for getting all clusters from a user 25 | - **POST /clusters/addCluster**: Route for adding a cluster 26 | - **DELETE /clusters/:clusterId**: Route for deleting a cluster 27 | - **PATCH /clusters/:clusterId**: Route for editing a cluster 28 | - **PATCH /clusters/favorites/:clusterId**: Route for toggling favorite status of a cluster 29 | - **GET /clusters/favorites**: Route for getting favorite clusters 30 | - **GET /clusters/notFavorites**: Route for getting not-favorite clusters 31 | 32 | /slack 33 | - **PATCH /slack/update**: Route for editing Slack link 34 | - **GET /slack**: Route for retrieving a Slack link 35 | 36 | /metrics 37 | - **GET /metrics/:clusterId**: Route for obtaining metrics from a cluster 38 | 39 | /settings 40 | - **GET /settings/colormode**: Route for obtaining user's color mode 41 | - **GET /settings/colormode/toggle**: Route for toggling a user's color mode 42 | 43 | /oauth/google 44 | - **POST /oauth/google**: Route for verifying user with Google OAuth 45 | 46 | ## Configuration 47 | 48 | Environment vairables: 49 | - 'MONGO_DB_USERNAME': Username for MongoDB. Look in docker-compose.yml file 50 | - 'MONGO_DB_PWD': Password for MongoDB. Hop over to the docker-compose.yml file 51 | 52 | MongoURI Connection String in server.js: 53 | - Use `mongoose.connect(mongoURI)` to connect to Docker containerized MongoDB image 54 | - Use `mongoose.connect(mongoURIAtlas)` to connect to external service MongoDB such as MongoDB Atlas 55 | 56 | ## Troubleshooting 57 | 58 | - Ensure MongoURI connection string is correct 59 | 60 | ## Contribution Guidelines 61 | 62 | - Message on Github or Slack 63 | - Good luck -------------------------------------------------------------------------------- /server/controllers/connectionStringController.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const connectionStringController = {}; 3 | 4 | /* ------------------------- CHECK CONNECTION STRING ------------------------ */ 5 | connectionStringController.checkConnection = async (req, res, next) => { 6 | console.log("In connectionStringController.checkConnection"); // testing 7 | const { connectionString } = req.body; // Destructure from req.body 8 | const userId = res.locals.userId; // Destructure from prior middleware 9 | 10 | try { 11 | const response = await axios.get(`${connectionString}/api/v1/query`, { 12 | params: {query: 'up'} // simple way to test connectivity 13 | }); 14 | 15 | // Persist online status 16 | console.log('cluster online status: ONLINE'); 17 | res.locals.onlineStatus = 'ONLINE' 18 | 19 | return next(); 20 | } catch (err) { 21 | return next(); 22 | } 23 | } 24 | 25 | 26 | 27 | // Export 28 | module.exports = connectionStringController; 29 | -------------------------------------------------------------------------------- /server/controllers/iFrameController.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const Cluster = require("../models/clusterModel.js"); 3 | 4 | const iFrameController = {}; 5 | 6 | /* ------------------------------- GET I-FRAME ------------------------------ */ 7 | iFrameController.getIFrame = async (req, res, next) => { 8 | console.log("In iFrameController.getIFrame"); // testing 9 | // const { clusterId } = req.params; // Destructure from req.params 10 | 11 | // hard code in // testing 12 | const clusterId = '6605f8ad3a826dad6f0072c2'; 13 | 14 | // Search Database 15 | try { 16 | const cluster = await Cluster.findById(clusterId); 17 | console.log('Response from database received'); 18 | 19 | // Error handling 20 | if (!cluster) return res.status(500).send('Error retrieving iFrame'); 21 | 22 | // Visualize cluster 23 | console.log('cluster: ', cluster); 24 | 25 | // Retrieve grafanaUrl 26 | const iFrame = cluster.grafanaUrl; 27 | console.log('iFrame: ', iFrame); 28 | 29 | // Persist data 30 | res.locals.iFrame = iFrame; 31 | return next(); 32 | 33 | } catch (err) { 34 | return next({ 35 | log: `iFrameController.getIFrame: ERROR ${err}`, 36 | status: 500, 37 | message: { err: "Error occurred in iFrameController.getIFrame." }, 38 | }); 39 | } 40 | } 41 | 42 | // Export 43 | module.exports = iFrameController; 44 | -------------------------------------------------------------------------------- /server/controllers/metricsController.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const { connection, connections } = require("mongoose"); 3 | const metricsController = {}; 4 | const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL; 5 | 6 | // Setting maximum rate of Slack notifications to once per minute 7 | const THROTTLE_TIME_IN_MS = 60000; 8 | const THROUGHPUT_THRESHOLD_UPPER = 1.2; // Explain business logic in comments here 9 | let lastNotificationTime = 0; // Initialize outside for closure 10 | 11 | 12 | /* ------------------------------- GET METRICS ------------------------------ */ 13 | metricsController.getMetrics = async (req, res, next) => { 14 | console.log("In metricsController.getMetrics"); // testing 15 | const { clusterId } = req.params; // Destructure from prior request params // Later use clusterId 16 | const userId = res.locals.userId; // Destructure from prior middleware // Later use userId 17 | 18 | // Prometheus query for throughput ** Must start producer/consumer scripts to see meaningful data 19 | const query = `rate(kafka_server_brokertopicmetrics_messagesin_total{topic="test-topic"}[1m])`; 20 | 21 | // // More visually interesting query, but useless metric 22 | // const query = `scrape_duration_seconds` 23 | 24 | try { 25 | // Explicitly print out our prometheus query // testing 26 | console.log('query: ', query); 27 | 28 | console.log('Sending query to Prometheus...'); 29 | // const prometheusIP = process.env.PROMETHEUS_IP; 30 | const prometheusIP = 'host.docker.internal'; 31 | let connectionString = `http://${prometheusIP}:9090`; 32 | console.log('connectionString: ', connectionString); 33 | 34 | 35 | // demo test - Switches to listen to Kafka cluster on another port 36 | if (clusterId === '12345') { 37 | console.log('Changing to listen to port 9063'); 38 | connectionString = `http://${prometheusIP}:9063`; 39 | console.log('connectionString changed: ', connectionString); 40 | } 41 | 42 | const queryResponse = await axios.get(`${connectionString}/api/v1/query`, { 43 | params: { query } 44 | }); 45 | 46 | console.log('Retrieved data successfully'); 47 | const queryData = queryResponse.data.data.result // This is the form of the response from Prometheus 48 | 49 | if (queryData.length < 1) { 50 | console.log('Data retrieved from Prometheus is empty'); 51 | res.locals.graphData = {}; // ensures graphData exists even if empty 52 | return next(); 53 | } 54 | 55 | // Persist data retrieved from Prometheus 56 | const timestamp = queryData[0].value[0]; 57 | const dataPointFloat = parseFloat(queryData[0].value[1]); 58 | const dataPoint = dataPointFloat.toFixed(2); 59 | 60 | console.log('timestamp: ', timestamp); 61 | console.log('dataPoint: ', dataPoint); 62 | res.locals.graphData = { timestamp, dataPoint }; 63 | 64 | // Converting to human-readable timestamp 65 | const readableTimestamp = new Date(timestamp * 1000).toISOString(); 66 | console.log('Readable Timestamp: ', readableTimestamp); 67 | 68 | return next(); 69 | } catch (err) { 70 | return next({ 71 | log: `metricsController.getMetrics: ERROR ${err}`, 72 | status: 500, 73 | message: { err: "Error occurred in metricsController.getMetrics." }, 74 | }); 75 | } 76 | }; 77 | 78 | 79 | /* --------------------------- SLACK NOTIFICATION --------------------------- */ 80 | metricsController.checkAndSendNotification = async (req, res, next) => { 81 | console.log("In metricsController.checkAndSendNotification"); // testing 82 | const { graphData } = res.locals; // Destructure from prior middleware 83 | 84 | // This is the threshold check 85 | if (!graphData.dataPoint || graphData.dataPoint <= THROUGHPUT_THRESHOLD_UPPER) { 86 | console.log('Metrics below threshold, proceeding') 87 | return next(); 88 | } 89 | 90 | // This is the throttle check 91 | const currentTime = Date.now(); 92 | const timeSinceLastNotification = currentTime - lastNotificationTime; 93 | if (timeSinceLastNotification <= THROTTLE_TIME_IN_MS) { 94 | console.log('Message is being throttled to avoid spamming'); 95 | return next(); 96 | } 97 | 98 | console.log('Metrics exceed threshold. Sending Slack notification...') 99 | 100 | // testing 101 | console.log('SLACK_WEBHOOK_URL: ', SLACK_WEBHOOK_URL); 102 | console.log('THROUGHPUT_THRESHOLD_UPPER: ', THROUGHPUT_THRESHOLD_UPPER); 103 | console.log('graphData.dataPoint: ', graphData.dataPoint) 104 | 105 | try { 106 | await axios.post(SLACK_WEBHOOK_URL, { 107 | text: `Alert set for: <${THROUGHPUT_THRESHOLD_UPPER}> messages per second. \nCurrent rate: <${graphData.dataPoint}> messages per second.` 108 | }); 109 | console.log(`Notification sent to Slack successfully`); 110 | lastNotificationTime = currentTime; // Update the time of the last notification 111 | return next(); 112 | 113 | } catch (err) { 114 | console.log(`Failed to send notification to Slack: ${err}`); 115 | } 116 | 117 | return next(); 118 | } 119 | 120 | // Export 121 | module.exports = metricsController; 122 | -------------------------------------------------------------------------------- /server/controllers/oAuthController.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const User = require("../models/userModel.js"); 3 | 4 | const oAuthController = {}; 5 | 6 | /* -------------------------- GOOGLE - CREATE USER -------------------------- */ 7 | oAuthController.googleCreateUser = async (req, res, next) => { 8 | console.log("In oAuthController.googleCreateUser"); // testing 9 | const { username, email, oAuthProvider } = req.body; // Destructure from req.body 10 | 11 | // Store user in database 12 | try { 13 | console.log('Searching database for record of user'); 14 | const isUserInDatabase = await User.findOne({ 15 | username: username, 16 | email: email, 17 | oAuthProvider: oAuthProvider 18 | }); 19 | console.log ('isUserInDatabase: ', isUserInDatabase); 20 | 21 | if (isUserInDatabase) { 22 | console.log('User found in database'); 23 | res.locals.userId = isUserInDatabase.id; 24 | res.locals.username = isUserInDatabase.username; 25 | return next(); 26 | } 27 | 28 | // User was not found in database, therefore we must create 29 | console.log('User was not found in database. Creating new user.') 30 | 31 | const user = await User.create({ 32 | username: username, 33 | email: email, 34 | oAuthProvider: oAuthProvider 35 | }); 36 | res.locals.userId = user.id; 37 | res.locals.username = user.username; 38 | console.log(`New user stored in database: <${user.username}> via Google OAuth`); 39 | return next(); 40 | } catch (err) { 41 | return next({ 42 | log: `oAuthController.googleCreateUser: ERROR ${err}`, 43 | status: 500, 44 | message: { err: "Error occurred in oAuthController.googleCreateUser." }, 45 | }); 46 | } 47 | }; 48 | 49 | /* -------------------------- GITHUB - CREATE USER -------------------------- */ 50 | oAuthController.githubCreateUser = async (req, res, next) => { 51 | console.log("In oAuthController.githubCreateUser"); // testing 52 | const { username, email, oAuthProvider } = req.body; // Destructure from req.body 53 | 54 | // Store user in database 55 | try { 56 | console.log('Searching database for record of user'); 57 | const isUserInDatabase = await User.findOne({ 58 | username: username, 59 | email: email, 60 | oAuthProvider: oAuthProvider 61 | }); 62 | console.log ('isUserInDatabase: ', isUserInDatabase); 63 | 64 | if (isUserInDatabase) { 65 | console.log('User found in database'); 66 | res.locals.userId = isUserInDatabase.id; 67 | res.locals.username = isUserInDatabase.username; 68 | return next(); 69 | } 70 | 71 | // User was not found in database, therefore we must create 72 | console.log('User was not found in database. Creating new user.') 73 | 74 | const user = await User.create({ 75 | username: username, 76 | email: email, 77 | oAuthProvider: oAuthProvider 78 | }); 79 | res.locals.userId = user.id; 80 | res.locals.username = user.username; 81 | console.log(`New user stored in database: <${user.username}> via Github OAuth`); 82 | return next(); 83 | } catch (err) { 84 | return next({ 85 | log: `oAuthController.githubCreateUser: ERROR ${err}`, 86 | status: 500, 87 | message: { err: "Error occurred in oAuthController.githubCreateUser." }, 88 | }); 89 | } 90 | }; 91 | 92 | // Export 93 | module.exports = oAuthController; -------------------------------------------------------------------------------- /server/controllers/settingsController.js: -------------------------------------------------------------------------------- 1 | const { color } = require("framer-motion"); 2 | const User = require("../models/userModel.js"); 3 | 4 | const settingsController = {}; 5 | 6 | /* ----------------------------- GET COLOR MODE ----------------------------- */ 7 | settingsController.getColor = async (req, res, next) => { 8 | console.log("In settingsController.getColor"); // testing 9 | const { userId } = res.locals; // Destructure from prior middleware 10 | 11 | // Get user from database 12 | try { 13 | const user = await User.findById(userId); 14 | if (!user) { 15 | return res.status(404).send("User not found"); 16 | } 17 | 18 | res.locals.colorMode = user.settings.colorMode; 19 | console.log("User color mode status retrieved"); 20 | return next(); 21 | } catch (err) { 22 | return next({ 23 | log: `settingsController.getColor: ERROR ${err}`, 24 | status: 500, 25 | message: { err: "Error occurred in settingsController.getColor." }, 26 | }); 27 | } 28 | }; 29 | 30 | /* ---------------------------- TOGGLE COLOR MODE --------------------------- */ 31 | settingsController.toggleColor = async (req, res, next) => { 32 | console.log("In settingsController.toggleColor"); // testing 33 | const { userId } = res.locals; // Destructure from prior middleware 34 | 35 | // Get user from database 36 | try { 37 | const user = await User.findById(userId); 38 | if (!user) { 39 | return res.status(404).send("User not found"); 40 | } 41 | // Toggle color mode 42 | let colorMode = user.settings.colorMode; 43 | colorMode !== 'dark' 44 | ? colorMode = 'dark' 45 | : colorMode = 'light'; 46 | 47 | user.settings.colorMode = colorMode; 48 | await user.save(); 49 | console.log(`User switched color mode in database: <${user.settings.colorMode}>`); 50 | 51 | res.locals.colorMode = colorMode; 52 | console.log("User color mode toggled successfully"); 53 | return next(); 54 | } catch (err) { 55 | return next({ 56 | log: `settingsController.toggleColor: ERROR ${err}`, 57 | status: 500, 58 | message: { err: "Error occurred in settingsController.toggleColor." }, 59 | }); 60 | } 61 | }; 62 | 63 | // Export 64 | module.exports = settingsController; 65 | -------------------------------------------------------------------------------- /server/controllers/slackController.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/userModel.js"); 2 | 3 | const slackController = {}; 4 | 5 | /* ---------------------------- ADD/UPDATE SLACK ---------------------------- */ 6 | slackController.updateSlack = async (req, res, next) => { 7 | console.log("In slackController.updateSlack"); // testing 8 | console.log('req.body contains: ', req.body); 9 | const { slackUrl } = req.body; // Destructure from req.body 10 | const { userId } = res.locals; // Destructure from prior middleware 11 | 12 | // Add slack link to database 13 | try { 14 | const user = await User.findByIdAndUpdate(userId, { 15 | slackUrl: slackUrl 16 | }, { new: true }); 17 | 18 | if (!user) { 19 | return res.status(404).send('User\'s slack url not found'); 20 | } 21 | 22 | console.log('Slack URL updated successfully: ', user.slackUrl); 23 | return next(); 24 | } catch (err) { 25 | return next({ 26 | log: `slackController.updateSlack: ERROR ${err}`, 27 | status: 500, 28 | message: { err: "Error occurred in slackController.updateSlack." }, 29 | }); 30 | } 31 | }; 32 | 33 | 34 | /* -------------------------------- GET SLACK ------------------------------- */ 35 | slackController.getSlack = async (req, res, next) => { 36 | console.log("In slackController.getSlack"); // testing 37 | const { userId } = res.locals; // Destructure from prior middleware 38 | 39 | // Get slack link from database 40 | try { 41 | const user = await User.findById(userId); 42 | 43 | if (!user) { 44 | return res.status(404).send('User\'s slack url not found'); 45 | } 46 | 47 | res.locals.slackUrl = user.slackUrl; 48 | console.log('Slack URL retrieved successfully: ', user.slackUrl); 49 | return next(); 50 | } catch (err) { 51 | return next({ 52 | log: `slackController.getSlack: ERROR ${err}`, 53 | status: 500, 54 | message: { err: "Error occurred in slackController.getSlack." }, 55 | }); 56 | } 57 | }; 58 | 59 | 60 | /* ------------------------------ DELETE SLACK ------------------------------ */ 61 | slackController.deleteSlack = async (req, res, next) => { 62 | console.log("In slackController.deleteSlack"); // testing 63 | const { userId } = res.locals; // Destructure from prior middleware 64 | 65 | // Delete slack link from database 66 | try { 67 | // there is a more absolute way with $unset, but this way is more semantic 68 | const user = await User.findByIdAndUpdate(userId, { 69 | slackUrl: '' 70 | }, { new: true }); // not necessary here, but better aligns with understanding 71 | 72 | if (!user) { 73 | return res.status(404).send('User\'s slack url not found'); 74 | } 75 | 76 | console.log('Slack URL deleted successfully'); 77 | return next(); 78 | } catch (err) { 79 | return next({ 80 | log: `slackController.deleteSlack: ERROR ${err}`, 81 | status: 500, 82 | message: { err: "Error occurred in slackController.deleteSlack." }, 83 | }); 84 | } 85 | }; 86 | 87 | // Export 88 | module.exports = slackController; 89 | -------------------------------------------------------------------------------- /server/controllers/testingController.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/userModel.js"); 2 | const Cluster = require("../models/clusterModel.js"); 3 | 4 | const testingController = {}; 5 | 6 | /* ------------------------ TESTING // GET ALL USERS ------------------------ */ 7 | testingController.getAllUsers = async (req, res, next) => { 8 | console.log("In testingController.getAllUsers"); // testing 9 | 10 | try { 11 | const users = await User.find({}); 12 | if (users.length === 0) { 13 | return res.status(200).json({ message: "No users have signed up" }); 14 | } else if (users.length > 0) { 15 | res.locals.allUsers = users; 16 | return next(); 17 | } 18 | } catch (err) { 19 | return next({ 20 | log: `testingController.getAllUsers: ERROR ${err}`, 21 | status: 500, 22 | message: { err: "Error occurred in testingController.getAllUsers." }, 23 | }); 24 | } 25 | }; 26 | 27 | 28 | /* ----------------------- TESTING // GET ALL CLUSTERS ---------------------- */ 29 | testingController.getAllClusters = async (req, res, next) => { 30 | console.log("In testingController.getAllClusters"); // testing 31 | 32 | try { 33 | const clusters = await Cluster.find({}); 34 | if (clusters.length === 0) { 35 | return res.status(200).json({ message: "No clusters have been created" }); 36 | } else if (clusters.length > 0) { 37 | res.locals.allClusters = clusters; 38 | return next(); 39 | } 40 | } catch (err) { 41 | return next({ 42 | log: `testingController.getAllClusters: ERROR ${err}`, 43 | status: 500, 44 | message: { err: "Error occurred in testingController.getAllClusters." }, 45 | }); 46 | } 47 | }; 48 | 49 | // Export 50 | module.exports = testingController; 51 | -------------------------------------------------------------------------------- /server/controllers/tokenController.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const SECRET_KEY = process.env.SECRET_KEY; 3 | 4 | const tokenController = {}; 5 | 6 | // ---------------------------- ISSUE TOKEN ---------------------------- // 7 | tokenController.issueToken = (req, res, next) => { 8 | console.log("In tokenController.issueToken"); // testing 9 | const { userId, username } = res.locals; // Destructure from prior middleware 10 | 11 | // Issue token 12 | const token = jwt.sign( 13 | { userId: userId, username: username}, 14 | SECRET_KEY, 15 | { expiresIn: '1h' } 16 | ); 17 | //res.cookie('token', 'none', { 18 | // expires: new Date(Date.now() + 5 * 1000), 19 | // httpOnly: true, 20 | // }) 21 | 22 | // Determine if running in a production environement 23 | // const isProduction = process.env.NODE_ENV === 'production'; 24 | 25 | // Store the token in HTTP-only cookie 26 | res.cookie('token', token, { 27 | expires: new Date(Date.now() + 60 * 60 * 1000), 28 | httpOnly: true, 29 | // secure: isProduction // use 'secure' flag only in production 30 | }); 31 | 32 | const shortenedToken = token.slice(-10) 33 | console.log(`Token from cookie: ...${shortenedToken}`); 34 | 35 | return next(); 36 | }; 37 | 38 | 39 | // ---------------------------- VERIFY TOKEN ---------------------------- // 40 | tokenController.verifyToken = (req, res, next) => { 41 | console.log("In tokenController.verifyToken"); // testing 42 | const token = req.cookies.token; // Destructure from cookies 43 | 44 | // Shorten the console log 45 | const shortenedToken = token.slice(-10); 46 | console.log(`Token from cookie: ...${shortenedToken}`); 47 | 48 | // Check token 49 | if (!token) { 50 | return res.status(403).send('A token is required for authentication'); 51 | } 52 | 53 | // Verify token, extract userId and username 54 | try { 55 | const decoded = jwt.verify(token, SECRET_KEY); 56 | console.log('Token verified.'); 57 | res.locals.userId = decoded.userId; 58 | res.locals.username = decoded.username; 59 | return next(); 60 | } catch (err) { 61 | return res.status(401).send('Invalid token'); 62 | } 63 | }; 64 | 65 | 66 | // Export 67 | module.exports = tokenController; -------------------------------------------------------------------------------- /server/models/clusterModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const clusterSchema = new Schema({ 5 | // Required 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | hostnameAndPort: { 11 | type: String, 12 | required: true, 13 | }, 14 | ownerId: { 15 | type: String, 16 | required: true 17 | }, 18 | // Not required 19 | dateAdded: { 20 | type: Date, 21 | default: Date.now 22 | }, 23 | dateLastVisited: { 24 | type: Date, 25 | default: Date.now 26 | }, 27 | favorite: { 28 | type: Boolean, 29 | default: false 30 | }, 31 | status: { 32 | type: String, 33 | default: 'online' 34 | }, 35 | numberOfBrokers: { 36 | type: Number, 37 | default: 1 38 | }, 39 | grafanaUrl: { 40 | type: String, 41 | default: '' 42 | }, 43 | }); 44 | 45 | const Cluster = mongoose.model("Cluster", clusterSchema); 46 | 47 | module.exports = Cluster; 48 | -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | const bcrypt = require("bcryptjs"); 4 | 5 | const SALT_WORK_FACTOR = 10; 6 | 7 | const userSchema = new Schema({ 8 | // Required 9 | username: { 10 | type: String, 11 | required: true, 12 | }, 13 | password: { 14 | type: String, 15 | required: false 16 | }, 17 | createdAt: { 18 | type: Date, 19 | default: Date.now 20 | }, 21 | lastVisited: { 22 | type: Date, 23 | default: Date.now 24 | }, 25 | firstName: { 26 | type: String 27 | }, 28 | lastName: { 29 | type: String 30 | }, 31 | birthday: { 32 | type: Date 33 | }, 34 | email: { 35 | type: String 36 | }, 37 | slackUrl: { 38 | type: String, 39 | default: '' 40 | }, 41 | promUrl: { 42 | type: String, 43 | default: '' 44 | }, 45 | settings: { 46 | colorMode: { 47 | type: String, 48 | default: 'light' 49 | }, 50 | language: { 51 | type: String, 52 | default: 'English' 53 | } 54 | }, 55 | oAuthProvider: { 56 | type: String, 57 | default: 'none' 58 | }, 59 | graphs: { 60 | type: [{}], 61 | default: [] 62 | }, 63 | }); 64 | userSchema.index({ username: 1, email: 1, oAuthProvider: 1 }, { unique: true }); 65 | 66 | // Pre-save hook to encrypt password using bcrypt.hash() 67 | userSchema.pre("save", async function (next) { 68 | try { 69 | // Only hash the password if it has been modified (or is new) 70 | if (!this.isModified('password')) return next(); 71 | 72 | const salt = await bcrypt.genSalt(SALT_WORK_FACTOR); 73 | this.password = await bcrypt.hash(this.password, salt); 74 | return next(); 75 | } catch (err) { 76 | return next(err); 77 | } 78 | }); 79 | 80 | const User = mongoose.model("User", userSchema); 81 | 82 | module.exports = User; 83 | -------------------------------------------------------------------------------- /server/routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const userController = require("../controllers/userController"); 4 | const tokenController = require("../controllers/tokenController"); 5 | 6 | // Route for user signup 7 | router.post( 8 | '/signup', 9 | userController.createUser, 10 | tokenController.issueToken, 11 | (req, res) => { 12 | return res.status(201).json({ message: `User registered successfully: ${res.locals.username}` }); 13 | }); 14 | 15 | // Route for user login 16 | router.post( 17 | '/login', 18 | userController.verifyUser, 19 | tokenController.issueToken, 20 | (req, res) => { 21 | return res.status(200).json({ message: `User logged in successfully: ${res.locals.username}` }); 22 | }); 23 | 24 | // Route for user logout 25 | router.get( 26 | '/logout', 27 | tokenController.verifyToken, 28 | userController.logout, 29 | (req, res) => { 30 | return res.status(201).json({ message: `User logged out successfully: ${res.locals.username}` }); 31 | }); 32 | 33 | // Route for changing user password 34 | router.patch( 35 | '/password/update', 36 | tokenController.verifyToken, 37 | userController.updatePassword, 38 | (req, res) => { 39 | return res.status(200).json({ message: 'User updated password successfully' }); 40 | } 41 | ) 42 | 43 | // Route for deleting a user account 44 | router.delete( 45 | '/account/delete', 46 | tokenController.verifyToken, 47 | userController.deleteAccount, 48 | (req, res) => { 49 | return res.status(200).json({ message: 'User account deleted successfully '}); 50 | } 51 | ) 52 | 53 | //Route for checking is user is new 54 | // router.get( 55 | // '/check-new-user', 56 | // tokenController.verifyToken, 57 | // userController.checkNewUsers, 58 | // (req, res) => { 59 | // return res.status(200).json({ isNewUser }) 60 | // } 61 | // ) 62 | 63 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/clustersRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const clusterController = require("../controllers/clusterController"); 4 | const tokenController = require("../controllers/tokenController"); 5 | 6 | // Route for getting all clusters from a user 7 | router.get( 8 | '/userClusters', 9 | tokenController.verifyToken, 10 | clusterController.getClusters, 11 | (req, res) => { 12 | console.log("Sending clusters to client..."); 13 | return res.status(200).json(res.locals.clusters); 14 | } 15 | ); 16 | 17 | // Route for adding a cluster 18 | router.post( 19 | '/addCluster', 20 | tokenController.verifyToken, 21 | clusterController.addCluster, 22 | (req, res) => { 23 | return res.status(201).json(res.locals.newCluster); 24 | } 25 | ); 26 | 27 | // Route for deleting a cluster 28 | router.delete( 29 | '/:clusterId', 30 | tokenController.verifyToken, 31 | clusterController.deleteCluster, 32 | (req, res) => { 33 | return res.status(200).json({ message: 'Cluster deleted successfully' }); 34 | } 35 | ); 36 | 37 | // Route for editing a cluster 38 | router.patch( 39 | '/:clusterId', 40 | tokenController.verifyToken, 41 | clusterController.updateCluster, 42 | (req, res) => { 43 | return res.status(200).json({ message: 'Cluster updated successfully' }); 44 | } 45 | ) 46 | 47 | // Route for toggling favorite status of a cluster 48 | router.patch( 49 | '/favorites/:clusterId', 50 | tokenController.verifyToken, 51 | clusterController.toggleFavorite, 52 | (req, res) => { 53 | return res.status(200).json({ message: 'Cluster favorites updated successfully' }); 54 | } 55 | ) 56 | 57 | // Route for getting favorite clusters 58 | router.get( 59 | '/favorites', 60 | tokenController.verifyToken, 61 | clusterController.getFavorites, 62 | (req, res) => { 63 | return res.status(200).json(res.locals.favoriteClusters); 64 | } 65 | ) 66 | 67 | // Route for getting not-favorite clusters 68 | router.get( 69 | '/notFavorites', 70 | tokenController.verifyToken, 71 | clusterController.getNotFavorites, 72 | (req, res) => { 73 | return res.status(200).json(res.locals.notFavoriteClusters); 74 | } 75 | ) 76 | 77 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/grafanaApiRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const tokenController = require("../controllers/tokenController"); 4 | const grafanaApiController = require("../controllers/grafanaApiController"); 5 | 6 | // Route for adding user data source then initializing user's dashboard to Grafana 7 | router.post( 8 | '/create-datasource', 9 | tokenController.verifyToken, 10 | grafanaApiController.addDatasource, 11 | grafanaApiController.createDashboard, 12 | grafanaApiController.displayDashboard, 13 | (req, res) => { 14 | console.log('Sending Grafana dashboard iFrame back to client...'); 15 | // return res.status(200).json({message: "Dashboard created from datasource successfully", data: res.locals.data}); 16 | return res.status(200).json(res.locals.iFrame); 17 | } 18 | ); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /server/routes/iFrameRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const iFrameController = require("../controllers/iFrameController"); 4 | const tokenController = require("../controllers/tokenController"); 5 | 6 | // Route for retrieving cluster's iFrame from database 7 | router.get( 8 | '/:clusterId', 9 | // tokenController.verifyToken, 10 | iFrameController.getIFrame, 11 | (req, res) => { 12 | console.log('Sending iFrame back to client...'); 13 | // return res.status(200).json({message: "Dashboard created from datasource successfully", data: res.locals.data}); 14 | return res.status(200).json(res.locals.iFrame); 15 | } 16 | ); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /server/routes/metricsRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const axios = require("axios"); 3 | const router = express.Router(); 4 | const metricsController = require("../controllers/metricsController"); 5 | const tokenController = require("../controllers/tokenController"); 6 | 7 | // WORK-IN-PROGRESS // Currently, does not take clusterId or userId into account 8 | // Route for getting metrics for a specific cluster 9 | router.get( 10 | "/:clusterId", 11 | tokenController.verifyToken, 12 | metricsController.getMetrics, 13 | metricsController.checkAndSendNotification, 14 | (req, res) => { 15 | console.log('Sending metrics back to client...'); 16 | return res.status(200).json(res.locals.graphData); 17 | } 18 | ); 19 | 20 | module.exports = router; 21 | 22 | -------------------------------------------------------------------------------- /server/routes/oAuthRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const tokenController = require("../controllers/tokenController"); 4 | const oAuthController = require("../controllers/oAuthController"); 5 | 6 | // Route for user sign up/in - Google 7 | router.post( 8 | "/google", 9 | oAuthController.googleCreateUser, 10 | tokenController.issueToken, 11 | (req, res) => { 12 | return res.status(201).json({ message: `User registered successfully: ${res.locals.username} via Google OAuth` }); 13 | } 14 | ) 15 | 16 | // Route for user sign up/in - Github 17 | router.post( 18 | "/github", 19 | oAuthController.githubCreateUser, 20 | tokenController.issueToken, 21 | (req, res) => { 22 | return res.status(201).json({ message: `User registered successfully: ${res.locals.username} via Github OAuth` }); 23 | } 24 | ) 25 | 26 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/settingsRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const settingsController = require("../controllers/settingsController"); 4 | const tokenController = require("../controllers/tokenController"); 5 | 6 | 7 | // WIP // Route for retrieving user's dark mode status 8 | router.get( 9 | '/colorMode', 10 | tokenController.verifyToken, 11 | settingsController.getColor, 12 | (req, res) => { 13 | console.log('Sending color mode and username back to client...'); 14 | return res.status(200).json({ colorMode: res.locals.colorMode, username: res.locals.username }) 15 | } 16 | ) 17 | 18 | 19 | // WIP // Route for toggling user's dark mode status 20 | router.get( 21 | '/colorMode/toggle', 22 | tokenController.verifyToken, 23 | settingsController.toggleColor, 24 | (req, res) => { 25 | console.log('Sending new color mode back to client...'); 26 | return res.status(200).json({ colorMode: res.locals.colorMode, username: res.locals.username }) 27 | } 28 | ) 29 | 30 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/slackRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const slackController = require("../controllers/slackController"); 4 | const tokenController = require("../controllers/tokenController"); 5 | 6 | // Route for editing Slack link 7 | router.patch( 8 | "/update", 9 | tokenController.verifyToken, 10 | slackController.updateSlack, 11 | (req, res) => { 12 | return res.status(200).json({ message: "Slack link updated successfully" }); 13 | } 14 | ); 15 | 16 | // Route for deleting Slack link 17 | router.get( 18 | "/delete", 19 | tokenController.verifyToken, 20 | slackController.deleteSlack, 21 | (req, res) => { 22 | return res.status(200).json({ message: "Slack link deleted successfully" }); 23 | } 24 | ); 25 | 26 | // Route for retrieving a Slack link 27 | router.get( 28 | "/", 29 | tokenController.verifyToken, 30 | slackController.getSlack, 31 | (req, res) => { 32 | console.log("Sending slackUrl and username to client..."); 33 | // return res.status(200).json(res.locals.slackUrl); 34 | return res.status(200).json({ slackUrl: res.locals.slackUrl, username: res.locals.username }) 35 | } 36 | ); 37 | 38 | module.exports = router; 39 | -------------------------------------------------------------------------------- /server/routes/testingRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const testingController = require("../controllers/testingController"); 4 | 5 | // TESTING // Route for getting ALL users 6 | router.get( 7 | '/users', 8 | testingController.getAllUsers, 9 | (req, res) => { 10 | console.log('TESTING ROUTE: Sending all users back to client...'); 11 | return res.status(200).json(res.locals.allUsers); 12 | }); 13 | 14 | 15 | // TESTING // Route for getting ALL clusters 16 | router.get( 17 | '/clusters', 18 | testingController.getAllClusters, 19 | (req, res) => { 20 | console.log('TESTING ROUTE: Sending all clusters back to client...'); 21 | return res.status(200).json(res.locals.allClusters); 22 | }); 23 | 24 | module.exports = router; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | // Access to environmental variables 2 | require("dotenv").config(); 3 | 4 | // Import dependencies 5 | const express = require("express"); 6 | const next = require("next"); 7 | const path = require("path"); 8 | const mongoose = require("mongoose"); 9 | const cors = require("cors"); 10 | const cookieParser = require("cookie-parser"); 11 | const axios = require('axios'); 12 | 13 | // Import routes 14 | const authRoutes = require("./routes/authRoutes"); 15 | const clustersRoutes = require("./routes/clustersRoutes"); 16 | const metricsRoutes = require("./routes/metricsRoutes"); 17 | const testingRoutes = require("./routes/testingRoutes"); 18 | const slackRoutes = require("./routes/slackRoutes"); 19 | const settingsRoutes = require("./routes/settingsRoutes"); 20 | const oAuthRoutes = require("./routes/oAuthRoutes"); 21 | const grafanaApiRoutes = require("./routes/grafanaApiRoutes"); 22 | const iFrameRoutes = require("./routes/iFrameRoutes"); 23 | 24 | // Setup Next app 25 | const PORT = 3001; 26 | const dev = process.env.NODE_ENV !== "production"; // dev = true if node_env IS NOT production 27 | const app = next({ dev }); // initializes an instance of a NextJS app 28 | const handle = app.getRequestHandler(); // handles page routing 29 | 30 | // Prepare to serve the NextJS app 31 | app.prepare().then(() => { 32 | const server = express(); 33 | 34 | // CORS middleware 35 | const corsOptions = { 36 | origin: "http://localhost:3000", 37 | credentials: true, 38 | }; 39 | server.use(cors(corsOptions)); 40 | 41 | // Middleware 42 | server.use(cookieParser()); 43 | server.use(express.json()); 44 | server.use(express.urlencoded({ extended: true })); 45 | 46 | // Connect to mongoDB 47 | const mongoURI = `mongodb://admin:supersecret@mongo`; 48 | const mongoURIAtlas = process.env.MONGODB_URI; 49 | 50 | mongoose.connect(mongoURI); 51 | mongoose.connection.once("open", () => { 52 | console.log("Connected to Database"); 53 | }); 54 | // options for mongoose.connect 55 | // {useNewUrlParser: true, 56 | // useUnifiedTopology: true, 57 | // serverSelectionTimeoutMS: 5000 // Timeout after 5s instead of 10s 58 | // } 59 | 60 | //================== TEST 61 | 62 | // Test MongoDB connection route 63 | server.get('/test-db', async (req, res) => { 64 | try { 65 | await mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true }); 66 | const connectionState = mongoose.connection.readyState; 67 | if (connectionState === 1) { 68 | res.status(200).json({ message: 'Successfully connected to MongoDB' }); 69 | } else { 70 | res.status(500).json({ message: 'Failed to connect to MongoDB', connectionState }); 71 | } 72 | } catch (error) { 73 | res.status(500).json({ message: 'Error connecting to MongoDB', error: error.message }); 74 | } 75 | }); 76 | 77 | 78 | //=========================== TEST 79 | 80 | //=========================== TEST 81 | server.get('/api/get-datasources', async (req, res) => { 82 | try { 83 | const response = await axios.get('http://grafana:3000/api/datasources', { 84 | headers: { 85 | 'Authorization': `Bearer ${process.env.GRAFANA_SERVICE_ACCOUNT_TOKEN}` 86 | } 87 | }); 88 | 89 | res.status(200).json(response.data); 90 | } catch (error) { 91 | console.error('Failed to get datasources:', error.response.data); 92 | res.status(500).json({ message: error.message }); 93 | } 94 | }); 95 | 96 | //================================================================================================ 97 | // this is the code to update the datasource file in the backend server so it won't be overwritten 98 | // const fs = require('fs'); 99 | // const yaml = require('js-yaml'); 100 | 101 | // server.put('/api/update-datasource-file/:id', (req, res) => { 102 | // try { 103 | // // Load the datasource configuration from the yml file 104 | // const fileContents = fs.readFileSync('/path/to/your/datasource.yml', 'utf8'); 105 | // const data = yaml.safeLoad(fileContents); 106 | 107 | // // Update the port in the datasource configuration 108 | // data.datasources[0].url = `http://prometheus:${newPort}/api/v1/query_range`; 109 | 110 | // // Write the updated configuration back to the yml file 111 | // const newYaml = yaml.safeDump(data); 112 | // fs.writeFileSync('/path/to/your/datasource.yml', newYaml, 'utf8'); 113 | 114 | // // Return a success response 115 | // res.status(200).json({ message: 'Datasource file updated successfully' }); 116 | // } catch (error) { 117 | // // Return an error response 118 | // console.error('Failed to update datasource file:', error); 119 | // res.status(500).json({ message: error.message }); 120 | // } 121 | // }); 122 | 123 | 124 | //============================= 125 | // Custom routes 126 | server.use("/auth", authRoutes); 127 | server.use("/clusters", clustersRoutes); 128 | server.use("/metrics", metricsRoutes); 129 | server.use("/testing", testingRoutes); // testing 130 | server.use("/slack", slackRoutes); 131 | server.use("/settings", settingsRoutes); 132 | server.use("/oauth", oAuthRoutes); 133 | server.use("/api", grafanaApiRoutes); 134 | server.use("/iframe", iFrameRoutes); 135 | 136 | // Fallback route 137 | server.get("*", (req, res) => { 138 | return handle(req, res); 139 | }); 140 | 141 | // Express global error handler 142 | server.use((err, req, res, next) => { 143 | const defaultObj = { 144 | log: "Express error handler caught unknown middleware error", 145 | status: 500, 146 | message: { err: "An error occurred" }, 147 | }; 148 | const errObj = Object.assign({}, defaultObj, err); 149 | console.log(errObj.log); 150 | return res.status(errObj.status).json(errObj.message); 151 | }); 152 | 153 | // Start server 154 | server.listen(PORT, () => { 155 | console.log(`🚀 Server launching on http://localhost:${PORT}`); 156 | }); 157 | }); 158 | -------------------------------------------------------------------------------- /src/app/404/page.jsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return
404!
; 3 | } -------------------------------------------------------------------------------- /src/app/_app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../styles/globals.css'; 3 | import { ChakraProvider } from '@chakra-ui/react'; 4 | import theme from '../styles/theme.js'; 5 | 6 | 7 | 8 | const App = ({ Component, pageProps }) => { 9 | return( 10 | 11 | 12 | 13 | )}; 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /src/app/about/page.jsx: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return
Testing about!
; 3 | } -------------------------------------------------------------------------------- /src/app/alerts/page.jsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import React, { useState } from "react"; 3 | import Navbar from "../../components/index/navbar"; 4 | import ConfigureCustom from "../../components/alerts/configureCustom"; 5 | import AlertHistoryGraph from '../../components/alerts/alertHistoryGraph'; 6 | import AlertsByMetricGraph from '../../components/alerts/alertsByMetricGraph'; 7 | import { Button, Heading, Flex, Box } from "@chakra-ui/react"; 8 | 9 | const Alerts = () => { 10 | const allMetrics = { 11 | "Log Segment Size By Topic": 17, 12 | "Total Bytes In": 6, 13 | "Total Number of Partitions": 16, 14 | "Metadata Error Count": 1, 15 | "Broker Disk Usage": 15, 16 | "JVM Memory Pool Bytes": 8, 17 | "Message Latency By Topic": 14, 18 | "Total CPU Process in Seconds": 7, 19 | "Total Messages Consumed Per Topic": 13, 20 | "Offline Partitions Count": 10, 21 | "Total Messages Produced Per Topic": 12, 22 | "Total Bytes Out": 5, 23 | "Under-Replicated Partitions Count": 11, 24 | "Consumer Lag": 3, 25 | "Active Controller Count": 9, 26 | "Total Failed Fetch Requests": 4, 27 | "Total Groups Rebalancing": 2, 28 | "Consumer Group Partition Lag": 21, 29 | "Consumer Group Maxiumum Lag": 18, 30 | "Consumer Group Current Offset": 22, 31 | "Consumer Group Lag in Seconds": 23, 32 | "Consumer Group Partition Assignment Strategy": 24, 33 | }; 34 | 35 | const [selectedMetricId, setSelectedMetricId] = useState([]); 36 | const [isOpen, setIsOpen] = useState(false); 37 | const [minThresholds, setMinThresholds] = useState( 38 | Array(Object.keys(allMetrics).length).fill("") 39 | ); 40 | const [maxThresholds, setMaxThresholds] = useState( 41 | Array(Object.keys(allMetrics).length).fill("") 42 | ); 43 | const [alerts, setAlerts] = useState([]); 44 | 45 | const openCloseCustomAlerts = () => { 46 | setIsOpen(!isOpen); 47 | }; 48 | 49 | const removeAlert = (indexToRemove) => { 50 | const updatedMinThresholds = [...minThresholds]; 51 | const updatedMaxThresholds = [...maxThresholds]; 52 | updatedMinThresholds.splice(indexToRemove, 1); 53 | updatedMaxThresholds.splice(indexToRemove, 1); 54 | setMinThresholds(updatedMinThresholds); 55 | setMaxThresholds(updatedMaxThresholds); 56 | }; 57 | 58 | const saveAlerts = () => { 59 | console.log("alerts"); 60 | }; 61 | 62 | 63 | return ( 64 | 65 | 66 | 67 | 74 | Alerts 75 | 76 | 79 | 80 | {isOpen && ( 81 | 90 | )} 91 |
92 | 100 | Alert Details 101 | 102 | {alerts.length === 0 ? ( 103 |

No alerts, you are all clear!

104 | ) : ( 105 |
{/* coming soon: Display alerts */}
106 | )} 107 |
108 |
109 | 117 | Alert History 118 | 119 | 120 |
121 | 122 |
123 |
124 | ); 125 | }; 126 | 127 | export default Alerts; 128 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.js: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import GoogleProvider from 'next-auth/providers/google'; 3 | import GithubProvider from 'next-auth/providers/github'; 4 | 5 | const handler = NextAuth({ 6 | providers: [ 7 | GoogleProvider({ 8 | clientId: process.env.AUTH_GOOGLE_ID, 9 | clientSecret: process.env.AUTH_GOOGLE_SECRET, 10 | 11 | //Let user choose new google account after logging out 12 | authorization: { 13 | params: { 14 | prompt: "consent", 15 | access_type: "offline", 16 | response_type: "code" 17 | } 18 | } 19 | }), 20 | GithubProvider({ 21 | clientId: process.env.AUTH_GITHUB_ID, 22 | clientSecret: process.env.AUTH_GITHUB_SECRET, 23 | 24 | //Let user choose new google account after logging out 25 | authorization: { 26 | params: { 27 | prompt: "consent", 28 | access_type: "offline", 29 | response_type: "code" 30 | } 31 | } 32 | }), 33 | ], 34 | callbacks: { 35 | async jwt({ token, account, profile }) { 36 | // Persist the OAuth access_token and or the user id to the token right after signin 37 | if (account) { 38 | token.provider = account.provider; 39 | } 40 | return token; 41 | }, 42 | async session({ session, token, user }) { 43 | // Send properties to the client, like an access_token and user id from a provider. 44 | session.user.provider = token.provider; 45 | 46 | return session; 47 | } 48 | }, 49 | session: {maxAge: 60 * 60}, 50 | secret: process.env.NEXTAUTH_SECRET 51 | }); 52 | export { handler as GET, handler as POST }; -------------------------------------------------------------------------------- /src/app/cluster-health/page.jsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState, useEffect } from 'react'; 4 | import Navbar from '../../components/index/navbar'; 5 | import { Heading, Box, Grid, GridItem } from '@chakra-ui/react'; 6 | 7 | const ClusterHealth = () => { 8 | const [windowWidth, setWindowWidth] = useState(0); 9 | 10 | // update the window width 11 | const handleResize = () => { 12 | setWindowWidth(window.innerWidth); 13 | }; 14 | 15 | // useEffect to run the handleResize function on window resize 16 | useEffect(() => { 17 | window.addEventListener('resize', handleResize); 18 | handleResize(); 19 | return () => window.removeEventListener('resize', handleResize); 20 | }, []); 21 | 22 | const clusterHealthMetrics = [ 23 | { title: 'Broker Disk Usage', panelId: 15 }, 24 | { title: 'JVM Memory Pool Bytes', panelId: 8 }, 25 | { title: 'Offline Partitions Count', panelId: 10 }, 26 | { title: 'Under Replicated Partitions Count', panelId: 11 }, 27 | { title: 'Active Controller Count', panelId: 9 }, 28 | { title: 'Total Failed Fetch Requests', panelId: 4 }, 29 | ]; 30 | 31 | const renderMetric = (metric) => ( 32 | 33 | )} 130 | 139 | {selectedMetricId.map((metric, index) => ( 140 | handleRemoveMetric(metric)} 144 | showFullDashboard={showFullDashboard} 145 | iFrameUrl={iFrameUrl} 146 | />))} 147 | 148 | 149 | ); 150 | }; 151 | 152 | export default Dashboard; 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/app/layout.jsx: -------------------------------------------------------------------------------- 1 | import { Providers } from './providers' 2 | 3 | export default function RootLayout({ 4 | children, 5 | }) { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | 12 | ) 13 | } -------------------------------------------------------------------------------- /src/app/login/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { 5 | Flex, 6 | Box, 7 | useColorModeValue 8 | } from "@chakra-ui/react"; 9 | import LoginForm from '../../components/login/loginForm'; // Import the LoginForm component 10 | 11 | const Login = () => { 12 | 13 | // states for properties in different color mode 14 | const loginBGImage = useColorModeValue('/kafka-kare-background-v2.jpg', '/kafka-kare-meerkat-background-v2.png'); 15 | const loginBGSize = useColorModeValue(200, 100); 16 | 17 | return ( 18 | // Flex container to center the content vertically and horizontally 19 | 23 | {/* Render the LoginForm component */} 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default Login; -------------------------------------------------------------------------------- /src/app/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState, useRef, useEffect } from "react"; 4 | import Link from "next/link"; 5 | import { Button, Image, Text, Box } from "@chakra-ui/react"; 6 | import { motion } from "framer-motion"; 7 | import BackgroundAnimation from "../ui/home-background-animation"; 8 | 9 | const Home = () => { 10 | 11 | return ( 12 | 13 | 16 | Kafka Kare 25 | {/* */} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default Home; 38 | -------------------------------------------------------------------------------- /src/app/providers.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import React from 'react'; 3 | import '../styles/globals.css'; 4 | import { ChakraProvider } from '@chakra-ui/react'; 5 | import theme from '../styles/theme.js'; 6 | import { SessionProvider } from 'next-auth/react'; 7 | 8 | 9 | export function Providers({ children }) { 10 | return ( 11 | 12 | 13 | {children} 14 | 15 | 16 | ) 17 | }; -------------------------------------------------------------------------------- /src/app/secret.js: -------------------------------------------------------------------------------- 1 | export default function Home() { 2 | return
Hello Team Nickelhack
; 3 | } -------------------------------------------------------------------------------- /src/app/signup/page.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { 5 | Flex, 6 | Box, 7 | useColorModeValue 8 | } from "@chakra-ui/react"; 9 | import SignupForm from '../../components/signup/signupForm'; // Import the SignupForm component 10 | 11 | const Signup = () => { 12 | 13 | // states for properties in different color mode 14 | const loginBGImage = useColorModeValue('/kafka-kare-background-v2.jpg', '/kafka-kare-meerkat-background-v2.png'); 15 | const loginBGSize = useColorModeValue(200, 100); 16 | 17 | return ( 18 | // Flex container to center the content vertically and horizontally 19 | 23 | {/* Render the SignupForm component */} 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default Signup; -------------------------------------------------------------------------------- /src/components/alerts/alertHistoryGraph.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import Chart from 'chart.js/auto'; 3 | 4 | 5 | const AlertHistoryGraph = () => { 6 | const chartRef = useRef(null); 7 | 8 | useEffect(() => { 9 | const labels = Array.from({ length: 30 }, (_, index) => { 10 | const date = new Date(); 11 | date.setDate(date.getDate() - index); 12 | return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); 13 | }); 14 | 15 | const data = generateZerosArray(30); 16 | 17 | if (chartRef.current) { 18 | const ctx = chartRef.current.getContext('2d'); 19 | 20 | // Destroy existing chart instance if it exists 21 | if (chartRef.current.chart) { 22 | chartRef.current.chart.destroy(); 23 | } 24 | 25 | // Create new chart instance 26 | chartRef.current.chart = new Chart(ctx, { 27 | type: 'line', 28 | data: { 29 | labels: labels, 30 | datasets: [ 31 | { 32 | label: "Alerts Over Past 30 Days", 33 | data: data, 34 | backgroundColor: "rgba(75, 192, 192, 0.2)", 35 | borderColor: "rgba(75, 192, 192, 1)", 36 | borderWidth: 1, 37 | }, 38 | ], 39 | }, 40 | options: { 41 | scales: { 42 | x: { 43 | type: "linear", 44 | text: "Day", 45 | ticks: { // Set the x-axis labels to the past 30 days 46 | callback: (value, index) => { 47 | const date = new Date(); 48 | date.setDate(date.getDate() - (29 - index)); 49 | return date.toLocaleDateString(); 50 | }, 51 | }, 52 | }, 53 | y: { 54 | min: 0, 55 | beginAtZero: true, 56 | text: "Number of Alerts", 57 | }, 58 | }, 59 | plugins: { 60 | title: { 61 | display: true, 62 | text: "Number of Alerts From the Past 30 Days", 63 | }, 64 | }, 65 | }, 66 | }); 67 | } 68 | }, []); 69 | 70 | const generateZerosArray = (length) => { 71 | return Array.from({ length: length }, () => 0); 72 | }; 73 | 74 | return ; 75 | }; 76 | 77 | export default AlertHistoryGraph; 78 | 79 | -------------------------------------------------------------------------------- /src/components/alerts/alertsByMetricGraph.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import Chart from 'chart.js/auto'; 3 | 4 | const AlertsByMetricGraph = ({ allMetrics }) => { 5 | const chartRef = useRef(null); 6 | 7 | useEffect(() => { 8 | if (allMetrics && chartRef.current) { 9 | const ctx = chartRef.current.getContext('2d'); 10 | 11 | // Destroy existing chart instance if it exists 12 | if (chartRef.current.chart) { 13 | chartRef.current.chart.destroy(); 14 | } 15 | 16 | // Create datasets from allMetrics object with initial values of 0 and random colors and border 17 | const datasets = Object.keys(allMetrics).map((metric, index) => ({ 18 | label: metric, 19 | data: Array(Object.keys(allMetrics).length).fill(0), 20 | backgroundColor: `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 0.2)`, 21 | borderColor: `rgba(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255}, 1)`, 22 | borderWidth: 1 23 | })); 24 | 25 | // Create new chart instance 26 | chartRef.current.chart = new Chart(ctx, { 27 | type: 'bar', 28 | data: { 29 | labels: Object.keys(allMetrics), 30 | datasets: datasets 31 | }, 32 | options: { 33 | indexAxis: 'y', 34 | scales: { 35 | x: { 36 | beginAtZero: true 37 | }, 38 | y: { 39 | stacked: true 40 | } 41 | }, 42 | plugins: { 43 | title: { 44 | display: true, 45 | text: 'All Time Number of Alerts by Metric' 46 | } 47 | } 48 | } 49 | }); 50 | } 51 | }, [allMetrics]); 52 | 53 | 54 | return ; 55 | }; 56 | 57 | export default AlertsByMetricGraph; 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/components/alerts/configureCustom.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Box, 4 | Button, 5 | Heading, 6 | Input, 7 | Text, 8 | } from "@chakra-ui/react"; 9 | 10 | const ConfigureCustom = ({ 11 | allMetrics, 12 | minThresholds, 13 | setMinThresholds, 14 | maxThresholds, 15 | setMaxThresholds, 16 | removeAlert, 17 | saveAlerts, 18 | }) => { 19 | 20 | return ( 21 | 22 | 23 | Configure Custom Alerts 24 | 25 | {Object.keys(allMetrics).map((metric, index) => ( 26 | 27 | {metric} 28 | { 33 | const updatedThresholds = [...minThresholds]; 34 | updatedThresholds[index] = e.target.value; 35 | setMinThresholds(updatedThresholds); 36 | }} 37 | /> 38 | { 43 | const updatedThresholds = [...maxThresholds]; 44 | updatedThresholds[index] = e.target.value; 45 | setMaxThresholds(updatedThresholds); 46 | }} 47 | /> 48 | 56 | 57 | ))} 58 | 59 | 60 | ); 61 | }; 62 | 63 | export default ConfigureCustom; 64 | 65 | -------------------------------------------------------------------------------- /src/components/clusters/addClusterModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | FormControl, FormLabel, FormErrorMessage, Input, InputGroup, InputLeftElement, Button, Icon, 4 | Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, 5 | } from '@chakra-ui/react'; 6 | import { SiApachekafka } from 'react-icons/si'; 7 | import { MdDriveFileRenameOutline } from 'react-icons/md'; 8 | import { clustersStore } from '../../store/clusters'; 9 | 10 | const AddClusterModal = ({ 11 | handleNewClusterClose, handleClusterNameChange, handleClusterPortChange, handleNewCluster 12 | }) => { 13 | 14 | // declare state variables 15 | const isNewClusterOpen = clustersStore(state => state.isNewClusterOpen); 16 | const clusterName = clustersStore(state => state.clusterName); 17 | const isClusterNameEmpty = clustersStore(state => state.isClusterNameEmpty); 18 | const clusterPort = clustersStore(state => state.clusterPort); 19 | const isClusterPortEmpty = clustersStore(state => state.isClusterPortEmpty); 20 | 21 | // declare reference modal initial focus 22 | const initialRef = React.useRef(null); 23 | 24 | return ( 25 | 26 | /* Add Cluster Modal */ 27 | 28 | 29 | 30 | 31 | {/* Title */} 32 | New Cluster 33 | 34 | 35 | 36 | {/* Name Input */} 37 | 38 |  Name: {clusterName} 39 | 40 | 41 | 42 | 43 | 47 | 48 | Cluster name shouldn't be Empty 49 | 50 | 51 | {/* Port Input */} 52 | 53 |  Hostname & Port: {clusterPort} 54 | 55 | 56 | 57 | 58 | 62 | 63 | Cluster port shouldn't be Empty 64 | 65 | 66 | 67 | 68 | {/* Cancel Button */} 69 | 70 | 71 | {/* Submit Button */} 72 | 73 | 74 | 75 | 76 | ); 77 | }; 78 | 79 | export default AddClusterModal; -------------------------------------------------------------------------------- /src/components/clusters/clusterCard.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Flex, Heading, Spacer, IconButton, Stack, StackDivider, Box, Text, Button, Icon, 4 | Card, CardHeader, CardBody, CardFooter, 5 | } from '@chakra-ui/react'; 6 | import { useRouter } from 'next/navigation'; 7 | import { CloseIcon, EditIcon } from '@chakra-ui/icons'; 8 | import { RxStar, RxStarFilled } from 'react-icons/rx'; 9 | import path from 'path'; 10 | import { clustersStore } from '../../store/clusters'; 11 | 12 | const ClusterCard = ({ clusterObj, handleFavoriteChange }) => { 13 | const { push } = useRouter(); 14 | 15 | // actions when editClusterModal open 16 | const handleEditClusterOpen = () => { 17 | clustersStore.setState({isEditClusterOpen: true}); 18 | clustersStore.setState({clusterName: clusterObj.name}); 19 | clustersStore.setState({oldClusterName: clusterObj.name}); 20 | clustersStore.setState({clusterPort: clusterObj.hostnameAndPort}); 21 | clustersStore.setState({editClusterID: clusterObj._id}); 22 | } 23 | 24 | // actions when deleteClusterModal open 25 | const handleDeleteClusterOpen = () => { 26 | clustersStore.setState({isDeleteClusterOpen: true}); 27 | clustersStore.setState({oldClusterName: clusterObj.name}); 28 | clustersStore.setState({deleteClusterID: clusterObj._id}); 29 | } 30 | 31 | return ( 32 | 33 | /* Cluster Card */ 34 | 35 | 36 | 37 | {/* Title */} 38 | {clusterObj.name} 39 | 40 | 41 | 42 | {/* Favorite Button */} 43 | : } 46 | onClick = {() => {handleFavoriteChange(clusterObj._id);}} 47 | /> 48 | 49 | 50 | 51 | {/* Delete Cluster Button */} 52 | } variant='ghost' 54 | onClick = {() => handleDeleteClusterOpen()} 55 | /> 56 | 57 | 58 | 59 | } spacing='4'> 60 | 61 | {/* Hostname and Port */} 62 | 63 | 64 | Hostname & Port 65 | 66 | 67 | {clusterObj.hostnameAndPort} 68 | 69 | 70 | 71 | {/* Any Information */} 72 | 73 | 74 | Something Else 75 | 76 | 77 | {'Something Else'} 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {/* Graphs Button */} 86 | 87 | 88 | 89 | 90 | {/* Edit Cluster Button */} 91 | handleEditClusterOpen()} icon={} /> 92 | 93 | 94 | 95 | ); 96 | }; 97 | 98 | export default ClusterCard; -------------------------------------------------------------------------------- /src/components/clusters/deleteClusterModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Button, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, 4 | } from '@chakra-ui/react'; 5 | import { clustersStore } from '../../store/clusters'; 6 | 7 | const DeleteClusterModal = ({ handleDeleteCluster }) => { 8 | 9 | // declare state variables 10 | const oldClusterName = clustersStore((state) => state.oldClusterName); 11 | const deleteClusterID = clustersStore(state => state.deleteClusterID); 12 | const isDeleteClusterOpen = clustersStore(state => state.isDeleteClusterOpen); 13 | 14 | return ( 15 | 16 | /* Delete Cluster Modal */ 17 | clustersStore.setState({isDeleteClusterOpen: false})} motionPreset='slideInBottom'> 18 | 19 | 20 | Delete Cluster: {oldClusterName} 21 | 22 | 23 | Are you sure? You can't undo this action afterwards. 24 | 25 | 26 | 27 | {/* Cancel Button */} 28 | 29 | 30 | {/* Delete Button */} 31 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default DeleteClusterModal; -------------------------------------------------------------------------------- /src/components/clusters/editClusterModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | FormControl, FormLabel, FormErrorMessage, Input, InputGroup, InputLeftElement, Button, Icon, 4 | Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, 5 | } from '@chakra-ui/react'; 6 | import { SiApachekafka } from 'react-icons/si'; 7 | import { MdDriveFileRenameOutline } from 'react-icons/md'; 8 | import { clustersStore } from '../../store/clusters'; 9 | 10 | const EditClusterModal = ({ 11 | handleEditClusterClose, handleClusterNameChange, handleClusterPortChange, handleEditCluster 12 | }) => { 13 | 14 | // declare state variables 15 | const clusterName = clustersStore(state => state.clusterName); 16 | const isClusterNameEmpty = clustersStore(state => state.isClusterNameEmpty); 17 | const clusterPort = clustersStore((state) => state.clusterPort); 18 | const isClusterPortEmpty = clustersStore((state) => state.isClusterPortEmpty); 19 | const isEditClusterOpen = clustersStore((state) => state.isEditClusterOpen); 20 | const oldClusterName = clustersStore((state) => state.oldClusterName); 21 | const editClusterID = clustersStore(state => state.editClusterID); 22 | 23 | // declare reference modal initial focus 24 | const initialRef = React.useRef(null); 25 | 26 | return ( 27 | 28 | /* Edit Cluster Modal */ 29 | 30 | 31 | 32 | 33 | {/* Title */} 34 | Edit Cluster: {oldClusterName} 35 | 36 | 37 | 38 | {/* Name Input */} 39 | 40 | New Name: {clusterName} 41 | 42 | 43 | 44 | 45 | 49 | 50 | Cluster name shouldn't be Empty 51 | 52 | 53 | {/* Port Input */} 54 | 55 | New Hostname & Port: {clusterPort} 56 | 57 | 58 | 59 | 60 | 64 | 65 | Cluster port shouldn't be Empty 66 | 67 | 68 | 69 | 70 | {/* Cancel Button */} 71 | 72 | 73 | {/* Submit Button */} 74 | 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | export default EditClusterModal; -------------------------------------------------------------------------------- /src/components/clusters/logoutModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Button, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, 4 | } from '@chakra-ui/react'; 5 | import { clustersStore } from '../../store/clusters'; 6 | 7 | const LogoutModal = ({ handleLogout }) => { 8 | 9 | // declare state variables 10 | const isLogoutModalOpen = clustersStore(state => state.isLogoutModalOpen); 11 | 12 | return ( 13 | 14 | /* Delete Cluster Modal */ 15 | clustersStore.setState({isLogoutModalOpen: false})} motionPreset='slideInBottom'> 16 | 17 | 18 | Logout 19 | 20 | 21 | Are you sure you want to log out? 22 | 23 | 24 | 25 | {/* Cancel Button */} 26 | 27 | 28 | {/* Delete Button */} 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default LogoutModal; -------------------------------------------------------------------------------- /src/components/clusters/mainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, SimpleGrid, Icon, Tabs, TabList, TabIndicator, TabPanels, Tab, TabPanel, useColorModeValue } from '@chakra-ui/react'; 3 | import { RxStar, RxStarFilled } from 'react-icons/rx'; 4 | import { clustersStore } from '../../store/clusters'; 5 | import ClusterCard from './mainContainer/clusterCard'; 6 | import EditClusterModal from './mainContainer/editClusterModal'; 7 | import DeleteClusterModal from './mainContainer/deleteClusterModal'; 8 | 9 | const MainContainer = () => { 10 | const notFavoriteStarColor = useColorModeValue('black', 'white'); 11 | 12 | // declare state variables 13 | const clusterDisplayMap = clustersStore(state => state.clusterDisplayMap); 14 | const clusterFavoriteDisplayMap = clustersStore(state => state.clusterFavoriteDisplayMap); 15 | const clusterNotFavoriteDisplayMap = clustersStore(state => state.clusterNotFavoriteDisplayMap); 16 | const alertTopics = [ 17 | {text: 'Topic_Count', color: 'orange'}, 18 | // {text: 'Request_Queue_Time_Max', color: 'red'}, 19 | {text: 'Broker_Count', color: 'orange'}, 20 | {text: 'Active_Controller', color: 'orange'}, 21 | //{text: 'Request_Queue_Time_Max', color: 'red'}, 22 | {text: 'Active_Controller', color: 'orange'}, 23 | //{text: 'Request_Queue_Time_Max', color: 'red'}, 24 | {text: 'Active_Controller', color: 'orange'}, 25 | //{text: 'Request_Queue_Time_Max', color: 'red'}, 26 | ].sort((a,b) => { 27 | if (a.color === 'red') return -1; 28 | else if (b.color === 'red') return 1; 29 | else if (a.color === 'orange') return -1; 30 | else if (b.color === 'orange') return 1; 31 | else return 0; 32 | }); 33 | 34 | return ( 35 | 36 | {}} style={{height: 'calc(100% - 32px)'}}> 37 | 38 | All 39 | Favorite 40 | Not Favorite 41 | 42 | 43 | 44 | 45 | 46 | {[...clusterDisplayMap].toSorted((a, b) => Date.parse(a[1].dateAdded) - Date.parse(b[1].dateAdded)) 47 | .map(([id, clusterObj]) => ( 48 | 49 | /* Cluster Card */ 50 | 51 | ))} 52 | 53 | 54 | 55 | 56 | {[...clusterFavoriteDisplayMap].toSorted((a, b) => Date.parse(a[1].dateAdded) - Date.parse(b[1].dateAdded)) 57 | .map(([id, clusterObj]) => ( 58 | 59 | /* Cluster Card */ 60 | 61 | ))} 62 | 63 | 64 | 65 | 66 | {[...clusterNotFavoriteDisplayMap].toSorted((a, b) => Date.parse(a[1].dateAdded) - Date.parse(b[1].dateAdded)) 67 | .map(([id, clusterObj]) => ( 68 | 69 | /* Cluster Card */ 70 | 71 | ))} 72 | 73 | 74 | 75 | {/* Edit Cluster Modal */} 76 | 77 | 78 | {/* Delete Cluster Modal */} 79 | 80 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default MainContainer; -------------------------------------------------------------------------------- /src/components/clusters/mainContainer/deleteClusterModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Button, Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, useToast, 4 | Alert, AlertIcon, AlertTitle, AlertDescription, 5 | } from '@chakra-ui/react'; 6 | import { clustersStore } from '../../../store/clusters'; 7 | import { handleDeleteCluster } from '../../../utils/clustersHandler'; 8 | 9 | const DeleteClusterModal = () => { 10 | 11 | // declare state variables 12 | const oldClusterName = clustersStore((state) => state.oldClusterName); 13 | const deleteClusterID = clustersStore(state => state.deleteClusterID); 14 | const isDeleteClusterOpen = clustersStore(state => state.isDeleteClusterOpen); 15 | 16 | // declare variable to use toast 17 | const toast = useToast(); 18 | 19 | return ( 20 | 21 | /* Delete Cluster Modal */ 22 | clustersStore.setState({isDeleteClusterOpen: false})} motionPreset='slideInBottom'> 23 | 24 | 25 | 26 | {/* Title */} 27 | Delete Cluster: {oldClusterName} 28 | 29 | 30 | {/* Warning */} 31 | 32 | 33 | 34 | Are you sure? You can't undo this action afterwards. 35 | 36 | 37 | 38 | 39 | {/* Cancel Button */} 40 | 41 | 42 | {/* Delete Button */} 43 | 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default DeleteClusterModal; -------------------------------------------------------------------------------- /src/components/clusters/mainContainer/editClusterModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | FormControl, FormLabel, FormErrorMessage, Input, InputGroup, InputLeftElement, Button, Icon, useToast, 4 | Modal, ModalOverlay, ModalContent, ModalHeader, ModalFooter, ModalBody, ModalCloseButton, 5 | } from '@chakra-ui/react'; 6 | import { SiApachekafka } from 'react-icons/si'; 7 | import { MdDriveFileRenameOutline } from 'react-icons/md'; 8 | import { clustersStore } from '../../../store/clusters'; 9 | import { handleEditClusterClose, handleEditCluster, handleClusterNameChange, handleClusterPortChange } from '../../../utils/clustersHandler'; 10 | 11 | const EditClusterModal = () => { 12 | 13 | // declare state variables 14 | const clusterName = clustersStore(state => state.clusterName); 15 | const isClusterNameEmpty = clustersStore(state => state.isClusterNameEmpty); 16 | const clusterPort = clustersStore((state) => state.clusterPort); 17 | const isClusterPortEmpty = clustersStore((state) => state.isClusterPortEmpty); 18 | const isEditClusterOpen = clustersStore((state) => state.isEditClusterOpen); 19 | const oldClusterName = clustersStore((state) => state.oldClusterName); 20 | const editClusterID = clustersStore(state => state.editClusterID); 21 | 22 | // declare reference modal initial focus 23 | const initialRef = React.useRef(null); 24 | 25 | // declare variable to use toast 26 | const toast = useToast(); 27 | 28 | return ( 29 | 30 | /* Edit Cluster Modal */ 31 | 32 | 33 | 34 | 35 | {/* Title */} 36 | Edit Cluster: {oldClusterName} 37 | 38 | 39 | 40 | {/* Name Input */} 41 | 42 | New Name: {clusterName} 43 | 44 | 45 | 46 | 47 | {clustersStore.setState({isClusterNameEmpty: false})}} 50 | /> 51 | 52 | Cluster name shouldn't be Empty 53 | 54 | 55 | {/* Port Input */} 56 | 57 | New Hostname & Port: {clusterPort} 58 | 59 | 60 | 61 | 62 | {clustersStore.setState({isClusterPortEmpty: false})}} 65 | /> 66 | 67 | Cluster port shouldn't be Empty 68 | 69 | 70 | 71 | 72 | {/* Cancel Button */} 73 | 74 | 75 | {/* Submit Button */} 76 | 77 | 78 | 79 | 80 | ); 81 | }; 82 | 83 | export default EditClusterModal; -------------------------------------------------------------------------------- /src/components/clusters/menuDrawer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | FormLabel, Box, Link, Code, Textarea, Flex, Icon, IconButton, 4 | Drawer, DrawerBody, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerContent, DrawerCloseButton, 5 | Step, StepDescription, StepIcon, StepIndicator, StepNumber, StepSeparator, StepStatus, StepTitle, Stepper 6 | } from '@chakra-ui/react'; 7 | import { RiSendPlane2Fill } from 'react-icons/ri'; 8 | import { clustersStore } from '../../store/clusters'; 9 | 10 | const MenuDrawer = ({ handleSlackWebhookURLSubmit }) => { 11 | 12 | // declare state variables 13 | const isDrawerOpen = clustersStore(state => state.isDrawerOpen); 14 | const slackWebhookURL = clustersStore(state => state.slackWebhookURL); 15 | 16 | // declare reference modal initial focus 17 | const initialRef = React.useRef(null); 18 | 19 | // check slack url format before submit 20 | const submitSlackWebhookUrl = () => { 21 | if (slackWebhookURL.slice(0, 34) === 'https://hooks.slack.com/services/T' 22 | && slackWebhookURL.indexOf('/B') - slackWebhookURL.indexOf('/T') >= 10 23 | && slackWebhookURL.lastIndexOf('/') - slackWebhookURL.indexOf('/B') >= 9 24 | && slackWebhookURL.length >= 77 25 | ) { 26 | alert('Right Format'); 27 | handleSlackWebhookURLSubmit(); 28 | } else { 29 | alert('Wrong Format'); 30 | } 31 | } 32 | 33 | // declare slack webhook step array 34 | // { title: (String), description: (Function that returns JSX)} 35 | const slackWebhookSteps = [ 36 | { title: 'First', description: () => ( 37 | <> 38 | Go to 39 | Create your Slack App 40 | and pick a name, choose a workspace to associate your app with, then click Create App button. 41 | 42 | )}, 43 | { title: 'Second', description: () => ( 44 | <> 45 | You should be redirect to 46 | App Management Dashboard 47 | . From here, select Incoming Webhooks at the left under Features, and toggle Activate Incoming Webhooks to on. 48 | 49 | )}, 50 | { title: 'Third', description: () => ( 51 | <> 52 | Now that incoming webhooks are enabled, the settings page should refresh and some additional options will appear. Click the Add New Webhook to Workspace button at the bottom. 53 | 54 | )}, 55 | { title: 'Fourth', description: () => ( 56 | <> 57 | Pick a channel that the app will post to, then select Authorize. If you need to add the incoming webhook to a private channel, you must first be in that channel. You'll be sent back to  58 | 59 | App Management Dashboard 60 | , where you should see your webhook URL, which will look something like this: 61 | https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX 62 | 63 | )}, 64 | ]; 65 | 66 | return ( 67 | 68 | /* Menu Drawer */ 69 | clustersStore.setState({isDrawerOpen: false})} isOpen={isDrawerOpen} initialFocusRef={initialRef}> 70 | 71 | 72 | 73 | 74 | {/* Title */} 75 | Menu 76 | 77 | {/* Content */} 78 | 79 | 80 | {/* URL Subtitle */} 81 | Slack Webhook URL 82 | 83 | 84 | 85 | {/* URL Input */} 86 |