├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── README_CN.md ├── kafka_sprout_logo_v3.svg ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── kafkasprout │ │ └── backend │ │ ├── AdminService.java │ │ ├── CheckPath.java │ │ ├── KafkaSproutApplication.java │ │ ├── StartBroker.java │ │ ├── StartZoo.java │ │ ├── Status.java │ │ ├── WebSocketConfig.java │ │ └── controllers │ │ ├── ClusterController.java │ │ ├── MetricsController.java │ │ ├── TopicsController.java │ │ └── ViewController.java ├── js │ ├── .eslintignore │ ├── .eslintrc.js │ ├── custom.d.ts │ ├── favicon.ico │ ├── index.html │ ├── index.tsx │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── UIComponents │ │ │ ├── Buttons.tsx │ │ │ ├── FlexContainer.tsx │ │ │ ├── GridSection.tsx │ │ │ ├── GridTitle.tsx │ │ │ ├── LabeledDropdown.tsx │ │ │ ├── LabeledInput.tsx │ │ │ ├── Logo.tsx │ │ │ ├── PopupContainer.tsx │ │ │ ├── TabView.tsx │ │ │ ├── constants.tsx │ │ │ └── withPopup.tsx │ │ ├── assets │ │ │ ├── kafka_sprout_logo.svg │ │ │ ├── kafka_sprout_title.svg │ │ │ └── question.svg │ │ └── components │ │ │ ├── App.tsx │ │ │ ├── BrokerConfig.tsx │ │ │ ├── BrokerDisplay.tsx │ │ │ ├── Main.tsx │ │ │ ├── MetricsDisplay.tsx │ │ │ ├── StartZookeeper.tsx │ │ │ ├── TopicConfig.tsx │ │ │ ├── TopicDelete.tsx │ │ │ ├── TopicDisplay.tsx │ │ │ └── TopicDoughnut.tsx │ ├── tsconfig.json │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js └── resources │ └── application.properties └── test └── java └── com └── kafkasprout └── backend ├── ClusterWebLayerTest.java ├── HttpRequestTest.java ├── KafkaSproutApplicationTests.java ├── MetricWebLayerTest.java ├── SmokeTests.java ├── TestingWebApplicationTest.java ├── TopicWebLayerTest.java ├── ViewWebLayerTest.java └── controllers ├── ClusterControllerTest.java ├── MetricControllerTest.java ├── TopicsControllerTest.java └── ViewControllerTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea/ 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### Node.js ### 36 | node_modules/ 37 | node/ 38 | built/ 39 | **/templates/index.html 40 | 41 | **/.DS_Store 42 | 43 | ### default values ### 44 | **/path.properties* 45 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Sprout/40c71ad837a5d207231aa549f721c88592f37083/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 |

2 | 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 |

11 | 12 |

Kafka Cluster Management UI Tool

13 | 14 | > [中文](README_CN.md) | English 15 | 16 | 17 | ## About 18 | 19 | Kafka Sprout is a web GUI that helps you quickly start up Zookeeper and Kafka servers on your local machine without any code configuration. Easily view, manage, and configure your Kafka topics and brokers with a push of a button. Kafka Sprout also displays relevant realtime metrics including Request Rate, Network I/O Rate, etc. Give it a spin and let us know what features you want next! 20 | 21 | 22 |
23 | 24 |

:wave: Goodbye Clunky Terminal Windows, Hello Web GUI

25 | 26 |

Start Cluster

27 | 28 |

29 | 30 |

Configure and Start Brokers

31 | 32 |

33 | 34 |

View, Add, and Delete Topics

35 | 36 |

37 | 38 | 39 | 40 | ## Features 41 | 42 | #### :rocket: Push to Start Zookeeper and Kafka Server 43 | 44 | #### :heavy_plus_sign: Quickly Add and Delete Topics 45 | 46 | #### :muscle: Setup Kafka Brokers with Ease 47 | 48 | #### :vertical_traffic_light: Monitor Key Performance Metrics 49 | 50 | #### :mag_right: Explore Topic and Broker Configurations 51 | 52 |
53 | 54 | ## Getting Started 55 | 56 | ## Installation 57 | 58 | 1. Clone this repo: 59 | ```sh 60 | git clone https://github.com/oslabs-beta/Kafka-Sprout.git 61 | ``` 62 | 2. Run the application in the root folder. This will also install node locally to build the frontend. 63 | ```sh 64 | mvn spring-boot:run # for Mac 65 | ./mvnw spring-boot:run # for Windows 66 | ``` 67 | 3. Go to http://localhost:8080/ on your browser. 68 | 69 | ## Set Up 70 | ### Requirements 71 | * Java (version 11 or newer) 72 | * Maven (version 3.6.3 or newer) 73 | * Kafka (version 0.11.0 or newer) 74 | * Node.js (version 12.18.3 or newer) 75 | 76 | ## Built With 77 | 78 | Spring Boot 79 | | Spring Web Socket 80 | | Styled Components 81 | | React 82 | | Typescript 83 | | Apache Kafka 84 | 85 | 86 | ## Contributors 87 | 88 | Brian Hong | Midori Yang | Nak Kim | Nicole Ip | Winford Lin 89 | 90 | ### Contributions Welcome! 91 | 92 | If you found this interesting or helpful at all, feel free to drop a :star: [![GitHub stars](https://img.shields.io/github/stars/oslabs-beta/kafka-sprout?style=social&label=Star&)](https://github.com/oslabs-beta/kafka-sprout/stargazers) :star: on this project to show your support! 93 | 94 | You can contribute by: 95 | 96 | * Raising any issues you find using Kafka Sprout 97 | * Fixing issues by opening Pull Requests 98 | * Improving documentation 99 | 100 | All bugs, tasks or enhancements are tracked as GitHub issues. Issues which might be a good start for new contributors are marked with "good-start" label. 101 | 102 | If you want to get in touch with us first before contributing, shoot us an email at kafkasprout@gmail.com. Kafka Sprout is actively maintained! 103 | 104 | ## License 105 | MIT 106 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | 8 | 9 | 10 |

11 | 12 |

Kafka Cluster Management UI Tool

13 | 14 | > [English](README.md) | 中文 15 | 16 | 17 | ## 关于 18 | 19 | Kafka Sprout 是一个 Web Gui,让用户容易和快速地开始 Apache Kafka 和 Zookeeper, 不需要代码配置。只需要按一下按钮,用户就可以查看,管理和配置Kafka Cluster。Kafka Sprout 也会让用户观察主要功能表现数据,包括 Request Rate 和 Network I/O Rate。 请试一下!让我们知道您接下来想要什么其他的功能! 20 | 21 | 22 |
23 | 24 |

:wave: 再见,多余的文件,向 Web GUI 问好!

25 | 26 |

开始 Kafka Cluster

27 | 28 |

29 | 30 |

简单的 Kafka Brokers 的设置

31 | 32 |

33 | 34 |

容易添加和删除 Topics

35 | 36 |

37 | 38 | ## 特点 39 | 40 | #### :rocket: 一按就可以开始 Apache Kafka 和 Zookeeper 41 | 42 | #### :heavy_plus_sign: 容易添加和删除 Topics 43 | 44 | #### :muscle: 简单的 Kafka Brokers 的设置 45 | 46 | #### :vertical_traffic_light: 可显示主要功能表现数据 47 | 48 | #### :mag_right: 查看Topic 和Broker 的各种设定 49 | 50 |
51 | 52 | ## 入门指南 53 | 54 | ## 安装 55 | 56 | 1. Clone this repo: 57 | ```sh 58 | git clone https://github.com/oslabs-beta/Kafka-Sprout.git 59 | ``` 60 | 2. Run the application in the root folder. This will also install node locally to build the frontend. 61 | ```sh 62 | mvn spring-boot:run # for Mac 63 | ./mvnw spring-boot:run # for Windows 64 | ``` 65 | 3. Go to http://localhost:8080/ on your browser. 66 | 67 | ## 设置 68 | #### 要求 69 | * Java (version 11 or newer) 70 | * Maven (version 3.6.3 or newer) 71 | * Kafka (version 0.11.0 or newer) 72 | * Node.js (version 12.18.3 or newer) 73 | 74 | ## 内置 75 | 76 | Spring Boot 77 | | Spring Web Socket 78 | | Styled Components 79 | | React 80 | | Typescript 81 | | Apache Kafka 82 | 83 | 84 | ## 贡献者 85 | 86 | Brian Hong | Midori Yang | Nak Kim | Nicole Ip | Winford Lin 87 | 88 | ### 贡献者欢迎!! 89 | 90 | 如果您觉得这个工具很有趣或者有帮助的话,请给一个 :star2: **星** :star2: ,以表示支持! 91 | 92 | 您可以通过以下方式做出建议: 93 | 94 | * 使用 Kafka Sprout 时,请提出任何的问题 95 | * 请通过 Pull Request 解决问题 96 | * 改善相关的文件 97 | 98 | All bugs, tasks or enhancements are tracked as GitHub issues. Issues which might be a good start for new contributors are marked with "good-start" label. 99 | 100 | 如果您想联系我们,请给我们发送电子邮件 kafkasprout@gmail.com 101 | 102 | 103 | ## License 104 | MIT 105 | -------------------------------------------------------------------------------- /kafka_sprout_logo_v3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Kafka Sprou 49 | 50 | 51 | 52 | 53 | 55 | 57 | 58 | 59 | 64 | 66 | 67 | 68 | 69 | 73 | 80 | 86 | 90 | 96 | 103 | 109 | 113 | 118 | 123 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.springframework.boot 6 | spring-boot-starter-parent 7 | 2.3.1.RELEASE 8 | 9 | 10 | com.kafkasprout 11 | backend 12 | 0.0.1-SNAPSHOT 13 | demo 14 | Spring Boot Backend for Kafka Sprout 15 | 16 | 17 | 1.8 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-websocket 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-web 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-thymeleaf 32 | 2.3.1.RELEASE 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-devtools 37 | runtime 38 | 39 | 40 | org.apache.kafka 41 | kafka-clients 42 | 2.5.0 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-test 47 | test 48 | 49 | 50 | org.junit.vintage 51 | junit-vintage-engine 52 | 53 | 54 | 55 | 56 | org.springframework 57 | spring-messaging 58 | 5.2.8.RELEASE 59 | 60 | 61 | 62 | junit 63 | junit 64 | 4.13 65 | test 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-maven-plugin 74 | 75 | 76 | com.github.eirslett 77 | frontend-maven-plugin 78 | 1.10.0 79 | 80 | v10.0.0 81 | 6.1.0 82 | src/main/js 83 | 84 | 85 | 86 | Install node and npm locally to the project 87 | 88 | install-node-and-npm 89 | 90 | 91 | 92 | npm install 93 | 94 | npm 95 | 96 | 97 | install 98 | 99 | 100 | 101 | Build frontend 102 | 103 | npm 104 | 105 | 106 | run build 107 | 108 | 109 | 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-compiler-plugin 114 | 3.8.0 115 | 116 | 8 117 | 8 118 | UTF-8 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/AdminService.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import org.apache.kafka.clients.admin.*; 4 | import org.apache.kafka.common.*; 5 | import org.apache.kafka.common.config.ConfigResource; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.*; 9 | import java.util.concurrent.ExecutionException; 10 | 11 | @Service 12 | public class AdminService { 13 | 14 | public AdminClient admin; 15 | private boolean isLive; 16 | 17 | public AdminService() { 18 | 19 | // starting bootstrap server in localhost: 9092 20 | Properties config = new Properties(); 21 | config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 22 | int ADMIN_CLIENT_TIMEOUT_MS = 5000; 23 | 24 | // testing to see if kafka server is live using admin client 25 | try (AdminClient admin = AdminClient.create(config)) { 26 | admin.listTopics(new ListTopicsOptions().timeoutMs(ADMIN_CLIENT_TIMEOUT_MS)).listings().get(); 27 | 28 | } catch (ExecutionException | InterruptedException ex) { 29 | isLive = false; 30 | return; 31 | } 32 | 33 | // creating Kafka Admin API 34 | admin = AdminClient.create(config); 35 | isLive = true; 36 | } 37 | 38 | // is Kafka Cluster Live 39 | public boolean isLive() { 40 | return isLive; 41 | } 42 | public void isLive(boolean live) { 43 | this.isLive = live; 44 | } 45 | 46 | public void startClient() { 47 | Properties config = new Properties(); 48 | config.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); 49 | this.admin = AdminClient.create(config); 50 | isLive(true); 51 | } 52 | 53 | // list current topics: returns an arraylist of topic names 54 | public ArrayList listTopics() throws ExecutionException, InterruptedException { 55 | ArrayList names = new ArrayList<>(); 56 | 57 | for (TopicListing topicListing : admin.listTopics().listings().get()) { 58 | names.add(topicListing.name()); 59 | } 60 | return names; 61 | } 62 | 63 | // create a topic: inputs json with key/value: name, partition, and replica 64 | public void createTopic(HashMap payload) { 65 | 66 | // parsing json request body 67 | String desiredName = payload.get("name").toString(); 68 | int desiredPartitions = Integer.parseInt(String.valueOf(payload.get("partition"))); 69 | short desiredReplicationFactor = Short.parseShort(String.valueOf(payload.get("replication"))); 70 | 71 | // creating new topic 72 | NewTopic newTopic = new NewTopic(desiredName, desiredPartitions, desiredReplicationFactor); 73 | admin.createTopics(Collections.singleton(newTopic)); 74 | 75 | } 76 | 77 | // deletes a topic: inputs json with key/value: name 78 | public void deleteTopic(HashMap payload) { 79 | 80 | // parsing json request body 81 | String desiredName = payload.get("name").toString(); 82 | 83 | admin.deleteTopics(Collections.singleton(desiredName)); 84 | } 85 | 86 | // returns Cluster Admin Metrics 87 | public Map metrics() throws ExecutionException, InterruptedException { 88 | Map json = new HashMap<>(); 89 | for(Map.Entry entry: admin.metrics().entrySet()) { 90 | 91 | // query for response rate, io-wait-time, network io rate, total requests sent 92 | 93 | if (entry.getKey().name().equals("response-rate") || entry.getKey().name().equals("io-wait-time-ns-avg") || entry.getKey().name().equals("network-io-rate") || entry.getKey().name().equals("request-total")) { 94 | json.put(entry.getKey().name(), String.valueOf(entry.getValue().metricValue())); 95 | } 96 | } 97 | return json; 98 | } 99 | 100 | // describes Topic and Broker Configurations: returns json of key topic and broker with configuration info 101 | public Map>> describeTopicAndBrokerConfig() throws ExecutionException, InterruptedException { 102 | // get all topics 103 | List allTopics = this.listTopics(); 104 | 105 | List allTopicConfig = new ArrayList<>(); 106 | 107 | // convert all topics to ConfigResource 108 | for (String topic : allTopics) { 109 | ConfigResource topicDesc = new ConfigResource(ConfigResource.Type.TOPIC, topic); 110 | allTopicConfig.add(topicDesc); 111 | } 112 | 113 | // get all topic configs 114 | Map topicResults = admin.describeConfigs(allTopicConfig).all().get(); 115 | 116 | Map>> json = new HashMap<>(); 117 | 118 | // topic and broker hashmaps 119 | Map> topic = new HashMap<>(); 120 | Map> broker = new HashMap<>(); 121 | 122 | // iterate over topic config resource 123 | for (Map.Entry configResource : topicResults.entrySet()) { 124 | String name = configResource.getKey().name(); 125 | Map topicContent = new HashMap<>(); 126 | for (ConfigEntry configEntry : configResource.getValue().entries()) { 127 | if (configEntry.name().equals("compression.type")) { 128 | topicContent.put("compressionType", configEntry.value()); 129 | } else if (configEntry.name().equals("min.insync.replicas")) { 130 | topicContent.put("minInsyncReplicas", configEntry.value()); 131 | } else if (configEntry.name().equals("message.timestamp.type")) { 132 | topicContent.put("messageTimeStampType", configEntry.value()); 133 | } else if (configEntry.name().equals("cleanup.policy")) { 134 | topicContent.put("cleanUpPolicy", configEntry.value()); 135 | } 136 | } 137 | topic.put(name, topicContent); 138 | } 139 | json.put("Topic", topic); 140 | 141 | // get all brokers and convert to ConfigResources 142 | List allBrokerConfig = new ArrayList<>(); 143 | 144 | Collection nodes = admin.describeCluster().nodes().get(); 145 | System.out.println(nodes); 146 | 147 | for(Node node : nodes){ 148 | ConfigResource brokerConfigResource = new ConfigResource(ConfigResource.Type.BROKER, String.valueOf(node.id())); 149 | allBrokerConfig.add(brokerConfigResource); 150 | } 151 | 152 | System.out.println(allBrokerConfig); 153 | 154 | // get all broker configs 155 | Map brokerResults = admin.describeConfigs(allBrokerConfig).all().get(); 156 | 157 | // create broker config resource 158 | for(Map.Entry configResource : brokerResults.entrySet()){ 159 | String id = configResource.getKey().name(); 160 | Map brokerContent = new HashMap<>(); 161 | 162 | // iterate over broker config 163 | for(ConfigEntry configEntry : configResource.getValue().entries()){ 164 | if (configEntry.name().equals("zookeeper.connect")){ 165 | brokerContent.put("zookeeperConnect",configEntry.value()); 166 | }else if (configEntry.name().equals("min.insync.replicas")){ 167 | brokerContent.put("minInsyncReplicas", configEntry.value()); 168 | }else if (configEntry.name().equals("log.dir")){ 169 | brokerContent.put("logDir", configEntry.value()); 170 | }else if (configEntry.name().equals("background.threads")){ 171 | brokerContent.put("backgroundThreads", configEntry.value()); 172 | }else if (configEntry.name().equals("compression.type")){ 173 | brokerContent.put("compressionType",configEntry.value()); 174 | }else if (configEntry.name().equals("log.retention.hours")){ 175 | brokerContent.put("logRetentionHours",configEntry.value()); 176 | }else if (configEntry.name().equals("message.max.bytes")){ 177 | brokerContent.put("messageMaxBytes",configEntry.value()); 178 | } 179 | } 180 | broker.put(id,brokerContent); 181 | } 182 | 183 | json.put("Brokers",broker); 184 | 185 | return json; 186 | } 187 | 188 | public Map describeTopicsAndBrokers() throws ExecutionException, InterruptedException { 189 | 190 | // grab list of topics 191 | Map topics = admin.describeTopics(this.listTopics()).all().get(); 192 | 193 | // return json 194 | Map json = new HashMap<>(); 195 | List brokerList = new ArrayList<>(); 196 | List topicList = new ArrayList<>(); 197 | 198 | // Topic Column 199 | String[] topicSpecs = new String[]{"Name", "Leader", "# of Partitions", "# of Replicas"}; 200 | topicList.add(Arrays.asList(topicSpecs)); 201 | 202 | // Broker Column 203 | String[] brokerSpecs = new String[]{"ID", "Host", "Port", "Controller", "# of Partitions"}; 204 | brokerList.add(Arrays.asList(brokerSpecs)); 205 | 206 | // Broker Nodes 207 | Collection nodeList = admin.describeCluster().nodes().get(); 208 | // Controller Broker 209 | int controllerID = admin.describeCluster().controller().get().id(); 210 | // Broker List 211 | Map brokerPartitionCount = new HashMap<>(); 212 | 213 | // topic traverse 214 | for(String name: topics.keySet()){ 215 | String[] info = new String[topicSpecs.length]; 216 | info[0] = topics.get(name).name(); 217 | info[1] = String.valueOf(topics.get(name).partitions().get(0).leader().port()); 218 | info[2] = String.valueOf(topics.get(name).partitions().size()); 219 | info[3] = String.valueOf(topics.get(name).partitions().get(0).replicas().size()); 220 | 221 | // grab partition count by brokers 222 | for(TopicPartitionInfo topicPartitionInfo : topics.get(name).partitions()){ 223 | for(Node replica : topicPartitionInfo.replicas()){ 224 | if (brokerPartitionCount.containsKey(replica.id())){ 225 | int newCount = brokerPartitionCount.get(replica.id()) + 1; 226 | brokerPartitionCount.put(replica.id(),newCount); 227 | }else { 228 | brokerPartitionCount.put(replica.id(),1); 229 | } 230 | } 231 | } 232 | topicList.add(Arrays.asList(info)); 233 | } 234 | 235 | // broker traverse 236 | for(Node node : nodeList){ 237 | String[] nodeInfo = new String[brokerSpecs.length]; 238 | nodeInfo[0] = String.valueOf(node.id()); 239 | nodeInfo[1] = node.host(); 240 | nodeInfo[2] = String.valueOf(node.port()); 241 | nodeInfo[3] = String.valueOf((node.id() == controllerID)); 242 | nodeInfo[4] = String.valueOf(brokerPartitionCount.get(node.id())); 243 | brokerList.add(Arrays.asList(nodeInfo)); 244 | } 245 | 246 | json.put("Brokers", brokerList); 247 | json.put("Topics", topicList); 248 | 249 | return json; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/CheckPath.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.FileNotFoundException; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | import java.util.HashMap; 11 | import java.util.Hashtable; 12 | import java.util.Properties; 13 | 14 | import org.springframework.stereotype.Service; 15 | 16 | @Service 17 | public class CheckPath { 18 | 19 | private Properties props; 20 | private File propsFile; 21 | 22 | public CheckPath() throws FileNotFoundException, IOException { 23 | this.props = new Properties(); 24 | this.propsFile = new File("src/main/java/com/kafkasprout/backend/path.properties"); 25 | 26 | try { 27 | if (!this.propsFile.exists()) { 28 | this.propsFile.createNewFile(); 29 | System.out.println("path.properties has been successfully created."); 30 | this.props.setProperty("path", ""); 31 | this.props.setProperty("port", "9093"); 32 | this.props.setProperty("id", "1"); 33 | this.props.setProperty("logPath", ""); 34 | OutputStream output = new FileOutputStream(propsFile); 35 | this.props.store(output, null); 36 | output.close(); 37 | } 38 | InputStream input = new FileInputStream(propsFile); 39 | this.props.load(input); 40 | input.close(); 41 | } catch (IOException e) { 42 | System.out.println("Error creating new file."); 43 | e.printStackTrace(); 44 | } 45 | } 46 | 47 | // Method for overwritting the path key in the path.properties file. It will be 48 | // updated every time a Zookeeper server is started. 49 | public void storePath(String path) throws FileNotFoundException, IOException { 50 | OutputStream output = null; 51 | try { 52 | output = new FileOutputStream(this.propsFile); 53 | this.props.setProperty("path", path); 54 | this.props.store(output, null); 55 | } catch (FileNotFoundException fnfe) { 56 | fnfe.printStackTrace(); 57 | } catch (IOException ioe) { 58 | ioe.printStackTrace(); 59 | } finally { 60 | output.close(); 61 | } 62 | } 63 | 64 | // Method for retrieving the path for all the Kafka properties files. 65 | public String retrievePath() throws FileNotFoundException, IOException { 66 | return this.props.getProperty("path"); 67 | } 68 | 69 | // Method for overwriting the path for log files, new port number, and broker ID 70 | // when starting up a Kafka cluster. 71 | public void storeProperties(HashMap payload) throws FileNotFoundException, IOException { 72 | OutputStream output = null; 73 | System.out.println(payload.get("properties")); 74 | try { 75 | props.setProperty("path", (String) payload.get("properties")); 76 | props.setProperty("port", Integer.toString(Integer.parseInt((String) payload.get("port")) + 1)); 77 | props.setProperty("id", Integer.toString(Integer.parseInt((String) payload.get("broker_id")) + 1)); 78 | props.setProperty("logPath", (String) payload.get("directory")); 79 | output = new FileOutputStream(this.propsFile); 80 | props.store(output, null); 81 | } catch (FileNotFoundException fnfe) { 82 | fnfe.printStackTrace(); 83 | } catch (IOException ioe) { 84 | ioe.printStackTrace(); 85 | } finally { 86 | output.close(); 87 | } 88 | } 89 | 90 | // Method for retrieving all data pertinent to starting Kafka cluster from 91 | // path.properties file. 92 | public Hashtable retrieveProperties() throws FileNotFoundException, IOException { 93 | return props; 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/KafkaSproutApplication.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class KafkaSproutApplication { 8 | 9 | public static void main(String[] args) { 10 | 11 | SpringApplication.run(KafkaSproutApplication.class, args); 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/StartBroker.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import java.io.*; 6 | import java.nio.file.Files; 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | import java.util.HashMap; 10 | 11 | public class StartBroker { 12 | 13 | @Autowired 14 | AdminService admin; 15 | 16 | // checks to see if broker server config file exists and and if it does, spins it up, if not call configure method to create one 17 | public static String start(HashMap payload) { 18 | try { 19 | String fileName = "server" + payload.get("broker_id") + ".properties"; 20 | String pathName = payload.get("properties") + File.separator + fileName; 21 | System.out.println(pathName); 22 | File myObj = new File(pathName); 23 | if (myObj.createNewFile()) { 24 | System.out.println("File created: " + myObj.getName()); 25 | return configure(fileName, payload); 26 | } else { 27 | System.out.println("File already exists."); 28 | return String.valueOf(run(payload.get("properties") + File.separator + fileName)); 29 | } 30 | } catch (IOException e) { 31 | System.out.println("An error occurred."); 32 | e.printStackTrace(); 33 | return "an error occurred in starting a broker"; 34 | } 35 | } 36 | 37 | // creates a folder to store log files 38 | public static void mkdir(String directory) throws IOException { 39 | System.out.println(directory + "*******************"); 40 | Path path = Paths.get(directory); 41 | 42 | if (!Files.exists(path)) { 43 | 44 | Files.createDirectory(path); 45 | System.out.println("Directory created"); 46 | } else { 47 | 48 | System.out.println("Directory already exists"); 49 | } 50 | } 51 | 52 | // creates broker config file and stores in the directory provided by the user 53 | public static String configure(String fileName, HashMap payload) throws IOException { 54 | mkdir((String) payload.get("directory") + "/kafka" + payload.get("broker_id")); 55 | 56 | try { 57 | String propertiesPath = payload.get("properties") + File.separator + fileName; 58 | FileWriter myWriter = new FileWriter(propertiesPath); 59 | myWriter.write("broker.id=" + payload.get("broker_id") + "\n" + 60 | "\n" + 61 | "num.network.threads=3\n" + 62 | "\n" + 63 | "num.io.threads=8\n" + 64 | "\n" + 65 | "socket.send.buffer.bytes=102400\n" + 66 | "\n" + 67 | "socket.receive.buffer.bytes=102400\n" + 68 | "\n" + 69 | "socket.request.max.bytes=104857600\n" + 70 | "\n" + 71 | "log.dirs=" + payload.get("directory") + "/kafka" + payload.get("broker_id") + "\n" + 72 | "\n" + 73 | "num.partitions=3\n" + 74 | "\n" + 75 | "num.recovery.threads.per.data.dir=1\n" + 76 | "\n" + 77 | "offsets.topic.replication.factor=1\n" + 78 | "\n" + 79 | "transaction.state.log.replication.factor=1\n" + 80 | "\n" + 81 | "transaction.state.log.min.isr=1\n" + 82 | "\n" + 83 | "log.retention.hours=168\n" + 84 | "\n" + 85 | "listeners=PLAINTEXT://:" + payload.get("port") + "\n" + 86 | "\n" + 87 | "log.segment.bytes=1073741824\n" + 88 | "\n" + 89 | "log.retention.check.interval.ms=300000\n" + 90 | "\n" + 91 | "zookeeper.connect=localhost:2181\n" + 92 | "\n" + 93 | "zookeeper.connection.timeout.ms=18000\n" + 94 | "\n" + 95 | "group.initial.rebalance.delay.ms=0\n"); 96 | myWriter.close(); 97 | System.out.println("Successfully wrote broker configurations to the file."); 98 | return String.valueOf(run(propertiesPath)); 99 | } catch (IOException e) { 100 | System.out.println("An error occurred in creating broker configuration file."); 101 | e.printStackTrace(); 102 | return "An error occurred in creating broker configuration file."; 103 | } 104 | } 105 | 106 | // called to spin up a broker 107 | public static boolean run(String propertiesPath) { 108 | String OS = System.getProperty("os.name").toLowerCase(); 109 | String[] command = new String[2]; 110 | command[0] = OS.contains("windows") ? "kafka-server-start.bat" : "kafka-server-start"; 111 | command[1] = propertiesPath; 112 | 113 | ProcessBuilder processBuilder = new ProcessBuilder(command); 114 | 115 | try { 116 | System.out.println("Starting kafka server"); 117 | Process process = processBuilder.start(); 118 | BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); 119 | String line; 120 | while ((line = reader.readLine()) != null) { 121 | System.out.println(line); 122 | if (line.matches(".*KafkaServer id=\\d+] started.*")) { 123 | return true; 124 | } 125 | } 126 | 127 | } catch (IOException e) { 128 | e.printStackTrace(); 129 | return false; 130 | } 131 | return false; 132 | } 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/StartZoo.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | 7 | public class StartZoo { 8 | 9 | private String path; 10 | private String OS; 11 | 12 | // Start Zookeeper Constructor 13 | public StartZoo(String path, String OS) { 14 | this.path = path; 15 | this.OS = OS; 16 | } 17 | 18 | // Process Builder to input command line arguments to start Zookeeper 19 | public boolean run() { 20 | String[] command = new String[2]; 21 | command[0] = OS.contains("windows") ? "zookeeper-server-start.bat" : "zookeeper-server-start"; 22 | command[1] = path + "/" + "zookeeper.properties"; 23 | 24 | ProcessBuilder processBuilder = new ProcessBuilder(command); 25 | 26 | try { 27 | System.out.println("Starting Zookeeper server"); 28 | Process process = processBuilder.start(); 29 | BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); 30 | String line; 31 | while ((line = reader.readLine()) != null) { 32 | System.out.println(line); 33 | if (line.contains("binding to port")) { 34 | 35 | // If Zookeeper server started successfully, start Kafka server 36 | 37 | System.out.println("Zookeeper available and bound to port"); 38 | boolean response = StartBroker.run(path + "/server.properties"); 39 | if (response) { 40 | return true; 41 | } 42 | } 43 | // [2020-07-15 17:13:41,105] INFO [KafkaServer id=0] shut down completed 44 | // (kafka.server.KafkaServer) 45 | } 46 | } catch (IOException e) { 47 | 48 | // Print stack trace if zookeeper or kafka server failed 49 | 50 | e.printStackTrace(); 51 | return false; 52 | } 53 | return false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/Status.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.IOException; 5 | import java.io.InputStreamReader; 6 | 7 | public class Status { 8 | private String OS; 9 | 10 | // Status Constructor 11 | public Status(String OS) { 12 | this.OS = OS; 13 | } 14 | 15 | // Process Builder to check Zookeeper status 16 | public String run() { 17 | 18 | String status = "Offline"; 19 | String[] command = new String[2]; 20 | command[0] = OS.contains("windows") ? "zkServer.bat" : "zkServer"; 21 | command[1] = "status"; 22 | 23 | 24 | ProcessBuilder processBuilder = new ProcessBuilder(command); 25 | 26 | try { 27 | System.out.println("Checking ZooKeeper Server"); 28 | Process process = processBuilder.start(); 29 | BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); 30 | String line; 31 | while ((line = reader.readLine()) != null) { 32 | if(line.contains("Error")){ 33 | status = "Offline"; 34 | process.destroy(); 35 | } else if(line.contains("Mode")){ 36 | status = "Online"; 37 | process.destroy(); 38 | } 39 | } 40 | } catch (IOException e) { 41 | e.printStackTrace(); 42 | } 43 | return status; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | // Configure Message Broker for WebSocket Metrics Functionality 14 | @Override 15 | public void configureMessageBroker(MessageBrokerRegistry config){ 16 | config.enableSimpleBroker("/topic/"); 17 | config.setApplicationDestinationPrefixes("/app"); 18 | } 19 | 20 | // Create a STOMP Connection for WebSocket Metrics Functionality 21 | @Override 22 | public void registerStompEndpoints(StompEndpointRegistry registry) { 23 | registry.addEndpoint("/socketConnect").withSockJS(); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/controllers/ClusterController.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend.controllers; 2 | 3 | import com.kafkasprout.backend.AdminService; 4 | import com.kafkasprout.backend.StartZoo; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.io.FileNotFoundException; 12 | import java.io.IOException; 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | import java.util.concurrent.ExecutionException; 16 | import java.util.Hashtable; 17 | 18 | import com.kafkasprout.backend.StartBroker; 19 | import com.kafkasprout.backend.Status; 20 | import com.kafkasprout.backend.CheckPath; 21 | 22 | 23 | @RestController 24 | public class ClusterController { 25 | 26 | @Autowired 27 | public AdminService admin; 28 | 29 | @Autowired 30 | public CheckPath checkPath; 31 | 32 | // check status of zookeeper and kafka servers 33 | @GetMapping("/checkStatus") 34 | public Map checkStatus() { 35 | Map status = new HashMap<>(); 36 | 37 | String OS = System.getProperty("os.name").toLowerCase(); 38 | Status checkStatus = new Status(OS); 39 | String zooStatus = checkStatus.run(); 40 | 41 | status.put("zookeeper", zooStatus); 42 | status.put("kafka", String.valueOf(admin.isLive())); 43 | 44 | return status; 45 | } 46 | 47 | // describe all topic and brokers 48 | @GetMapping("/describeTopicsAndBrokers") 49 | public Map describeTopicsAndBrokers() throws ExecutionException, InterruptedException { 50 | return admin.describeTopicsAndBrokers(); 51 | } 52 | 53 | @GetMapping("/describeBrokers") 54 | public Object describeBrokers() throws ExecutionException, InterruptedException { 55 | Map info = admin.describeTopicsAndBrokers(); 56 | return info.get("Brokers"); 57 | } 58 | 59 | // start broker: requires a json input 60 | @PostMapping("/startBroker") 61 | public String mapping(@RequestBody HashMap payload) throws FileNotFoundException, IOException { 62 | checkPath.storeProperties(payload); 63 | return StartBroker.start(payload); 64 | } 65 | 66 | // start cluster 67 | @PostMapping("/startCluster") 68 | public boolean start(@RequestBody HashMap payload) throws FileNotFoundException, IOException { 69 | String configPath = payload.get("path"); 70 | // String configPath = "C:\\kafka_2.12-2.5.0\\config"; 71 | String OS = System.getProperty("os.name").toLowerCase(); 72 | StartZoo zooThread = new StartZoo(configPath, OS); 73 | boolean isZoo = zooThread.run(); 74 | // boolean isZoo = true; 75 | if (isZoo) { 76 | admin.startClient(); 77 | } 78 | checkPath.storePath(configPath); 79 | 80 | return isZoo; 81 | } 82 | 83 | // retrieves path for properties files when starting Kafka/Zookeeper servers 84 | @GetMapping("/getPath") 85 | public String checkPath() throws FileNotFoundException, IOException { 86 | //CheckPath pathCheck = new CheckPath(); 87 | return checkPath.retrievePath(); 88 | } 89 | 90 | // retrieves path for log files, broker ID, and port number for starting up Kafka cluster 91 | @GetMapping("/getProperties") 92 | public Hashtable checkProperties() throws FileNotFoundException, IOException { 93 | //CheckPath pathCheck = new CheckPath(); 94 | // returns a hash table, which is received as a json object in the frontend 95 | return checkPath.retrieveProperties(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/controllers/MetricsController.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend.controllers; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ExecutionException; 5 | 6 | import com.kafkasprout.backend.AdminService; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.messaging.handler.annotation.MessageMapping; 10 | import org.springframework.messaging.handler.annotation.SendTo; 11 | import org.springframework.messaging.simp.SimpMessagingTemplate; 12 | import org.springframework.scheduling.annotation.EnableScheduling; 13 | import org.springframework.scheduling.annotation.Scheduled; 14 | import org.springframework.stereotype.Controller; 15 | 16 | @EnableScheduling 17 | @Controller 18 | public class MetricsController { 19 | 20 | @Autowired 21 | public AdminService admin; 22 | 23 | @Autowired 24 | private SimpMessagingTemplate template; 25 | 26 | // Scheduled metric push every second using Web Sockets 27 | @Scheduled(fixedRate = 1000) 28 | public void metrics() throws ExecutionException, InterruptedException { 29 | // System.out.println("scheduled"); 30 | // if Kafka Cluster is Live 31 | if (admin.isLive()) { 32 | this.template.convertAndSend("/topic/metrics", admin.metrics()); 33 | } 34 | } 35 | 36 | // test for web socket message broker connectivity 37 | @MessageMapping("/test") 38 | @SendTo("/topic/metrics") 39 | public Map greeting(Map message) throws Exception { 40 | System.out.println(message.get("message")); 41 | return message; 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/controllers/TopicsController.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend.controllers; 2 | 3 | import com.kafkasprout.backend.AdminService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import java.util.*; 11 | import java.util.concurrent.ExecutionException; 12 | 13 | @RestController 14 | public class TopicsController { 15 | 16 | @Autowired 17 | public AdminService admin; 18 | 19 | // describe Topics of Cluster 20 | @GetMapping("/describeTopics") 21 | public Object listTopics() throws ExecutionException, InterruptedException { 22 | Map info = admin.describeTopicsAndBrokers(); 23 | return info.get("Topics"); 24 | } 25 | 26 | // create Topic of Cluster: requires json input with key/value of name, desired replication factor, and desired partition count 27 | @PostMapping("/createTopics") 28 | public void createTopic(@RequestBody HashMap payload) { 29 | admin.createTopic(payload); 30 | } 31 | 32 | // delete Topic of Cluster: requires json input with name 33 | @PostMapping("/deleteTopics") 34 | public void deleteTopic(@RequestBody HashMap payload) { 35 | admin.deleteTopic(payload); 36 | } 37 | 38 | // describe Topic and Broker Configuration of Cluster 39 | @GetMapping("/describeTopicAndBrokerConfig") 40 | public Map>> describeTopicsConfig() throws ExecutionException, InterruptedException { 41 | return admin.describeTopicAndBrokerConfig(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/kafkasprout/backend/controllers/ViewController.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend.controllers; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | 6 | @Controller 7 | public class ViewController { 8 | 9 | // Global Route 10 | @RequestMapping("/") 11 | public String index() { 12 | return "index"; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/main/js/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage -------------------------------------------------------------------------------- /src/main/js/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | }; -------------------------------------------------------------------------------- /src/main/js/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: string; 3 | export default content; 4 | } -------------------------------------------------------------------------------- /src/main/js/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Kafka-Sprout/40c71ad837a5d207231aa549f721c88592f37083/src/main/js/favicon.ico -------------------------------------------------------------------------------- /src/main/js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Kafka Sprout 7 | 8 | 9 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/js/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './src/components/App'; 4 | import { createGlobalStyle } from 'styled-components'; 5 | import constants from './src/UIComponents/constants'; 6 | 7 | const GlobalStyle = createGlobalStyle` 8 | body { 9 | margin: 0; 10 | font-family: ${constants.LIBRE_FRANKLIN}; 11 | background-color: ${constants.BODY_BACKGROUND}; 12 | } 13 | #root { 14 | width: 100vw; 15 | height: 100vh; 16 | padding: 1rem; 17 | box-sizing: border-box; 18 | } 19 | `; 20 | 21 | ReactDOM.render(<>, document.getElementById('root')); 22 | -------------------------------------------------------------------------------- /src/main/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kafka-start-frontend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.prod.js", 8 | "dev": "webpack-dev-server --open --config webpack.dev.js", 9 | "watch": "webpack --watch -d", 10 | "install-if-needed": "npm-install-if-needed" 11 | }, 12 | "keywords": [], 13 | "author": "OS Labs", 14 | "license": "ISC", 15 | "dependencies": { 16 | "chart.js": "^2.9.3", 17 | "chartjs-plugin-streaming": "^1.8.0", 18 | "react": "^16.5.2", 19 | "react-chartjs-2": "^2.10.0", 20 | "react-dom": "^16.5.2", 21 | "react-loader-spinner": "^3.1.14", 22 | "react-router-dom": "^5.2.0", 23 | "react-stomp": "^5.0.0", 24 | "react-tooltip": "^4.2.7", 25 | "reactjs-popup": "^1.5.0", 26 | "rest": "^1.3.1", 27 | "styled-components": "^5.1.1" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.1.0", 31 | "@babel/preset-env": "^7.1.0", 32 | "@babel/preset-react": "^7.0.0", 33 | "@types/node": "^14.0.23", 34 | "@types/react": "^16.9.43", 35 | "@types/react-dom": "^16.9.8", 36 | "@types/react-loader-spinner": "^3.1.0", 37 | "@types/styled-components": "^5.1.1", 38 | "@typescript-eslint/eslint-plugin": "^3.8.0", 39 | "@typescript-eslint/parser": "^3.8.0", 40 | "babel-loader": "^8.0.2", 41 | "clean-webpack-plugin": "^3.0.0", 42 | "eslint": "^7.6.0", 43 | "file-loader": "^6.0.0", 44 | "html-webpack-harddisk-plugin": "^1.0.2", 45 | "html-webpack-plugin": "^4.3.0", 46 | "react-hot-loader": "^4.12.21", 47 | "ts-loader": "^8.0.1", 48 | "typescript": "^3.9.7", 49 | "uglifyjs-webpack-plugin": "^2.2.0", 50 | "webpack": "^4.19.1", 51 | "webpack-cli": "^3.1.0", 52 | "webpack-dev-server": "^3.11.0", 53 | "webpack-merge": "^5.1.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/Buttons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import constants from './constants'; 4 | import Popup from 'reactjs-popup'; 5 | 6 | const crossBrowserTransition = (value: string) => ` 7 | transition: ${value}; 8 | -webkit-transition: ${value}; 9 | -moz-transition: ${value}; 10 | `; 11 | 12 | const BUTTON_TRANSITION = crossBrowserTransition("0.2s"); 13 | 14 | /** 15 | * Basic button with default grey-green background color and white text 16 | */ 17 | export const Button = styled.button` 18 | font-size: 1rem; 19 | padding: 0.5rem; 20 | box-sizing: border-box; 21 | border-radius: 4px; 22 | color: #fff; 23 | background-color: ${constants.GREEN}; 24 | border: solid 1px ${constants.GREEN}; 25 | cursor: pointer; 26 | ${BUTTON_TRANSITION} 27 | &:active { 28 | transform: scale(0.9); 29 | } 30 | &:focus { 31 | outline: none; 32 | } 33 | `; 34 | 35 | /** 36 | * Button to start the cluster (has more padding) 37 | */ 38 | export const StartClusterButton = styled(Button)` 39 | background-color: ${constants.GREEN}; 40 | border: solid 1px ${constants.GREEN}; 41 | padding: 1rem; 42 | `; 43 | 44 | /** 45 | * Button with white background and grey-green text (and border) 46 | */ 47 | export const WhiteButton = styled(Button)` 48 | background-color: #fff; 49 | color: ${constants.GREEN}; 50 | `; 51 | 52 | interface ButtonWithPopupProps { 53 | children: string, 54 | popup: React.ReactElement 55 | } 56 | 57 | /** 58 | * Button with Popup component attached 59 | * The children of ButtonWithPopup gets passed down into the Button, so it should be a String 60 | * @param popup The React element to show in the popup 61 | */ 62 | export const ButtonWithPopup = (props: ButtonWithPopupProps) => { 63 | return ( 64 | {props.children}} position="bottom center"> 65 | {props.popup} 66 | 67 | ); 68 | }; 69 | 70 | /** 71 | * WhiteButton with Popup component attached 72 | * The children of ButtonWithPopup gets passed down into the Button, so it should be a String 73 | * @param popup The React element to show in the popup 74 | */ 75 | export const WhiteButtonWithPopup = (props: ButtonWithPopupProps) => { 76 | return ( 77 | {props.children}} position="bottom center"> 78 | {props.popup} 79 | 80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/FlexContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | interface FlexContainerProps { 4 | flexDirection?: 'row' | 'column' | 'row-reverse' | 'column-reverse' 5 | justifyContent?: 'flex-start' | 'flex-end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' | 'start' | 'end' | 'left' | 'right', 6 | alignItems?: 'stretch' | 'flex-start' | 'flex-end' | 'center' | 'baseline' | 'first baseline' | 'last baseline' | 'start' | 'end' | 'self-start' | 'self-end', 7 | addlStyles?: string, 8 | } 9 | 10 | /** 11 | * Div with CSS flexbox enabled that takes in standard flex properties. 12 | * Content is centered by default, and width and height are 100% by default 13 | * Additional styles can be provided in a string which should be 14 | * formatted like CSS. 15 | */ 16 | const FlexContainer = styled.div` 17 | display: flex; 18 | flex-direction: ${props => props.flexDirection || 'row'}; 19 | justify-content: ${props => props.justifyContent || 'center'}; 20 | align-items: ${props => props.alignItems || 'center'}; 21 | ${props => props.addlStyles || ''} 22 | `; 23 | 24 | export default FlexContainer; -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/GridSection.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import constants from './constants'; 4 | import Popup from 'reactjs-popup'; 5 | import withPopup from './withPopup'; 6 | 7 | /** 8 | * A CSS grid container div 9 | * @prop {Number} columns - The number of columns 10 | */ 11 | export const GridContainer = styled.div<{ columns: number }>` 12 | display: grid; 13 | grid-template-columns: repeat( 14 | ${(props) => props.columns}, 15 | minmax(2rem, auto) 16 | ); 17 | column-gap: 1px; 18 | border: 1px solid ${constants.DARK_GREEN}; 19 | width: 100%; 20 | box-sizing: border-box; 21 | `; 22 | 23 | interface RowProps { 24 | rowNum?: number, 25 | content: string[], 26 | } 27 | 28 | /** 29 | * Row that renders content into basic Cells. 30 | */ 31 | export const ContentRow = (props: RowProps) => { 32 | const cells = props.content.map((content) => {content}); 33 | return <>{cells}; 34 | }; 35 | 36 | /** 37 | * Row that renders content into HeaderCells. 38 | */ 39 | export const HeaderRow = (props: RowProps) => { 40 | const cells = props.content.map((header) => ( 41 | {header} 42 | )); 43 | return <>{cells}; 44 | }; 45 | 46 | 47 | interface TopicListConfigProps { 48 | popup: { 49 | cleanUpPolicy: string; 50 | minInsyncReplicas: string; 51 | messageTimeStampType: string; 52 | compressionType: string; 53 | }; 54 | } 55 | 56 | const TopicConfigInfo = (props: TopicListConfigProps) => { 57 | console.log("from ConfigInfo", props); 58 | const { cleanUpPolicy, minInsyncReplicas, messageTimeStampType, compressionType } = props.popup; 59 | const configInfo = ["Clean Up Policy:", cleanUpPolicy, "Min Insyc Replicas:", minInsyncReplicas, "Time Stamp:", messageTimeStampType, "Compression Type:", compressionType]; 60 | return ( 61 | null 62 | //
63 | // Clean Up Policy 64 | // {props.popup.cleanUpPolicy} 65 | // Min Insync Replicas 66 | // {props.popup.minInsyncReplicas} 67 | // Message Time Stamp Type 68 | // {props.popup.messageTimeStampType} 69 | // Compression Type 70 | // {props.popup.compressionType} 71 | //
72 | // 73 | ); 74 | }; 75 | 76 | const ConfigInfoRow = styled.div` 77 | display: flex; 78 | flex-direction: column; 79 | `; 80 | 81 | interface RowWithConfigProps extends RowProps { 82 | configInfo?: string[], 83 | } 84 | 85 | export const TopicRow = (props: RowWithConfigProps) => { 86 | const cells = props.content.map((content, index) => { 87 | if (index === 0) { 88 | return withPopup( 89 | {content}, 90 | 91 | ); 92 | } else { 93 | return {content}; 94 | } 95 | }); 96 | return <>{cells}; 97 | }; 98 | 99 | interface BrokerListConfigProps { 100 | [popup: string]: { 101 | backgroundThreads: string; 102 | compressionType: string; 103 | logDir: string; 104 | logRetentionHours: string; 105 | messageMaxBytes: string; 106 | minInsyncReplicas: string; 107 | zookeeperConnect: string; 108 | }; 109 | } 110 | 111 | const BrokerConfigInfo = (props: BrokerListConfigProps) => { 112 | console.log("from ConfigInfo", props); 113 | return ( 114 |
115 | Background Threads 116 | {props.popup.backgroundThreads} 117 | Compression Type 118 | {props.popup.compressionType} 119 | Log Directory 120 | {props.popup.logDir} 121 | Log Retention Hours 122 | {props.popup.logRetentionHours} 123 | Message Max Bytes 124 | {props.popup.messageMaxBytes} 125 | Min Insync Replicas 126 | {props.popup.minInsyncReplicas} 127 | Zookeeper Connect 128 | {props.popup.zookeeperConnect} 129 |
130 | ); 131 | }; 132 | 133 | export const BrokerRow = (props: RowWithConfigProps) => { 134 | const cells = props.content.map((content, index) => { 135 | if (index === 0) { 136 | return withPopup( 137 | {content}, 138 | 139 | ); 140 | } else { 141 | return {content}; 142 | } 143 | }); 144 | return <>{cells}; 145 | }; 146 | 147 | interface CellProps { 148 | backgroundColor?: string, 149 | color?: string, 150 | } 151 | 152 | /** 153 | * Basic div to act as a cell for CSS grid components. 154 | * Default appearance is white BG with black text and content centered. 155 | */ 156 | const Cell = styled.div` 157 | display: flex; 158 | justify-content: center; 159 | align-items: center; 160 | padding: 0.5rem; 161 | background-color: ${props => props.backgroundColor || 'white'}; 162 | color: ${props => props.color || 'black'}; 163 | box-sizing: border-box; 164 | `; 165 | 166 | interface AltBGCellProps extends CellProps { 167 | rowNum: number 168 | } 169 | 170 | /** 171 | * Cell with alternating background color for alternating row colors. 172 | * Colors are hard-coded for now. 173 | */ 174 | const AltBGCell = styled(Cell) ` 175 | background-color: ${props => props.rowNum % 2 === 1 ? constants.DARK_GREY_GREEN : constants.GREY_GREEN}; 176 | color: white; 177 | `; 178 | 179 | const AltBGCellwithPointer = styled(AltBGCell)`cursor: pointer;`; 180 | 181 | /** 182 | * Header cell for grid with alternating rows. 183 | */ 184 | const HeaderCell = styled(Cell)` 185 | background-color: white; 186 | color: ${constants.DARK_GREEN}; 187 | `; 188 | -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/GridTitle.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | /** 4 | * Outer container for the grid title section. 5 | * Horizontal flex container where items are vertically aligned 6 | * and have margins between them 7 | */ 8 | export const GridTitleContainer = styled.div` 9 | display: flex; 10 | align-items: center; 11 | & > * { 12 | margin-right: 1rem; 13 | } 14 | `; 15 | 16 | /** 17 | * Inline h3 element 18 | */ 19 | export const GridTitle = styled.h3<{children: string}>` 20 | display: inline-block; 21 | `; -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/LabeledDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | interface DropdownProps { 5 | name: string, 6 | label: string, 7 | options: string[], 8 | value: string, 9 | [key: string]: any 10 | } 11 | 12 | /** 13 | * Basic select element with label element positioned on top of dropdown. 14 | * Width of select is hardcoded to 100% but code could be edited. 15 | * Select is controlled component for now. 16 | */ 17 | const LabeledDropdown = (props: DropdownProps) => { 18 | return ( 19 | <> 20 | 21 | 24 | 25 | ); 26 | }; 27 | 28 | const Select = styled.select` 29 | width: 100%; 30 | padding: 0.25rem; 31 | box-sizing: border-box; 32 | ` 33 | 34 | export default LabeledDropdown; -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/LabeledInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import ReactTooltip from 'react-tooltip'; 4 | import questionSVG from '../assets/question.svg'; 5 | import FlexContainer from './FlexContainer'; 6 | 7 | interface LabeledInputProps { 8 | className?: string, 9 | name: string, 10 | labelText: string, 11 | toolTipText?: string, 12 | vertical?, 13 | horizontal?, 14 | value?: string, 15 | [key: string]: any, 16 | } 17 | 18 | /** 19 | * A text input with label attached. 20 | * Pass an empty prop 'vertical' or 'horizontal' to determine whether the label will be 21 | * on top or to the left of the input. 22 | * @prop {String} name value to pass into 'name' attribute of input and 'for' attribute of label 23 | * @prop {String} labelText 24 | * @prop {String} toolTipText (optional) adds a tooltip to the label for further description 25 | */ 26 | const LabeledInput = (props: LabeledInputProps) => { 27 | const label = props.toolTipText ? 28 | : 29 | ; 30 | return ( 31 | 32 | {label} 33 | 34 | 35 | ); 36 | }; 37 | 38 | const TooltipImage = styled.img` 39 | height: 1rem; 40 | width: auto; 41 | `; 42 | 43 | const Label = styled.label` 44 | display: flex; 45 | align-items: center; 46 | img { 47 | margin-left: 0.25rem; 48 | } 49 | margin-bottom: 0.25rem; 50 | `; 51 | 52 | const LabelWithToolTip = props => { 53 | return ( 54 | <> 55 | 58 | 59 | 60 | {props.toolTipText} 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default LabeledInput; -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import logoWithTitle from '../assets/kafka_sprout_title.svg'; 4 | import logo from '../assets/kafka_sprout_logo.svg'; 5 | 6 | export const LogoWithTitle = (props) => { 7 | return ; 8 | } 9 | 10 | export const Logo = (props) => { 11 | return ; 12 | } 13 | 14 | const Image = styled.img<{styles: string}>` 15 | ${props => props.styles} 16 | `; -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/PopupContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | /** 4 | * Div element container for contents of Popups 5 | */ 6 | const PopupContainer = styled.div` 7 | padding: 0.25rem 0.5rem; 8 | box-sizing: border-box; 9 | & > * { 10 | margin: 0.25rem 0 11 | } 12 | `; 13 | 14 | export default PopupContainer; -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/TabView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import FlexContainer from './FlexContainer'; 3 | import styled from 'styled-components'; 4 | import constants from './constants'; 5 | 6 | //interface TabViewProps { 7 | // children?: React.ReactElement[] | React.ReactElement 8 | //} 9 | 10 | export const TabView = (props) => { 11 | const [selected, setSelected] = useState('Brokers'); 12 | 13 | return ( 14 | 24 | child.props.tabName)} setSelected={setSelected} /> 25 | {props.children.filter(child => child.props.tabName === selected)} 26 | 27 | ); 28 | }; 29 | 30 | interface TabContentProps { 31 | tabName: string, 32 | children?: React.ReactElement[] | React.ReactElement 33 | } 34 | 35 | /** 36 | * Wrapper component for elements belonging to one tabbed area. 37 | * TabContent should be assigned a tabName that will appear 38 | * in the TabMenu. 39 | */ 40 | export const TabContent = (props: TabContentProps) => { 41 | return ( 42 | 47 | {props.children} 48 | 49 | ) 50 | } 51 | 52 | /** 53 | * Component to render the clickable tabs above the TabContent. 54 | */ 55 | const TabMenu = (props) => { 56 | return ( 57 | 66 | {props.tabs.map(tab => ( 67 | props.setSelected(tab)}> 68 | {tab} 69 | 70 | ))} 71 | 72 | ) 73 | } 74 | 75 | const Tab = styled.div<{ selected: boolean }>` 76 | color: ${constants.DARK_GREEN}; 77 | cursor: pointer; 78 | padding: 0.5rem 1rem; 79 | box-sizing: content-box; 80 | border-radius: ${constants.BORDER_RADIUS} ${constants.BORDER_RADIUS} 0 0; 81 | border: 1px solid ${props => props.selected ? constants.DARK_GREEN : 'transparent'}; 82 | border-bottom: ${props => props.selected ? `1px solid ${constants.BODY_BACKGROUND}` : '0'}; 83 | ${props => props.selected ? 'margin-bottom: -1px;' : ''} 84 | `; -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/constants.tsx: -------------------------------------------------------------------------------- 1 | const constants = { 2 | // COLORS 3 | DARK_GREY_GREEN: 'hsl(100, 30%, 47%)', 4 | GREY_GREEN: 'hsl(100, 30%, 55%)', 5 | DARK_GREEN: 'hsl(100, 33%, 41%)', 6 | GREEN: 'hsl(100, 42%, 50%)', 7 | LIGHT_GREEN: '#B2DBA0', 8 | LIGHTER_GREEN: '#D1E9C7', 9 | BODY_BACKGROUND: 'hsl(90, 10%, 96%)', 10 | 11 | // NUMBERS 12 | BORDER_RADIUS: '4px', 13 | 14 | // FONT-FAMILY 15 | LIBRE_FRANKLIN: "'Libre Franklin', sans-serif", 16 | }; 17 | 18 | export default constants; 19 | -------------------------------------------------------------------------------- /src/main/js/src/UIComponents/withPopup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Popup from 'reactjs-popup'; 3 | 4 | const withPopup = (component, popup) => { 5 | return ( 6 | 7 | {popup} 8 | 9 | ); 10 | } 11 | 12 | export default withPopup; -------------------------------------------------------------------------------- /src/main/js/src/assets/kafka_sprout_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 13 | 14 | 15 | 17 | 19 | 20 | 21 | 24 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/js/src/assets/kafka_sprout_title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Kafka Sprou 49 | 50 | 51 | 52 | 53 | 55 | 57 | 58 | 59 | 64 | 66 | 67 | 68 | 69 | 73 | 80 | 86 | 90 | 96 | 103 | 109 | 113 | 118 | 123 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/main/js/src/assets/question.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 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 | -------------------------------------------------------------------------------- /src/main/js/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import StartZookeeper from './StartZookeeper'; 3 | import Main from './Main'; 4 | import Loader from 'react-loader-spinner'; 5 | import constants from '../UIComponents/constants'; 6 | import FlexContainer from '../UIComponents/FlexContainer'; 7 | 8 | interface StatusModel { 9 | zookeeper: '' | 'Offline' | 'Online', 10 | kafka: '' | 'true' | 'false' 11 | } 12 | 13 | const App = () => { 14 | // State hook for Zookeeper server status 15 | const [status, setStatus] = useState({ 16 | zookeeper: '', 17 | kafka: '' 18 | }); 19 | 20 | // Sends GET request when app initializes to receive status on Zookeeper server 21 | // TODO: Figure out how to check Zookeeper status on Windows 22 | useEffect(() => { 23 | if (navigator.userAgent.toLowerCase().indexOf('windows') < 0) { // Check for user's operating system, only perform if not Windows 24 | fetch('/checkStatus') 25 | .then(res => res.json()) 26 | .then(status => { 27 | setStatus(status); 28 | }) 29 | .catch(err => { 30 | throw new Error('Error in fetching Zookeeper status: ' + err); 31 | }); 32 | } else { 33 | setStatus({ 34 | zookeeper: 'Online', 35 | kafka: 'true' 36 | }); 37 | } 38 | }, []); 39 | 40 | if (status.zookeeper === 'Offline') { 41 | return ; 42 | } else if (status.zookeeper === 'Online') { 43 | return
; 44 | } else { 45 | // Load loading bar 46 | return ( 47 | 48 | 54 | 55 | ); 56 | } 57 | }; 58 | 59 | export default App; 60 | -------------------------------------------------------------------------------- /src/main/js/src/components/BrokerConfig.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useLayoutEffect } from 'react'; 2 | import PopupContainer from '../UIComponents/PopupContainer'; 3 | import { Button } from "../UIComponents/Buttons"; 4 | import LabeledInput from "../UIComponents/LabeledInput"; 5 | import Loader from 'react-loader-spinner'; 6 | import constants from '../UIComponents/constants'; 7 | 8 | interface ConfigModel { 9 | // broker.id 10 | broker_id: number | ''; 11 | // log.dirs 12 | directory: string; 13 | // listeners PLAINTEST://:9093 14 | port: string; 15 | // path to save properties file 16 | properties: string; 17 | } 18 | 19 | type Props = { 20 | [key: string]: any; 21 | }; 22 | 23 | export const BrokerConfig: React.FC = (props: Props) => { 24 | const [config, setConfig] = useState({ broker_id: '', directory: '', port: '', properties: '' }); 25 | const [error, setError] = useState(''); 26 | const [loading, setLoading] = useState(false); 27 | 28 | const updateConfig = e => { 29 | setConfig({ 30 | ...config, 31 | [e.target.name]: e.target.value 32 | }); 33 | }; 34 | 35 | const getProperties = () => { 36 | fetch('/getProperties') 37 | .then(res => res.json()) 38 | .then(res => { 39 | console.log(res); 40 | setConfig({ 41 | properties: res.path, 42 | directory: res.logPath, 43 | broker_id: res.id, 44 | port: res.port 45 | }); 46 | }); 47 | }; 48 | 49 | useLayoutEffect(() => { 50 | getProperties(); 51 | }, []); 52 | 53 | const handleSubmit = () => { 54 | const validateConfig = { ...config }; 55 | validateConfig.directory = validateConfig.directory.replace(/\\/g, '/'); 56 | validateConfig.properties = validateConfig.properties.replace(/\\/g, '/'); 57 | setLoading(true); 58 | fetch("/startBroker", { 59 | method: "POST", 60 | headers: { 61 | 'Content-Type': 'application/json' 62 | }, 63 | body: JSON.stringify(validateConfig) 64 | }) 65 | .then(res => res.text()) 66 | .then(res => { 67 | setLoading(false) 68 | if (res === 'true') { 69 | props.updateBrokerList(); 70 | setError(''); 71 | } 72 | else { 73 | throw new Error(res); 74 | } 75 | }) 76 | .catch(err => { 77 | setError('Error in starting broker: ' + err); 78 | }); 79 | }; 80 | 81 | return ( 82 | 83 | 91 | 99 | 107 | 115 | {loading ? : } 116 | {error.length > 0 &&
{"please try again \n"} {error}
} 117 |
118 | ); 119 | }; -------------------------------------------------------------------------------- /src/main/js/src/components/BrokerDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import FlexContainer from '../UIComponents/FlexContainer'; 3 | import { 4 | GridContainer, 5 | HeaderRow, 6 | BrokerRow, 7 | } from "../UIComponents/GridSection"; 8 | import { GridTitleContainer, GridTitle } from "../UIComponents/GridTitle"; 9 | import { ButtonWithPopup } from "../UIComponents/Buttons"; 10 | import { BrokerConfig } from "./BrokerConfig"; 11 | 12 | export const BrokerDisplay = props => { 13 | const [brokerConfig, setBrokerConfig] = useState([]); 14 | 15 | const updateList = async () => { 16 | const res = await fetch("/describeTopicAndBrokerConfig"); 17 | const data = await res.json(); 18 | setBrokerConfig(data.Brokers); 19 | }; 20 | 21 | useEffect(() => { 22 | updateList(); 23 | }, []); 24 | 25 | const headers = props.brokerData[0]; 26 | const rows = props.brokerData.slice(1, props.brokerData.length); 27 | 28 | return ( 29 | * { 38 | width: 100%; 39 | max-width: 50rem; 40 | }` 41 | } 42 | > 43 | 44 | Brokers 45 | } 47 | > 48 | + Add Broker 49 | 50 | 51 | 52 | 53 | {rows.map((row, index) => ( 54 | 55 | ))} 56 | 57 | 58 | ); 59 | }; 60 | 61 | export default BrokerDisplay; 62 | -------------------------------------------------------------------------------- /src/main/js/src/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import TopicDisplay from './TopicDisplay'; 3 | import BrokerDisplay from './BrokerDisplay'; 4 | import Loader from 'react-loader-spinner'; 5 | import constants from '../UIComponents/constants'; 6 | import MetricsDisplay from './MetricsDisplay'; 7 | import FlexContainer from "../UIComponents/FlexContainer"; 8 | import { TabView, TabContent } from '../UIComponents/TabView'; 9 | 10 | 11 | const Main = props => { 12 | const [broker, setBroker] = useState(null); 13 | const [topic, setTopic] = useState(null); 14 | const [isLoaded, setIsLoaded] = useState(false); 15 | 16 | const updateBrokerList = () => { 17 | fetch('/describeBrokers') 18 | .then(res => res.json()) 19 | .then(res => { 20 | setBroker(res); 21 | }) 22 | .catch(err => { 23 | throw new Error('Error in getting brokers' + err); 24 | }); 25 | }; 26 | 27 | const updateTopicList = () => { 28 | fetch('/describeTopics') 29 | .then(res => res.json()) 30 | .then(res => { 31 | setTopic(res); 32 | }) 33 | .catch(err => { 34 | throw new Error('Error in getting topics' + err); 35 | }); 36 | }; 37 | 38 | const updateList = async () => { 39 | const res = await fetch('/describeTopicsAndBrokers'); 40 | if (!res.ok) { 41 | throw new Error('Error in loading data' + res); 42 | } 43 | const data = await res.json(); 44 | setTopic(data.Topics); 45 | setBroker(data.Brokers); 46 | }; 47 | 48 | useEffect(() => { 49 | updateList().then(() => setIsLoaded(true)); 50 | }, []); 51 | 52 | if (isLoaded) { 53 | if (props.status === 'false') { 54 | return ( 55 | 56 | 57 | 58 | 59 | ); 60 | } else { 61 | return ( 62 | 63 | 64 | 65 | 66 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | } else { 79 | return ( 80 | 81 | 87 | 88 | ); 89 | } 90 | }; 91 | 92 | export default Main; 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/main/js/src/components/MetricsDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import SockJsClient from "react-stomp"; 3 | import { Line } from "react-chartjs-2"; 4 | import "chartjs-plugin-streaming"; 5 | import FlexContainer from '../UIComponents/FlexContainer'; 6 | import { Logo } from '../UIComponents/Logo'; 7 | import constants from '../UIComponents/constants' 8 | 9 | const MetricsDisplay = () => { 10 | const clientRef = useRef(null); 11 | const [waitTime, setWaitTime] = useState(0); 12 | const [requestTotal, setRequestTotal] = useState(0); 13 | const [networkRate, setNetworkRate] = useState([]); 14 | const [responseRate, setResponseRate] = useState([]); 15 | 16 | const networkData = { 17 | labels: new Array(10).fill(''), 18 | datasets: [ 19 | { 20 | label: "Network I/O Rate (/s)", 21 | fill: false, 22 | lineTension: 0.1, 23 | backgroundColor: "rgba(75,192,192,0.4)", 24 | borderColor: constants.GREEN, //"rgba(75,192,192,1)", 25 | borderCapStyle: "butt", 26 | borderDash: [], 27 | borderDashOffset: 0.0, 28 | borderJoinStyle: "miter", 29 | pointBorderColor: constants.GREEN, 30 | pointBackgroundColor: "#fff", 31 | pointBorderWidth: 1, 32 | pointHoverRadius: 5, 33 | pointHoverBackgroundColor: constants.GREEN, 34 | pointHoverBorderColor: "rgba(220,220,220,1)", 35 | pointHoverBorderWidth: 2, 36 | pointRadius: 1, 37 | pointHitRadius: 10, 38 | data: networkRate, 39 | }, 40 | ], 41 | }; 42 | 43 | const responseData = { 44 | labels: new Array(10).fill(''), 45 | datasets: [ 46 | { 47 | label: "Response Rate (/s)", 48 | fill: false, 49 | lineTension: 0.1, 50 | backgroundColor: "rgba(75,192,192,0.4)", 51 | borderColor: constants.GREEN, //"rgba(75,192,192,1)" 52 | borderCapStyle: "butt", 53 | borderDash: [], 54 | borderDashOffset: 0.0, 55 | borderJoinStyle: "miter", 56 | pointBorderColor: constants.GREEN, 57 | pointBackgroundColor: "#fff", 58 | pointBorderWidth: 1, 59 | pointHoverRadius: 5, 60 | pointHoverBackgroundColor: constants.GREEN, 61 | pointHoverBorderColor: "rgba(220,220,220,1)", 62 | pointHoverBorderWidth: 2, 63 | pointRadius: 1, 64 | pointHitRadius: 10, 65 | data: responseRate, 66 | }, 67 | ], 68 | }; 69 | 70 | const networkOptions = { 71 | scales: { 72 | yAxes: [ 73 | { 74 | display: true, 75 | ticks: { 76 | suggestedMin: -1, // minimum will be 0, unless there is a lower value 77 | suggestedMax: 2, 78 | }, 79 | }, 80 | ], 81 | }, 82 | }; 83 | 84 | const responseOptions = { 85 | scales: { 86 | yAxes: [ 87 | { 88 | display: true, 89 | ticks: { 90 | suggestedMin: -1, 91 | suggestedMax: 1, 92 | }, 93 | }, 94 | ], 95 | }, 96 | }; 97 | 98 | const client = ( 99 | { 103 | setWaitTime(Number(msg["io-wait-time-ns-avg"])); 104 | setRequestTotal(Number(msg["request-total"])); 105 | let network = networkRate.concat(Number(msg["network-io-rate"])); 106 | if (network.length > 11) { 107 | network = network.slice(1); 108 | } 109 | 110 | setNetworkRate(network); 111 | 112 | let response = responseRate.concat(Number(msg["response-rate"])); 113 | if (response.length > 11) { 114 | response = response.slice(1); 115 | } 116 | 117 | setResponseRate(response); 118 | }} 119 | ref={(client) => { 120 | clientRef.current = client; 121 | }} 122 | /> 123 | ); 124 | 125 | return ( 126 | 137 | *:first-child { 145 | margin-right: 1rem 146 | }` 147 | } 148 | > 149 | 150 |
151 |

{`Average I/O Time: ${waitTime ? (waitTime / 1000000).toFixed(3) : 0} ms`}

152 |

{`Total Requests: ${requestTotal}`}

153 |
154 |
155 | 156 | {client} 157 | 158 | 159 |
160 | ); 161 | }; 162 | 163 | export default MetricsDisplay; 164 | -------------------------------------------------------------------------------- /src/main/js/src/components/StartZookeeper.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useLayoutEffect } from 'react'; 2 | import LabeledInput from '../UIComponents/LabeledInput'; 3 | import FlexContainer from '../UIComponents/FlexContainer'; 4 | import { StartClusterButton } from '../UIComponents/Buttons'; 5 | import Loader from 'react-loader-spinner'; 6 | import constants from '../UIComponents/constants'; 7 | import { LogoWithTitle } from '../UIComponents/Logo'; 8 | 9 | const StartZookeeper = props => { 10 | const [configPath, setConfigPath] = useState(''); 11 | const [error, setError] = useState(''); 12 | const [isLoading, setIsLoading] = useState(false); 13 | 14 | const handleChange = e => { 15 | setConfigPath(e.target.value); 16 | }; 17 | 18 | const getPath = () => { 19 | fetch('/getPath') 20 | .then(res => res.text()) 21 | .then(res => {console.log('getPath', res); setConfigPath(res)}); 22 | }; 23 | 24 | const handleClick = (e: React.MouseEvent) => { 25 | e.preventDefault(); 26 | setIsLoading(true); 27 | 28 | let path = configPath.trim(); 29 | path = path.replace(/\\/g, '/'); 30 | 31 | const request = { path }; 32 | 33 | fetch('/startCluster', { 34 | method: 'POST', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | }, 38 | body: JSON.stringify(request), 39 | }) 40 | .then(res => res.json()) 41 | .then(res => { 42 | setIsLoading(false); 43 | if (res === true) { 44 | props.setStatus({ 45 | zookeeper: 'Online', 46 | kafka: 'true', 47 | }); 48 | setError(''); 49 | } else { 50 | throw new Error('Error in starting cluster'); 51 | } 52 | }) 53 | .catch((err) => { 54 | setError(err); 55 | }); 56 | }; 57 | 58 | useLayoutEffect(() => { 59 | getPath(); 60 | }, []); 61 | 62 | return ( 63 | * { 69 | margin: 0.5rem 0; 70 | }` 71 | } 72 | > 73 | 74 |

Welcome!

75 | 83 | {isLoading ? ( 84 | 90 | ) : ( 91 | { 93 | handleClick(e); 94 | }} 95 | > 96 | Start Cluster 97 | 98 | )} 99 | {error.length > 0 &&
{error}
} 100 |
101 | ); 102 | }; 103 | 104 | export default StartZookeeper; 105 | -------------------------------------------------------------------------------- /src/main/js/src/components/TopicConfig.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import PopupContainer from "../UIComponents/PopupContainer"; 3 | import { Button } from "../UIComponents/Buttons"; 4 | import LabeledInput from "../UIComponents/LabeledInput"; 5 | import Loader from "react-loader-spinner"; 6 | import constants from "../UIComponents/constants"; 7 | 8 | interface ConfigModel { 9 | // topic name 10 | name: number; 11 | // partition count 12 | partition: string; 13 | // replication factor 14 | replication: string; 15 | } 16 | 17 | type Props = { 18 | [key: string]: any; 19 | }; 20 | 21 | const TopicConfig: React.FC = (props: Props) => { 22 | const [config, setConfig] = useState({ 23 | name: null, 24 | partition: '', 25 | replication: '', 26 | }); 27 | const [error, setError] = useState(''); 28 | const [loading, setLoading] = useState(false); 29 | 30 | const updateConfig = (e) => { 31 | setConfig({ 32 | ...config, 33 | [e.target.name]: e.target.value, 34 | }); 35 | }; 36 | 37 | const handleSubmit = () => { 38 | setLoading(true); 39 | fetch('/createTopics', { 40 | method: 'POST', 41 | headers: { 42 | 'Content-Type': 'application/json', 43 | }, 44 | body: JSON.stringify({ 45 | name: config.name, 46 | partition: config.partition, 47 | replication: config.replication, 48 | }), 49 | }).then((res) => { 50 | console.log(res); 51 | setLoading(false); 52 | if (res.ok) { 53 | props.updateTopicList(); 54 | setError(''); 55 | } else { 56 | setError('Error in creating topic'); 57 | } 58 | }); 59 | }; 60 | 61 | return ( 62 | 63 | 70 | 77 | 84 | {loading ? ( 85 | 91 | ) : ( 92 | 93 | )} 94 | {error.length > 0 &&
{error}
} 95 |
96 | ); 97 | }; 98 | 99 | export default TopicConfig; 100 | -------------------------------------------------------------------------------- /src/main/js/src/components/TopicDelete.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PopupContainer from '../UIComponents/PopupContainer'; 3 | import { Button } from '../UIComponents/Buttons'; 4 | import LabeledDropdown from '../UIComponents/LabeledDropdown'; 5 | 6 | interface TopicDeleteProps { 7 | topicNames: string[] 8 | [key: string]: any 9 | } 10 | 11 | const TopicDelete = (props: TopicDeleteProps) => { 12 | const [value, setValue] = useState(''); 13 | const [error, setError] = useState(''); 14 | 15 | const handleSubmit = () => { 16 | fetch('/deleteTopics', { 17 | method: 'POST', 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | }, 21 | body: JSON.stringify({ name: value }), 22 | }) 23 | .then(res => { 24 | if (res.ok) { 25 | props.updateTopicList(); 26 | setError(''); 27 | } 28 | else { 29 | setError('Error in deleting topic'); 30 | } 31 | }); 32 | }; 33 | 34 | const handleChange = e => { 35 | setValue(e.target.value); 36 | }; 37 | 38 | if (navigator.userAgent.toLowerCase().indexOf('windows') > 0) { 39 | return ( 40 | 41 |

42 | Safely deleting topics is not possible on Windows OS, 43 | as documented in KAFKA-1194. 44 |

45 |

46 | If you tried to delete a topic and the cluster fails to start because of an AccessDeniedException, 47 | try deleting the contents of the kafka and zookeeper folders in your Kafka data folder. 48 |

49 |
) 50 | } else { 51 | return ( 52 | 53 | 60 | {/* 61 | */} 64 | 65 | {error.length > 0 &&
{error}
} 66 |
67 | ); 68 | } 69 | }; 70 | 71 | export default TopicDelete; 72 | -------------------------------------------------------------------------------- /src/main/js/src/components/TopicDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import FlexContainer from '../UIComponents/FlexContainer'; 3 | import { 4 | GridContainer, 5 | HeaderRow, 6 | ContentRow, 7 | TopicRow, 8 | } from '../UIComponents/GridSection'; 9 | import { GridTitleContainer, GridTitle } from '../UIComponents/GridTitle'; 10 | import { ButtonWithPopup, WhiteButtonWithPopup } from '../UIComponents/Buttons'; 11 | import TopicConfig from './TopicConfig'; 12 | import TopicDelete from './TopicDelete'; 13 | import TopicDoughnut from './TopicDoughnut'; 14 | 15 | const TopicDisplay = props => { 16 | const [topicConfig, setTopicConfig] = useState([]); 17 | 18 | const updateList = async () => { 19 | const res = await fetch('/describeTopicAndBrokerConfig'); 20 | const data = await res.json(); 21 | setTopicConfig(data.Topic); 22 | }; 23 | 24 | useEffect(() => { 25 | updateList(); 26 | }, []); 27 | 28 | const headers = props.topicData[0]; 29 | const rows = props.topicData.slice(1, props.topicData.length); 30 | // NOTE: this relies on the topic name always being the first thing in the row' 31 | const topicNames = rows.map(row => row[0]); 32 | 33 | return ( 34 | * { 43 | width: 100%; 44 | max-width: 50rem; 45 | }` 46 | } 47 | > 48 | 49 | Topics 50 | } 52 | > 53 | + Add Topic 54 | 55 | 61 | } 62 | > 63 | Delete Topic 64 | 65 | 66 | 67 | 68 | {rows.map((row, index) => ( 69 | 70 | ))} 71 | 72 | 73 | 74 | ); 75 | }; 76 | 77 | export default TopicDisplay; 78 | -------------------------------------------------------------------------------- /src/main/js/src/components/TopicDoughnut.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Doughnut } from 'react-chartjs-2'; 3 | 4 | const TopicDoughnut = props => { 5 | const labels = props.content.map(arr => { 6 | return arr[0]; 7 | }); 8 | 9 | const partitions = props.content.map(arr => { 10 | return arr[2]; 11 | }); 12 | 13 | const colors = partitions.map(() => { 14 | const h = 150 - Math.floor(Math.random() * (50/10+1)) * 10; // range 180-250 interval of 10 15 | const s = 60 - Math.floor(Math.random() * (30/5+1)) * 5; 16 | const l = 80 - Math.floor(Math.random() * (30/5+1)) * 5; // range 50 - 80 interval of 10 17 | return `hsl(${h},${s}%,${l}%)`; 18 | }) 19 | 20 | const data = { 21 | labels: labels, 22 | datasets: [ 23 | { 24 | data: partitions, 25 | backgroundColor: colors, 26 | hoverBackgroundColor: colors, 27 | }, 28 | ], 29 | }; 30 | return ; 31 | }; 32 | 33 | export default TopicDoughnut; 34 | -------------------------------------------------------------------------------- /src/main/js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "jsx": "react", 5 | "watch": true, 6 | "target": "es5", 7 | "lib": ["es6", "dom"], 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/js/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: [ 7 | './index.tsx' 8 | ], 9 | output: { 10 | path: path.resolve(__dirname, '../resources/static/built/'), 11 | // Spring Boot serves static files from resources/static folder 12 | // So script file paths generated in HtmlWebpackPlugin should be 13 | // relative to static folder 14 | publicPath: '/built/', 15 | filename: '[name].bundle.js', 16 | }, 17 | optimization: { 18 | splitChunks: { 19 | chunks: 'all', 20 | minSize: 0, 21 | }, 22 | }, 23 | plugins: [ 24 | new CleanWebpackPlugin(), 25 | new HtmlWebpackPlugin({ 26 | alwaysWriteToDisk: true, 27 | template: './index.html', 28 | favicon: './favicon.ico', 29 | // make sure html gets saved into templates folder 30 | filename: path.resolve(__dirname, '../resources/templates/index.html') 31 | }), 32 | ], 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.tsx?$/, 37 | use: 'ts-loader', 38 | exclude: /node_modules/, 39 | }, 40 | { 41 | test: /\.(jpg|png|svg)$/, 42 | loader: 'file-loader', 43 | }, 44 | ], 45 | }, 46 | resolve: { 47 | extensions: ['.ts', '.tsx', '.js'], 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/main/js/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); 3 | const { mergeWithCustomize, customizeArray } = require('webpack-merge'); 4 | const common = require('./webpack.common.js'); 5 | 6 | module.exports = mergeWithCustomize({ 7 | customizeArray: customizeArray({ 8 | // arrays append by default, so change to prepend where needed 9 | entry: 'prepend', 10 | }) 11 | })(common, { 12 | mode: 'development', 13 | entry: [ 14 | // activate HMR for React 15 | 'react-hot-loader/patch', 16 | // bundle the client for webpack-dev-server 17 | // and connect to the provided endpoint 18 | 'webpack-dev-server/client?http://localhost:8888', 19 | // bundle the client for hot reloading 20 | // only- means to only hot reload for successful updates 21 | 'webpack/hot/only-dev-server' 22 | ], 23 | plugins: [ 24 | new HtmlWebpackHarddiskPlugin() 25 | ], 26 | devtool: 'sourcemaps', 27 | devServer: { 28 | hot: true, 29 | contentBase: [path.resolve(__dirname, '.'), path.resolve(__dirname, '../resources/static/built/')], 30 | proxy: { 31 | '/': 'http://localhost:8080', 32 | ignorePath: true, 33 | changeOrigin: true, 34 | secure: false, 35 | }, 36 | publicPath: '/built/', 37 | port: 8888, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /src/main/js/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 2 | const { merge } = require('webpack-merge'); 3 | const common = require('./webpack.common.js'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'production', 7 | optimization: { 8 | minimizer: [new UglifyJsPlugin()], 9 | } 10 | }); -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/ClusterWebLayerTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import static org.hamcrest.Matchers.containsString; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 8 | 9 | import com.kafkasprout.backend.controllers.ClusterControllerTest; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 14 | import org.springframework.test.web.servlet.MockMvc; 15 | 16 | @WebMvcTest(ClusterControllerTest.class) 17 | // tag::test[] 18 | public class ClusterWebLayerTest { 19 | @Autowired 20 | private MockMvc mockMvc; 21 | 22 | // placeholder 23 | @Test 24 | public void shouldReturnDefaultMessage() throws Exception { 25 | this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()) 26 | .andExpect(content().string(containsString(""))); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/HttpRequestTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 8 | import org.springframework.boot.test.web.client.TestRestTemplate; 9 | import org.springframework.boot.web.server.LocalServerPort; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) 14 | 15 | public class HttpRequestTest { 16 | 17 | 18 | @LocalServerPort 19 | private int port; 20 | 21 | @Autowired 22 | private TestRestTemplate restTemplate; 23 | 24 | 25 | // placeholder 26 | @Test 27 | public void greetingShouldReturnDefaultMessage() throws Exception { 28 | assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/", 29 | String.class)).contains(""); 30 | } 31 | 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/KafkaSproutApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class KafkaSproutApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/MetricWebLayerTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.test.web.servlet.MockMvc; 6 | 7 | import static org.hamcrest.Matchers.containsString; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | 13 | public class MetricWebLayerTest { 14 | @Autowired 15 | private MockMvc mockMvc; 16 | 17 | // placeholder 18 | @Test 19 | public void shouldReturnDefaultMessage() throws Exception { 20 | this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()) 21 | .andExpect(content().string(containsString(""))); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/SmokeTests.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import com.kafkasprout.backend.controllers.ClusterControllerTest; 4 | import com.kafkasprout.backend.controllers.MetricsController; 5 | import com.kafkasprout.backend.controllers.TopicsController; 6 | import com.kafkasprout.backend.controllers.ViewController; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | @SpringBootTest 14 | public class SmokeTests { 15 | @Autowired 16 | private ClusterControllerTest clusterController; 17 | 18 | @Autowired 19 | private TopicsController topicsController; 20 | 21 | @Autowired 22 | private ViewController viewController; 23 | 24 | @Autowired 25 | private MetricsController metricsController; 26 | 27 | @Test 28 | public void contextLoads() throws Exception{ 29 | assertThat(clusterController).isNotNull(); 30 | assertThat(viewController).isNotNull(); 31 | assertThat(metricsController).isNotNull(); 32 | assertThat(topicsController).isNotNull(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/TestingWebApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import static org.hamcrest.Matchers.containsString; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 6 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.test.web.servlet.MockMvc; 15 | 16 | @SpringBootTest 17 | @AutoConfigureMockMvc 18 | public class TestingWebApplicationTest { 19 | 20 | @Autowired 21 | private MockMvc mockMvc; 22 | 23 | // placeholder 24 | @Test 25 | public void shouldReturnDefaultMessage() throws Exception { 26 | this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()) 27 | .andExpect(content().string(containsString(""))); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/TopicWebLayerTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.test.web.servlet.MockMvc; 6 | 7 | import static org.hamcrest.Matchers.containsString; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | 13 | public class TopicWebLayerTest { 14 | @Autowired 15 | private MockMvc mockMvc; 16 | 17 | // placeholder 18 | @Test 19 | public void shouldReturnDefaultMessage() throws Exception { 20 | this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()) 21 | .andExpect(content().string(containsString(""))); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/ViewWebLayerTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.test.web.servlet.MockMvc; 6 | 7 | import static org.hamcrest.Matchers.containsString; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | 13 | public class ViewWebLayerTest { 14 | @Autowired 15 | private MockMvc mockMvc; 16 | 17 | // placeholder 18 | @Test 19 | public void shouldReturnDefaultMessage() throws Exception { 20 | this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk()) 21 | .andExpect(content().string(containsString(""))); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/controllers/ClusterControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend.controllers; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | import org.springframework.test.web.servlet.MockMvc; 10 | 11 | import static org.hamcrest.Matchers.containsString; 12 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 16 | 17 | @RunWith(SpringRunner.class) 18 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 19 | @AutoConfigureMockMvc 20 | 21 | public class ClusterControllerTest { 22 | @Autowired 23 | private MockMvc mockMvc; 24 | 25 | //placeholder 26 | @Test 27 | public void shouldReturnDefaultMessage() throws Exception { 28 | this.mockMvc.perform(get("/")) 29 | .andDo(print()).andExpect(status().isOk()) 30 | .andExpect(content().string(containsString(""))); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/controllers/MetricControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend.controllers; 2 | 3 | import org.junit.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.test.web.servlet.MockMvc; 6 | 7 | import static org.hamcrest.Matchers.containsString; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | 13 | public class MetricControllerTest { 14 | @Autowired 15 | private MockMvc mockMvc; 16 | 17 | //placeholder 18 | @Test 19 | public void shouldReturnDefaultMessage() throws Exception { 20 | this.mockMvc.perform(get("/")) 21 | .andDo(print()).andExpect(status().isOk()) 22 | .andExpect(content().string(containsString(""))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/controllers/TopicsControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend.controllers; 2 | 3 | import org.junit.Test; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.test.web.servlet.MockMvc; 6 | 7 | import static org.hamcrest.Matchers.containsString; 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 9 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 10 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 11 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 12 | 13 | public class TopicsControllerTest { 14 | @Autowired 15 | private MockMvc mockMvc; 16 | 17 | //placeholder 18 | @Test 19 | public void shouldReturnDefaultMessage() throws Exception { 20 | this.mockMvc.perform(get("/")) 21 | .andDo(print()).andExpect(status().isOk()) 22 | .andExpect(content().string(containsString(""))); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/kafkasprout/backend/controllers/ViewControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.kafkasprout.backend.controllers; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | import org.springframework.test.web.servlet.MockMvc; 10 | 11 | import static org.hamcrest.Matchers.containsString; 12 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 14 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 16 | 17 | @RunWith(SpringRunner.class) 18 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 19 | @AutoConfigureMockMvc 20 | public class ViewControllerTest { 21 | 22 | @Autowired 23 | private MockMvc mockMvc; 24 | 25 | @Test 26 | public void shouldReturnDefaultMessage() throws Exception { 27 | this.mockMvc.perform(get("/")) 28 | .andDo(print()).andExpect(status().isOk()) 29 | .andExpect(content().string(containsString("Kafka Sprout"))); 30 | } 31 | } --------------------------------------------------------------------------------