Skip to content

Commit

Permalink
Merge pull request #26998 from alukin/feature/kafka-client-dev-ui-squ…
Browse files Browse the repository at this point in the history
…ashed

Introduce a Kafka Client DevUI component
  • Loading branch information
gsmet authored Sep 13, 2022
2 parents 7cfcecc + 1750554 commit 5e6a3c9
Show file tree
Hide file tree
Showing 51 changed files with 3,465 additions and 33 deletions.
12 changes: 12 additions & 0 deletions build-parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@

<!-- Webjars used by the Dev UI -->
<webjar.bootstrap.version>4.6.1</webjar.bootstrap.version>
<webjar.bootstrap-multiselect.version>0.9.15</webjar.bootstrap-multiselect.version>
<webjar.bootstrap-icons.version>1.9.1</webjar.bootstrap-icons.version>
<webjar.font-awesome.version>6.1.2</webjar.font-awesome.version>
<webjar.jquery.version>3.6.1</webjar.jquery.version>
<webjar.codemirror.version>5.62.2</webjar.codemirror.version>
Expand Down Expand Up @@ -309,6 +311,16 @@
<artifactId>bootstrap</artifactId>
<version>${webjar.bootstrap.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap-multiselect</artifactId>
<version>${webjar.bootstrap-multiselect.version}</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>bootstrap-icons</artifactId>
<version>${webjar.bootstrap-icons.version}</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>font-awesome</artifactId>
Expand Down
4 changes: 4 additions & 0 deletions extensions/kafka-client/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-caffeine-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http-dev-console-spi</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ public class KafkaBuildTimeConfig {
*/
@ConfigItem
public KafkaDevServicesBuildTimeConfig devservices;

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand All @@ -71,6 +72,7 @@
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.LogCategoryBuildItem;
import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem;
import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem;
Expand All @@ -82,9 +84,14 @@
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.deployment.logging.LogCleanupFilterBuildItem;
import io.quarkus.deployment.pkg.NativeConfig;
import io.quarkus.kafka.client.runtime.KafkaBindingConverter;
import io.quarkus.kafka.client.runtime.KafkaRecorder;
import io.quarkus.dev.spi.DevModeType;
import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem;
import io.quarkus.devconsole.spi.DevConsoleWebjarBuildItem;
import io.quarkus.kafka.client.runtime.*;
import io.quarkus.kafka.client.runtime.KafkaRuntimeConfigProducer;
import io.quarkus.kafka.client.runtime.ui.KafkaTopicClient;
import io.quarkus.kafka.client.runtime.ui.KafkaUiRecorder;
import io.quarkus.kafka.client.runtime.ui.KafkaUiUtils;
import io.quarkus.kafka.client.serialization.BufferDeserializer;
import io.quarkus.kafka.client.serialization.BufferSerializer;
import io.quarkus.kafka.client.serialization.JsonArrayDeserializer;
Expand All @@ -95,6 +102,7 @@
import io.quarkus.kafka.client.serialization.JsonbSerializer;
import io.quarkus.kafka.client.serialization.ObjectMapperDeserializer;
import io.quarkus.kafka.client.serialization.ObjectMapperSerializer;
import io.quarkus.maven.dependency.GACT;
import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem;

public class KafkaProcessor {
Expand Down Expand Up @@ -144,6 +152,11 @@ public class KafkaProcessor {

static final DotName PARTITION_ASSIGNER = DotName
.createSimple("org.apache.kafka.clients.consumer.internals.PartitionAssignor");
private static final GACT DEVCONSOLE_WEBJAR_ARTIFACT_KEY = new GACT("io.quarkus",
"quarkus-kafka-client-deployment", null, "jar");
private static final String DEVCONSOLE_WEBJAR_STATIC_RESOURCES_PATH = "dev-static/";
public static final String KAFKA_ADMIN_PATH = "kafka-admin";
public static final String KAFKA_RESOURCES_ROOT_PATH = "kafka-ui";

@BuildStep
FeatureBuildItem feature() {
Expand All @@ -165,7 +178,8 @@ void silenceUnwantedConfigLogs(BuildProducer<LogCleanupFilterBuildItem> logClean

List<String> ignoredMessages = new ArrayList<>();
for (String ignoredConfigProperty : ignoredConfigProperties) {
ignoredMessages.add("The configuration '" + ignoredConfigProperty + "' was supplied but isn't a known config.");
ignoredMessages
.add("The configuration '" + ignoredConfigProperty + "' was supplied but isn't a known config.");
}

logCleanupFilters.produce(new LogCleanupFilterBuildItem("org.apache.kafka.clients.consumer.ConsumerConfig",
Expand Down Expand Up @@ -478,4 +492,41 @@ void registerServiceBinding(Capabilities capabilities,
KafkaBindingConverter.class.getName()));
}
}

// Kafka UI related stuff

@BuildStep
public AdditionalBeanBuildItem kafkaClientBeans() {
return AdditionalBeanBuildItem.builder()
.addBeanClass(KafkaAdminClient.class)
.addBeanClass(KafkaTopicClient.class)
.addBeanClass(KafkaUiUtils.class)
.setUnremovable()
.build();
}

@BuildStep(onlyIf = IsDevelopment.class)
@Record(ExecutionTime.RUNTIME_INIT)
public void registerKafkaUiExecHandler(
BuildProducer<DevConsoleRouteBuildItem> routeProducer,
KafkaUiRecorder recorder) {
routeProducer.produce(DevConsoleRouteBuildItem.builder()
.method("POST")
.handler(recorder.kafkaControlHandler())
.path(KAFKA_ADMIN_PATH)
.bodyHandlerRequired()
.build());
}

@BuildStep(onlyIf = IsDevelopment.class)
public DevConsoleWebjarBuildItem setupWebJar(LaunchModeBuildItem launchModeBuildItem) {
if (launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) {
return null;
}
return DevConsoleWebjarBuildItem.builder().artifactKey(DEVCONSOLE_WEBJAR_ARTIFACT_KEY)
.root(DEVCONSOLE_WEBJAR_STATIC_RESOURCES_PATH)
.routeRoot(KAFKA_RESOURCES_ROOT_PATH)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const api = '/q/dev/io.quarkus.quarkus-kafka-client/kafka-admin';
export const ui = 'kafka-ui';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Navigator from './pages/navigator.js'

const navigator = new Navigator();
$(document).ready(
() => {
navigator.navigateToDefaultPage();
}
);

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {doPost, errorPopUp} from "../web/web.js";
import {createTableItem} from "../util/contentManagement.js";
import {toggleSpinner} from "../util/spinner.js";

export default class AccessControlListPage {
constructor(containerId) {
this.containerId = containerId;
Object.getOwnPropertyNames(AccessControlListPage.prototype).forEach((key) => {
if (key !== 'constructor') {
this[key] = this[key].bind(this);
}
});
}


open() {
const req = {
action: "getAclInfo"
};
toggleSpinner(this.containerId);
doPost(req, (data) => {
setTimeout(() => {
this.updateInfo(data);
toggleSpinner(this.containerId);
}, 2000);
}, data => {
errorPopUp("Error getting Kafka ACL info: ", data);
});
}

updateInfo(data) {
$('#acluster-id').html(data.clusterId);
$('#acluster-controller').html(data.broker);
$('#acluster-acl').html(data.aclOperations);

const acls = data.entries;
let aclTable = $('#acl-table tbody');
aclTable.empty();
for (let i = 0; i < acls.length; i++) {
const e = acls[i];
let tableRow = $("<tr/>");
tableRow.append(createTableItem(e.operation));
tableRow.append(createTableItem(e.prinipal));
tableRow.append(createTableItem(e.perm));
tableRow.append(createTableItem(e.pattern));
aclTable.append(tableRow);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {CollapseRow, createTableHead, createTableItem, createTableItemHtml} from "../util/contentManagement.js";

export default class ConsumerGroupDetailsPage {
constructor(containerId) {
this.containerId = containerId;
Object.getOwnPropertyNames(ConsumerGroupDetailsPage.prototype).forEach((key) => {
if (key !== 'constructor') {
this[key] = this[key].bind(this);
}
});
}

open(params) {
const membersData = params[1];
let consumerGroupsTable = $('#consumer-group-details-table tbody');
consumerGroupsTable.empty();
for (let i = 0; i < membersData.length; i++) {
const d = membersData[i];
const groupId = "group-" + d.memberId;

let tableRow = $("<tr/>");
let collapseRow;
if (d.partitions.length > 0) {
collapseRow = new CollapseRow(groupId);
tableRow.append(createTableItemHtml(collapseRow.arrow));
} else {
tableRow.append(createTableItem(""));
}

const memberId = $("<b/>")
.text(d.clientId);
const id = d.memberId.substring(d.clientId.length);
const text = $("<p/>")
.append(memberId)
.append(id);
tableRow.append(createTableItemHtml(text));
tableRow.append(createTableItem(d.host));
tableRow.append(createTableItem("" + new Set(d.partitions.map(x => x.partition)).size));
tableRow.append(createTableItem("" + d.partitions.map(x => x.lag).reduce((l, r) => l + r, 0)));

if (d.partitions.length > 0) {
const content = this.createConsumerGroupCollapseInfo(d);
tableRow.addClass("pointer")
tableRow.click(() => collapseRow.collapse());
consumerGroupsTable.append(tableRow);
consumerGroupsTable.append(collapseRow
.getCollapseContent(tableRow.children().length, content)
.addClass("no-hover"));
} else {
consumerGroupsTable.append(tableRow);
}
}
}

createConsumerGroupCollapseInfo(dataItem) {
const collapseContent = $("<table/>")
.addClass("table")
.addClass("table-sm")
.addClass("no-hover");

const headers = $("<tr/>")
.addClass("no-hover")
.append(createTableHead("Topic"))
.append(createTableHead("Partition"))
.append(createTableHead("Lag"));
const head = $("<thead/>")
.append(headers);

const body = $("<tbody/>");
for (let partition of dataItem.partitions) {
const row = $("<tr/>")
.addClass("no-hover");
row.append(createTableItemHtml(partition.topic))
row.append(createTableItemHtml(partition.partition))
row.append(createTableItemHtml(partition.lag))
body.append(row);
}

collapseContent.append(head);
collapseContent.append(body);

return collapseContent;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {createTableItem} from "../util/contentManagement.js";
import {doPost, errorPopUp} from "../web/web.js";
import {pages} from "./navigator.js";
import {toggleSpinner} from "../util/spinner.js";

export default class ConsumerGroupPage {
constructor(navigator, containerId) {
this.containerId = containerId;
this.navigator = navigator;
Object.getOwnPropertyNames(ConsumerGroupPage.prototype).forEach((key) => {
if (key !== 'constructor') {
this[key] = this[key].bind(this);
}
});
}

open() {
toggleSpinner(this.containerId);
const req = {
action: "getInfo", key: "0", value: "0"
};
doPost(req, (data) => {
this.updateConsumerGroups(data.consumerGroups);
toggleSpinner(this.containerId);
}, data => {
errorPopUp("Error getting Kafka info: ", data);
toggleSpinner(this.containerId);
});
}

updateConsumerGroups(data) {
let consumerGroupsTable = $('#consumer-groups-table tbody');
consumerGroupsTable.empty();
for (let i = 0; i < data.length; i++) {
const d = data[i];
let tableRow = $("<tr/>");
tableRow.append(createTableItem(d.state));
tableRow.append(createTableItem(d.name));
tableRow.append(createTableItem(d.coordinatorId));
tableRow.append(createTableItem(d.protocol));
tableRow.append(createTableItem(d.members.length));
tableRow.append(createTableItem(d.lag));
tableRow.click(() => this.navigator.navigateTo(pages.CONSUMER_GROUPS_DETAILS, [d.name, d.members]));
consumerGroupsTable.append(tableRow);
}
}
}
Loading

0 comments on commit 5e6a3c9

Please sign in to comment.