Skip to content

Commit ea44b9a

Browse files
authored
Add ability to create topics via the web ui (#93)
* Fix minor UI issues * Fix minor UI issues * minor UI tweaks * minor UI tweaks * Rework Views index datatable * Add ability to create new topics via the UI * cleanup * code style violations * add test coverage, enforce admin role on creating topics * Only show create topic link if has permissions to create a topic, add test coverage
1 parent 501f146 commit ea44b9a

File tree

26 files changed

+896
-20
lines changed

26 files changed

+896
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
66

77
- Added new Stream consumer management page at /configuration/stream
88
- Added ability to disable user authentication, allowing for anonymous users to access the web service.
9+
- Added ability to create topics via the UI.
910
- Updated SpringBoot framework from 1.5.x to 2.0.5.
1011

1112
### Breaking Changes

dev-cluster/pom.xml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>kafka-webview</artifactId>
7+
<groupId>org.sourcelab</groupId>
8+
<version>2.0.0</version>
9+
</parent>
10+
<modelVersion>4.0.0</modelVersion>
11+
12+
<artifactId>dev-cluster</artifactId>
13+
<version>2.0.0</version>
14+
15+
<!-- Require Maven 3.3.9 -->
16+
<prerequisites>
17+
<maven>3.3.9</maven>
18+
</prerequisites>
19+
20+
<!-- Module Description and Ownership -->
21+
<name>Kafka Dev Cluster</name>
22+
<description>A simple embedded kafka cluster for developing against.</description>
23+
<url>https://github.com/Crim/kafka-webview</url>
24+
<developers>
25+
<developer>
26+
<name>Stephen Powis</name>
27+
<email>[email protected]</email>
28+
<organization>SourceLab.org</organization>
29+
<organizationUrl>https://www.sourcelab.org/</organizationUrl>
30+
</developer>
31+
</developers>
32+
33+
<!-- MIT License -->
34+
<licenses>
35+
<license>
36+
<name>MIT License</name>
37+
<url>http://www.opensource.org/licenses/mit-license.php</url>
38+
</license>
39+
</licenses>
40+
41+
<!-- Module Properties -->
42+
<properties>
43+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
44+
45+
<!-- Log4J Version -->
46+
<log4j2.version>2.8.2</log4j2.version>
47+
</properties>
48+
49+
50+
<dependencies>
51+
<!-- Include Kafka-JUnit-Core -->
52+
<dependency>
53+
<groupId>com.salesforce.kafka.test</groupId>
54+
<artifactId>kafka-junit-core</artifactId>
55+
<version>3.0.1</version>
56+
</dependency>
57+
58+
<!-- Include Kafka 1.1.x -->
59+
<dependency>
60+
<groupId>org.apache.kafka</groupId>
61+
<artifactId>kafka_2.11</artifactId>
62+
<version>1.1.1</version>
63+
</dependency>
64+
<dependency>
65+
<groupId>org.apache.kafka</groupId>
66+
<artifactId>kafka-clients</artifactId>
67+
<version>1.1.1</version>
68+
</dependency>
69+
70+
<!-- Logging -->
71+
<dependency>
72+
<groupId>org.apache.logging.log4j</groupId>
73+
<artifactId>log4j-api</artifactId>
74+
<version>${log4j2.version}</version>
75+
</dependency>
76+
<dependency>
77+
<groupId>org.apache.logging.log4j</groupId>
78+
<artifactId>log4j-core</artifactId>
79+
<version>${log4j2.version}</version>
80+
</dependency>
81+
<dependency>
82+
<groupId>org.apache.logging.log4j</groupId>
83+
<artifactId>log4j-slf4j-impl</artifactId>
84+
<version>${log4j2.version}</version>
85+
</dependency>
86+
</dependencies>
87+
88+
<build>
89+
<plugins>
90+
<!-- This is just an internal module to ease development -->
91+
<!-- Module not for external consumption -->
92+
<plugin>
93+
<groupId>org.apache.maven.plugins</groupId>
94+
<artifactId>maven-deploy-plugin</artifactId>
95+
<version>2.7</version>
96+
<configuration>
97+
<skip>true</skip>
98+
</configuration>
99+
</plugin>
100+
</plugins>
101+
</build>
102+
103+
104+
</project>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* MIT License
3+
*
4+
* Copyright (c) 2017, 2018 SourceLab.org (https://github.com/Crim/kafka-webview/)
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package org.sourcelab.kafka.devcluster;
26+
27+
import com.salesforce.kafka.test.KafkaTestCluster;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
/**
32+
* Simple applicatin for firing up a Kafka cluster of 1 or more nodes. This exists solely
33+
* because standing up a real multi-node kafka cluster is more complicated than putting this together.
34+
*
35+
* Not intended for external consumption.
36+
*/
37+
public class DevCluster {
38+
private static final Logger logger = LoggerFactory.getLogger(DevCluster.class);
39+
40+
/**
41+
* Main entry point
42+
* @param args command line args.
43+
*/
44+
public static void main(final String[] args) throws Exception {
45+
// Right now we accept one parameter, the number of nodes in the cluster.
46+
final int clusterSize;
47+
if (args.length > 0) {
48+
clusterSize = Integer.parseInt(args[0]);
49+
} else {
50+
clusterSize = 1;
51+
}
52+
53+
logger.info("Starting up kafka cluster with {} brokers", clusterSize);
54+
55+
// Create a test cluster
56+
final KafkaTestCluster kafkaTestCluster = new KafkaTestCluster(clusterSize);
57+
58+
// Start the cluster.
59+
kafkaTestCluster.start();
60+
61+
kafkaTestCluster
62+
.getKafkaBrokers()
63+
.stream()
64+
.forEach((broker) -> logger.info("Started broker with Id {} at {}", broker.getBrokerId(), broker.getConnectString()));
65+
66+
logger.info("Cluster started at: {}", kafkaTestCluster.getKafkaConnectString());
67+
68+
// Wait forever.
69+
Thread.currentThread().join();
70+
}
71+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Configuration xmlns="http://logging.apache.org/log4j/2.0/config">
3+
4+
<Appenders>
5+
<Console name="STDOUT" target="SYSTEM_OUT">
6+
<PatternLayout pattern="%-5p | %d{yyyy-MM-dd HH:mm:ss} | [%t] %C{2} (%F:%L) - %m%n"/>
7+
</Console>
8+
</Appenders>
9+
10+
<Loggers>
11+
<Root level="info">
12+
<AppenderRef ref="STDOUT"/>
13+
<AppenderRef ref="FILE"/>
14+
</Root>
15+
</Loggers>
16+
17+
</Configuration>

kafka-webview-ui/src/main/frontend/js/app.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,25 @@ var ApiClient = {
254254
.getJSON('/api/cluster/' + clusterId + '/broker/' + brokerId + '/config', '', callback)
255255
.fail(ApiClient.defaultErrorHandler);
256256
},
257+
createTopic: function(clusterId, name, partitions, replicas, callback) {
258+
var payload = JSON.stringify({
259+
name: name,
260+
partitions: partitions,
261+
replicas: replicas
262+
});
263+
jQuery.ajax({
264+
type: 'POST',
265+
url: '/api/cluster/' + clusterId + '/create/topic',
266+
data: payload,
267+
dataType: 'json',
268+
headers: ApiClient.getCsrfHeader(),
269+
success: callback,
270+
error: ApiClient.defaultErrorHandler,
271+
beforeSend: function(xhr) {
272+
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
273+
}
274+
});
275+
},
257276
defaultErrorHandler: function(jqXHR, textStatus, errorThrown) {
258277
// convert response to json
259278
var response = jQuery.parseJSON(jqXHR.responseText);
@@ -273,6 +292,19 @@ var UITools = {
273292
jQuery(alertContainer)
274293
.append(alertElement);
275294

295+
if (timeoutInSecs) {
296+
setTimeout(function() {
297+
jQuery(alertElement).alert('close');
298+
}, timeoutInSecs * 1000);
299+
}
300+
},
301+
showSuccess: function(message, timeoutInSecs) {
302+
var alertContainer = jQuery(UITools.alertContainerId);
303+
304+
var alertElement = jQuery.parseHTML('<div class="alert alert-dismissable alert-success"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button> <i class="icon-check"></i> <span><strong>Success </strong>' + message + '</span></div>');
305+
jQuery(alertContainer)
306+
.append(alertElement);
307+
276308
if (timeoutInSecs) {
277309
setTimeout(function() {
278310
jQuery(alertElement).alert('close');

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/configuration/SecurityConfig.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,22 @@ private void enableUserAuth(final HttpSecurity http) throws Exception {
109109
.authorizeRequests()
110110
// Paths to static resources are available to anyone
111111
.antMatchers("/register/**", "/login/**", "/vendors/**", "/css/**", "/js/**", "/img/**")
112-
.permitAll()
112+
.permitAll()
113113
// Users can edit their own profile
114114
.antMatchers("/configuration/user/edit/**", "/configuration/user/update")
115-
.fullyAuthenticated()
116-
// But other Configuration paths require ADMIN role.
117-
.antMatchers("/configuration/**")
118-
.hasRole("ADMIN")
115+
.fullyAuthenticated()
116+
// Define admin only paths
117+
.antMatchers(
118+
// Configuration
119+
"/configuration/**",
120+
121+
// Create topic
122+
"/api/cluster/*/create/**"
123+
).hasRole("ADMIN")
124+
119125
// All other requests must be authenticated
120126
.anyRequest()
121-
.fullyAuthenticated()
127+
.fullyAuthenticated()
122128
.and()
123129

124130
// Define how you login

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/BaseController.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,5 +129,4 @@ protected boolean hasRole(final String role) {
129129
}
130130
return false;
131131
}
132-
133132
}

kafka-webview-ui/src/main/java/org/sourcelab/kafka/webview/ui/controller/api/ApiController.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.sourcelab.kafka.webview.ui.manager.kafka.dto.ApiErrorResponse;
3636
import org.sourcelab.kafka.webview.ui.manager.kafka.dto.ConfigItem;
3737
import org.sourcelab.kafka.webview.ui.manager.kafka.dto.ConsumerState;
38+
import org.sourcelab.kafka.webview.ui.manager.kafka.dto.CreateTopic;
3839
import org.sourcelab.kafka.webview.ui.manager.kafka.dto.KafkaResults;
3940
import org.sourcelab.kafka.webview.ui.manager.kafka.dto.NodeDetails;
4041
import org.sourcelab.kafka.webview.ui.manager.kafka.dto.NodeList;
@@ -61,7 +62,9 @@
6162
import org.springframework.web.bind.annotation.ResponseBody;
6263
import org.springframework.web.bind.annotation.ResponseStatus;
6364

65+
import java.util.ArrayList;
6466
import java.util.Collection;
67+
import java.util.Comparator;
6568
import java.util.HashSet;
6669
import java.util.List;
6770
import java.util.Map;
@@ -278,13 +281,55 @@ public Collection<TopicDetails> getAllTopicsDetails(@PathVariable final Long id)
278281
// Now get details about all the topics
279282
final Map<String, TopicDetails> results = operations.getTopicDetails(topicList.getTopicNames());
280283

281-
// Return just the TopicDetails
282-
return results.values();
284+
// Sort the results by name
285+
final List<TopicDetails> sortedResults = new ArrayList<>(results.values());
286+
sortedResults.sort(Comparator.comparing(TopicDetails::getName));
287+
288+
// Return values.
289+
return sortedResults;
283290
} catch (final Exception e) {
284291
throw new ApiException("TopicDetails", e);
285292
}
286293
}
287294

295+
/**
296+
* POST Create new topic on cluster.
297+
* This should require ADMIN role.
298+
*/
299+
@ResponseBody
300+
@RequestMapping(path = "/cluster/{id}/create/topic", method = RequestMethod.POST, produces = "application/json")
301+
public ResultResponse createTopic(@PathVariable final Long id, @RequestBody final CreateTopicRequest createTopicRequest) {
302+
// Retrieve cluster
303+
final Cluster cluster = retrieveClusterById(id);
304+
305+
final String name = createTopicRequest.getName();
306+
if (name == null || name.trim().isEmpty()) {
307+
throw new ApiException("CreateTopic", "Invalid topic name");
308+
}
309+
310+
final Integer partitions = createTopicRequest.getPartitions();
311+
if (partitions == null || partitions < 1) {
312+
throw new ApiException("CreateTopic", "Invalid partitions value");
313+
}
314+
315+
final Short replicas = createTopicRequest.getReplicas();
316+
if (replicas == null || replicas < 1) {
317+
throw new ApiException("CreateTopic", "Invalid replicas value");
318+
}
319+
320+
final CreateTopic createTopic = new CreateTopic(name, partitions, replicas);
321+
322+
// Create new Operational Client
323+
try (final KafkaOperations operations = createOperationsClient(cluster)) {
324+
final boolean result = operations.createTopic(createTopic);
325+
326+
// Quick n dirty json response
327+
return new ResultResponse("CreateTopic", result, "");
328+
} catch (final Exception e) {
329+
throw new ApiException("CreateTopic", e);
330+
}
331+
}
332+
288333
/**
289334
* GET Nodes within a cluster.
290335
*/

0 commit comments

Comments
 (0)