├── .DS_Store ├── .babelrc ├── .env ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __tests__ └── user.test.js ├── dist ├── bundle.js └── bundle.js.LICENSE.txt ├── electron.js ├── kafka-playground ├── .DS_Store ├── .gitignore ├── Dockerfile ├── FETCH_HEAD ├── Utils │ ├── .DS_Store │ └── scripts │ │ ├── .DS_Store │ │ └── prometheus.yml ├── babel.config.js ├── data-generator │ ├── dg.css │ ├── index.html │ ├── main.js │ ├── main.ts │ ├── server.js │ └── tsconfig.json ├── docker-compose.yml ├── package.json ├── plugin │ └── natel-discrete-panel │ │ ├── .circleci │ │ └── config.yml │ │ ├── .gitignore │ │ ├── .prettierrc.js │ │ ├── LICENSE │ │ ├── README.md │ │ ├── RELEASE.md │ │ ├── jest.config.js │ │ ├── package.json │ │ ├── src │ │ ├── canvas-metric.ts │ │ ├── distinct-points.ts │ │ ├── img │ │ │ ├── discrete_logo.svg │ │ │ ├── screenshot-multiple.png │ │ │ ├── screenshot-options-1.png │ │ │ ├── screenshot-options-2.png │ │ │ ├── screenshot-single-1.png │ │ │ ├── screenshot-single-2.png │ │ │ ├── screenshot-single-3.png │ │ │ └── screenshot-single-4.png │ │ ├── module.test.ts │ │ ├── module.ts │ │ ├── partials │ │ │ ├── editor.colors.html │ │ │ ├── editor.legend.html │ │ │ ├── editor.mappings.html │ │ │ ├── editor.options.html │ │ │ └── module.html │ │ └── plugin.json │ │ ├── tsconfig.json │ │ └── yarn.lock ├── provisioning │ ├── dashboards │ │ ├── dashboard.yaml │ │ └── totalMessages.json │ └── datasources │ │ └── datasource.yml ├── src │ ├── consumer.js │ ├── consumer2.js │ ├── producer.js │ ├── producer2.js │ ├── topic.js │ └── topic2.js ├── streaming_data │ ├── consumer-jd.js │ ├── docker-compose.yml │ ├── index.js │ ├── package.json │ ├── producer.js │ └── topic.js └── webpack.config.js ├── package.json ├── server ├── db │ └── db.js ├── models │ └── metricsData.js ├── routes │ └── user.js └── server.js ├── src ├── App.css ├── App.js ├── MainDashboard.js ├── assets │ ├── KafKareLarge.png │ ├── KafKareMedium.png │ ├── KafKareSmall.png │ ├── KafkareTsmall.png │ └── icons │ │ ├── mac │ │ └── icon.icns │ │ ├── png │ │ └── icon.png │ │ └── win │ │ └── icon.ico ├── components │ ├── Brokers.js │ ├── Cpu.js │ ├── Health.js │ ├── Lag.js │ ├── Topics.js │ ├── TopicsDrill.js │ ├── VirtualMem.js │ ├── Zookeeper.js │ ├── auth │ │ └── auth.js │ └── views │ │ ├── Navbar.js │ │ ├── loginPage.js │ │ └── registerPage.js ├── index.html └── index.js ├── tsconfig.json └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BROWSER=none -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5501, 3 | "deno.enable": false 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Kafkare](./src/assets/KafKareLarge.png) 2 | 3 |
A system monitoring tool for Kafka.
4 | 5 |
6 | 7 |

8 | Kafkare 9 |

10 | 11 |
12 | 13 |

14 | GitHub 15 | GitHub issues 16 | GitHub last commit 17 | GitHub Repo stars 18 |

19 | 20 | 21 | ## Table of Contents 22 | 1. [Features](#Features) 23 | 2. [Overview](#Overview) 24 | 3. [Documentation and Demo](#Documentation-and-Demo) 25 | 1. [Two ways to generate sample kafka data](#Documentation-and-Demo) 26 | 1. [Manual data entry](#Running-the-demo-kafka-cluster-and-manually-enter-data) 27 | 2. [Streaming API](#Running-the-demo-kafka-cluster-and-using-the-API-for-constant-data-generation) 28 | 2. [Running the Dashboard Application](#Running-the-dashboard) 29 | 4. [Setup](#Connecting-to-an-existing-instance-of-Kafka) 30 | 1. [Connecting to an existing instance of Kafka](#Connecting-to-an-existing-instance-of-Kafka) 31 | 2. [Updating User Database](#Connecting-the-user-database) 32 | 5. [FAQ](#FAQ) 33 | 6. [License](#License) 34 | 7. [Authors](#Authors) 35 | 36 | ## Features 37 | 38 | - Cross-platform Kafka monitoring, real-time data display desktop application 39 | - Metrics monitored are based on feedback from real life Kafka deployment crashes and best practices 40 | - Fullstack integration, leveraging user authentication to ensure only authorized members can review the dashboard 41 | 42 | ## Overview 43 | 44 | Kafkare is a cross-platform Kafka monitoring dashboard application used to oversee the health of the Kafka cluster. 45 | Several key metrics are displayed including consumer lag time, number of topics, as well as system metrics like cpu usage, and available memory. 46 | Users can register for an account and login to access the dashboard. Passwords are encrypted with Bcrypt and stored in an external database. 47 | 48 | ## Documentation and Demo 49 | 50 | 51 | ### Demo Setup 52 | 53 |
QUICK START
54 |
55 | 56 | There are two ways to generate your Kafka data: 57 | 1. Manually - For a controlled amount of data produced 58 | 2. Using an API - For a constant stream of data produced 59 | 60 | 61 | #### Running the demo kafka cluster and manually enter data 62 |
63 | From root directory (Kafkare) go into the kafka-playground folder: 64 | 65 | In the terminal: 66 | 67 | Install all dependencies 68 |
69 | ```sh 70 | npm install 71 | ``` 72 |
73 | Set up the docker containers, we have a prebuilt kafka cluster for the demo 74 |
75 | 76 | ```sh 77 | export HOST_IP=$(ifconfig | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $2 }' | cut -f2 -d: | head -n1) 78 | docker-compose up 79 | ``` 80 | 81 |
82 | 83 | Run the data generator application to create topics and consumers: 84 |
85 | ```sh 86 | npm run build-testbed 87 | npm run testbed 88 | ``` 89 | You can go to your browser and enter in the address bar: 90 |
91 | ```sh 92 | localhost:8181 93 | ``` 94 |
95 | to see the data generator 96 | 97 | In the data generator, put in a new topic for the broker and submit it. 98 | Then put in the number of messages you want to produce and submit. This will create that many messages to the kafka cluster. 99 | 100 | 101 | #### Running the demo kafka cluster and using the API for constant data generation 102 |
103 | From root directory (Kafkare) go into the kafka-playground folder/streaming_data: 104 | 105 | In the terminal: 106 | 107 | Install all dependencies 108 |
109 | 110 | ```sh 111 | npm install 112 | ``` 113 | 114 | Go back to the kafka-playground folder and install the dependencies there as well: 115 | In the terminal: 116 | 117 | Install all dependencies 118 |
119 | ```sh 120 | npm install 121 | ``` 122 | 123 |
124 | Set up the docker containers, we have a prebuilt kafka cluster for the demo 125 |
126 | 127 | ```sh 128 | export HOST_IP=$(ifconfig | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $2 }' | cut -f2 -d: | head -n1) 129 | docker-compose up 130 | ``` 131 | 132 |
133 | Run the data streaming application to create topics and consumers: 134 | 135 |
136 | You can start your consumer by going to a new terminal, then open the Kafkare/kafka-playground directory. Now run: 137 | 138 | ```sh 139 | npm run consumer 140 | ``` 141 | 142 | Open a new terminal window. From the root directory (Kafkare) go into the kafka-playground directory. 143 | Now create the topic in Kafka by running: 144 | 145 | ```sh 146 | npm run topic 147 | ``` 148 | 149 | Finally, to create the generate the stream, run in the terminal: 150 | 151 | ```sh 152 | npm run producer 153 | ``` 154 | 155 | #### __*Running the dashboard*__ 156 | 157 | Open a new terminal. 158 | In the root directory (Kafkare), run in the terminal 159 |
160 | 161 | ```sh 162 | npm install 163 | npm run build 164 | npm start 165 | ``` 166 | 167 | You will see a login page where you can either login with an existing account or create a new account to login with. 168 | 169 | To create a new account, click the register button in the login page. After registering an account, you will be prompted to login in with the account. 170 | 171 | After successfully logging in, a desktop application with the Kafka monitoring dashboards will load and you can start monitoring the running kafka cluster. 172 | 173 | ## Connecting to an existing instance of Kafka 174 | In the kafka-playground/ directory, edit the docker-compose.yml file. Add the following environment variables with relavant information to Kafka-exporter: 175 | Environment Variable | Description 176 | ---------------------|------------ 177 | KAFKA_SERVER | Addresses (host:port) of Kafka server. 178 | SASL_USERNAME | SASL user name. 179 | SASL_PASSWORD | SASL user password. 180 | 181 | ```yml 182 | kafka_exporter: 183 | image: danielqsj/kafka-exporter 184 | ports: 185 | - '9308:9308' 186 | environment: 187 | KAFKA_SERVER: 188 | SASL_USERNAME: 189 | SASL_PASSWORD: 190 | ``` 191 | 192 | ## Connecting the user database 193 | In the server/db/ directory, edit the db.js file. Within the file, change the value of the PG_URI variable to the postgres database you are using. 194 | 195 | ```javascript 196 | const PG_URI = 197 | 'postgres://:@.db.elephantsql.com:5432/'; 198 | ``` 199 | 200 | ## FAQ 201 | #### Docker Compose Error 202 | **Q1.** I'm getting this error when I use docker-compose up 203 | 204 | ```sh 205 | During handling of the above exception, another exception occurred: 206 | 207 | Traceback (most recent call last): 208 | File "docker-compose", line 3, in 209 | File "compose\cli\main.py", line 67, in main 210 | File "compose\cli\main.py", line 123, in perform_command 211 | File "compose\cli\command.py", line 69, in project_from_options 212 | File "compose\cli\command.py", line 132, in get_project 213 | File "compose\cli\docker_client.py", line 43, in get_client 214 | File "compose\cli\docker_client.py", line 170, in docker_client 215 | File "site-packages\docker\api\client.py", line 188, in __init__ 216 | File "site-packages\docker\api\client.py", line 213, in _retrieve_server_version 217 | docker.errors.DockerException: Error while fetching server API version: (2, 'CreateFile', 'The system cannot find the file specified.') 218 | ``` 219 | 220 | **A1.** Make sure Docker Desktop is up and running. 221 |
222 |
223 | 224 | **Q2:** Why doesn't Kafka doesn't start when I use docker-compose. 225 | 226 | **A2:** Make sure your hostIP is defined. 227 | On iOS or Linux use: 228 | ```sh 229 | export HOST_IP=$(ifconfig | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $2 }' | cut -f2 -d: | head -n1) 230 | ``` 231 | 232 | For Windows Users use: 233 | ```sh 234 | export HOST_IP=$(ipconfig | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $14 }' | cut -f2 -d: | head -n1) 235 | ``` 236 | 237 | ## License 238 | 239 | The JavaScript Templates script is released under the 240 | [MIT license](https://opensource.org/licenses/MIT). 241 | 242 | ## Authors 243 | 244 | [Jenniel Figuereo](https://github.com/jfiguereo89) 245 | [Jiaxin Li](https://github.com/lijiaxingogo) 246 | [Joel Beger](https://github.com/jtbeger) 247 | [Wai Fai Lau](https://github.com/wlau8088/) -------------------------------------------------------------------------------- /__tests__/user.test.js: -------------------------------------------------------------------------------- 1 | const supertest = require('supertest'); 2 | 3 | const { app, server } = require('../server/server'); 4 | const request = supertest(app); 5 | 6 | describe('User Endpoint Test Suite', () => { 7 | const userData = {}; 8 | 9 | // test creation of user 10 | it('create a user', async (done) => { 11 | const payload = { 12 | name: 'TestName', 13 | email: 'testEmail@gmail.com', 14 | password: 'testPass', 15 | }; 16 | 17 | const response = await request.post('/user/signup').send(payload); 18 | 19 | const { id, name, email, success } = response.body; 20 | userData.testId = id; 21 | expect(response.status).toBe(200); 22 | expect(typeof response.body).toBe('object'); 23 | 24 | expect(typeof id).toBe('number'); 25 | expect(name).toBe('TestName'); 26 | expect(email).toBe('testEmail@gmail.com'); 27 | expect(success).toBe(true); 28 | done(); 29 | }); 30 | 31 | // test user login 32 | it('get newly created user', async (done) => { 33 | const payload = { 34 | email: 'testEmail@gmail.com', 35 | password: 'testPass', 36 | }; 37 | const response = await request.post('/user/login').send(payload); 38 | const { loginSuccess, userId } = response.body; 39 | 40 | expect(response.status).toBe(200); 41 | expect(typeof userId).toBe('number'); 42 | expect(loginSuccess).toBe(true); 43 | done(); 44 | }); 45 | 46 | // test deletion of user 47 | it('delete the user', async (done) => { 48 | const response = await request.delete(`/user/${userData.testId}`); 49 | expect(response.body).toBe(1); 50 | done(); 51 | }); 52 | }); 53 | 54 | server.close(); 55 | -------------------------------------------------------------------------------- /dist/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2017 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /** @license React v0.20.1 14 | * scheduler.production.min.js 15 | * 16 | * Copyright (c) Facebook, Inc. and its affiliates. 17 | * 18 | * This source code is licensed under the MIT license found in the 19 | * LICENSE file in the root directory of this source tree. 20 | */ 21 | 22 | /** @license React v16.13.1 23 | * react-is.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** @license React v17.0.1 32 | * react-dom.production.min.js 33 | * 34 | * Copyright (c) Facebook, Inc. and its affiliates. 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ 39 | 40 | /** @license React v17.0.1 41 | * react.production.min.js 42 | * 43 | * Copyright (c) Facebook, Inc. and its affiliates. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE file in the root directory of this source tree. 47 | */ 48 | -------------------------------------------------------------------------------- /electron.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron'); 2 | var path = require('path'); 3 | 4 | function createWindow() { 5 | const win = new BrowserWindow({ 6 | width: 1740, 7 | height: 984, 8 | icon: path.join(__dirname, 'assets/icons/png/64x64.png'), 9 | webPreferences: { 10 | nodeIntegration: true, 11 | }, 12 | }); 13 | 14 | win.loadFile('./src/index.html'); 15 | } 16 | 17 | app.whenReady().then(createWindow); 18 | 19 | app.on('window-all-closed', () => { 20 | if (process.platform !== 'darwin') { 21 | app.quit(); 22 | } 23 | }); 24 | 25 | app.on('activate', () => { 26 | if (BrowserWindow.getAllWindows().length === 0) { 27 | createWindow(); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /kafka-playground/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/.DS_Store -------------------------------------------------------------------------------- /kafka-playground/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /kafka-playground/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | COPY . /image 4 | 5 | WORKDIR /image 6 | 7 | RUN npm i 8 | 9 | EXPOSE 5000 10 | 11 | CMD ["npm", "run", "testbed"] -------------------------------------------------------------------------------- /kafka-playground/FETCH_HEAD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/FETCH_HEAD -------------------------------------------------------------------------------- /kafka-playground/Utils/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/Utils/.DS_Store -------------------------------------------------------------------------------- /kafka-playground/Utils/scripts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/Utils/scripts/.DS_Store -------------------------------------------------------------------------------- /kafka-playground/Utils/scripts/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 5s # By default, scrape targets every 15 seconds. 4 | evaluation_interval: 5s # By default, scrape targets every 15 seconds. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | scrape_configs: 8 | - job_name: 'kafka_exporter' 9 | scrape_interval: 5s 10 | static_configs: 11 | - targets: ['kafka_exporter:9308'] 12 | labels: 13 | group: 'kafka_exporter' 14 | 15 | - job_name: 'prometheus' 16 | static_configs: 17 | - targets: ['localhost:9090', 'node-exporter:9100'] 18 | 19 | # - job_name: 'node-ex' 20 | # static_configs: 21 | # - targets: ['node-exporter:9100'] 22 | -------------------------------------------------------------------------------- /kafka-playground/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /kafka-playground/data-generator/dg.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/data-generator/dg.css -------------------------------------------------------------------------------- /kafka-playground/data-generator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Data Generator 7 | 8 | 9 |

Data Generator

10 |
11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /kafka-playground/data-generator/main.ts: -------------------------------------------------------------------------------- 1 | import * as io from 'socket.io-client'; 2 | const sayHi = document.getElementById('sayHi'); 3 | const socket = io(); 4 | socket.on('connect', () => { 5 | console.log('socket connected'); 6 | }); 7 | sayHi.addEventListener('click', () => { 8 | console.log('hifdsaf'); 9 | }); 10 | // create topic and define the parition number 11 | const sendTopic = (socket: any, topic: string) => { 12 | topic = topic === '' ? 'default-topic' : topic; 13 | const sendTopic: any = { 14 | topic, 15 | }; 16 | socket.emit('sendTopic', sendTopic); 17 | }; 18 | // add click functionlity to create topic 19 | const btn_topic = document.getElementById('btn-topic'); 20 | btn_topic.addEventListener('click', () => { 21 | const topic = document.getElementById('topic') as HTMLInputElement; 22 | const topicVal = topic.value; 23 | console.log(topicVal); 24 | topic.value = ''; 25 | sendTopic(socket, topicVal); 26 | }); 27 | 28 | // subscribe to produceMsg and emit data 29 | const produceMsg = (socket: any, topic: string, msg: string, n: number) => { 30 | const produceTopic: any = { 31 | topic, 32 | msg, 33 | n, 34 | }; 35 | socket.emit('produceMsg', produceTopic); 36 | }; 37 | 38 | const btn_produce_msg = document.getElementById('btn-msg'); 39 | btn_produce_msg.addEventListener('click', () => { 40 | const topicElement = document.getElementById( 41 | 'producer-topic' 42 | ) as HTMLInputElement; 43 | const msgElement = document.getElementById( 44 | 'producer-msg' 45 | ) as HTMLInputElement; 46 | const nElement = document.getElementById( 47 | 'producer-number' 48 | ) as HTMLInputElement; 49 | const topic = topicElement.value; 50 | const msg = msgElement.value; 51 | const n = Number(nElement.value); 52 | produceMsg(socket, topic, msg, n); 53 | }); 54 | 55 | // subscribe to consumer msg and send data 56 | const consumeMsg = (socket: any, topic: string) => { 57 | const msg: any = { 58 | topic, 59 | }; 60 | socket.emit('consumeMsg', msg); 61 | }; 62 | const btn_consume_msg = document.getElementById('btn-comsumer'); 63 | btn_consume_msg.addEventListener('click', () => { 64 | const topicElement = document.getElementById( 65 | 'consume-topic' 66 | ) as HTMLInputElement; 67 | const topic = topicElement.value; 68 | consumeMsg(socket, topic); 69 | }); 70 | -------------------------------------------------------------------------------- /kafka-playground/data-generator/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const { Kafka } = require('kafkajs'); 4 | const ip = require('ip'); 5 | const host = process.env.HOST_IP || ip.address(); 6 | const socketio = require('socket.io'); 7 | const app = express(); 8 | const { runTopic } = require('../src/topic'); 9 | const { runProducer } = require('../src/producer'); 10 | const { runConsumer } = require('../src/consumer'); 11 | const PORT = 8181; 12 | 13 | app.post('/topic', (req, res) => { 14 | const data = req.body; 15 | console.log(data); 16 | res.send; 17 | }); 18 | app.use(express.json()); 19 | app.get('/', (req, res) => { 20 | res.sendFile(path.resolve(__dirname, './index.html')); 21 | }); 22 | app.get('/main.js', (req, res) => { 23 | res.sendFile(path.resolve(__dirname, './main.js')); 24 | }); 25 | const server = app.listen(PORT); 26 | const io = socketio(server); 27 | io.on('connection', (socket) => { 28 | console.log('socket connected'); 29 | 30 | socket.on('sendTopic', (data, n) => { 31 | console.log('Im listening for topic sending in'); 32 | console.log(data); 33 | runTopic(data.topic, 2); 34 | 35 | //runTopic(data.topic, 2); 36 | }); 37 | socket.on('produceMsg', (data) => { 38 | console.log('Im listening for producingMsg'); 39 | console.log(data); 40 | for (let i = 0; i <= data.n; i++) { 41 | runProducer(data.topic, data.msg); 42 | } 43 | }); 44 | socket.on('consumeMsg', (data) => { 45 | console.log('Im listening for consomeMsg'); 46 | console.log(data); 47 | runConsumer(data.topic); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /kafka-playground/data-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": true, 4 | "sourceMap": true, 5 | "module": "commonjs" 6 | }, 7 | "include": ["*.ts"], 8 | "exclude": ["/types/"] 9 | } 10 | -------------------------------------------------------------------------------- /kafka-playground/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | zookeeper: 4 | image: wurstmeister/zookeeper:latest 5 | ports: 6 | - '2181:2181' 7 | kafka: 8 | image: wurstmeister/kafka:2.11-1.1.1 9 | ports: 10 | - '9092:9092' 11 | links: 12 | - zookeeper 13 | environment: 14 | KAFKA_ADVERTISED_HOST_NAME: ${HOST_IP} 15 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 16 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' 17 | KAFKA_DELETE_TOPIC_ENABLE: 'true' 18 | KAFKA_CREATE_TOPICS: 'topic-test:1:1' 19 | volumes: 20 | - /var/run/docker.sock:/var/run/docker.sock 21 | 22 | kafka2: 23 | image: wurstmeister/kafka:2.11-1.1.1 24 | ports: 25 | - '9094:9092' 26 | links: 27 | - zookeeper 28 | environment: 29 | KAFKA_ADVERTISED_HOST_NAME: ${HOST_IP} 30 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 31 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' 32 | KAFKA_DELETE_TOPIC_ENABLE: 'true' 33 | KAFKA_CREATE_TOPICS: 'topic-test:1:1' 34 | volumes: 35 | - /var/run/docker.sock:/var/run/docker.sock 36 | 37 | kafka3: 38 | image: wurstmeister/kafka:2.11-1.1.1 39 | ports: 40 | - '9095:9092' 41 | links: 42 | - zookeeper 43 | environment: 44 | KAFKA_ADVERTISED_HOST_NAME: ${HOST_IP} 45 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 46 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' 47 | KAFKA_DELETE_TOPIC_ENABLE: 'true' 48 | KAFKA_CREATE_TOPICS: 'topic-test:1:1' 49 | volumes: 50 | - /var/run/docker.sock:/var/run/docker.sock 51 | 52 | kafka4: 53 | image: wurstmeister/kafka:2.11-1.1.1 54 | ports: 55 | - '9096:9092' 56 | links: 57 | - zookeeper 58 | environment: 59 | KAFKA_ADVERTISED_HOST_NAME: ${HOST_IP} 60 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 61 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' 62 | KAFKA_DELETE_TOPIC_ENABLE: 'true' 63 | KAFKA_CREATE_TOPICS: 'topic-test:1:1' 64 | volumes: 65 | - /var/run/docker.sock:/var/run/docker.sock 66 | 67 | kafka_exporter: 68 | image: danielqsj/kafka-exporter 69 | ports: 70 | - '9308:9308' 71 | depends_on: 72 | - kafka 73 | - kafka2 74 | - kafka3 75 | - kafka4 76 | restart: always 77 | 78 | prometheus: 79 | image: prom/prometheus 80 | ports: 81 | - 9090:9090 82 | volumes: 83 | - $PWD/Utils/scripts/prometheus.yml:/etc/prometheus/prometheus.yml 84 | # - ./data/prometheus/data:/prometheus 85 | depends_on: 86 | - kafka_exporter 87 | # - alertmanager 88 | # - cadvisor 89 | - node-exporter 90 | restart: always 91 | 92 | node-exporter: 93 | image: prom/node-exporter:latest 94 | container_name: monitoring_node_exporter 95 | restart: always 96 | expose: 97 | - 9100 98 | 99 | grafana: 100 | image: grafana/grafana 101 | ports: 102 | - '3000:3000' 103 | volumes: 104 | - $PWD/plugin:/var/lib/grafana/plugins 105 | - $PWD/provisioning/:/etc/grafana/provisioning/ 106 | # - ./data/grafana:/var/lib/grafana 107 | depends_on: 108 | - prometheus 109 | environment: 110 | GF_SECURITY_ALLOW_EMBEDDING: 'true' 111 | GF_INSTALL_PLUGINS: natel-discrete-panel 112 | GF_PATHS_PLUGINS: /var/lib/grafana/plugins 113 | GF_AUTH_ANONYMOUS_ENABLED: 'true' 114 | 115 | # alertmanager: 116 | # image: prom/alertmanager 117 | # ports: 118 | # - 9093:9093 119 | # depends_on: 120 | # - kafka_exporter 121 | 122 | # cadvisor: 123 | # image: google/cadvisor:latest 124 | # container_name: monitoring_cadvisor 125 | # restart: always 126 | # volumes: 127 | # - /:/rootfs:ro 128 | # - /var/run:/var/run:rw 129 | # - /sys:/sys:ro 130 | # - /var/lib/docker/:/var/lib/docker:ro 131 | # expose: 132 | # - 8085 -------------------------------------------------------------------------------- /kafka-playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kafkare-playground", 3 | "version": "1.0.0", 4 | "description": "Production Project", 5 | "main": "index.js", 6 | "scripts": { 7 | "consumer": "node streaming_data/consumer-jd.js", 8 | "producer": "node streaming_data/index.js", 9 | "topic": "node streaming_data/topic.js", 10 | "consumerDir": "nodemon src/consumer2.js", 11 | "producerDir": "node src/producer2.js", 12 | "topicDir": "nodemon src/topic2.js", 13 | "test": "echo \"Error: no test specified\" && exit 1", 14 | "testbed": "nodemon data-generator/server.js", 15 | "build-testbed": "webpack data-generator/main.ts -o data-generator/main.js", 16 | "data-generator-dev": "cross-env NODE_ENV=development concurrently \"webpack-dev-server --open\" \"node kafka-playground/data-generator/main.ts\"" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/lijiaxingogo/KafKare.git" 21 | }, 22 | "author": "", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/lijiaxingogo/KafKare/issues" 26 | }, 27 | "homepage": "https://github.com/lijiaxingogo/KafKare#readme", 28 | "dependencies": { 29 | "@types/socket.io-client": "^1.4.34", 30 | "concurrently": "^5.3.0", 31 | "cross-env": "^7.0.3", 32 | "express": "^4.17.1", 33 | "ip": "^1.1.5", 34 | "kafkajs": "^1.15.0", 35 | "nodemon": "^2.0.6", 36 | "socket.io": "^3.0.4", 37 | "socket.io-client": "^3.0.4", 38 | "ts-loader": "^8.0.1", 39 | "twitter": "^1.7.1", 40 | "typescript": "^4.1.3", 41 | "webpack": "^5.6.0", 42 | "webpack-cli": "^3.2.3", 43 | "webpack-dev-server": "^3.11.0" 44 | }, 45 | "devDependencies": { 46 | "@babel/core": "^7.11.0", 47 | "@babel/preset-env": "^7.10.4", 48 | "@babel/preset-typescript": "^7.10.4", 49 | "babel-jest": "^26.2.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | parameters: 4 | ssh-fingerprint: 5 | type: string 6 | default: ${GITHUB_SSH_FINGERPRINT} 7 | 8 | aliases: 9 | # Workflow filters 10 | - &filter-only-master 11 | branches: 12 | only: master 13 | - &filter-only-release 14 | branches: 15 | only: /^v[1-9]*[0-9]+\.[1-9]*[0-9]+\.x$/ 16 | 17 | workflows: 18 | plugin_workflow: 19 | jobs: 20 | - build 21 | 22 | executors: 23 | default_exec: # declares a reusable executor 24 | docker: 25 | - image: srclosson/grafana-plugin-ci-alpine:latest 26 | e2e_exec: 27 | docker: 28 | - image: srclosson/grafana-plugin-ci-e2e:latest 29 | 30 | jobs: 31 | build: 32 | executor: default_exec 33 | steps: 34 | - checkout 35 | - restore_cache: 36 | name: restore node_modules 37 | keys: 38 | - build-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} 39 | - run: 40 | name: Install dependencies 41 | command: | 42 | mkdir ci 43 | [ -f ~/project/node_modules/.bin/grafana-toolkit ] || yarn install --frozen-lockfile 44 | - save_cache: 45 | name: save node_modules 46 | paths: 47 | - ~/project/node_modules 48 | key: build-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} 49 | - run: 50 | name: Build and test frontend 51 | command: ./node_modules/.bin/grafana-toolkit plugin:ci-build 52 | - run: 53 | name: Move results to ci folder 54 | command: ./node_modules/.bin/grafana-toolkit plugin:ci-build --finish 55 | - run: 56 | name: Package distribution 57 | command: | 58 | ./node_modules/.bin/grafana-toolkit plugin:ci-package 59 | - persist_to_workspace: 60 | root: . 61 | paths: 62 | - ci/jobs/package 63 | - ci/packages 64 | - ci/dist 65 | - ci/grafana-test-env 66 | - store_artifacts: 67 | path: ci 68 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | rpt2_cache/ 3 | dist/ 4 | ci/ 5 | coverage/ 6 | 7 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json"), 3 | }; 4 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Natel Energy 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 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/README.md: -------------------------------------------------------------------------------- 1 | ## Discrete Panel 2 | 3 | This panel shows discrete values in a horizontal graph. This lets show state transitions clearly. It is a good 4 | choice to display string or boolean data 5 | 6 | 7 | ### Screenshots 8 | 9 | ![example](https://raw.githubusercontent.com/NatelEnergy/grafana-discrete-panel/master/src/img/screenshot-multiple.png) 10 | ![example](https://raw.githubusercontent.com/NatelEnergy/grafana-discrete-panel/master/src/img/screenshot-single-1.png) 11 | ![example](https://raw.githubusercontent.com/NatelEnergy/grafana-discrete-panel/master/src/img/screenshot-single-2.png) 12 | ![example](https://raw.githubusercontent.com/NatelEnergy/grafana-discrete-panel/master/src/img/screenshot-single-3.png) 13 | ![example](https://raw.githubusercontent.com/NatelEnergy/grafana-discrete-panel/master/src/img/screenshot-single-4.png) 14 | ![options](https://raw.githubusercontent.com/NatelEnergy/grafana-discrete-panel/master/src/img/screenshot-options-1.png) 15 | ![options](https://raw.githubusercontent.com/NatelEnergy/grafana-discrete-panel/master/src/img/screenshot-options-2.png) 16 | 17 | ### Building 18 | 19 | To complie, run: 20 | 21 | ``` 22 | yarn install 23 | yarn build 24 | ``` 25 | 26 | ### Releasing 27 | 28 | This plugin uses [release-it](https://github.com/webpro/release-it) to release to GitHub. 29 | 30 | ``` 31 | env GITHUB_TOKEN=your_token yarn release-it patch 32 | ``` 33 | 34 | ### Roadmap 35 | 36 | - TODO: full annotation support 37 | - TODO: better documentation 38 | - release v1.0 39 | 40 | #### Changelog 41 | 42 | 43 | ##### v0.1.0 44 | 45 | - works with Grafana 7 (naming fixed) 46 | - Building with `@grafana/toolkit` 47 | - Supports DataFrame directly for 6.4+ 48 | 49 | 50 | ##### v0.0.9 51 | 52 | - Remove `dist` from master 53 | - Use webpack build 54 | - FIX: Use background color to clear the background 55 | - Configurable duration resolution option (thanks @clink-aaron) 56 | - deploy using release-it 57 | - Don't hide series names on hover 58 | 59 | ##### v0.0.8 60 | 61 | - Support Snapshots (thanks @londonanthonyoleary) 62 | - Direct link rendered image now works. 63 | - Support UTC date display 64 | - Fix display issue with 5.1 65 | - Merge distinct values in legend unless showing the name 66 | - Basic Annotation Support 67 | - Fix mapping numeric data to text 68 | 69 | ##### v0.0.7 70 | 71 | - Switch to typescript 72 | - Override applyPanelTimeOverrides rather than issueQueries to extend time 73 | - Support numeric unit conversion 74 | - New rendering pipeline (thanks @jonyrock) 75 | - Don't detect duplicate colors from metrics 76 | - Formatting with prettier.js 77 | - Only hide hover text when it collides 78 | - Show time axis (copied from novatec-grafana-discrete-panel) 79 | - Improved text collision behavior 80 | 81 | ##### v0.0.6 82 | 83 | - Fix for grafana 4.5 (thanks @alin-amana) 84 | 85 | ##### v0.0.5 86 | 87 | - Support results from the table format 88 | - Support results in ascending or decending order 89 | - Configure legend percentage decimal points 90 | - Legend can show transition count and distinct value count 91 | - Clamp percentage stats within the query time window 92 | - Changed the grafana dependency version to 4.x.x, since 3.x.x was not really supported 93 | - Fixed issues with tooltip hover position 94 | - Option to expand 'from' query so the inital state can avoid 'null' 95 | 96 | ##### v0.0.4 97 | 98 | - Support shared tooltips (not just crosshair) 99 | 100 | ##### v0.0.3 101 | 102 | - Configure more colors (retzkek) 103 | - Fix tooltips (retzkek) 104 | - Configure Text Size 105 | - Support shared crosshair 106 | 107 | ##### v0.0.2 108 | 109 | - Use the panel time shift. 110 | 111 | ##### v0.0.1 112 | 113 | - First working version 114 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/RELEASE.md: -------------------------------------------------------------------------------- 1 | filling in the history: 2 | 3 | ``` 4 | git tag -a v0.0.8 5e8e975c881e443d72ac17830aca986c86ca95a2 -m "Release v0.0.8"; 5 | git tag -a v0.0.7 e9b0f671547beadc5870856d4f2a4b5082df0d4b -m "Release v0.0.7"; 6 | git tag -a v0.0.6 38fbda226a2431e6ef44e2b1a6c5759f3fbf4268 -m "Release v0.0.6"; 7 | git tag -a v0.0.5 10d9ec42ecee32d9b4a35385be5c89410176ea8d -m "Release v0.0.5"; 8 | git tag -a v0.0.4 39a4eb44ab7f8d082478050058ead371a12aa5e2 -m "Release v0.0.4"; 9 | git tag -a v0.0.3 91be5c8cc445cbb1c6d113f27712c9ada6081de2 -m "Release v0.0.3"; 10 | git tag -a v0.0.2 6d963171005734b0392525854576bd21c5ca1661 -m "Release v0.0.2"; 11 | git tag -a v0.0.1 d729bd3b0cc71e4de1aab934065c72931fd4487e -m "Release v0.0.1"; 12 | git push --tags 13 | ``` 14 | 15 | Remove all old releases: 16 | 17 | ``` 18 | #Delete local tags. 19 | git tag -d $(git tag -l) 20 | #Fetch remote tags. 21 | git fetch 22 | #Delete remote tags. 23 | git push origin --delete $(git tag -l) # Pushing once should be faster than multiple times 24 | #Delete local tags. 25 | git tag -d $(git tag -l) 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/jest.config.js: -------------------------------------------------------------------------------- 1 | // This file is needed because it is used by vscode and other tools that 2 | // call `jest` directly. However, unless you are doing anything special 3 | // do not edit this file 4 | 5 | const standard = require('@grafana/toolkit/src/config/jest.plugin.config'); 6 | 7 | // This process will use the same config that `yarn test` is using 8 | module.exports = standard.jestConfig(); 9 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "natel-discrete-panel", 3 | "version": "0.1.0", 4 | "description": "Discrete Events in Grafana", 5 | "scripts": { 6 | "build": "grafana-toolkit plugin:build", 7 | "test": "grafana-toolkit plugin:test", 8 | "dev": "grafana-toolkit plugin:dev", 9 | "watch": "grafana-toolkit plugin:dev --watch" 10 | }, 11 | "author": "ryantxu", 12 | "license": "MIT", 13 | "keywords": [ 14 | "discrete", 15 | "canvas", 16 | "grafana", 17 | "plugin", 18 | "panel" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/NatelEnergy/grafana-discrete-panel.git" 23 | }, 24 | "devDependencies": { 25 | "@grafana/toolkit": "latest", 26 | "@grafana/data": "latest", 27 | "@grafana/ui": "latest", 28 | "@types/grafana": "github:CorpGlory/types-grafana.git", 29 | "@types/lodash": "4.14.149", 30 | "jquery": "^3.2.1", 31 | "lodash": "^4.17.10", 32 | "moment": "^2.22.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/canvas-metric.ts: -------------------------------------------------------------------------------- 1 | import { MetricsPanelCtrl } from 'grafana/app/plugins/sdk'; 2 | 3 | /* eslint-disable id-blacklist, no-restricted-imports, @typescript-eslint/ban-types */ 4 | import moment from 'moment'; 5 | 6 | import $ from 'jquery'; 7 | 8 | import appEvents from 'grafana/app/core/app_events'; 9 | 10 | // Expects a template with: 11 | //
12 | export class CanvasPanelCtrl extends MetricsPanelCtrl { 13 | data: any; 14 | mouse: any; 15 | $tooltip: any; 16 | wrap: any; 17 | canvas: any; 18 | context: any; 19 | _devicePixelRatio: number; 20 | 21 | /** @ngInject */ 22 | constructor($scope, $injector) { 23 | super($scope, $injector); 24 | 25 | this.data = null; 26 | this.mouse = { 27 | position: null, 28 | down: null, 29 | }; 30 | this.$tooltip = $('
'); 31 | 32 | this.events.on('panel-initialized', this.onPanelInitialized.bind(this)); 33 | this.events.on('refresh', this.onRefresh.bind(this)); 34 | this.events.on('render', this.onRender.bind(this)); 35 | 36 | this._devicePixelRatio = 1; 37 | if (window.devicePixelRatio !== undefined) { 38 | this._devicePixelRatio = window.devicePixelRatio; 39 | } 40 | } 41 | 42 | onPanelInitialized() { 43 | //console.log("onPanelInitalized()"); 44 | this.render(); 45 | } 46 | 47 | onRefresh() { 48 | //console.log("onRefresh()"); 49 | this.render(); 50 | } 51 | 52 | // Typically you will override this 53 | onRender() { 54 | if (!this.context) { 55 | console.log('No context!'); 56 | return; 57 | } 58 | console.log('canvas render', this.mouse); 59 | 60 | const rect = this.wrap.getBoundingClientRect(); 61 | 62 | const height = Math.max(this.height, 100); 63 | const width = rect.width; 64 | this.canvas.width = width; 65 | this.canvas.height = height; 66 | 67 | const centerV = height / 2; 68 | 69 | const ctx = this.context; 70 | ctx.lineWidth = 1; 71 | ctx.textBaseline = 'middle'; 72 | 73 | let time = ''; 74 | if (this.mouse.position != null) { 75 | time = this.dashboard.formatDate(moment(this.mouse.position.ts)); 76 | } 77 | 78 | ctx.fillStyle = '#999999'; 79 | ctx.fillRect(0, 0, width, height); 80 | ctx.fillStyle = '#111111'; 81 | ctx.font = '24px "Open Sans", Helvetica, Arial, sans-serif'; 82 | ctx.textAlign = 'left'; 83 | ctx.fillText('Mouse @ ' + time, 10, centerV); 84 | 85 | if (this.mouse.position != null) { 86 | if (this.mouse.down != null) { 87 | const xmin = Math.min(this.mouse.position.x, this.mouse.down.x); 88 | const xmax = Math.max(this.mouse.position.x, this.mouse.down.x); 89 | 90 | // Fill canvas using 'destination-out' and alpha at 0.05 91 | ctx.globalCompositeOperation = 'destination-out'; 92 | ctx.fillStyle = 'rgba(255, 255, 255, 0.6)'; 93 | ctx.beginPath(); 94 | ctx.fillRect(0, 0, xmin, height); 95 | ctx.fill(); 96 | 97 | ctx.beginPath(); 98 | ctx.fillRect(xmax, 0, width, height); 99 | ctx.fill(); 100 | ctx.globalCompositeOperation = 'source-over'; 101 | } else { 102 | ctx.strokeStyle = '#111'; 103 | ctx.beginPath(); 104 | ctx.moveTo(this.mouse.position.x, 0); 105 | ctx.lineTo(this.mouse.position.x, height); 106 | ctx.lineWidth = 3; 107 | ctx.stroke(); 108 | 109 | ctx.beginPath(); 110 | ctx.moveTo(this.mouse.position.x, 0); 111 | ctx.lineTo(this.mouse.position.x, height); 112 | ctx.strokeStyle = '#e22c14'; 113 | ctx.lineWidth = 2; 114 | ctx.stroke(); 115 | } 116 | } 117 | } 118 | 119 | clearTT() { 120 | this.$tooltip.detach(); 121 | } 122 | 123 | getMousePosition(evt) { 124 | const elapsed = this.range.to - this.range.from; 125 | const rect = this.canvas.getBoundingClientRect(); 126 | const x = evt.offsetX; // - rect.left; 127 | const ts = this.range.from + elapsed * (x / parseFloat(rect.width)); 128 | const y = evt.clientY - rect.top; 129 | 130 | return { 131 | x: x, 132 | y: y, 133 | yRel: y / parseFloat(rect.height), 134 | ts: ts, 135 | evt: evt, 136 | }; 137 | } 138 | 139 | onGraphHover(evt, showTT, isExternal) { 140 | console.log('HOVER', evt, showTT, isExternal); 141 | } 142 | 143 | onMouseClicked(where, event) { 144 | console.log('CANVAS CLICKED', where, event); 145 | this.render(); 146 | } 147 | 148 | onMouseSelectedRange(range, event) { 149 | console.log('CANVAS Range', range, event); 150 | } 151 | 152 | link(scope, elem, attrs, ctrl) { 153 | this.wrap = elem.find('.canvas-spot')[0]; 154 | this.canvas = document.createElement('canvas'); 155 | this.wrap.appendChild(this.canvas); 156 | 157 | $(this.canvas).css('cursor', 'pointer'); 158 | $(this.wrap).css('width', '100%'); 159 | 160 | // console.log( 'link', this ); 161 | 162 | this.context = this.canvas.getContext('2d'); 163 | this.canvas.addEventListener( 164 | 'mousemove', 165 | evt => { 166 | if (!this.range) { 167 | return; // skip events before we have loaded 168 | } 169 | 170 | this.mouse.position = this.getMousePosition(evt); 171 | const info = { 172 | pos: { 173 | pageX: evt.pageX, 174 | pageY: evt.pageY, 175 | x: this.mouse.position.ts, 176 | y: this.mouse.position.y, 177 | panelRelY: this.mouse.position.yRel, 178 | panelRelX: this.mouse.position.xRel, 179 | }, 180 | evt: evt, 181 | panel: this.panel, 182 | }; 183 | appEvents.emit('graph-hover', info); 184 | if (this.mouse.down != null) { 185 | $(this.canvas).css('cursor', 'col-resize'); 186 | } 187 | }, 188 | false 189 | ); 190 | 191 | this.canvas.addEventListener( 192 | 'mouseout', 193 | evt => { 194 | if (this.mouse.down == null) { 195 | this.mouse.position = null; 196 | this.onRender(); 197 | this.$tooltip.detach(); 198 | appEvents.emit('graph-hover-clear'); 199 | } 200 | }, 201 | false 202 | ); 203 | 204 | this.canvas.addEventListener( 205 | 'mousedown', 206 | evt => { 207 | this.mouse.down = this.getMousePosition(evt); 208 | }, 209 | false 210 | ); 211 | 212 | this.canvas.addEventListener( 213 | 'mouseenter', 214 | evt => { 215 | if (this.mouse.down && !evt.buttons) { 216 | this.mouse.position = null; 217 | this.mouse.down = null; 218 | this.onRender(); 219 | this.$tooltip.detach(); 220 | appEvents.emit('graph-hover-clear'); 221 | } 222 | $(this.canvas).css('cursor', 'pointer'); 223 | }, 224 | false 225 | ); 226 | 227 | this.canvas.addEventListener( 228 | 'mouseup', 229 | evt => { 230 | this.$tooltip.detach(); 231 | const up = this.getMousePosition(evt); 232 | if (this.mouse.down != null) { 233 | if (up.x === this.mouse.down.x && up.y === this.mouse.down.y) { 234 | this.mouse.position = null; 235 | this.mouse.down = null; 236 | this.onMouseClicked(up, evt); 237 | } else { 238 | const min = Math.min(this.mouse.down.ts, up.ts); 239 | const max = Math.max(this.mouse.down.ts, up.ts); 240 | const range = { from: moment.utc(min), to: moment.utc(max) }; 241 | this.mouse.position = up; 242 | this.onMouseSelectedRange(range, evt); 243 | } 244 | } 245 | this.mouse.down = null; 246 | this.mouse.position = null; 247 | }, 248 | false 249 | ); 250 | 251 | this.canvas.addEventListener( 252 | 'dblclick', 253 | evt => { 254 | this.mouse.position = null; 255 | this.mouse.down = null; 256 | this.onRender(); 257 | this.$tooltip.detach(); 258 | appEvents.emit('graph-hover-clear'); 259 | 260 | console.log('TODO, ZOOM OUT'); 261 | }, 262 | true 263 | ); 264 | 265 | // global events 266 | appEvents.on( 267 | 'graph-hover', 268 | event => { 269 | // ignore other graph hover events if shared tooltip is disabled 270 | const isThis = event.panel.id === this.panel.id; 271 | if (!this.dashboard.sharedTooltipModeEnabled() && !isThis) { 272 | return; 273 | } 274 | 275 | // ignore if other panels are fullscreen 276 | if (this.otherPanelInFullscreenMode()) { 277 | return; 278 | } 279 | 280 | // Calculate the mouse position when it came from somewhere else 281 | if (!isThis) { 282 | if (!event.pos.x || !this.range) { 283 | // NOTE, this happens when a panel has no data 284 | // console.log('Invalid hover point', event); 285 | return; 286 | } 287 | 288 | const ts = event.pos.x; 289 | const rect = this.canvas.getBoundingClientRect(); 290 | const elapsed = this.range.to - this.range.from; 291 | const x = ((ts - this.range.from) / elapsed) * rect.width; 292 | 293 | this.mouse.position = { 294 | x: x, 295 | y: event.pos.panelRelY * rect.height, 296 | yRel: event.pos.panelRelY, 297 | ts: ts, 298 | gevt: event, 299 | }; 300 | //console.log( "Calculate mouseInfo", event, this.mouse.position); 301 | } 302 | 303 | this.onGraphHover(event, isThis || !this.dashboard.sharedCrosshairModeOnly(), !isThis); 304 | }, 305 | scope 306 | ); 307 | 308 | appEvents.on( 309 | 'graph-hover-clear', 310 | (event, info) => { 311 | this.mouse.position = null; 312 | this.mouse.down = null; 313 | this.render(); 314 | this.$tooltip.detach(); 315 | }, 316 | scope 317 | ); 318 | 319 | // scope.$on('$destroy', () => { 320 | // this.$tooltip.destroy(); 321 | // elem.off(); 322 | // elem.remove(); 323 | // }); 324 | } 325 | 326 | // Utility Functions for time axis 327 | //--------------------------------- 328 | 329 | time_format(range: number, secPerTick: number): string { 330 | const oneDay = 86400000; 331 | const oneYear = 31536000000; 332 | 333 | if (secPerTick <= 45) { 334 | return '%H:%M:%S'; 335 | } 336 | if (secPerTick <= 7200 || range <= oneDay) { 337 | return '%H:%M'; 338 | } 339 | if (secPerTick <= 80000) { 340 | return '%m/%d %H:%M'; 341 | } 342 | if (secPerTick <= 2419200 || range <= oneYear) { 343 | return '%m/%d'; 344 | } 345 | return '%Y-%m'; 346 | } 347 | 348 | getTimeResolution(estTimeInterval: number): number { 349 | const timeIntInSecs = estTimeInterval / 1000; 350 | 351 | if (timeIntInSecs <= 30) { 352 | return 30 * 1000; 353 | } 354 | 355 | if (timeIntInSecs <= 60) { 356 | return 60 * 1000; 357 | } 358 | 359 | if (timeIntInSecs <= 60 * 5) { 360 | return 5 * 60 * 1000; 361 | } 362 | 363 | if (timeIntInSecs <= 60 * 10) { 364 | return 10 * 60 * 1000; 365 | } 366 | 367 | if (timeIntInSecs <= 60 * 30) { 368 | return 30 * 60 * 1000; 369 | } 370 | 371 | if (timeIntInSecs <= 60 * 60) { 372 | return 60 * 60 * 1000; 373 | } 374 | 375 | if (timeIntInSecs <= 60 * 60) { 376 | return 60 * 60 * 1000; 377 | } 378 | 379 | if (timeIntInSecs <= 2 * 60 * 60) { 380 | return 2 * 60 * 60 * 1000; 381 | } 382 | 383 | if (timeIntInSecs <= 6 * 60 * 60) { 384 | return 6 * 60 * 60 * 1000; 385 | } 386 | 387 | if (timeIntInSecs <= 12 * 60 * 60) { 388 | return 12 * 60 * 60 * 1000; 389 | } 390 | 391 | if (timeIntInSecs <= 24 * 60 * 60) { 392 | return 24 * 60 * 60 * 1000; 393 | } 394 | 395 | if (timeIntInSecs <= 2 * 24 * 60 * 60) { 396 | return 2 * 24 * 60 * 60 * 1000; 397 | } 398 | 399 | if (timeIntInSecs <= 7 * 24 * 60 * 60) { 400 | return 7 * 24 * 60 * 60 * 1000; 401 | } 402 | 403 | if (timeIntInSecs <= 30 * 24 * 60 * 60) { 404 | return 30 * 24 * 60 * 60 * 1000; 405 | } 406 | 407 | return 6 * 30 * 24 * 60 * 60 * 1000; 408 | } 409 | 410 | roundDate(timeStamp, roundee) { 411 | timeStamp -= timeStamp % roundee; //subtract amount of time since midnight 412 | return timeStamp; 413 | } 414 | 415 | formatDate(d, fmt) { 416 | const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 417 | const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 418 | if (typeof d.strftime === 'function') { 419 | return d.strftime(fmt); 420 | } 421 | 422 | const r: string[] = []; 423 | let escape = false; 424 | const hours = d.getHours(); 425 | const isAM = hours < 12; 426 | let hours12; 427 | 428 | if (hours > 12) { 429 | hours12 = hours - 12; 430 | } else if (hours === 0) { 431 | hours12 = 12; 432 | } else { 433 | hours12 = hours; 434 | } 435 | 436 | for (let i = 0; i < fmt.length; ++i) { 437 | let c = fmt.charAt(i); 438 | 439 | if (escape) { 440 | switch (c) { 441 | case 'a': 442 | c = '' + dayNames[d.getDay()]; 443 | break; 444 | case 'b': 445 | c = '' + monthNames[d.getMonth()]; 446 | break; 447 | case 'd': 448 | c = this.leftPad(d.getDate(), ''); 449 | break; 450 | case 'e': 451 | c = this.leftPad(d.getDate(), ' '); 452 | break; 453 | case 'h': // For back-compat with 0.7; remove in 1.0 454 | case 'H': 455 | c = this.leftPad(hours, null); 456 | break; 457 | case 'I': 458 | c = this.leftPad(hours12, null); 459 | break; 460 | case 'l': 461 | c = this.leftPad(hours12, ' '); 462 | break; 463 | case 'm': 464 | c = this.leftPad(d.getMonth() + 1, ''); 465 | break; 466 | case 'M': 467 | c = this.leftPad(d.getMinutes(), null); 468 | break; 469 | // quarters not in Open Group's strftime specification 470 | case 'q': 471 | c = '' + (Math.floor(d.getMonth() / 3) + 1); 472 | break; 473 | case 'S': 474 | c = this.leftPad(d.getSeconds(), null); 475 | break; 476 | case 'y': 477 | c = this.leftPad(d.getFullYear() % 100, null); 478 | break; 479 | case 'Y': 480 | c = '' + d.getFullYear(); 481 | break; 482 | case 'p': 483 | c = isAM ? '' + 'am' : '' + 'pm'; 484 | break; 485 | case 'P': 486 | c = isAM ? '' + 'AM' : '' + 'PM'; 487 | break; 488 | case 'w': 489 | c = '' + d.getDay(); 490 | break; 491 | } 492 | r.push(c); 493 | escape = false; 494 | } else { 495 | if (c === '%') { 496 | escape = true; 497 | } else { 498 | r.push(c); 499 | } 500 | } 501 | } 502 | 503 | return r.join(''); 504 | } 505 | 506 | leftPad(n, pad) { 507 | n = '' + n; 508 | pad = '' + (pad == null ? '0' : pad); 509 | return n.length === 1 ? pad + n : n; 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/distinct-points.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export class PointInfo { 4 | val: string; 5 | start: number; // timestamp 6 | ms = 0; // elapsed time 7 | 8 | constructor(val: string, start: number) { 9 | this.val = val; 10 | this.start = start; 11 | } 12 | } 13 | 14 | export class LegendValue { 15 | val: string; 16 | ms = 0; // elapsed time 17 | count = 0; 18 | per = 0; 19 | 20 | constructor(val: string) { 21 | this.val = val; 22 | } 23 | } 24 | 25 | export class DistinctPoints { 26 | changes: PointInfo[] = []; 27 | legendInfo: LegendValue[] = []; 28 | last: PointInfo | null = null; 29 | asc = false; 30 | transitionCount = 0; 31 | distinctValuesCount = 0; 32 | elapsed = 0; 33 | 34 | constructor(public name) {} 35 | 36 | // ts numeric ms, 37 | // val is the normalized value 38 | add(ts: number, val: string) { 39 | if (this.last == null) { 40 | this.last = { 41 | val: val, 42 | start: ts, 43 | ms: 0, 44 | }; 45 | this.changes.push(this.last); 46 | } else if (ts === this.last.start) { 47 | console.log('skip point with duplicate timestamp', ts, val); 48 | return; 49 | } else { 50 | if (this.changes.length === 1) { 51 | this.asc = ts > this.last.start; 52 | } 53 | 54 | if (ts > this.last.start !== this.asc) { 55 | console.log('skip out of order point', ts, val); 56 | return; 57 | } 58 | 59 | // Same value 60 | if (val === this.last.val) { 61 | if (!this.asc) { 62 | this.last.start = ts; 63 | } 64 | } else { 65 | this.last = { 66 | val: val, 67 | start: ts, 68 | ms: 0, 69 | }; 70 | this.changes.push(this.last); 71 | } 72 | } 73 | } 74 | 75 | finish(ctrl) { 76 | if (this.changes.length < 1) { 77 | console.log('no points found!'); 78 | return; 79 | } 80 | 81 | if (!this.asc) { 82 | this.last = this.changes[0]; 83 | _.reverse(this.changes); 84 | } 85 | 86 | if (!this.last) { 87 | return; 88 | } 89 | 90 | // Add a point beyond the controls 91 | if (this.last.start < ctrl.range.to) { 92 | const until = ctrl.range.to + 1; 93 | // let now = Date.now(); 94 | // if(this.last.start < now && ctrl.range.to > now) { 95 | // until = now; 96 | // } 97 | 98 | // This won't be shown, but will keep the count consistent 99 | this.changes.push({ 100 | val: this.last.val, 101 | start: until, 102 | ms: 0, 103 | }); 104 | } 105 | 106 | this.transitionCount = 0; 107 | const distinct = new Map(); 108 | let last: PointInfo = this.changes[0]; 109 | for (let i = 1; i < this.changes.length; i++) { 110 | const pt = this.changes[i]; 111 | 112 | let s = last.start; 113 | let e = pt.start; 114 | if (s < ctrl.range.from) { 115 | s = ctrl.range.from; 116 | } else if (s < ctrl.range.to) { 117 | this.transitionCount++; 118 | } 119 | 120 | if (e > ctrl.range.to) { 121 | e = ctrl.range.to; 122 | } 123 | 124 | last.ms = e - s; 125 | if (last.ms > 0) { 126 | if (distinct.has(last.val)) { 127 | const v = distinct.get(last.val)!; 128 | v.ms += last.ms; 129 | v.count++; 130 | } else { 131 | distinct.set(last.val, { val: last.val, ms: last.ms, count: 1, per: 0 }); 132 | } 133 | } 134 | last = pt; 135 | } 136 | 137 | const elapsed = ctrl.range.to - ctrl.range.from; 138 | this.elapsed = elapsed; 139 | 140 | distinct.forEach((value: LegendValue, key: any) => { 141 | value.per = value.ms / elapsed; 142 | this.legendInfo.push(value); 143 | }); 144 | this.distinctValuesCount = _.size(this.legendInfo); 145 | 146 | if (!ctrl.isTimeline) { 147 | this.legendInfo = _.orderBy(this.legendInfo, ['ms'], ['desc']); 148 | } 149 | } 150 | 151 | static combineLegend(data: DistinctPoints[], ctrl: any): DistinctPoints { 152 | if (data.length === 1) { 153 | return data[0]; 154 | } 155 | 156 | const merged: DistinctPoints = new DistinctPoints('merged'); 157 | let elapsed = 0; 158 | const distinct = new Map(); 159 | _.forEach(data, (m: DistinctPoints) => { 160 | merged.transitionCount += m.transitionCount; 161 | elapsed += m.elapsed; 162 | 163 | _.forEach(m.legendInfo, (leg: LegendValue) => { 164 | if (distinct.has(leg.val)) { 165 | const v = distinct.get(leg.val)!; 166 | v.ms += leg.ms; 167 | v.count += leg.count; 168 | // per gets recalculated at the end 169 | } else { 170 | distinct.set(leg.val, { val: leg.val, ms: leg.ms, count: leg.count, per: 0 }); 171 | } 172 | }); 173 | }); 174 | 175 | merged.elapsed = elapsed; 176 | distinct.forEach((value: LegendValue, key: any) => { 177 | value.per = value.ms / elapsed; 178 | merged.legendInfo.push(value); 179 | }); 180 | merged.distinctValuesCount = _.size(merged.legendInfo); 181 | return merged; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/img/discrete_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-multiple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-multiple.png -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-options-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-options-1.png -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-options-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-options-2.png -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-single-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-single-1.png -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-single-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-single-2.png -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-single-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-single-3.png -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-single-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/kafka-playground/plugin/natel-discrete-panel/src/img/screenshot-single-4.png -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/module.test.ts: -------------------------------------------------------------------------------- 1 | describe('placeholder test', () => { 2 | it('should return true', () => { 3 | expect(true).toBeTruthy(); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/module.ts: -------------------------------------------------------------------------------- 1 | import { CanvasPanelCtrl } from './canvas-metric'; 2 | import { DistinctPoints, LegendValue } from './distinct-points'; 3 | import { isArray } from 'lodash'; 4 | 5 | import { 6 | DataQueryResponseData, 7 | LegacyResponseData, 8 | DataFrame, 9 | guessFieldTypes, 10 | toDataFrame, 11 | getTimeField, 12 | getFieldDisplayName, 13 | Field, 14 | ArrayVector, 15 | FieldType, 16 | } from '@grafana/data'; 17 | 18 | import _ from 'lodash'; 19 | import $ from 'jquery'; 20 | import kbn from 'grafana/app/core/utils/kbn'; 21 | 22 | import appEvents from 'grafana/app/core/app_events'; 23 | 24 | /* eslint-disable id-blacklist, no-restricted-imports, @typescript-eslint/ban-types */ 25 | import moment from 'moment'; 26 | 27 | const grafanaColors = [ 28 | '#7EB26D', 29 | '#EAB839', 30 | '#6ED0E0', 31 | '#EF843C', 32 | '#E24D42', 33 | '#1F78C1', 34 | '#BA43A9', 35 | '#705DA0', 36 | '#508642', 37 | '#CCA300', 38 | '#447EBC', 39 | '#C15C17', 40 | '#890F02', 41 | '#0A437C', 42 | '#6D1F62', 43 | '#584477', 44 | '#B7DBAB', 45 | '#F4D598', 46 | '#70DBED', 47 | '#F9BA8F', 48 | '#F29191', 49 | '#82B5D8', 50 | '#E5A8E2', 51 | '#AEA2E0', 52 | '#629E51', 53 | '#E5AC0E', 54 | '#64B0C8', 55 | '#E0752D', 56 | '#BF1B00', 57 | '#0A50A1', 58 | '#962D82', 59 | '#614D93', 60 | '#9AC48A', 61 | '#F2C96D', 62 | '#65C5DB', 63 | '#F9934E', 64 | '#EA6460', 65 | '#5195CE', 66 | '#D683CE', 67 | '#806EB7', 68 | '#3F6833', 69 | '#967302', 70 | '#2F575E', 71 | '#99440A', 72 | '#58140C', 73 | '#052B51', 74 | '#511749', 75 | '#3F2B5B', 76 | '#E0F9D7', 77 | '#FCEACA', 78 | '#CFFAFF', 79 | '#F9E2D2', 80 | '#FCE2DE', 81 | '#BADFF4', 82 | '#F9D9F9', 83 | '#DEDAF7', 84 | ]; // copied from public/app/core/utils/colors.ts because of changes in grafana 4.6.0 85 | //(https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) 86 | 87 | class DiscretePanelCtrl extends CanvasPanelCtrl { 88 | static templateUrl = 'partials/module.html'; 89 | static scrollable = true; 90 | 91 | defaults = { 92 | display: 'timeline', // or 'stacked' 93 | rowHeight: 50, 94 | valueMaps: [{ value: 'null', op: '=', text: 'N/A' }], 95 | rangeMaps: [{ from: 'null', to: 'null', text: 'N/A' }], 96 | colorMaps: [{ text: 'N/A', color: '#CCC' }], 97 | metricNameColor: '#000000', 98 | valueTextColor: '#000000', 99 | timeTextColor: '#d8d9da', 100 | crosshairColor: '#8F070C', 101 | backgroundColor: 'rgba(128,128,128,0.1)', 102 | lineColor: 'rgba(0,0,0,0.1)', 103 | textSize: 24, 104 | textSizeTime: 12, 105 | extendLastValue: true, 106 | writeLastValue: true, 107 | writeAllValues: false, 108 | writeMetricNames: false, 109 | showTimeAxis: true, 110 | showLegend: true, 111 | showLegendNames: true, 112 | showLegendValues: true, 113 | showLegendPercent: true, 114 | highlightOnMouseover: true, 115 | expandFromQueryS: 0, 116 | legendSortBy: '-ms', 117 | units: 'short', 118 | timeOptions: [ 119 | { 120 | name: 'Years', 121 | value: 'years', 122 | }, 123 | { 124 | name: 'Months', 125 | value: 'months', 126 | }, 127 | { 128 | name: 'Weeks', 129 | value: 'weeks', 130 | }, 131 | { 132 | name: 'Days', 133 | value: 'days', 134 | }, 135 | { 136 | name: 'Hours', 137 | value: 'hours', 138 | }, 139 | { 140 | name: 'Minutes', 141 | value: 'minutes', 142 | }, 143 | { 144 | name: 'Seconds', 145 | value: 'seconds', 146 | }, 147 | { 148 | name: 'Milliseconds', 149 | value: 'milliseconds', 150 | }, 151 | ], 152 | timePrecision: { 153 | name: 'Minutes', 154 | value: 'minutes', 155 | }, 156 | useTimePrecision: false, 157 | }; 158 | 159 | annotations: any = []; 160 | data: DistinctPoints[] = []; 161 | legend: DistinctPoints[] = []; 162 | 163 | externalPT = false; 164 | isTimeline = true; 165 | isStacked = false; 166 | hoverPoint: any = null; 167 | colorMap: any = {}; 168 | unitFormats: any = null; // only used for editor 169 | formatter: any = null; 170 | 171 | _colorsPaleteCash: any = null; 172 | _renderDimensions: any = {}; 173 | _selectionMatrix: string[][] = []; 174 | 175 | fieldNamer = (field: Field, frame?: DataFrame, allFrames?: DataFrame[]): string => { 176 | if (field.config.displayName) { 177 | return field.config.displayName; 178 | } 179 | return field.name; 180 | }; 181 | 182 | /** @ngInject */ 183 | constructor($scope, $injector, public annotationsSrv) { 184 | super($scope, $injector); 185 | 186 | // defaults configs 187 | _.defaultsDeep(this.panel, this.defaults); 188 | this.panel.display = 'timeline'; // Only supported version now 189 | 190 | this.events.on('init-edit-mode', this.onInitEditMode.bind(this)); 191 | this.events.on('render', this.onRender.bind(this)); 192 | this.events.on('refresh', this.onRefresh.bind(this)); 193 | 194 | // 6.4+ use DataFrames 195 | (this as any).useDataFrames = true; 196 | this.events.on('data-frames-received', this.onDataFramesReceived.bind(this)); 197 | this.events.on('data-snapshot-load', this.onSnapshotLoad.bind(this)); 198 | this.events.on('data-error', this.onDataError.bind(this)); 199 | // Try to load the 7+ naming strategy 200 | try { 201 | const a = getFieldDisplayName({ name: 'a', config: {}, type: FieldType.number, values: new ArrayVector([]) }); 202 | if (a === 'a') { 203 | this.fieldNamer = getFieldDisplayName; 204 | } 205 | } catch (err) { 206 | console.warn('Using 6x style field names', err); 207 | } 208 | } 209 | 210 | onPanelInitialized() { 211 | this.updateColorInfo(); 212 | this.onConfigChanged(); 213 | } 214 | 215 | onDataError(err) { 216 | this.annotations = []; 217 | console.log('onDataError', err); 218 | } 219 | 220 | onInitEditMode() { 221 | this.unitFormats = kbn.getUnitFormats(); 222 | 223 | this.addEditorTab('Options', 'public/plugins/natel-discrete-panel/partials/editor.options.html', 1); 224 | this.addEditorTab('Legend', 'public/plugins/natel-discrete-panel/partials/editor.legend.html', 3); 225 | this.addEditorTab('Colors', 'public/plugins/natel-discrete-panel/partials/editor.colors.html', 4); 226 | this.addEditorTab('Mappings', 'public/plugins/natel-discrete-panel/partials/editor.mappings.html', 5); 227 | this.editorTabIndex = 1; 228 | this.refresh(); 229 | } 230 | 231 | onRender() { 232 | if (this.data == null || !this.context) { 233 | return; 234 | } 235 | 236 | this._updateRenderDimensions(); 237 | this._updateSelectionMatrix(); 238 | this._updateCanvasSize(); 239 | this._renderRects(); 240 | this._renderTimeAxis(); 241 | this._renderLabels(); 242 | this._renderAnnotations(); 243 | this._renderSelection(); 244 | this._renderCrosshair(); 245 | 246 | this.renderingCompleted(); 247 | } 248 | 249 | showLegandTooltip(pos, info) { 250 | let body = '
' + info.val + '
'; 251 | 252 | body += '
'; 253 | if (info.count > 1) { 254 | body += info.count + ' times
for
'; 255 | } 256 | 257 | body += this.formatDuration(moment.duration(info.ms)); 258 | 259 | if (info.count > 1) { 260 | body += '
total'; 261 | } 262 | body += '
'; 263 | 264 | this.$tooltip.html(body).place_tt(pos.pageX + 20, pos.pageY); 265 | } 266 | 267 | clearTT() { 268 | this.$tooltip.detach(); 269 | } 270 | 271 | formatValue(val): string { 272 | if (_.isNumber(val)) { 273 | if (this.panel.rangeMaps) { 274 | for (let i = 0; i < this.panel.rangeMaps.length; i++) { 275 | const map = this.panel.rangeMaps[i]; 276 | 277 | // value/number to range mapping 278 | const from = parseFloat(map.from); 279 | const to = parseFloat(map.to); 280 | if (to >= val && from <= val) { 281 | return map.text; 282 | } 283 | } 284 | } 285 | 286 | // Convert it to a string first 287 | if (this.formatter) { 288 | val = this.formatter(val, this.panel.decimals); 289 | } 290 | } 291 | 292 | const isNull = _.isNil(val); 293 | if (!isNull && !_.isString(val)) { 294 | val = val.toString(); // convert everything to a string 295 | } 296 | 297 | for (let i = 0; i < this.panel.valueMaps.length; i++) { 298 | const map = this.panel.valueMaps[i]; 299 | // special null case 300 | if (map.value === 'null') { 301 | if (isNull) { 302 | return map.text; 303 | } 304 | continue; 305 | } 306 | 307 | if (val === map.value) { 308 | return map.text; 309 | } 310 | } 311 | 312 | if (isNull) { 313 | return 'null'; 314 | } 315 | return val; 316 | } 317 | 318 | getColor(val) { 319 | if (_.has(this.colorMap, val)) { 320 | return this.colorMap[val]; 321 | } 322 | if (this._colorsPaleteCash[val] === undefined) { 323 | const c = grafanaColors[this._colorsPaleteCash.length % grafanaColors.length]; 324 | this._colorsPaleteCash[val] = c; 325 | this._colorsPaleteCash.length++; 326 | } 327 | return this._colorsPaleteCash[val]; 328 | } 329 | 330 | randomColor() { 331 | const letters = 'ABCDE'.split(''); 332 | let color = '#'; 333 | for (let i = 0; i < 3; i++) { 334 | color += letters[Math.floor(Math.random() * letters.length)]; 335 | } 336 | return color; 337 | } 338 | 339 | // Override the 340 | applyPanelTimeOverrides() { 341 | super.applyPanelTimeOverrides(); 342 | 343 | if (this.panel.expandFromQueryS && this.panel.expandFromQueryS > 0) { 344 | const from = this.range.from.subtract(this.panel.expandFromQueryS, 's'); 345 | this.range.from = from; 346 | this.range.raw.from = from; 347 | } 348 | } 349 | 350 | // This should only be called from the snapshot callback 351 | onSnapshotLoad(dataList: LegacyResponseData[]) { 352 | this.onDataFramesReceived(getProcessedDataFrames(dataList)); 353 | } 354 | 355 | // Directly support DataFrame 356 | onDataFramesReceived(frames: DataFrame[]) { 357 | $(this.canvas).css('cursor', 'pointer'); 358 | 359 | const data: DistinctPoints[] = []; 360 | frames.forEach(frame => { 361 | const time = getTimeField(frame).timeField; 362 | if (time) { 363 | frame.fields.forEach(field => { 364 | if (field !== time) { 365 | const res = new DistinctPoints(this.fieldNamer(field, frame, frames)); 366 | for (let i = 0; i < time.values.length; i++) { 367 | res.add(time.values.get(i), this.formatValue(field.values.get(i))); 368 | } 369 | res.finish(this); 370 | data.push(res); 371 | } 372 | }); 373 | } 374 | }); 375 | this.data = data; 376 | this.updateLegendMetrics(); 377 | 378 | // Annotations Query 379 | this.annotationsSrv 380 | .getAnnotations({ 381 | dashboard: this.dashboard, 382 | panel: this.panel, // {id: 4}, // 383 | range: this.range, 384 | }) 385 | .then( 386 | result => { 387 | this.loading = false; 388 | if (result.annotations && result.annotations.length > 0) { 389 | this.annotations = result.annotations; 390 | } else { 391 | this.annotations = null; 392 | } 393 | this.onRender(); 394 | }, 395 | () => { 396 | this.loading = false; 397 | this.annotations = null; 398 | this.onRender(); 399 | console.log('ERRR', this); 400 | } 401 | ); 402 | } 403 | 404 | updateLegendMetrics(notify?: boolean) { 405 | if (!this.data || !this.panel.showLegend || this.panel.showLegendNames || this.data.length <= 1) { 406 | this.legend = this.data; 407 | } else { 408 | this.legend = [DistinctPoints.combineLegend(this.data, this)]; 409 | } 410 | 411 | if (notify) { 412 | this.onConfigChanged(); 413 | } 414 | } 415 | 416 | removeColorMap(map) { 417 | const index = _.indexOf(this.panel.colorMaps, map); 418 | this.panel.colorMaps.splice(index, 1); 419 | this.updateColorInfo(); 420 | } 421 | 422 | updateColorInfo() { 423 | const cm = {}; 424 | for (let i = 0; i < this.panel.colorMaps.length; i++) { 425 | const m = this.panel.colorMaps[i]; 426 | if (m.text) { 427 | cm[m.text] = m.color; 428 | } 429 | } 430 | this._colorsPaleteCash = {}; 431 | this._colorsPaleteCash.length = 0; 432 | this.colorMap = cm; 433 | this.render(); 434 | } 435 | 436 | addColorMap(what) { 437 | if (what === 'curent') { 438 | _.forEach(this.data, metric => { 439 | if (metric.legendInfo) { 440 | _.forEach(metric.legendInfo, info => { 441 | if (!_.has(this.colorMap, info.val)) { 442 | const v = { text: info.val, color: this.getColor(info.val) }; 443 | this.panel.colorMaps.push(v); 444 | this.colorMap[info.val] = v; 445 | } 446 | }); 447 | } 448 | }); 449 | } else { 450 | this.panel.colorMaps.push({ text: '???', color: this.randomColor() }); 451 | } 452 | this.updateColorInfo(); 453 | } 454 | 455 | removeValueMap(map) { 456 | const index = _.indexOf(this.panel.valueMaps, map); 457 | this.panel.valueMaps.splice(index, 1); 458 | this.render(); 459 | } 460 | 461 | addValueMap() { 462 | this.panel.valueMaps.push({ value: '', op: '=', text: '' }); 463 | } 464 | 465 | removeRangeMap(rangeMap) { 466 | const index = _.indexOf(this.panel.rangeMaps, rangeMap); 467 | this.panel.rangeMaps.splice(index, 1); 468 | this.render(); 469 | } 470 | 471 | addRangeMap() { 472 | this.panel.rangeMaps.push({ from: '', to: '', text: '' }); 473 | } 474 | 475 | onConfigChanged(update = false) { 476 | this.isTimeline = this.panel.display === 'timeline'; 477 | this.isStacked = this.panel.display === 'stacked'; 478 | 479 | this.formatter = null; 480 | if (this.panel.units && 'none' !== this.panel.units) { 481 | this.formatter = kbn.valueFormats[this.panel.units]; 482 | } 483 | 484 | if (update) { 485 | this.refresh(); 486 | } else { 487 | this.render(); 488 | } 489 | } 490 | 491 | formatDuration(duration) { 492 | if (!this.panel.useTimePrecision) { 493 | return duration.humanize(); 494 | } 495 | 496 | const dir: any = {}; 497 | let hasValue = false; 498 | let limit = false; 499 | 500 | for (const o of this.panel.timeOptions) { 501 | dir[o.value] = parseInt(duration.as(o.value), 10); 502 | hasValue = dir[o.value] || hasValue; 503 | duration.subtract(moment.duration(dir[o.value], o.value)); 504 | limit = this.panel.timePrecision.value === o.value || limit; 505 | 506 | // always show a value in case it is less than the configured 507 | // precision 508 | if (limit && hasValue) { 509 | break; 510 | } 511 | } 512 | 513 | const rs = Object.keys(dir).reduce((carry, key) => { 514 | const value = dir[key]; 515 | if (!value) { 516 | return carry; 517 | } 518 | key = value < 2 ? key.replace(/s$/, '') : key; 519 | return `${carry} ${value} ${key},`; 520 | }, ''); 521 | 522 | return rs.substr(0, rs.length - 1); 523 | } 524 | 525 | getLegendDisplay(info, metric) { 526 | let disp = info.val; 527 | if (this.panel.showLegendPercent || this.panel.showLegendCounts || this.panel.showLegendTime) { 528 | disp += ' ('; 529 | let hassomething = false; 530 | if (this.panel.showLegendTime) { 531 | disp += this.formatDuration(moment.duration(info.ms)); 532 | hassomething = true; 533 | } 534 | 535 | if (this.panel.showLegendPercent) { 536 | if (hassomething) { 537 | disp += ', '; 538 | } 539 | 540 | let dec = this.panel.legendPercentDecimals; 541 | if (_.isNil(dec)) { 542 | if (info.per > 0.98 && metric.changes.length > 1) { 543 | dec = 2; 544 | } else if (info.per < 0.02) { 545 | dec = 2; 546 | } else { 547 | dec = 0; 548 | } 549 | } 550 | disp += kbn.valueFormats.percentunit(info.per, dec); 551 | hassomething = true; 552 | } 553 | 554 | if (this.panel.showLegendCounts) { 555 | if (hassomething) { 556 | disp += ', '; 557 | } 558 | disp += info.count + 'x'; 559 | } 560 | disp += ')'; 561 | } 562 | return disp; 563 | } 564 | 565 | //------------------ 566 | // Mouse Events 567 | //------------------ 568 | 569 | showTooltip(evt, point, isExternal) { 570 | let from = point.start; 571 | let to = point.start + point.ms; 572 | let time = point.ms; 573 | let val = point.val; 574 | 575 | if (this.mouse.down != null) { 576 | from = Math.min(this.mouse.down.ts, this.mouse.position.ts); 577 | to = Math.max(this.mouse.down.ts, this.mouse.position.ts); 578 | time = to - from; 579 | val = 'Zoom To:'; 580 | } 581 | 582 | let body = '
' + val + '
'; 583 | 584 | body += '
'; 585 | body += this.dashboard.formatDate(moment(from)) + '
'; 586 | body += 'to
'; 587 | body += this.dashboard.formatDate(moment(to)) + '

'; 588 | body += this.formatDuration(moment.duration(time)) + '
'; 589 | body += '
'; 590 | 591 | let pageX = 0; 592 | let pageY = 0; 593 | if (isExternal) { 594 | const rect = this.canvas.getBoundingClientRect(); 595 | pageY = rect.top + evt.pos.panelRelY * rect.height; 596 | if (pageY < 0 || pageY > $(window).innerHeight()) { 597 | // Skip Hidden tooltip 598 | this.$tooltip.detach(); 599 | return; 600 | } 601 | pageY += $(window).scrollTop(); 602 | 603 | const elapsed = this.range.to - this.range.from; 604 | const pX = (evt.pos.x - this.range.from) / elapsed; 605 | pageX = rect.left + pX * rect.width; 606 | } else { 607 | pageX = evt.evt.pageX; 608 | pageY = evt.evt.pageY; 609 | } 610 | 611 | this.$tooltip.html(body).place_tt(pageX + 20, pageY + 5); 612 | } 613 | 614 | onGraphHover(evt, showTT, isExternal) { 615 | this.externalPT = false; 616 | if (this.data && this.data.length) { 617 | let hover: any = null; 618 | let j = Math.floor(this.mouse.position.y / this.panel.rowHeight); 619 | if (j < 0) { 620 | j = 0; 621 | } 622 | if (j >= this.data.length) { 623 | j = this.data.length - 1; 624 | } 625 | 626 | if (this.isTimeline) { 627 | hover = this.data[j].changes[0]; 628 | for (let i = 0; i < this.data[j].changes.length; i++) { 629 | if (this.data[j].changes[i].start > this.mouse.position.ts) { 630 | break; 631 | } 632 | hover = this.data[j].changes[i]; 633 | } 634 | this.hoverPoint = hover; 635 | 636 | if (this.annotations && !isExternal && this._renderDimensions) { 637 | if (evt.pos.y > this._renderDimensions.rowsHeight - 5) { 638 | const min = _.isUndefined(this.range.from) ? null : this.range.from.valueOf(); 639 | const max = _.isUndefined(this.range.to) ? null : this.range.to.valueOf(); 640 | const width = this._renderDimensions.width; 641 | 642 | const anno = _.find(this.annotations, a => { 643 | if (a.isRegion) { 644 | return evt.pos.x > a.time && evt.pos.x < a.timeEnd; 645 | } 646 | const annoX = ((a.time - min) / (max - min)) * width; 647 | const mouseX = evt.evt.offsetX; 648 | return annoX > mouseX - 5 && annoX < mouseX + 5; 649 | }); 650 | if (anno) { 651 | console.log('TODO, hover ', anno); 652 | // See: https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/graph/jquery.flot.events.js#L10 653 | this.$tooltip.html(anno.text).place_tt(evt.evt.pageX + 20, evt.evt.pageY + 5); 654 | return; 655 | } 656 | } 657 | } 658 | 659 | if (showTT) { 660 | this.externalPT = isExternal; 661 | this.showTooltip(evt, hover, isExternal); 662 | } 663 | this.onRender(); // refresh the view 664 | } else if (!isExternal) { 665 | if (this.isStacked) { 666 | hover = this.data[j].legendInfo[0]; 667 | // for (let i = 0; i < this.data[j].legendInfo.length; i++) { 668 | // if (this.data[j].legendInfo[i].x > this.mouse.position.x) { 669 | // break; 670 | // } 671 | // hover = this.data[j].legendInfo[i]; 672 | // } 673 | this.hoverPoint = hover; 674 | this.onRender(); // refresh the view 675 | 676 | if (showTT) { 677 | this.externalPT = isExternal; 678 | this.showLegandTooltip(evt.evt, hover); 679 | } 680 | } 681 | } 682 | } else { 683 | this.$tooltip.detach(); // make sure it is hidden 684 | } 685 | } 686 | 687 | onMouseClicked(where, event) { 688 | if (event.metaKey === true || event.ctrlKey === true) { 689 | console.log('TODO? Create Annotation?', where, event); 690 | return; 691 | } 692 | 693 | const pt = this.hoverPoint; 694 | if (pt && pt.start) { 695 | const range = { from: moment.utc(pt.start), to: moment.utc(pt.start + pt.ms) }; 696 | this.timeSrv.setTime(range); 697 | this.clear(); 698 | } 699 | } 700 | 701 | onMouseSelectedRange(range, event) { 702 | if (event.metaKey === true || event.ctrlKey === true) { 703 | console.log('TODO? Create range annotation?', range, event); 704 | return; 705 | } 706 | this.timeSrv.setTime(range); 707 | this.clear(); 708 | } 709 | 710 | clear() { 711 | this.mouse.position = null; 712 | this.mouse.down = null; 713 | this.hoverPoint = null; 714 | $(this.canvas).css('cursor', 'wait'); 715 | appEvents.emit('graph-hover-clear'); 716 | this.render(); 717 | } 718 | 719 | _updateRenderDimensions() { 720 | this._renderDimensions = {}; 721 | 722 | const rect = (this._renderDimensions.rect = this.wrap.getBoundingClientRect()); 723 | const rows = (this._renderDimensions.rows = this.data.length); 724 | const rowHeight = (this._renderDimensions.rowHeight = this.panel.rowHeight); 725 | const rowsHeight = (this._renderDimensions.rowsHeight = rowHeight * rows); 726 | const timeHeight = this.panel.showTimeAxis ? 14 + this.panel.textSizeTime : 0; 727 | const height = (this._renderDimensions.height = rowsHeight + timeHeight); 728 | const width = (this._renderDimensions.width = rect.width); 729 | this._renderDimensions.height = height; 730 | 731 | // First render? 732 | if (!this.range) { 733 | this.range = { 734 | to: 2000, 735 | from: 1000, 736 | }; 737 | } 738 | 739 | let top = 0; 740 | const elapsed = this.range.to - this.range.from; 741 | 742 | this._renderDimensions.matrix = []; 743 | _.forEach(this.data, metric => { 744 | const positions: any[] = []; 745 | 746 | if (this.isTimeline) { 747 | let point = metric.changes[0]; 748 | for (let i = 0; i < metric.changes.length; i++) { 749 | point = metric.changes[i]; 750 | if (point.start <= this.range.to) { 751 | const xt = Math.max(point.start - this.range.from, 0); 752 | const x = (xt / elapsed) * width; 753 | positions.push(x); 754 | } 755 | } 756 | } 757 | 758 | if (this.isStacked) { 759 | let point: LegendValue; 760 | let start = this.range.from; 761 | for (let i = 0; i < metric.legendInfo.length; i++) { 762 | point = metric.legendInfo[i]; 763 | const xt = Math.max(start - this.range.from, 0); 764 | const x = (xt / elapsed) * width; 765 | positions.push(x); 766 | start += point.ms; 767 | } 768 | } 769 | 770 | this._renderDimensions.matrix.push({ 771 | y: top, 772 | positions: positions, 773 | }); 774 | 775 | top += rowHeight; 776 | }); 777 | } 778 | 779 | _updateSelectionMatrix() { 780 | const selectionPredicates = { 781 | all: () => { 782 | return true; 783 | }, 784 | crosshairHover: function(i, j) { 785 | if (j + 1 === this.data[i].changes.length) { 786 | return this.data[i].changes[j].start <= this.mouse.position.ts; 787 | } 788 | return ( 789 | this.data[i].changes[j].start <= this.mouse.position.ts && 790 | this.mouse.position.ts < this.data[i].changes[j + 1].start 791 | ); 792 | }, 793 | mouseX: function(i, j) { 794 | const row = this._renderDimensions.matrix[i]; 795 | if (j + 1 === row.positions.length) { 796 | return row.positions[j] <= this.mouse.position.x; 797 | } 798 | return row.positions[j] <= this.mouse.position.x && this.mouse.position.x < row.positions[j + 1]; 799 | }, 800 | metric: function(i) { 801 | return this.data[i] === this._selectedMetric; 802 | }, 803 | legendItem: function(i, j) { 804 | if (this.data[i] !== this._selectedMetric) { 805 | return false; 806 | } 807 | return this._selectedLegendItem.val === this._getVal(i, j); 808 | }, 809 | }; 810 | 811 | function getPredicate() { 812 | if (this._selectedLegendItem !== undefined) { 813 | return 'legendItem'; 814 | } 815 | if (this._selectedMetric !== undefined) { 816 | return 'metric'; 817 | } 818 | if (this.mouse.down !== null) { 819 | return 'all'; 820 | } 821 | if (this.panel.highlightOnMouseover && this.mouse.position != null) { 822 | if (this.isTimeline) { 823 | return 'crosshairHover'; 824 | } 825 | if (this.isStacked) { 826 | return 'mouseX'; 827 | } 828 | } 829 | return 'all'; 830 | } 831 | 832 | const pn = getPredicate.bind(this)(); 833 | const predicate = selectionPredicates[pn].bind(this); 834 | this._selectionMatrix = []; 835 | for (let i = 0; i < this._renderDimensions.matrix.length; i++) { 836 | const rs: any[] = []; 837 | const r = this._renderDimensions.matrix[i]; 838 | for (let j = 0; j < r.positions.length; j++) { 839 | rs.push(predicate(i, j)); 840 | } 841 | this._selectionMatrix.push(rs); 842 | } 843 | } 844 | 845 | _updateCanvasSize() { 846 | this.canvas.width = this._renderDimensions.width * this._devicePixelRatio; 847 | this.canvas.height = this._renderDimensions.height * this._devicePixelRatio; 848 | 849 | $(this.canvas).css('width', this._renderDimensions.width + 'px'); 850 | $(this.canvas).css('height', this._renderDimensions.height + 'px'); 851 | 852 | this.context.scale(this._devicePixelRatio, this._devicePixelRatio); 853 | } 854 | 855 | _getVal(metricIndex, rectIndex) { 856 | let point: any; 857 | if (this.isTimeline) { 858 | point = this.data[metricIndex].changes[rectIndex]; 859 | } 860 | if (this.isStacked) { 861 | point = this.data[metricIndex].legendInfo[rectIndex]; 862 | } 863 | return point.val; 864 | } 865 | 866 | _renderRects() { 867 | const matrix = this._renderDimensions.matrix; 868 | const ctx = this.context; 869 | 870 | // Clear the background 871 | ctx.fillStyle = this.panel.backgroundColor; 872 | ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); 873 | 874 | _.forEach(this.data, (metric, i) => { 875 | const rowObj = matrix[i]; 876 | for (let j = 0; j < rowObj.positions.length; j++) { 877 | const currentX = rowObj.positions[j]; 878 | let nextX = this._renderDimensions.width; 879 | if (j + 1 !== rowObj.positions.length) { 880 | nextX = rowObj.positions[j + 1]; 881 | } 882 | ctx.fillStyle = this.getColor(this._getVal(i, j)); 883 | const globalAlphaTemp = ctx.globalAlpha; 884 | if (!this._selectionMatrix[i][j]) { 885 | ctx.globalAlpha = 0.3; 886 | } 887 | ctx.fillRect(currentX, matrix[i].y, nextX - currentX, this._renderDimensions.rowHeight); 888 | ctx.globalAlpha = globalAlphaTemp; 889 | } 890 | 891 | if (i > 0) { 892 | const top = matrix[i].y; 893 | ctx.strokeStyle = this.panel.lineColor; 894 | ctx.beginPath(); 895 | ctx.moveTo(0, top); 896 | ctx.lineTo(this._renderDimensions.width, top); 897 | ctx.stroke(); 898 | } 899 | }); 900 | } 901 | 902 | _renderLabels() { 903 | const ctx = this.context; 904 | ctx.lineWidth = 1; 905 | ctx.textBaseline = 'middle'; 906 | ctx.font = this.panel.textSize + 'px "Open Sans", Helvetica, Arial, sans-serif'; 907 | 908 | const offset = 2; 909 | const rowHeight = this._renderDimensions.rowHeight; 910 | _.forEach(this.data, (metric, i) => { 911 | const { y, positions } = this._renderDimensions.matrix[i]; 912 | 913 | const centerY = y + rowHeight / 2; 914 | // let labelPositionMetricName = y + rectHeight - this.panel.textSize / 2; 915 | // let labelPositionLastValue = y + rectHeight - this.panel.textSize / 2; 916 | // let labelPositionValue = y + this.panel.textSize / 2; 917 | const labelPositionMetricName = centerY; 918 | const labelPositionLastValue = centerY; 919 | const labelPositionValue = centerY; 920 | 921 | let minTextSpot = 0; 922 | let maxTextSpot = this._renderDimensions.width; 923 | if (this.panel.writeMetricNames) { 924 | ctx.fillStyle = this.panel.metricNameColor; 925 | ctx.textAlign = 'left'; 926 | ctx.fillText(metric.name, offset, labelPositionMetricName); 927 | minTextSpot = offset + ctx.measureText(metric.name).width + 2; 928 | } 929 | 930 | let hoverTextStart = -1; 931 | let hoverTextEnd = -1; 932 | 933 | if (this.mouse.position) { 934 | for (let j = 0; j < positions.length; j++) { 935 | if (positions[j] <= this.mouse.position.x) { 936 | if (j >= positions.length - 1 || positions[j + 1] >= this.mouse.position.x) { 937 | let val = this._getVal(i, j); 938 | ctx.fillStyle = this.panel.valueTextColor; 939 | ctx.textAlign = 'left'; 940 | hoverTextStart = positions[j] + offset; 941 | if (hoverTextStart < minTextSpot) { 942 | hoverTextStart = minTextSpot + 2; 943 | val = ': ' + val; 944 | } 945 | 946 | ctx.fillText(val, hoverTextStart, labelPositionValue); 947 | const txtinfo = ctx.measureText(val); 948 | hoverTextEnd = hoverTextStart + txtinfo.width + 4; 949 | break; 950 | } 951 | } 952 | } 953 | } 954 | 955 | if (this.panel.writeLastValue) { 956 | const val = this._getVal(i, positions.length - 1); 957 | ctx.fillStyle = this.panel.valueTextColor; 958 | ctx.textAlign = 'right'; 959 | const txtinfo = ctx.measureText(val); 960 | const xval = this._renderDimensions.width - offset - txtinfo.width; 961 | if (xval > hoverTextEnd) { 962 | ctx.fillText(val, this._renderDimensions.width - offset, labelPositionLastValue); 963 | maxTextSpot = this._renderDimensions.width - ctx.measureText(val).width - 10; 964 | } 965 | } 966 | 967 | if (this.panel.writeAllValues) { 968 | ctx.fillStyle = this.panel.valueTextColor; 969 | ctx.textAlign = 'left'; 970 | for (let j = 0; j < positions.length; j++) { 971 | const val = this._getVal(i, j); 972 | let nextX = this._renderDimensions.width; 973 | if (j + 1 !== positions.length) { 974 | nextX = positions[j + 1]; 975 | } 976 | 977 | const x = positions[j]; 978 | if (x > minTextSpot) { 979 | const width = nextX - x; 980 | if (maxTextSpot > x + width) { 981 | // This clips the text within the given bounds 982 | ctx.save(); 983 | ctx.rect(x, y, width, rowHeight); 984 | ctx.clip(); 985 | 986 | ctx.fillText(val, x + offset, labelPositionValue); 987 | ctx.restore(); 988 | } 989 | } 990 | } 991 | } 992 | }); 993 | } 994 | 995 | _renderSelection() { 996 | if (this.mouse.down === null) { 997 | return; 998 | } 999 | if (this.mouse.position === null) { 1000 | return; 1001 | } 1002 | if (!this.isTimeline) { 1003 | return; 1004 | } 1005 | 1006 | const ctx = this.context; 1007 | const height = this._renderDimensions.height; 1008 | 1009 | const xmin = Math.min(this.mouse.position.x, this.mouse.down.x); 1010 | const xmax = Math.max(this.mouse.position.x, this.mouse.down.x); 1011 | 1012 | ctx.fillStyle = 'rgba(110, 110, 110, 0.5)'; 1013 | ctx.strokeStyle = 'rgba(110, 110, 110, 0.5)'; 1014 | ctx.beginPath(); 1015 | ctx.fillRect(xmin, 0, xmax - xmin, height); 1016 | ctx.strokeRect(xmin, 0, xmax - xmin, height); 1017 | } 1018 | 1019 | _renderTimeAxis() { 1020 | if (!this.panel.showTimeAxis) { 1021 | return; 1022 | } 1023 | 1024 | const ctx = this.context; 1025 | // const rows = this.data.length; 1026 | // const rowHeight = this.panel.rowHeight; 1027 | // const height = this._renderDimensions.height; 1028 | const width = this._renderDimensions.width; 1029 | const top = this._renderDimensions.rowsHeight; 1030 | 1031 | const headerColumnIndent = 0; // header inset (zero for now) 1032 | 1033 | ctx.font = this.panel.textSizeTime + 'px "Open Sans", Helvetica, Arial, sans-serif'; 1034 | ctx.fillStyle = this.panel.timeTextColor; 1035 | ctx.textAlign = 'left'; 1036 | ctx.strokeStyle = this.panel.timeTextColor; 1037 | ctx.textBaseline = 'top'; 1038 | ctx.setLineDash([7, 5]); // dashes are 5px and spaces are 3px 1039 | ctx.lineDashOffset = 0; 1040 | 1041 | const min = _.isUndefined(this.range.from) ? null : this.range.from.valueOf(); 1042 | const max = _.isUndefined(this.range.to) ? null : this.range.to.valueOf(); 1043 | const minPxInterval = ctx.measureText('12/33 24:59').width * 2; 1044 | const estNumTicks = width / minPxInterval; 1045 | const estTimeInterval = (max - min) / estNumTicks; 1046 | const timeResolution = this.getTimeResolution(estTimeInterval); 1047 | const pixelStep = (timeResolution / (max - min)) * width; 1048 | let nextPointInTime = this.roundDate(min, timeResolution) + timeResolution; 1049 | let xPos = headerColumnIndent + ((nextPointInTime - min) / (max - min)) * width; 1050 | 1051 | const timeFormat = this.time_format(max - min, timeResolution / 1000); 1052 | let displayOffset = 0; 1053 | if (this.dashboard.getTimezone() === 'utc') { 1054 | displayOffset = new Date().getTimezoneOffset() * 60000; 1055 | } 1056 | 1057 | while (nextPointInTime < max) { 1058 | // draw ticks 1059 | ctx.beginPath(); 1060 | ctx.moveTo(xPos, top + 5); 1061 | ctx.lineTo(xPos, 0); 1062 | ctx.lineWidth = 1; 1063 | ctx.stroke(); 1064 | 1065 | // draw time label 1066 | const date = new Date(nextPointInTime + displayOffset); 1067 | const dateStr = this.formatDate(date, timeFormat); 1068 | const xOffset = ctx.measureText(dateStr).width / 2; 1069 | ctx.fillText(dateStr, xPos - xOffset, top + 10); 1070 | 1071 | nextPointInTime += timeResolution; 1072 | xPos += pixelStep; 1073 | } 1074 | } 1075 | 1076 | _renderCrosshair() { 1077 | if (this.mouse.down != null) { 1078 | return; 1079 | } 1080 | if (this.mouse.position === null) { 1081 | return; 1082 | } 1083 | if (!this.isTimeline) { 1084 | return; 1085 | } 1086 | 1087 | const ctx = this.context; 1088 | const rows = this.data.length; 1089 | //let rowHeight = this.panel.rowHeight; 1090 | const height = this._renderDimensions.height; 1091 | 1092 | ctx.beginPath(); 1093 | ctx.moveTo(this.mouse.position.x, 0); 1094 | ctx.lineTo(this.mouse.position.x, height); 1095 | ctx.strokeStyle = this.panel.crosshairColor; 1096 | ctx.setLineDash([]); 1097 | ctx.lineWidth = 1; 1098 | ctx.stroke(); 1099 | 1100 | // Draw a Circle around the point if showing a tooltip 1101 | if (this.externalPT && rows > 1) { 1102 | ctx.beginPath(); 1103 | ctx.arc(this.mouse.position.x, this.mouse.position.y, 3, 0, 2 * Math.PI, false); 1104 | ctx.fillStyle = this.panel.crosshairColor; 1105 | ctx.fill(); 1106 | ctx.lineWidth = 1; 1107 | } 1108 | } 1109 | 1110 | _renderAnnotations() { 1111 | if (!this.panel.showTimeAxis) { 1112 | return; 1113 | } 1114 | if (!this.annotations) { 1115 | return; 1116 | } 1117 | 1118 | const ctx = this.context; 1119 | //const rows = this.data.length; 1120 | const rowHeight = this.panel.rowHeight; 1121 | //const height = this._renderDimensions.height; 1122 | const width = this._renderDimensions.width; 1123 | const top = this._renderDimensions.rowsHeight; 1124 | 1125 | const headerColumnIndent = 0; // header inset (zero for now) 1126 | ctx.font = this.panel.textSizeTime + 'px "Open Sans", Helvetica, Arial, sans-serif'; 1127 | ctx.fillStyle = '#7FE9FF'; 1128 | ctx.textAlign = 'left'; 1129 | ctx.strokeStyle = '#7FE9FF'; 1130 | 1131 | ctx.textBaseline = 'top'; 1132 | ctx.setLineDash([3, 3]); 1133 | ctx.lineDashOffset = 0; 1134 | ctx.lineWidth = 2; 1135 | 1136 | const min = _.isUndefined(this.range.from) ? null : this.range.from.valueOf(); 1137 | const max = _.isUndefined(this.range.to) ? null : this.range.to.valueOf(); 1138 | //let xPos = headerColumnIndent; 1139 | 1140 | _.forEach(this.annotations, anno => { 1141 | ctx.setLineDash([3, 3]); 1142 | 1143 | let isAlert = false; 1144 | if (anno.source.iconColor) { 1145 | ctx.fillStyle = anno.source.iconColor; 1146 | ctx.strokeStyle = anno.source.iconColor; 1147 | } else if (anno.annotation === undefined) { 1148 | // grafana annotation 1149 | ctx.fillStyle = '#7FE9FF'; 1150 | ctx.strokeStyle = '#7FE9FF'; 1151 | } else { 1152 | isAlert = true; 1153 | ctx.fillStyle = '#EA0F3B'; //red 1154 | ctx.strokeStyle = '#EA0F3B'; 1155 | } 1156 | 1157 | this._drawVertical(ctx, anno.time, min, max, headerColumnIndent, top, width, isAlert); 1158 | 1159 | //do the TO rangeMap 1160 | if (anno.isRegion) { 1161 | this._drawVertical(ctx, anno.timeEnd, min, max, headerColumnIndent, top, width, isAlert); 1162 | 1163 | //draw horizontal line at bottom 1164 | const xPosStart = headerColumnIndent + ((anno.time - min) / (max - min)) * width; 1165 | const xPosEnd = headerColumnIndent + ((anno.timeEnd - min) / (max - min)) * width; 1166 | 1167 | // draw ticks 1168 | ctx.beginPath(); 1169 | ctx.moveTo(xPosStart, top + 5); 1170 | ctx.lineTo(xPosEnd, top + 5); 1171 | 1172 | ctx.lineWidth = 4; 1173 | ctx.setLineDash([]); 1174 | ctx.stroke(); 1175 | //end horizontal 1176 | //do transparency 1177 | if (isAlert === false) { 1178 | ctx.save(); 1179 | ctx.fillStyle = '#7FE9FF'; 1180 | ctx.globalAlpha = 0.2; 1181 | ctx.fillRect(xPosStart, 0, xPosEnd - xPosStart, rowHeight); 1182 | ctx.stroke(); 1183 | ctx.restore(); 1184 | } 1185 | } 1186 | }); 1187 | } 1188 | 1189 | _drawVertical(ctx, timeVal, min, max, headerColumnIndent, top, width, isAlert) { 1190 | const xPos = headerColumnIndent + ((timeVal - min) / (max - min)) * width; 1191 | 1192 | // draw ticks 1193 | ctx.lineWidth = 1; 1194 | ctx.beginPath(); 1195 | ctx.moveTo(xPos, top + 5); 1196 | ctx.lineTo(xPos, 0); 1197 | ctx.stroke(); 1198 | 1199 | // draw triangle 1200 | ctx.moveTo(xPos + 0, top); 1201 | ctx.lineTo(xPos - 5, top + 7); 1202 | ctx.lineTo(xPos + 5, top + 7); 1203 | ctx.fill(); 1204 | 1205 | // draw alert label 1206 | if (isAlert === true) { 1207 | const dateStr = '\u25B2'; 1208 | const xOffset = ctx.measureText(dateStr).width / 2; 1209 | ctx.fillText(dateStr, xPos - xOffset, top + 10); 1210 | } 1211 | } 1212 | } 1213 | 1214 | /** 1215 | * All panels will be passed tables that have our best guess at colum type set 1216 | * 1217 | * This is also used by PanelChrome for snapshot support 1218 | */ 1219 | export function getProcessedDataFrames(results?: DataQueryResponseData[]): DataFrame[] { 1220 | if (!results || !isArray(results)) { 1221 | return []; 1222 | } 1223 | 1224 | const dataFrames: DataFrame[] = []; 1225 | 1226 | for (const result of results) { 1227 | const dataFrame = guessFieldTypes(toDataFrame(result)); 1228 | 1229 | // clear out any cached calcs 1230 | for (const field of dataFrame.fields) { 1231 | const f = field as any; 1232 | f.calcs = undefined; 1233 | f.state = undefined; 1234 | } 1235 | 1236 | dataFrames.push(dataFrame); 1237 | } 1238 | 1239 | return dataFrames; 1240 | } 1241 | 1242 | export { DiscretePanelCtrl as PanelCtrl }; 1243 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/partials/editor.colors.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
Text Colors
4 |
5 |
6 | 7 | Metric Names 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | Value Text 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 | Time Text 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 |
37 | 38 | Background 39 | 40 | 41 | 42 | 43 | 44 |
45 | 46 |
47 | 48 | Lines 49 | 50 | 51 | 52 | 53 | 54 |
55 |
56 |
57 | 58 | 59 |
60 |
Color Mappings
61 |
62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 | 76 |
77 | 81 | 85 |
86 |
87 |
88 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/partials/editor.legend.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |
7 |
Legend
8 | 13 | 18 | 23 | 28 |
29 | 30 |
31 |
Values
32 | 37 | 38 |
39 | 40 |
41 | 47 |
48 |
49 | 50 |
51 | 52 | 53 |
54 | 55 | 56 | 61 | 62 | 67 | 68 | 73 | 74 |
75 | 76 | 77 |
78 | 79 |
80 |
81 | 82 |
83 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/partials/editor.mappings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
Value Mappings
5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 22 |
23 |
24 |
25 | 26 |
27 |
Range Mappings
28 |
29 |
30 | 31 | 32 | 33 | From 34 | 35 | To 36 | 37 | Text 38 | 39 |
40 | 41 |
42 | 46 |
47 |
48 |
49 | 50 | 51 | 52 |
53 |
Numeric Conversion
54 |
55 |
56 | 57 |
60 |
61 | 62 |
63 | 64 | 65 |
66 | 67 |
68 |
69 | 70 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/partials/editor.options.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
Display
4 | 5 | 28 | 29 |
30 | 31 | 38 |
39 |
40 | 41 | 48 |
49 |
50 | 51 | 58 |
59 | 60 | 67 | 68 | 75 | 76 | 83 | 84 | 91 |
92 | 93 |
94 |
Hover tooltip
95 | 102 | 103 | 110 | 111 |
112 | 119 |
120 |
121 | 122 |
123 |
Query
124 |
125 | 132 | 139 |   Seconds 140 |
141 |
142 |
143 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/partials/module.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | {{ metric.name }}: 7 |
12 |
{{ ctrl.getLegendDisplay( info, metric ) }} 14 |
15 |
16 |   Transitions: {{metric.transitionCount}} 17 |
18 |
19 |   Distinct: {{metric.distinctValuesCount}} 20 |
21 |
22 |
23 |
-------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Discrete", 4 | "id": "natel-discrete-panel", 5 | 6 | "info": { 7 | "description": "Discrete Events grafana", 8 | "author": { 9 | "name": "Natel Energy" 10 | }, 11 | "keywords": [ "discrete", "events", "strings" ], 12 | "logos": { 13 | "small": "img/discrete_logo.svg", 14 | "large": "img/discrete_logo.svg" 15 | }, 16 | "links": [ 17 | {"name": "Project site", "url": "https://github.com/NatelEnergy/grafana-discrete-panel"}, 18 | {"name": "MIT License", "url": "https://github.com/NatelEnergy/grafana-discrete-panel/blob/master/LICENSE"}, 19 | {"name": "Natel Energy", "url": "http://www.natelenergy.com/"} 20 | ], 21 | "screenshots": [ 22 | { "name": "Single State", "path": "img/screenshot-single-1.png" }, 23 | { "name": "Tooltips", "path": "img/screenshot-single-2.png" }, 24 | { "name": "Summary Tooltips", "path": "img/screenshot-single-3.png" }, 25 | { "name": "Show all values", "path": "img/screenshot-single-4.png" }, 26 | { "name": "Multiple Results", "path": "img/screenshot-multiple.png" }, 27 | { "name": "Options", "path": "img/screenshot-options-1.png" }, 28 | { "name": "Legend", "path": "img/screenshot-options-2.png" } 29 | ], 30 | "version": "%VERSION%", 31 | "updated": "%TODAY%" 32 | }, 33 | 34 | "dependencies": { 35 | "grafanaVersion": "6.4.x", 36 | "plugins": [ ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /kafka-playground/plugin/natel-discrete-panel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json", 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "rootDir": "./src", 6 | "baseUrl": "./src", 7 | "typeRoots": ["./node_modules/@types"], 8 | "noImplicitAny": false, 9 | "noImplicitThis": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /kafka-playground/provisioning/dashboards/dashboard.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | # an unique provider name 5 | - name: 'Prometheus' 6 | # org id. will default to orgId 1 if not specified 7 | orgId: 1 8 | # name of the dashboard folder. Required 9 | folder: '' 10 | # folder UID. will be automatically generated if not specified 11 | folderUid: '' 12 | # provider type. Required 13 | type: file 14 | # disable dashboard deletion 15 | disableDeletion: false 16 | # enable dashboard editing 17 | editable: true 18 | # how often Grafana will scan for changed dashboards 19 | updateIntervalSeconds: 10 20 | options: 21 | # path to dashboard files on disk. Required 22 | path: /etc/grafana/provisioning/dashboards -------------------------------------------------------------------------------- /kafka-playground/provisioning/dashboards/totalMessages.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": "-- Grafana --", 7 | "enable": true, 8 | "hide": true, 9 | "iconColor": "rgba(0, 211, 255, 1)", 10 | "name": "Annotations & Alerts", 11 | "type": "dashboard" 12 | } 13 | ] 14 | }, 15 | "description": "Metrics for the Kafka cluster", 16 | "editable": true, 17 | "gnetId": null, 18 | "graphTooltip": 0, 19 | "links": [], 20 | "panels": [ 21 | { 22 | "datasource": "Prometheus", 23 | "description": "Shows the number of under replicated partitions for each topic. If any are greater than 1, make sure your broker servers for the topic are healthy. ", 24 | "fieldConfig": { 25 | "defaults": { 26 | "custom": { 27 | "align": null, 28 | "filterable": false 29 | }, 30 | "mappings": [], 31 | "thresholds": { 32 | "mode": "absolute", 33 | "steps": [ 34 | { 35 | "color": "green", 36 | "value": null 37 | }, 38 | { 39 | "color": "red", 40 | "value": 1 41 | } 42 | ] 43 | } 44 | }, 45 | "overrides": [] 46 | }, 47 | "gridPos": { 48 | "h": 8, 49 | "w": 12, 50 | "x": 0, 51 | "y": 0 52 | }, 53 | "id": 22, 54 | "options": { 55 | "orientation": "auto", 56 | "reduceOptions": { 57 | "calcs": [ 58 | "last" 59 | ], 60 | "fields": "", 61 | "values": false 62 | }, 63 | "showThresholdLabels": false, 64 | "showThresholdMarkers": true 65 | }, 66 | "pluginVersion": "7.3.5", 67 | "targets": [ 68 | { 69 | "expr": "sum by (topic)(kafka_topic_partition_under_replicated_partition)", 70 | "interval": "", 71 | "legendFormat": "{{topic}}", 72 | "queryType": "randomWalk", 73 | "refId": "A" 74 | } 75 | ], 76 | "timeFrom": null, 77 | "timeShift": null, 78 | "title": "Under Replicated Partitions", 79 | "type": "gauge" 80 | }, 81 | { 82 | "aliasColors": {}, 83 | "bars": false, 84 | "dashLength": 10, 85 | "dashes": false, 86 | "datasource": "Prometheus", 87 | "description": "Number of consumer groups ", 88 | "fieldConfig": { 89 | "defaults": { 90 | "custom": { 91 | "align": null, 92 | "filterable": false 93 | }, 94 | "mappings": [], 95 | "thresholds": { 96 | "mode": "absolute", 97 | "steps": [ 98 | { 99 | "color": "green", 100 | "value": null 101 | }, 102 | { 103 | "color": "red", 104 | "value": 80 105 | } 106 | ] 107 | } 108 | }, 109 | "overrides": [] 110 | }, 111 | "fill": 1, 112 | "fillGradient": 0, 113 | "gridPos": { 114 | "h": 8, 115 | "w": 12, 116 | "x": 0, 117 | "y": 8 118 | }, 119 | "hiddenSeries": false, 120 | "id": 20, 121 | "legend": { 122 | "avg": false, 123 | "current": false, 124 | "max": false, 125 | "min": false, 126 | "show": true, 127 | "total": false, 128 | "values": false 129 | }, 130 | "lines": true, 131 | "linewidth": 1, 132 | "nullPointMode": "null", 133 | "options": { 134 | "alertThreshold": true 135 | }, 136 | "percentage": false, 137 | "pluginVersion": "7.3.5", 138 | "pointradius": 2, 139 | "points": false, 140 | "renderer": "flot", 141 | "seriesOverrides": [], 142 | "spaceLength": 10, 143 | "stack": false, 144 | "steppedLine": false, 145 | "targets": [ 146 | { 147 | "expr": "kafka_consumergroup_members", 148 | "interval": "", 149 | "legendFormat": "", 150 | "queryType": "randomWalk", 151 | "refId": "A" 152 | } 153 | ], 154 | "thresholds": [], 155 | "timeFrom": null, 156 | "timeRegions": [], 157 | "timeShift": null, 158 | "title": "Consumer Group Members", 159 | "tooltip": { 160 | "shared": true, 161 | "sort": 0, 162 | "value_type": "individual" 163 | }, 164 | "type": "graph", 165 | "xaxis": { 166 | "buckets": null, 167 | "mode": "time", 168 | "name": null, 169 | "show": true, 170 | "values": [] 171 | }, 172 | "yaxes": [ 173 | { 174 | "format": "short", 175 | "label": null, 176 | "logBase": 1, 177 | "max": null, 178 | "min": null, 179 | "show": true 180 | }, 181 | { 182 | "format": "short", 183 | "label": null, 184 | "logBase": 1, 185 | "max": null, 186 | "min": null, 187 | "show": true 188 | } 189 | ], 190 | "yaxis": { 191 | "align": false, 192 | "alignLevel": null 193 | } 194 | }, 195 | { 196 | "backgroundColor": "rgba(128,128,128,0.1)", 197 | "colorMaps": [ 198 | { 199 | "$$hashKey": "object:439", 200 | "color": "#F2495C", 201 | "text": "null" 202 | }, 203 | { 204 | "$$hashKey": "object:505", 205 | "color": "#FADE2A", 206 | "text": "Not Running" 207 | }, 208 | { 209 | "$$hashKey": "object:506", 210 | "color": "#73BF69", 211 | "text": "Running" 212 | } 213 | ], 214 | "crosshairColor": "#8F070C", 215 | "datasource": "Prometheus", 216 | "description": "Determines if zookeeper is running based on whether zookeeper is maintaining partition replicas. Ensure zookeeper is running. ", 217 | "display": "timeline", 218 | "expandFromQueryS": 0, 219 | "extendLastValue": true, 220 | "fieldConfig": { 221 | "defaults": { 222 | "custom": { 223 | "align": null, 224 | "filterable": false 225 | }, 226 | "mappings": [], 227 | "thresholds": { 228 | "mode": "absolute", 229 | "steps": [ 230 | { 231 | "color": "green", 232 | "value": null 233 | }, 234 | { 235 | "color": "red", 236 | "value": 80 237 | } 238 | ] 239 | } 240 | }, 241 | "overrides": [] 242 | }, 243 | "gridPos": { 244 | "h": 8, 245 | "w": 12, 246 | "x": 0, 247 | "y": 16 248 | }, 249 | "highlightOnMouseover": true, 250 | "id": 18, 251 | "legendSortBy": "-ms", 252 | "lineColor": "rgba(0,0,0,0.1)", 253 | "metricNameColor": "#000000", 254 | "pluginVersion": "7.3.5", 255 | "rangeMaps": [ 256 | { 257 | "$$hashKey": "object:289", 258 | "from": "1", 259 | "text": "Running", 260 | "to": "999999999" 261 | }, 262 | { 263 | "$$hashKey": "object:305", 264 | "from": "-999", 265 | "text": "Not Running", 266 | "to": "0" 267 | } 268 | ], 269 | "rowHeight": 50, 270 | "showLegend": true, 271 | "showLegendNames": true, 272 | "showLegendPercent": true, 273 | "showLegendValues": true, 274 | "showTimeAxis": true, 275 | "targets": [ 276 | { 277 | "expr": "sum(kafka_topic_partition_replicas)", 278 | "interval": "", 279 | "legendFormat": " ", 280 | "queryType": "randomWalk", 281 | "refId": "A" 282 | } 283 | ], 284 | "textSize": 24, 285 | "textSizeTime": 12, 286 | "timeFrom": null, 287 | "timeOptions": [ 288 | { 289 | "name": "Years", 290 | "value": "years" 291 | }, 292 | { 293 | "name": "Months", 294 | "value": "months" 295 | }, 296 | { 297 | "name": "Weeks", 298 | "value": "weeks" 299 | }, 300 | { 301 | "name": "Days", 302 | "value": "days" 303 | }, 304 | { 305 | "name": "Hours", 306 | "value": "hours" 307 | }, 308 | { 309 | "name": "Minutes", 310 | "value": "minutes" 311 | }, 312 | { 313 | "name": "Seconds", 314 | "value": "seconds" 315 | }, 316 | { 317 | "name": "Milliseconds", 318 | "value": "milliseconds" 319 | } 320 | ], 321 | "timePrecision": { 322 | "name": "Minutes", 323 | "value": "minutes" 324 | }, 325 | "timeShift": null, 326 | "timeTextColor": "#d8d9da", 327 | "title": "Zookeeper Status", 328 | "type": "natel-discrete-panel", 329 | "units": "short", 330 | "useTimePrecision": false, 331 | "valueMaps": [ 332 | { 333 | "$$hashKey": "object:287", 334 | "op": "=", 335 | "text": "Not Running", 336 | "value": "null" 337 | }, 338 | { 339 | "$$hashKey": "object:291", 340 | "op": "=", 341 | "text": "", 342 | "value": "" 343 | } 344 | ], 345 | "valueTextColor": "#000000", 346 | "writeAllValues": false, 347 | "writeLastValue": true, 348 | "writeMetricNames": false 349 | }, 350 | { 351 | "datasource": "Prometheus", 352 | "description": "Displays the total messages split by topic. ", 353 | "fieldConfig": { 354 | "defaults": { 355 | "custom": {}, 356 | "mappings": [], 357 | "thresholds": { 358 | "mode": "absolute", 359 | "steps": [ 360 | { 361 | "color": "green", 362 | "value": null 363 | }, 364 | { 365 | "color": "red", 366 | "value": 80 367 | } 368 | ] 369 | } 370 | }, 371 | "overrides": [] 372 | }, 373 | "gridPos": { 374 | "h": 8, 375 | "w": 12, 376 | "x": 0, 377 | "y": 24 378 | }, 379 | "id": 16, 380 | "options": { 381 | "colorMode": "value", 382 | "graphMode": "area", 383 | "justifyMode": "auto", 384 | "orientation": "auto", 385 | "reduceOptions": { 386 | "calcs": [ 387 | "mean" 388 | ], 389 | "fields": "", 390 | "values": false 391 | }, 392 | "textMode": "auto" 393 | }, 394 | "pluginVersion": "7.3.5", 395 | "targets": [ 396 | { 397 | "expr": "kafka_consumergroup_current_offset_sum", 398 | "interval": "", 399 | "legendFormat": "{{topic}}", 400 | "queryType": "randomWalk", 401 | "refId": "A" 402 | } 403 | ], 404 | "timeFrom": null, 405 | "timeShift": null, 406 | "title": "Messages (by Topic)", 407 | "type": "stat" 408 | }, 409 | { 410 | "aliasColors": {}, 411 | "bars": false, 412 | "dashLength": 10, 413 | "dashes": false, 414 | "datasource": "Prometheus", 415 | "description": "This is the memory usage of the Prometheus program. If the system runs out of memory, Kafka will crash. ", 416 | "fieldConfig": { 417 | "defaults": { 418 | "custom": {}, 419 | "mappings": [], 420 | "thresholds": { 421 | "mode": "absolute", 422 | "steps": [ 423 | { 424 | "color": "green", 425 | "value": null 426 | }, 427 | { 428 | "color": "red", 429 | "value": 80 430 | } 431 | ] 432 | }, 433 | "unit": "percentunit" 434 | }, 435 | "overrides": [] 436 | }, 437 | "fill": 1, 438 | "fillGradient": 0, 439 | "gridPos": { 440 | "h": 8, 441 | "w": 12, 442 | "x": 0, 443 | "y": 32 444 | }, 445 | "hiddenSeries": false, 446 | "id": 14, 447 | "legend": { 448 | "avg": false, 449 | "current": false, 450 | "max": false, 451 | "min": false, 452 | "show": true, 453 | "total": false, 454 | "values": false 455 | }, 456 | "lines": true, 457 | "linewidth": 1, 458 | "nullPointMode": "null", 459 | "options": { 460 | "alertThreshold": true 461 | }, 462 | "percentage": false, 463 | "pluginVersion": "7.3.5", 464 | "pointradius": 2, 465 | "points": false, 466 | "renderer": "flot", 467 | "seriesOverrides": [], 468 | "spaceLength": 10, 469 | "stack": false, 470 | "steppedLine": false, 471 | "targets": [ 472 | { 473 | "expr": "(node_memory_MemTotal_bytes - node_memory_MemFree_bytes - node_memory_Buffers_bytes - node_memory_Cached_bytes)/node_memory_MemTotal_bytes", 474 | "interval": "", 475 | "legendFormat": "Prometheus", 476 | "queryType": "randomWalk", 477 | "refId": "A" 478 | } 479 | ], 480 | "thresholds": [ 481 | { 482 | "$$hashKey": "object:382", 483 | "colorMode": "critical", 484 | "fill": true, 485 | "line": true, 486 | "op": "gt", 487 | "value": 90, 488 | "yaxis": "left" 489 | } 490 | ], 491 | "timeFrom": null, 492 | "timeRegions": [], 493 | "timeShift": null, 494 | "title": "Memory Usage ", 495 | "tooltip": { 496 | "shared": true, 497 | "sort": 0, 498 | "value_type": "individual" 499 | }, 500 | "type": "graph", 501 | "xaxis": { 502 | "buckets": null, 503 | "mode": "time", 504 | "name": null, 505 | "show": true, 506 | "values": [] 507 | }, 508 | "yaxes": [ 509 | { 510 | "format": "percentunit", 511 | "label": null, 512 | "logBase": 1, 513 | "max": null, 514 | "min": null, 515 | "show": true 516 | }, 517 | { 518 | "format": "short", 519 | "label": null, 520 | "logBase": 1, 521 | "max": null, 522 | "min": null, 523 | "show": true 524 | } 525 | ], 526 | "yaxis": { 527 | "align": false, 528 | "alignLevel": null 529 | } 530 | }, 531 | { 532 | "aliasColors": {}, 533 | "bars": false, 534 | "dashLength": 10, 535 | "dashes": false, 536 | "datasource": "Prometheus", 537 | "description": "This is the number of replicated partitions in different brokers. We want more than one replication for a more secure system. ", 538 | "fieldConfig": { 539 | "defaults": { 540 | "custom": { 541 | "align": null, 542 | "filterable": false 543 | }, 544 | "mappings": [], 545 | "thresholds": { 546 | "mode": "absolute", 547 | "steps": [ 548 | { 549 | "color": "green", 550 | "value": null 551 | }, 552 | { 553 | "color": "red", 554 | "value": 80 555 | } 556 | ] 557 | } 558 | }, 559 | "overrides": [] 560 | }, 561 | "fill": 1, 562 | "fillGradient": 0, 563 | "gridPos": { 564 | "h": 8, 565 | "w": 12, 566 | "x": 0, 567 | "y": 40 568 | }, 569 | "hiddenSeries": false, 570 | "id": 12, 571 | "legend": { 572 | "avg": false, 573 | "current": false, 574 | "max": false, 575 | "min": false, 576 | "show": true, 577 | "total": false, 578 | "values": false 579 | }, 580 | "lines": true, 581 | "linewidth": 1, 582 | "nullPointMode": "null", 583 | "options": { 584 | "alertThreshold": true 585 | }, 586 | "percentage": false, 587 | "pluginVersion": "7.3.5", 588 | "pointradius": 2, 589 | "points": false, 590 | "renderer": "flot", 591 | "seriesOverrides": [], 592 | "spaceLength": 10, 593 | "stack": false, 594 | "steppedLine": false, 595 | "targets": [ 596 | { 597 | "expr": "sum(kafka_topic_partition_replicas)", 598 | "interval": "", 599 | "legendFormat": "", 600 | "queryType": "randomWalk", 601 | "refId": "A" 602 | } 603 | ], 604 | "thresholds": [], 605 | "timeFrom": null, 606 | "timeRegions": [], 607 | "timeShift": null, 608 | "title": "Number of topic partition replicas", 609 | "tooltip": { 610 | "shared": true, 611 | "sort": 0, 612 | "value_type": "individual" 613 | }, 614 | "type": "graph", 615 | "xaxis": { 616 | "buckets": null, 617 | "mode": "time", 618 | "name": null, 619 | "show": true, 620 | "values": [] 621 | }, 622 | "yaxes": [ 623 | { 624 | "format": "short", 625 | "label": null, 626 | "logBase": 1, 627 | "max": null, 628 | "min": null, 629 | "show": true 630 | }, 631 | { 632 | "format": "short", 633 | "label": null, 634 | "logBase": 1, 635 | "max": null, 636 | "min": null, 637 | "show": true 638 | } 639 | ], 640 | "yaxis": { 641 | "align": false, 642 | "alignLevel": null 643 | } 644 | }, 645 | { 646 | "aliasColors": {}, 647 | "bars": false, 648 | "dashLength": 10, 649 | "dashes": false, 650 | "datasource": "Prometheus", 651 | "description": "This is the total lag time for the consumers receiving messages in the kafka cluster. ", 652 | "fieldConfig": { 653 | "defaults": { 654 | "custom": {}, 655 | "mappings": [], 656 | "thresholds": { 657 | "mode": "absolute", 658 | "steps": [ 659 | { 660 | "color": "green", 661 | "value": null 662 | }, 663 | { 664 | "color": "red", 665 | "value": 80 666 | } 667 | ] 668 | } 669 | }, 670 | "overrides": [] 671 | }, 672 | "fill": 1, 673 | "fillGradient": 0, 674 | "gridPos": { 675 | "h": 8, 676 | "w": 12, 677 | "x": 0, 678 | "y": 48 679 | }, 680 | "hiddenSeries": false, 681 | "id": 10, 682 | "legend": { 683 | "avg": false, 684 | "current": false, 685 | "max": false, 686 | "min": false, 687 | "show": true, 688 | "total": false, 689 | "values": false 690 | }, 691 | "lines": true, 692 | "linewidth": 1, 693 | "nullPointMode": "null", 694 | "options": { 695 | "alertThreshold": true 696 | }, 697 | "percentage": false, 698 | "pluginVersion": "7.3.5", 699 | "pointradius": 2, 700 | "points": false, 701 | "renderer": "flot", 702 | "seriesOverrides": [], 703 | "spaceLength": 10, 704 | "stack": false, 705 | "steppedLine": false, 706 | "targets": [ 707 | { 708 | "expr": "-kafka_consumergroup_lag_sum", 709 | "interval": "", 710 | "legendFormat": "", 711 | "queryType": "randomWalk", 712 | "refId": "A" 713 | } 714 | ], 715 | "thresholds": [], 716 | "timeFrom": null, 717 | "timeRegions": [], 718 | "timeShift": null, 719 | "title": "Consumer Lag", 720 | "tooltip": { 721 | "shared": true, 722 | "sort": 0, 723 | "value_type": "individual" 724 | }, 725 | "type": "graph", 726 | "xaxis": { 727 | "buckets": null, 728 | "mode": "time", 729 | "name": null, 730 | "show": true, 731 | "values": [] 732 | }, 733 | "yaxes": [ 734 | { 735 | "format": "short", 736 | "label": null, 737 | "logBase": 1, 738 | "max": null, 739 | "min": null, 740 | "show": true 741 | }, 742 | { 743 | "format": "short", 744 | "label": null, 745 | "logBase": 1, 746 | "max": null, 747 | "min": null, 748 | "show": true 749 | } 750 | ], 751 | "yaxis": { 752 | "align": false, 753 | "alignLevel": null 754 | } 755 | }, 756 | { 757 | "datasource": "Prometheus", 758 | "description": "This is the number of brokers in the kafka cluster. ", 759 | "fieldConfig": { 760 | "defaults": { 761 | "custom": { 762 | "align": null, 763 | "filterable": false 764 | }, 765 | "mappings": [], 766 | "thresholds": { 767 | "mode": "absolute", 768 | "steps": [ 769 | { 770 | "color": "green", 771 | "value": null 772 | }, 773 | { 774 | "color": "red", 775 | "value": 80 776 | } 777 | ] 778 | } 779 | }, 780 | "overrides": [] 781 | }, 782 | "gridPos": { 783 | "h": 8, 784 | "w": 12, 785 | "x": 0, 786 | "y": 56 787 | }, 788 | "id": 8, 789 | "options": { 790 | "colorMode": "value", 791 | "graphMode": "none", 792 | "justifyMode": "auto", 793 | "orientation": "auto", 794 | "reduceOptions": { 795 | "calcs": [ 796 | "last" 797 | ], 798 | "fields": "", 799 | "values": false 800 | }, 801 | "textMode": "auto" 802 | }, 803 | "pluginVersion": "7.3.5", 804 | "targets": [ 805 | { 806 | "expr": "sum(kafka_brokers)", 807 | "interval": "", 808 | "legendFormat": "", 809 | "queryType": "randomWalk", 810 | "refId": "A" 811 | } 812 | ], 813 | "timeFrom": null, 814 | "timeShift": null, 815 | "title": "Number of Brokers", 816 | "transparent": true, 817 | "type": "stat" 818 | }, 819 | { 820 | "datasource": "Prometheus", 821 | "description": "This is the percent of CPU usage for your system. Keep it lower than 80% for a buffer. If this percentage hits 100%, kafka will have large performance issues. ", 822 | "fieldConfig": { 823 | "defaults": { 824 | "custom": {}, 825 | "mappings": [], 826 | "thresholds": { 827 | "mode": "absolute", 828 | "steps": [ 829 | { 830 | "color": "green", 831 | "value": null 832 | }, 833 | { 834 | "color": "#EAB839", 835 | "value": 60 836 | }, 837 | { 838 | "color": "red", 839 | "value": 80 840 | } 841 | ] 842 | }, 843 | "unit": "percent" 844 | }, 845 | "overrides": [] 846 | }, 847 | "gridPos": { 848 | "h": 8, 849 | "w": 12, 850 | "x": 0, 851 | "y": 64 852 | }, 853 | "id": 6, 854 | "options": { 855 | "colorMode": "value", 856 | "graphMode": "area", 857 | "justifyMode": "auto", 858 | "orientation": "auto", 859 | "reduceOptions": { 860 | "calcs": [ 861 | "lastNotNull" 862 | ], 863 | "fields": "", 864 | "values": false 865 | }, 866 | "textMode": "auto" 867 | }, 868 | "pluginVersion": "7.3.5", 869 | "targets": [ 870 | { 871 | "expr": "avg by (instance,mode) (irate(node_cpu_seconds_total{mode='system'}[5m])) *500", 872 | "interval": "", 873 | "legendFormat": "", 874 | "queryType": "randomWalk", 875 | "refId": "A" 876 | } 877 | ], 878 | "timeFrom": null, 879 | "timeShift": null, 880 | "title": "CPU Usage Percentage", 881 | "type": "stat" 882 | }, 883 | { 884 | "datasource": "Prometheus", 885 | "description": "This shows the total space available on your computer. Make sure to have more than 100 GB to meet operational standards.", 886 | "fieldConfig": { 887 | "defaults": { 888 | "custom": {}, 889 | "mappings": [], 890 | "thresholds": { 891 | "mode": "absolute", 892 | "steps": [ 893 | { 894 | "color": "red", 895 | "value": null 896 | }, 897 | { 898 | "color": "#EAB839", 899 | "value": 30 900 | }, 901 | { 902 | "color": "green", 903 | "value": 100 904 | } 905 | ] 906 | }, 907 | "unit": "decbytes" 908 | }, 909 | "overrides": [] 910 | }, 911 | "gridPos": { 912 | "h": 8, 913 | "w": 12, 914 | "x": 0, 915 | "y": 72 916 | }, 917 | "id": 4, 918 | "options": { 919 | "colorMode": "value", 920 | "graphMode": "none", 921 | "justifyMode": "auto", 922 | "orientation": "auto", 923 | "reduceOptions": { 924 | "calcs": [ 925 | "last" 926 | ], 927 | "fields": "", 928 | "values": false 929 | }, 930 | "textMode": "auto" 931 | }, 932 | "pluginVersion": "7.3.5", 933 | "targets": [ 934 | { 935 | "expr": "Max(node_filesystem_avail_bytes)", 936 | "interval": "", 937 | "legendFormat": "", 938 | "queryType": "randomWalk", 939 | "refId": "A" 940 | } 941 | ], 942 | "timeFrom": null, 943 | "timeShift": null, 944 | "title": "Amount of available space", 945 | "type": "stat" 946 | }, 947 | { 948 | "datasource": "Prometheus", 949 | "description": "Total messages received since cluster opened.", 950 | "fieldConfig": { 951 | "defaults": { 952 | "color": { 953 | "mode": "thresholds" 954 | }, 955 | "custom": { 956 | "align": null, 957 | "filterable": false 958 | }, 959 | "mappings": [], 960 | "noValue": "0", 961 | "thresholds": { 962 | "mode": "absolute", 963 | "steps": [ 964 | { 965 | "color": "rgb(68, 129, 196)", 966 | "value": null 967 | } 968 | ] 969 | }, 970 | "unit": "Messages" 971 | }, 972 | "overrides": [] 973 | }, 974 | "gridPos": { 975 | "h": 9, 976 | "w": 12, 977 | "x": 0, 978 | "y": 80 979 | }, 980 | "id": 2, 981 | "options": { 982 | "colorMode": "value", 983 | "graphMode": "none", 984 | "justifyMode": "auto", 985 | "orientation": "auto", 986 | "reduceOptions": { 987 | "calcs": [ 988 | "max" 989 | ], 990 | "fields": "", 991 | "values": false 992 | }, 993 | "textMode": "auto" 994 | }, 995 | "pluginVersion": "7.3.5", 996 | "targets": [ 997 | { 998 | "expr": "sum(kafka_consumergroup_current_offset_sum)", 999 | "interval": "", 1000 | "legendFormat": "", 1001 | "queryType": "randomWalk", 1002 | "refId": "A" 1003 | } 1004 | ], 1005 | "timeFrom": null, 1006 | "timeShift": null, 1007 | "title": "Total Messages", 1008 | "type": "stat" 1009 | } 1010 | ], 1011 | "refresh": "5s", 1012 | "schemaVersion": 26, 1013 | "style": "dark", 1014 | "tags": [ 1015 | "kafka" 1016 | ], 1017 | "templating": { 1018 | "list": [] 1019 | }, 1020 | "time": { 1021 | "from": "now-30m", 1022 | "to": "now" 1023 | }, 1024 | "timepicker": { 1025 | "refresh_intervals": [ 1026 | "5s", 1027 | "10s", 1028 | "30s", 1029 | "1m", 1030 | "5m", 1031 | "15m" 1032 | ] 1033 | }, 1034 | "timezone": "", 1035 | "title": "Kafka", 1036 | "uid": "2LyxeP1Mk", 1037 | "version": 1 1038 | } -------------------------------------------------------------------------------- /kafka-playground/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 | # whats 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. direct or proxy. 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: admin 34 | # enable/disable with credentials headers 35 | withCredentials: 36 | # mark as default datasource. Max one per org 37 | isDefault: 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 -------------------------------------------------------------------------------- /kafka-playground/src/consumer.js: -------------------------------------------------------------------------------- 1 | //const Kafka = require("kafkajs").Kafka 2 | const { Kafka } = require('kafkajs'); 3 | const ip = require('ip'); 4 | const host = process.env.HOST_IP || ip.address(); 5 | 6 | const runConsumer = async (topic) => { 7 | try { 8 | const kafka = new Kafka({ 9 | clientId: 'myapp', 10 | brokers: [`${host}:9092`], 11 | }); 12 | 13 | const consumer = kafka.consumer({ groupId: 'test' }); 14 | console.log('Connecting.....'); 15 | await consumer.connect(); 16 | console.log('Connected!'); 17 | 18 | await consumer.subscribe({ 19 | topic: topic, 20 | fromBeginning: true, 21 | }); 22 | 23 | console.log(`Subscribed to topic ${topic}`); 24 | await consumer.run({ 25 | eachMessage: async (result) => { 26 | console.log('The result is actually ', result); 27 | console.log( 28 | `RVD Msg ${result.message.value} on partition ${result.partition}` 29 | ); 30 | }, 31 | }); 32 | } catch (ex) { 33 | console.error(`Something bad happened ${ex}`); 34 | } 35 | }; 36 | 37 | module.exports = { runConsumer }; 38 | -------------------------------------------------------------------------------- /kafka-playground/src/consumer2.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require('kafkajs'); 2 | const ip = require('ip'); 3 | const host = process.env.HOST_IP || ip.address(); 4 | 5 | run(); 6 | async function run() { 7 | try { 8 | const kafka = new Kafka({ 9 | clientId: 'myapp', 10 | brokers: [`${host}:9092`], 11 | }); 12 | 13 | const consumer = kafka.consumer({ groupId: 'test' }); 14 | console.log('Connecting.....'); 15 | await consumer.connect(); 16 | console.log('Connected!'); 17 | 18 | await consumer.subscribe({ 19 | topic: 'Users', 20 | fromBeginning: true, 21 | }); 22 | 23 | console.log('Subscribed!'); 24 | await consumer.run({ 25 | eachMessage: async (result) => { 26 | console.log('The result is actually ', result); 27 | console.log( 28 | `RVD Msg ${result.message.value} on partition ${result.partition}` 29 | ); 30 | }, 31 | }); 32 | } catch (ex) { 33 | console.error(`Something bad happened ${ex}`); 34 | } finally { 35 | } 36 | } -------------------------------------------------------------------------------- /kafka-playground/src/producer.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require('kafkajs'); 2 | 3 | const ip = require('ip'); 4 | //const { module } = require('../../webpack.config'); 5 | const host = process.env.HOST_IP || ip.address(); 6 | 7 | const runProducer = async (topic, msg) => { 8 | try { 9 | const kafka = new Kafka({ 10 | clientId: 'myapp', 11 | brokers: [`${host}:9092`], 12 | }); 13 | 14 | const producer = kafka.producer(); 15 | console.log('Connecting.....'); 16 | await producer.connect(); 17 | console.log('Connected!'); 18 | //A-M 0 , N-Z 1 19 | const partition = msg[0] < 'N' ? 0 : 1; 20 | const result = await producer.send({ 21 | topic: topic, 22 | messages: [ 23 | { 24 | value: msg, 25 | partition: partition, 26 | }, 27 | ], 28 | }); 29 | 30 | console.log(`Send Successfully! ${JSON.stringify(result)}`); 31 | 32 | //await producer.disconnect(); 33 | } catch (ex) { 34 | console.error(`Something bad happened ${ex}`); 35 | } 36 | }; 37 | module.exports = { runProducer }; 38 | -------------------------------------------------------------------------------- /kafka-playground/src/producer2.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require('kafkajs'); 2 | const msg = process.argv[2]; 3 | const ip = require('ip'); 4 | const host = process.env.HOST_IP || ip.address(); 5 | run(); 6 | async function run() { 7 | try { 8 | const kafka = new Kafka({ 9 | clientId: 'myapp', 10 | brokers: [`${host}:9092`], 11 | }); 12 | 13 | const producer = kafka.producer(); 14 | console.log('Connecting.....'); 15 | await producer.connect(); 16 | console.log('Connected!'); 17 | //A-M 0 , N-Z 1 18 | const partition = msg[0] < 'N' ? 0 : 1; 19 | const result = await producer.send({ 20 | topic: 'Users', 21 | messages: [ 22 | { 23 | value: msg, 24 | partition: partition, 25 | }, 26 | ], 27 | }); 28 | console.log('This is the result: ', result); 29 | 30 | console.log(`Send Successfully! ${JSON.stringify(result)}`); 31 | await producer.disconnect(); 32 | } catch (ex) { 33 | console.error(`Something bad happened ${ex}`); 34 | } finally { 35 | process.exit(0); 36 | } 37 | } -------------------------------------------------------------------------------- /kafka-playground/src/topic.js: -------------------------------------------------------------------------------- 1 | //const Kafka = require("kafkajs").Kafka 2 | const { Kafka } = require('kafkajs'); 3 | const ip = require('ip'); 4 | //const { module } = require('../../webpack.config'); 5 | // const dns = require('dns'); 6 | // const w3 = dns.lookupService('localhost', 9092, (err, value) => { if (err){console.log(err); return;}console.log(value)}); 7 | const host = process.env.HOST_IP || ip.address(); 8 | 9 | const runTopic = async (topic, n) => { 10 | try { 11 | const kafka = new Kafka({ 12 | clientId: 'myapp', 13 | // ssl: true, 14 | brokers: [`${host}:9092`], 15 | }); 16 | 17 | const admin = kafka.admin(); 18 | console.log('Connecting.....'); 19 | await admin.connect(); 20 | console.log('Connected!'); 21 | //A-M, N-Z 22 | await admin.createTopics({ 23 | topics: [ 24 | { 25 | topic: topic, 26 | numPartitions: n, 27 | }, 28 | ], 29 | }); 30 | console.log( 31 | `Created Successfully! The topic name is ${topic}, number of partitions is ${n}` 32 | ); 33 | const allTopics = await admin.listTopics(); 34 | console.log(allTopics); 35 | //await admin.disconnect(); 36 | } catch (ex) { 37 | console.error(`Something bad happened ${ex}`); 38 | } 39 | // finally { 40 | // process.exit(0); 41 | // } 42 | }; 43 | //runTopic('happpy', 2); 44 | module.exports = { runTopic }; 45 | -------------------------------------------------------------------------------- /kafka-playground/src/topic2.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require('kafkajs'); 2 | const ip = require('ip'); 3 | const host = process.env.HOST_IP || ip.address(); 4 | run(); 5 | async function run() { 6 | try { 7 | const kafka = new Kafka({ 8 | clientId: 'myapp', 9 | brokers: [`${host}:9092`], 10 | }); 11 | 12 | const admin = kafka.admin(); 13 | console.log('Connecting.....'); 14 | await admin.connect(); 15 | console.log('Connected!'); 16 | //A-M, N-Z 17 | await admin.createTopics({ 18 | topics: [ 19 | { 20 | topic: 'Users', 21 | numPartitions: 2, 22 | }, 23 | ], 24 | }); 25 | console.log('Created Successfully!'); 26 | await admin.disconnect(); 27 | } catch (ex) { 28 | console.error(`Something bad happened ${ex}`); 29 | } finally { 30 | process.exit(0); 31 | } 32 | } -------------------------------------------------------------------------------- /kafka-playground/streaming_data/consumer-jd.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require('kafkajs'); 2 | const ip = require('ip'); 3 | 4 | const host = process.env.HOST_IP || ip.address(); 5 | 6 | async function run() { 7 | try { 8 | const kafka = new Kafka({ 9 | clientId: 'myapp2', 10 | brokers: [`${host}:9092`], 11 | }); 12 | 13 | const consumer = kafka.consumer({ groupId: 'tech-jobs' }); 14 | console.log('Connecting.....'); 15 | await consumer.connect(); 16 | console.log('Connected!'); 17 | 18 | await consumer.subscribe({ 19 | topic: 'jobs', 20 | fromBeginning: true, 21 | }); 22 | 23 | console.log('Subscribed!'); 24 | await consumer.run({ 25 | eachMessage: async (result) => { 26 | console.log( 27 | 'The consumed message is ', 28 | JSON.parse(result.message.value.toString()) 29 | ); 30 | }, 31 | }); 32 | } catch (ex) { 33 | console.error(`Error in consuming ${ex}`); 34 | } finally { 35 | } 36 | } 37 | 38 | run(); 39 | -------------------------------------------------------------------------------- /kafka-playground/streaming_data/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | zookeeper: 4 | image: wurstmeister/zookeeper:latest 5 | ports: 6 | - '2181:2181' 7 | kafka: 8 | image: wurstmeister/kafka:2.11-1.1.1 9 | ports: 10 | - '9092:9092' 11 | links: 12 | - zookeeper 13 | environment: 14 | KAFKA_ADVERTISED_HOST_NAME: ${HOST_IP} 15 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 16 | KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' 17 | KAFKA_DELETE_TOPIC_ENABLE: 'true' 18 | KAFKA_CREATE_TOPICS: 'topic-test:1:1' 19 | volumes: 20 | - /var/run/docker.sock:/var/run/docker.sock 21 | -------------------------------------------------------------------------------- /kafka-playground/streaming_data/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const fetch = require('node-fetch'); 3 | const { runProducer } = require('./producer'); 4 | 5 | let url = 'https://data.cityofnewyork.us/resource/kpav-sd4t.json'; 6 | 7 | let settings = { method: 'Get' }; 8 | 9 | const getJobData = () => { 10 | fetch(url, settings) 11 | .then((res) => res.json()) 12 | .then((json) => { 13 | for (let i = 0; i < json.length; i++) { 14 | setTimeout(() => { 15 | const data = [ 16 | { 17 | salary: `${json[i].job_id}: ${json[i].salary_range_from}-${json[i].salary_range_to}`, 18 | }, 19 | ]; 20 | runProducer('jobs', JSON.stringify(data)); 21 | }, i * 1000); 22 | } 23 | }); 24 | }; 25 | getJobData(); 26 | 27 | //Here is an example of an element of the json array from the fetch request 28 | /*{ 29 | job_id: '416232', 30 | agency: 'DEPT OF HEALTH/MENTAL HYGIENE', 31 | posting_type: 'Internal', 32 | number_of_positions: '2', 33 | business_title: 'Principal Administrative Associate, Bureau of Environmental Disease and Injury Prevention/Healthy Homes Program', 34 | civil_service_title: 'PRINCIPAL ADMINISTRATIVE ASSOC', 35 | title_classification: 'Competitive-1', 36 | title_code_no: '10124', 37 | level: '02', 38 | job_category: 'Administration & Human Resources Health', 39 | full_time_part_time_indicator: 'F', 40 | career_level: 'Experienced (non-manager)', 41 | salary_range_from: '49390', 42 | salary_range_to: '61341.84', 43 | salary_frequency: 'Annual', 44 | work_location: '125 Worth Street, Nyc', 45 | division_work_unit: 'Healthy Homes Progam', 46 | job_description: '**OPEN TO PERMANENT PRINCIPAL ADMINISTRATIVE ASSOCIATES ONLY. YOU MUST CLEARLY STATE YOUR CIVIL SERVICE STATUS ON YOUR RESUME OR COVER LETTER. FAILURE TO DO SO WILL RESULT IN YOUR DISQUALIFICATION. The mission of the Bureau of Environmental Disease and Injury Prevention is to prevent environmental disease and injury in homes, communities and the workplace, and to protect health by promoting healthy environments and health equity. The Bureau is comprised of four Programs - Healthy Homes, Environmental Health Assessment and Communications, Poison Control Center, and Injury and Violence Prevention. DUTIES WILL INCLUDE BUT NOT BE LIMITED TO: •\tSupervise unit staff as assigned in new job procedures, program rules, policies and procedures. •\tAnswer telephone calls and correspondence requests from public and other agencies. •\tMaintain records/datasets to document hotline associated activities. •\tPrepare educational materials for distribution or mailing. •\tProvide owners/agents with the necessary information needed to comply with Commissioner’s Order to abate/remediate. •\tReview work of staff and take corrective action to maintain proficiency. •\tMaintain all patient/medical documents in a confidential manner. •\tPerform other required duties as assigned.', 47 | minimum_qual_requirements: "1. A baccalaureate degree from an accredited college and three years of satisfactory full-time progressively responsible clerical/administrative experience, one year of which must have been in an administrative capacity or supervising staff performing clerical/administrative work of more than moderate difficulty; or 2. An associate degree or 60 semester credits from an accredited college and four years of satisfactory full-time progressively responsible clerical/administrative experience including one year of the administrative supervisory experience described in 1 above; or 3. A four-year high school diploma or its educational equivalent approved by a State's department of education or a recognized accrediting organization and five years of satisfactory full-time progressively responsible clerical/administrative experience including one year of the administrative supervisory experience as described in 1 above; 4. Education and/or experience equivalent to 1, 2, or 3 above. However, all candidates must possess the one year of administrative or supervisory experience as described in 1 above. Education above the high school level may be substituted for the general clerical/administrative experience (but not for the one year of administrative or supervisory experience described in 1 above) at a rate of 30 semester credits from an accredited college for 6 months of experience up to a maximum of 3½ years.", 48 | preferred_skills: 'Candidates should have a strong customer service background, excellent oral and written communication skills, courteous telephone manner, basic computer skills with the desire to learn additional skills.', 49 | additional_information: '**IMPORTANT NOTES TO ALL CANDIDATES: Please note: If you are called for an interview you will be required to bring to your interview copies of original documentation, such as: • A document that establishes identity for employment eligibility, such as: A Valid U.S. Passport, Permanent Resident Card/Green Card, or Driver’s license. • Proof of Education according to the education requirements of the civil service title. • Current Resume • Proof of Address/NYC Residency dated within the last 60 days, such as: Recent Utility Bill (i.e. Telephone, Cable, Mobile Phone) Additional documentation may be required to evaluate your qualification as outlined in this posting’s “Minimum Qualification Requirements” section. Examples of additional documentation may be, but not limited to: college transcript, experience verification or professional trade licenses. If after your interview you are the selected candidate you will be contacted to schedule an on-boarding appointment. By the time of this appointment you will be asked to produce the originals of the above documents along with your original Social Security card. **LOAN FORGIVENESS The federal government provides student loan forgiveness through its Public Service Loan Forgiveness Program (PSLF) to all qualifying public service employees. Working with the DOHMH qualifies you as a public service employee and you may be able to take advantage of this program while working full-time and meeting the program’s other requirements. Please visit the Public Service Loan Forgiveness Program site to view the eligibility requirements: https://studentaid.ed.gov/sa/repay-loans/forgiveness-cancellation/public-service', 50 | to_apply: 'Apply online with a cover letter to https://a127-jobs.nyc.gov/. In the Job ID search bar, enter: job ID number # 416232. We appreciate the interest and thank all applicants who apply, but only those candidates under consideration will be contacted. The NYC Health Department is committed to recruiting and retaining a diverse and culturally responsive workforce. We strongly encourage people of color, people with disabilities, veterans, women, and lesbian, gay, bisexual, and transgender and gender non-conforming persons to apply. All applicants will be considered without regard to actual or perceived race, color, national origin, religion, sexual orientation, marital or parental status, disability, sex, gender identity or expression, age, prior record of arrest; or any other basis prohibited by law.', 51 | residency_requirement: 'New York City residency is generally required within 90 days of appointment. However, City Employees in certain titles who have worked for the City for 2 continuous years may also be eligible to reside in Nassau, Suffolk, Putnam, Westchester, Rockland, or Orange County. To determine if the residency requirement applies to you, please discuss with the agency representative at the time of interview.', 52 | posting_date: '2020-12-07T00:00:00.000', 53 | post_until: '07-MAR-2021', 54 | posting_updated: '2020-12-07T00:00:00.000', 55 | process_date: '2020-12-29T00:00:00.000' 56 | }, 57 | */ 58 | -------------------------------------------------------------------------------- /kafka-playground/streaming_data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "streaming_data", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "topic": "node topic.js", 9 | "index": "node index.js", 10 | "jd": "node consumer-jd.js" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "ip": "^1.1.5", 16 | "kafkajs": "^1.15.0", 17 | "node-fetch": "^2.6.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /kafka-playground/streaming_data/producer.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require('kafkajs'); 2 | 3 | const ip = require('ip'); 4 | 5 | const host = process.env.HOST_IP || ip.address(); 6 | 7 | const runProducer = async (topic, msg) => { 8 | try { 9 | const kafka = new Kafka({ 10 | clientId: 'myapp2', 11 | brokers: [`${host}:9092`], 12 | }); 13 | 14 | const producer = kafka.producer(); 15 | console.log('Connecting.....'); 16 | await producer.connect(); 17 | console.log('Connected!'); 18 | 19 | const partArr = [0, 1, 2]; 20 | const partition = partArr[Math.floor(Math.random() * partArr.length)]; 21 | 22 | const result = await producer.send({ 23 | topic: topic, 24 | messages: [ 25 | { 26 | value: msg, 27 | partition: partition, 28 | }, 29 | ], 30 | }); 31 | 32 | console.log(`Send Successfully! ${JSON.stringify(result)}`); 33 | 34 | await producer.disconnect(); 35 | } catch (ex) { 36 | console.error(`There was an error producing messages ${ex}`); 37 | } 38 | }; 39 | 40 | module.exports = { runProducer }; 41 | -------------------------------------------------------------------------------- /kafka-playground/streaming_data/topic.js: -------------------------------------------------------------------------------- 1 | const { Kafka } = require('kafkajs'); 2 | const ip = require('ip'); 3 | 4 | const host = process.env.HOST_IP || ip.address(); 5 | 6 | async function run(topic, n) { 7 | try { 8 | const kafka = new Kafka({ 9 | clientId: 'myapp2', 10 | brokers: [`${host}:9092`], 11 | }); 12 | 13 | const admin = kafka.admin(); 14 | console.log('Connecting.....'); 15 | await admin.connect(); 16 | console.log('Connected!'); 17 | 18 | await admin.createTopics({ 19 | topics: [ 20 | { 21 | topic: topic, 22 | numPartitions: n, 23 | }, 24 | ], 25 | }); 26 | console.log( 27 | `Created Successfully! The topic name is ${topic}, number of partitions is ${n}`, 28 | ); 29 | const allTopics = await admin.listTopics(); 30 | console.log(allTopics); 31 | await admin.disconnect(); 32 | } catch (ex) { 33 | console.error(`There was an error creating topic ${ex}`); 34 | } 35 | } 36 | 37 | run('jobs', 3); 38 | -------------------------------------------------------------------------------- /kafka-playground/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: './main.ts', 6 | output: { 7 | filename: 'main.js', 8 | path: path.resolve(__dirname), 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.tsx?$/, 14 | use: 'ts-loader', 15 | exclude: /node_modules/, 16 | }, 17 | { 18 | // Converts jsx to ES5 19 | test: /\.(js|jsx)$/, 20 | exclude: /(node_modules)/, 21 | loader: 'babel-loader', 22 | // use: { //For windows users, instead of the presets here, put in a .babelrc file with the presets. 23 | // presets: ['@babel/preset-env', '@babel/preset-react'], 24 | // }, 25 | }, 26 | ], 27 | }, 28 | resolve: { 29 | extensions: ['.tsx', '.ts', '.js'], 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kafkare", 3 | "productName": "KafKare", 4 | "version": "1.0.0", 5 | "description": "Production Project", 6 | "main": "electron.js", 7 | "scripts": { 8 | "start": "cross-env NODE_ENV=production concurrently \"node server/server.js\" \"electron .\"", 9 | "build": "cross-env NODE_ENV=production webpack", 10 | "dev": "cross-env NODE_ENV=development concurrently \"webpack-dev-server --open\" \"nodemon server/server.js\"", 11 | "test": "jest --forceExit", 12 | "data-generator-dev": "cross-env NODE_ENV=development concurrently \"webpack-dev-server --open\" \"node kafka-playground/data-generator/main.ts\"", 13 | "package-mac": "electron-packager . --overwrite --platform=darwin --arch=x64 --icon=src/assets/icons/mac/icon.icns --prune=true --out=release-builds" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/lijiaxingogo/KafKare.git" 18 | }, 19 | "author": "", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/lijiaxingogo/KafKare/issues" 23 | }, 24 | "homepage": "https://github.com/lijiaxingogo/KafKare#readme", 25 | "dependencies": { 26 | "@babel/plugin-transform-runtime": "^7.13.10", 27 | "@chakra-ui/icons": "^1.0.2", 28 | "@chakra-ui/react": "^1.1.0", 29 | "@emotion/react": "^11.1.4", 30 | "@emotion/styled": "^11.0.0", 31 | "antd": "^4.9.4", 32 | "axios": "^0.21.1", 33 | "bcrypt": "^5.0.0", 34 | "concurrently": "^5.3.0", 35 | "cookie-parser": "^1.4.5", 36 | "cors": "^2.8.5", 37 | "cross-env": "^7.0.3", 38 | "css-loader": "^5.0.1", 39 | "express": "^4.17.1", 40 | "formik": "^2.2.6", 41 | "framer-motion": "^3.1.1", 42 | "jest": "^26.6.3", 43 | "jsonwebtoken": "^8.5.1", 44 | "kafkajs": "^1.15.0", 45 | "moment": "^2.29.1", 46 | "nodemon": "^2.0.6", 47 | "pg": "^8.5.1", 48 | "react": "^17.0.1", 49 | "react-dom": "^17.0.1", 50 | "react-router-dom": "^5.2.0", 51 | "sass": "^1.30.0", 52 | "sass-loader": "^10.1.0", 53 | "style-loader": "^2.0.0", 54 | "supertest": "^6.1.3", 55 | "url-loader": "^4.1.1", 56 | "yup": "^0.32.8" 57 | }, 58 | "devDependencies": { 59 | "@babel/core": "^7.12.3", 60 | "@babel/preset-env": "^7.12.1", 61 | "@babel/preset-react": "^7.12.10", 62 | "@webpack-cli/serve": "^1.1.0", 63 | "babel-core": "^6.26.3", 64 | "babel-loader": "^8.2.2", 65 | "babel-polyfill": "^6.26.0", 66 | "electron": "^11.1.1", 67 | "electron-packager": "^15.2.0", 68 | "install": "^0.13.0", 69 | "npm": "^6.14.9", 70 | "ts-loader": "^8.0.12", 71 | "typescript": "^4.1.2", 72 | "webpack": "^5.6.0", 73 | "webpack-cli": "^3.2.3", 74 | "webpack-dev-server": "^3.11.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /server/db/db.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | const PG_URI = 4 | 'postgres://zoszwsct:3VvqfB9_GFyMZULPbXOAW3pjXt8aqVFF@suleiman.db.elephantsql.com:5432/zoszwsct'; 5 | 6 | // create a new pool here using the connection string above 7 | const pool = new Pool({ 8 | connectionString: PG_URI, 9 | }); 10 | module.exports = { 11 | query: (text, params, callback) => { 12 | console.log('executed query -->', text); 13 | return pool.query(text, params, callback); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /server/models/metricsData.js: -------------------------------------------------------------------------------- 1 | const { Pool, Client } = require('pg'); 2 | 3 | const PG_URI = 4 | 'postgres://zoszwsct:3VvqfB9_GFyMZULPbXOAW3pjXt8aqVFF@suleiman.db.elephantsql.com:5432/zoszwsct'; 5 | 6 | const pool = new Pool({ 7 | PG_URI, 8 | }); 9 | 10 | pool.query('SELECT NOW()', (err, res) => { 11 | console.log(err, res); 12 | pool.end(); 13 | }); 14 | 15 | const client = new Client({ 16 | PG_URI, 17 | }); 18 | client.connect(); 19 | 20 | client.query('SELECT NOW()', (err, res) => { 21 | console.log(err, res); 22 | client.end(); 23 | }); 24 | 25 | module.exports = { 26 | query: (text, params, callback) => { 27 | console.log('executed query', text); 28 | return pool.query(text, params, callback); 29 | }, 30 | }; 31 | // 32 | -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-else-return */ 2 | const db = require('../db/db'); 3 | const express = require('express'); 4 | const bcrypt = require('bcrypt'); 5 | const moment = require('moment'); 6 | const jwt = require('jsonwebtoken'); 7 | const router = express.Router(); 8 | const saltRounds = 10; 9 | 10 | router.post('/signup', (req, res, next) => { 11 | const { name, email, password } = req.body; 12 | 13 | bcrypt.hash(password, saltRounds, (err, hash) => { 14 | const insertUsers = `INSERT INTO users(name, email, password) VALUES($1, $2, $3) RETURNING _id, email, name`; 15 | const signupValue = [name, email, hash]; 16 | db.query(insertUsers, signupValue) 17 | .then((data) => { 18 | const output = data.rows[0]; 19 | console.log('user data is', data.rows); 20 | return res.json({ 21 | id: output._id, 22 | name: output.name, 23 | email: output.email, 24 | success: true, 25 | }); 26 | }) 27 | 28 | .catch((error) => { 29 | next({ 30 | log: `ERROR: Error creating new user:${error}.`, 31 | message: { 32 | err: 33 | 'Error occurred in /signup. Check server logs for more details.', 34 | }, 35 | }); 36 | }); 37 | }); 38 | }); 39 | 40 | router.post('/login', async (req, res, next) => { 41 | const { email, password } = req.body; 42 | const findUser = `SELECT * FROM users WHERE email=$1`; 43 | const userValue = [email]; 44 | const insertJWT = `UPDATE users SET token=$1, tokenexp=$2 WHERE email=$3`; 45 | const findJWT = `SELECT token, tokenexp from users WHERE email=$1`; 46 | try { 47 | const { rows } = await db.query(findUser, userValue); 48 | console.log(rows); 49 | if (!rows) { 50 | return res.json({ 51 | loginSuccess: false, 52 | message: 'Auth failed, email not found', 53 | }); 54 | } 55 | const isMatch = await bcrypt.compare(password, rows[0].password); 56 | console.log(isMatch); 57 | if (!isMatch) { 58 | return res.json({ loginSuccess: false, message: 'Wrong password' }); 59 | } else { 60 | const token = jwt.sign(rows[0]._id.toString(16), 'secret'); 61 | const oneHour = moment().add(1, 'hour').valueOf(); 62 | const insertToken = [token, oneHour, email]; 63 | await db.query(insertJWT, insertToken); 64 | res.cookie('w_authExp', rows[0].tokenexp, { httpOnly: true }); 65 | res.cookie('w_auth', rows[0].token, { httpOnly: false }); 66 | 67 | return res.status(200).json({ 68 | loginSuccess: true, 69 | userId: rows[0]._id, 70 | }); 71 | } 72 | } catch (err) { 73 | return res.json({ 74 | loginSuccess: false, 75 | message: 'Auth failed, email not found', 76 | }); 77 | } 78 | }); 79 | 80 | router.get('/auth', (req, res) => { 81 | console.log('whatscookie', req.cookies.w_auth); 82 | const token = req.cookies.w_auth; 83 | const findToken = `SELECT * FROM users WHERE token=$1`; 84 | const tokenValue = [token]; 85 | db.query(findToken, tokenValue) 86 | .then((data) => { 87 | if (!data) { 88 | return res.json({ 89 | isAuth: false, 90 | error: true, 91 | }); 92 | } 93 | console.log(data.rows[0]); 94 | return res.status(200).json({ 95 | _id: data.rows[0]._id, 96 | isAuth: true, 97 | email: data.rows[0].email, 98 | name: data.rows[0].name, 99 | }); 100 | }) 101 | .catch((err) => res.send(err)); 102 | }); 103 | 104 | // deletes a user, returns 1 on success 105 | router.delete('/:id', (req, res) => { 106 | const { id } = req.params; 107 | const query = `DELETE FROM users WHERE _id = $1`; 108 | const inputs = [id]; 109 | 110 | db.query(query, inputs) 111 | .then((data) => { 112 | if (data) { 113 | return res.json(data.rowCount); 114 | } 115 | }) 116 | .catch((err) => res.send(err)); 117 | }); 118 | 119 | module.exports = router; 120 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const userRouter = require('./routes/user'); 4 | const app = express(); 5 | const cookieParser = require('cookie-parser'); 6 | const PORT = 3002; 7 | const cors = require('cors'); 8 | app.use(cors()); 9 | app.use(express.json()); 10 | app.use(cookieParser()); 11 | app.get('/', (req, res) => { 12 | res.sendFile(path.resolve(__dirname, '../src/index.html')); 13 | }); 14 | app.use('/user', userRouter); 15 | // global error handling 16 | app.use((err, req, res, next) => { 17 | const defaultErr = { 18 | log: 'Express error handler caught unknown middleware error', 19 | status: 400, 20 | message: { err: 'An error occurred' }, 21 | }; 22 | const errorObj = { ...defaultErr, ...err }; 23 | console.log(errorObj.log); 24 | return res.status(errorObj.status).json(errorObj.message); 25 | }); 26 | 27 | // start server 28 | const server = app.listen(PORT, function () { 29 | console.log(`server listening on ${PORT}`); 30 | }); 31 | 32 | module.exports = { server, app }; -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-width: 1805px; 3 | min-height: 100vh; 4 | background-image: linear-gradient(rgb(169, 169, 223), rgb(9, 1, 28)); 5 | } 6 | 7 | .logo-container { 8 | display: flex; 9 | margin-bottom: 20px; 10 | padding: 10px 0px 10px 0px; 11 | border-bottom: 2px solid rgb(124, 121, 121); 12 | } 13 | 14 | .body { 15 | background-color: transparent; 16 | /* background-image: linear-gradient(rgb(169, 169, 223), rgb(9, 1, 28)); */ 17 | padding: 20px; 18 | border-radius: 20px; 19 | line-height: 1.5; 20 | } 21 | 22 | .logout { 23 | display: flex; 24 | justify-content: end; 25 | align-items: flex-end; 26 | width: auto; 27 | } 28 | 29 | img { 30 | margin: auto; 31 | width: auto; 32 | height: auto; 33 | } 34 | 35 | iframe { 36 | border-bottom-left-radius: 25px; 37 | border-bottom-right-radius: 25px; 38 | border-top-right-radius: 25px; 39 | box-shadow: 5px 5px 2px 1px rgba(9, 9, 9, 0.139); 40 | } 41 | 42 | .green { 43 | color: rgb(4, 165, 4); 44 | } 45 | 46 | .component { 47 | display: flex; 48 | flex-direction: column; 49 | align-items: center; 50 | } 51 | 52 | .header-container { 53 | display: grid; 54 | grid-template-columns: repeat(auto-fit, minmax(575px, 1fr)); 55 | grid-template-rows: 170px; 56 | grid-gap: 20px; 57 | margin-bottom: 20px; 58 | } 59 | 60 | .grid-container { 61 | display: grid; 62 | grid-template-columns: repeat(2, 49.4%); 63 | grid-auto-rows: minmax(200px, auto); 64 | grid-gap: 20px; 65 | /* border: solid black 3px; */ 66 | } 67 | 68 | .grid-container .component { 69 | /* border: solid red 3px; */ 70 | padding: 25px 0px 25px 0px; 71 | } 72 | 73 | .grid-item { 74 | display: grid; 75 | /* border: dotted red 2px; */ 76 | background-color: #add5f7; 77 | border-radius: 20px; 78 | place-items: center; 79 | border: solid rgb(255, 255, 255) 1px; 80 | box-shadow: 5px 5px 2px 1px rgba(9, 9, 9, 0.139); 81 | } 82 | 83 | .app { 84 | display: flex; 85 | flex-direction: column; 86 | justify-content: center; 87 | align-items: center; 88 | text-align: center; 89 | min-height: 1025px; 90 | margin: 0; 91 | border-radius: 20px; 92 | font-family: 'Comic Sans MS', 'Comic Sans', cursive; 93 | background-color: transparent; 94 | } 95 | 96 | .noborder { 97 | border-bottom: none; 98 | flex-direction: column; 99 | } 100 | 101 | .app-1 { 102 | display: flex; 103 | flex-direction: column; 104 | justify-content: center; 105 | align-items: center; 106 | font-family: 'Comic Sans MS', 'Comic Sans', cursive; 107 | min-height: 984px; 108 | margin: 0; 109 | border-radius: 20px; 110 | background-color: transparent; 111 | } 112 | 113 | a { 114 | text-decoration: none; 115 | color: rgb(53, 51, 66); 116 | font-style: italic; 117 | font-weight: bold; 118 | } 119 | 120 | input.error { 121 | border-color: #e86668; 122 | } 123 | 124 | .input-feedback { 125 | color: #e86668; 126 | height: 5px; 127 | margin-top: -2px; 128 | margin-left: 18px; 129 | font-size: large; 130 | } 131 | 132 | input { 133 | outline: none; 134 | border-radius: 10px; 135 | border: none; 136 | height: 20px; 137 | width: 80%; 138 | padding-left: 10px; 139 | } 140 | 141 | .checkBox { 142 | display: inline-block; 143 | font-size: 1em; 144 | } 145 | 146 | button { 147 | outline: none; 148 | top: 40px; 149 | font-family: 'proxima-nova', sans-serif; 150 | font-weight: bold; 151 | font-size: 13px; 152 | text-transform: uppercase !important; 153 | letter-spacing: 2px; 154 | color: white; 155 | cursor: hand; 156 | text-align: center; 157 | text-transform: capitalize; 158 | border: 2px solid white; 159 | border-radius: 50px; 160 | position: relative; 161 | overflow: hidden !important; 162 | -webkit-transition: all 0.3s ease-in-out; 163 | -moz-transition: all 0.3s ease-in-out; 164 | -o-transition: all 0.3s ease-in-out; 165 | transition: all 0.3s ease-in-out; 166 | background: transparent; 167 | z-index: 10; 168 | } 169 | 170 | button:hover { 171 | color: #80ffd3 !important; 172 | background-color: #e86668; 173 | } 174 | 175 | .special:hover { 176 | background-color: #85a2bb; 177 | } 178 | 179 | .body.driller { 180 | display: flex; 181 | flex-direction: column; 182 | min-width: 1805px; 183 | height: 984px; 184 | } 185 | 186 | .body.driller .grid-container { 187 | display: flex; 188 | flex-direction: column; 189 | 190 | align-items: center; 191 | } 192 | 193 | .drillTopics.component { 194 | width: 60%; 195 | margin-top: -30px; 196 | margin-bottom: 40px; 197 | } 198 | 199 | .aspan { 200 | padding: 0; 201 | margin: -30px 0px -30px 0px; 202 | } 203 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainDashboard from './MainDashboard'; 3 | import TopicsDrill from './components/TopicsDrill'; 4 | import registerPage from './components/views/registerPage'; 5 | import loginPage from './components/views/loginPage'; 6 | import { HashRouter as Router, Switch, Route } from 'react-router-dom'; 7 | 8 | import NavBar from './components/views/Navbar.js'; 9 | 10 | const App = () => ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/MainDashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Brokers from './components/Brokers'; 3 | import Topics from './components/Topics'; 4 | import Zookeeper from './components/Zookeeper'; 5 | import VirtualMem from './components/VirtualMem'; 6 | import Cpu from './components/Cpu'; 7 | import Health from './components/Health'; 8 | import Lag from './components/Lag'; 9 | import mainLogo from './assets/KafKareTsmall.png'; 10 | import { BrowserRouter as Router, Link, Switch, Route } from 'react-router-dom'; 11 | 12 | const MainDashboard = () => { 13 | return ( 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 | {/*
45 | 46 |
*/} 47 |
48 | 49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 | ); 57 | }; 58 | 59 | export default MainDashboard; 60 | -------------------------------------------------------------------------------- /src/assets/KafKareLarge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/src/assets/KafKareLarge.png -------------------------------------------------------------------------------- /src/assets/KafKareMedium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/src/assets/KafKareMedium.png -------------------------------------------------------------------------------- /src/assets/KafKareSmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/src/assets/KafKareSmall.png -------------------------------------------------------------------------------- /src/assets/KafkareTsmall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/src/assets/KafkareTsmall.png -------------------------------------------------------------------------------- /src/assets/icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/src/assets/icons/mac/icon.icns -------------------------------------------------------------------------------- /src/assets/icons/png/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/src/assets/icons/png/icon.png -------------------------------------------------------------------------------- /src/assets/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KafKare/994d7b6fd5a6af46de9e588c5cdbd0fea08d1210/src/assets/icons/win/icon.ico -------------------------------------------------------------------------------- /src/components/Brokers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Brokers() { 4 | return ( 5 |
6 | 12 |
13 | ); 14 | } 15 | 16 | export default Brokers; 17 | -------------------------------------------------------------------------------- /src/components/Cpu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Cpu() { 4 | return ( 5 |
6 | 12 |
13 | ); 14 | } 15 | 16 | export default Cpu; 17 | -------------------------------------------------------------------------------- /src/components/Health.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Health() { 4 | return ( 5 |
6 | 12 |
13 | ); 14 | } 15 | 16 | export default Health; 17 | -------------------------------------------------------------------------------- /src/components/Lag.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Lag() { 4 | return ( 5 |
6 | 12 |
13 | ); 14 | } 15 | 16 | export default Lag; 17 | -------------------------------------------------------------------------------- /src/components/Topics.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Topics() { 4 | return ( 5 |
6 | 13 |
14 | ); 15 | } 16 | 17 | export default Topics; 18 | -------------------------------------------------------------------------------- /src/components/TopicsDrill.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import mainLogo from '../assets/KafKareTsmall.png'; 3 | import { BrowserRouter as Router, Link, Switch, Route } from 'react-router-dom'; 4 | import { withRouter } from 'react-router-dom'; 5 | 6 | function TopicDrill() { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | 27 |
28 |
29 | 35 |
36 |
37 |
38 | ); 39 | } 40 | 41 | export default withRouter(TopicDrill); 42 | // export default TopicDrill; 43 | -------------------------------------------------------------------------------- /src/components/VirtualMem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function VirtualMem() { 4 | return ( 5 |
6 | 12 |
13 | ); 14 | } 15 | 16 | export default VirtualMem; 17 | -------------------------------------------------------------------------------- /src/components/Zookeeper.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Zookeeper() { 4 | return ( 5 |
6 | 13 |
14 | ); 15 | } 16 | 17 | export default Zookeeper; 18 | -------------------------------------------------------------------------------- /src/components/auth/auth.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | export default function PrivateRoute({ component: Component }) { 5 | const [isAuth, setAuth] = useState([ 6 | { 7 | _id: '', 8 | isAuth: false, 9 | email: '', 10 | name: '', 11 | }, 12 | ]); 13 | useEffect(() => { 14 | let isMounted = true; 15 | const fetchData = async () => { 16 | const result = await axios.get('http://localhost:3002/user/auth'); 17 | console.log(result); 18 | console.log('whats result data', result.data); 19 | setAuth(result.data); 20 | }; 21 | 22 | fetchData(); 23 | return () => { 24 | isMounted = false; 25 | }; 26 | }, []); 27 | console.log('isAuth', isAuth); 28 | return ( 29 | 31 | isAuth === true ? ( 32 | 33 | ) : ( 34 | 37 | ) 38 | } 39 | /> 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/views/Navbar.js: -------------------------------------------------------------------------------- 1 | import mainLogo from '../../assets/KafKareTsmall.png'; 2 | import React from 'react'; 3 | function NavBar() { 4 | return ( 5 | 8 | ); 9 | } 10 | export default NavBar; 11 | -------------------------------------------------------------------------------- /src/components/views/loginPage.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import mainLogo from '../../assets/KafKareTsmall.png'; 3 | import { withRouter, Link } from 'react-router-dom'; 4 | import { Formik } from 'formik'; 5 | import * as Yup from 'yup'; 6 | import { Form, Input, Button, Checkbox, Typography } from 'antd'; 7 | import { EmailIcon, LockIcon } from '@chakra-ui/icons'; 8 | import axios from 'axios'; 9 | import { Container, Flex, Center } from '@chakra-ui/react'; 10 | const { Title } = Typography; 11 | const loginPage = (props) => { 12 | const rememberMeChecked = localStorage.getItem('rememberMe') ? true : false; 13 | 14 | const [formErrorMessage, setFormErrorMessage] = useState(''); 15 | 16 | return ( 17 | { 31 | setTimeout(() => { 32 | let dataToSubmit = { 33 | email: values.email, 34 | password: values.password, 35 | }; 36 | 37 | axios 38 | .post('http://localhost:3002/user/login', dataToSubmit) 39 | .then((response) => { 40 | console.log(response); 41 | console.log('what response issssssssss', response.data); 42 | if (response.data.loginSuccess) { 43 | window.localStorage.setItem('userId', response.userId); 44 | props.history.push('/dashboard'); 45 | } else { 46 | setFormErrorMessage('Check out your Account or Password again'); 47 | } 48 | }) 49 | .catch((err) => { 50 | setFormErrorMessage('Check out your Account or Password again'); 51 | setTimeout(() => { 52 | setFormErrorMessage(''); 53 | }, 3000); 54 | }); 55 | setSubmitting(false); 56 | }, 500); 57 | }} 58 | > 59 | {(props) => { 60 | const { 61 | values, 62 | touched, 63 | errors, 64 | isSubmitting, 65 | handleChange, 66 | handleBlur, 67 | handleSubmit, 68 | } = props; 69 | return ( 70 |
71 | 169 |
170 | ); 171 | }} 172 |
173 | ); 174 | }; 175 | export default withRouter(loginPage); 176 | -------------------------------------------------------------------------------- /src/components/views/registerPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Formik } from 'formik'; 4 | import * as Yup from 'yup'; 5 | import { Form, Input, Button } from 'antd'; 6 | import { Container, Flex, Center } from '@chakra-ui/react'; 7 | import axios from 'axios'; 8 | 9 | const formItemLayout = { 10 | labelCol: { 11 | xs: { span: 24 }, 12 | sm: { span: 8 }, 13 | }, 14 | wrapperCol: { 15 | xs: { span: 24 }, 16 | sm: { span: 16 }, 17 | }, 18 | }; 19 | const tailFormItemLayout = { 20 | wrapperCol: { 21 | xs: { 22 | span: 24, 23 | offset: 0, 24 | }, 25 | sm: { 26 | span: 16, 27 | offset: 8, 28 | }, 29 | }, 30 | }; 31 | const registerPage = (props) => { 32 | return ( 33 | { 54 | setTimeout(() => { 55 | let dataToSubmit = { 56 | email: values.email, 57 | password: values.password, 58 | name: values.name, 59 | }; 60 | 61 | axios 62 | .post('http://localhost:3002/user/signup', dataToSubmit) 63 | .then((res) => { 64 | console.log('register page', res.data); 65 | if (res.data.success) { 66 | props.history.push('/'); 67 | } else { 68 | alert('Fail to register, please try it again!'); 69 | } 70 | }); 71 | 72 | setSubmitting(false); 73 | }, 500); 74 | }} 75 | > 76 | {(props) => { 77 | const { 78 | values, 79 | touched, 80 | errors, 81 | isSubmitting, 82 | handleChange, 83 | handleBlur, 84 | handleSubmit, 85 | } = props; 86 | return ( 87 |
88 | 89 |
90 | 99 |

Sign up

100 |
106 | 107 | 121 | {errors.name && touched.name && ( 122 |
{errors.name}
123 | )} 124 |
125 |
126 | 134 | 148 | {errors.email && touched.email && ( 149 |
{errors.email}
150 | )} 151 |
152 | 153 | 163 | 177 | {errors.password && touched.password && ( 178 |
{errors.password}
179 | )} 180 |
181 | 182 | 183 | 197 | {errors.confirmPassword && touched.confirmPassword && ( 198 |
199 | {errors.confirmPassword} 200 |
201 | )} 202 |
203 | 204 | 205 | 208 | 209 | 210 | 211 | 212 | 213 |
214 |
{' '} 215 |
216 |
217 |
218 | ); 219 | }} 220 |
221 | ); 222 | }; 223 | export default registerPage; 224 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | KafKare 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { render } from 'react-dom'; 5 | import App from './App'; 6 | import './App.css'; 7 | 8 | import { ChakraProvider } from '@chakra-ui/react'; 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('app') 14 | ); 15 | 16 | //below makes hot reloading available 17 | module.hot.accept(); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true 9 | } 10 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const e = require('express'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | mode: process.env.NODE_ENV, 7 | entry: path.resolve(__dirname, './src/index.js'), // This is the start of the dependency graph that webpack will build from. 8 | output: { 9 | // This is where we want webpack to build our bundle. 10 | path: path.resolve(__dirname, 'dist'), 11 | filename: 'bundle.js', 12 | }, 13 | //React hot loader 14 | plugins: [new webpack.HotModuleReplacementPlugin()], 15 | // We need this for our webpack dev server 16 | devServer: { 17 | publicPath: 'http://localhost:8080/dist/', 18 | proxy: { 19 | // Webpack dev server does not hit the backend. But if we want a request that use the backend, we need a proxy that says if there's a fetch on /api, redirect any request to localhost:3000. 20 | '/': 'http://localhost:3002/', 21 | }, 22 | //below line is for hot module replacement 23 | hot: true, 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | // Converts tsx to ES5 29 | test: /\.tsx?$/i, 30 | exclude: /(node_modules)/, 31 | loader: 'ts-loader', 32 | // use: { //For windows users, instead of the presets here, put in a .babelrc file with the presets. 33 | // presets: ['@babel/preset-env', '@babel/preset-react'], 34 | // }, 35 | }, 36 | { 37 | // Converts jsx to ES5 38 | test: /\.(js|jsx)$/, 39 | exclude: /(node_modules)/, 40 | loader: 'babel-loader', 41 | // use: { //For windows users, instead of the presets here, put in a .babelrc file with the presets. 42 | // presets: ['@babel/preset-env', '@babel/preset-react'], 43 | // }, 44 | }, 45 | { 46 | test: /\.(css|scss)$/i, 47 | use: [ 48 | // Creates `style` nodes from JS strings (reads this loader last) 49 | 'style-loader', 50 | // Translates CSS into CommonJS (reads this second) 51 | 'css-loader', 52 | // Compiles Sass to CSS (reads this first) 53 | 'sass-loader', 54 | ], 55 | }, 56 | { 57 | test: /\.(jpg|jpeg|png)$/, 58 | use: { 59 | loader: 'url-loader', 60 | }, 61 | }, 62 | ], 63 | }, 64 | resolve: { 65 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 66 | }, 67 | }; 68 | --------------------------------------------------------------------------------