├── .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: [](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 | {props.label}
21 |
22 | {props.options.sort().map(opt => {opt} )}
23 |
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 | {props.labelText} ;
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 |
56 | {props.labelText}
57 |
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 ? : Start Broker }
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 | Create Topic
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 | {/*Select a topic to delete:
61 |
62 | {props.topicNames.sort().map(name => {name} )}
63 | */}
64 | Delete Topic
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 | }
--------------------------------------------------------------------------------