Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a Kafka Client DevUI component #26998

Merged
merged 3 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.*;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please try to avoid star imports

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