diff --git a/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/devui/KafkaDevUIProcessor.java b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/devui/KafkaDevUIProcessor.java
new file mode 100644
index 0000000000000..d035f55b5ca0e
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/devui/KafkaDevUIProcessor.java
@@ -0,0 +1,55 @@
+package io.quarkus.kafka.client.deployment.devui;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.deployment.IsDevelopment;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
+import io.quarkus.devui.spi.page.CardPageBuildItem;
+import io.quarkus.devui.spi.page.Page;
+import io.quarkus.kafka.client.runtime.devui.KafkaJsonRPCService;
+
+/**
+ * Kafka Dev UI (v2)
+ */
+public class KafkaDevUIProcessor {
+
+ private static final Logger log = Logger.getLogger(KafkaDevUIProcessor.class);
+
+ @BuildStep(onlyIf = IsDevelopment.class)
+ public CardPageBuildItem pages() {
+ CardPageBuildItem cardPageBuildItem = new CardPageBuildItem();
+
+ cardPageBuildItem.addPage(Page.webComponentPageBuilder()
+ .icon("font-awesome-solid:folder-tree")
+ .componentLink("qwc-kafka-topics.js")
+ .title("Topics"));
+ // TODO: Implement this. This is also not implemented in the old Dev UI
+ // cardPageBuildItem.addPage(Page.webComponentPageBuilder()
+ // .icon("font-awesome-solid:file-circle-check")
+ // .componentLink("qwc-kafka-schema-registry.js")
+ // .title("Schema registry"));
+
+ cardPageBuildItem.addPage(Page.webComponentPageBuilder()
+ .icon("font-awesome-solid:inbox")
+ .componentLink("qwc-kafka-consumer-groups.js")
+ .title("Consumer groups"));
+
+ cardPageBuildItem.addPage(Page.webComponentPageBuilder()
+ .icon("font-awesome-solid:key")
+ .componentLink("qwc-kafka-access-control-list.js")
+ .title("Access control list"));
+
+ cardPageBuildItem.addPage(Page.webComponentPageBuilder()
+ .icon("font-awesome-solid:circle-nodes")
+ .componentLink("qwc-kafka-nodes.js")
+ .title("Nodes"));
+
+ return cardPageBuildItem;
+ }
+
+ @BuildStep(onlyIf = IsDevelopment.class)
+ JsonRPCProvidersBuildItem createJsonRPCService() {
+ return new JsonRPCProvidersBuildItem(KafkaJsonRPCService.class);
+ }
+}
diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-access-control-list.js b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-access-control-list.js
new file mode 100644
index 0000000000000..3aa5e62c727a9
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-access-control-list.js
@@ -0,0 +1,68 @@
+import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';
+import { JsonRpc } from 'jsonrpc';
+import '@vaadin/progress-bar';
+
+/**
+ * This component shows the Kafka Access Control List
+ */
+export class QwcKafkaAccessControlList extends QwcHotReloadElement {
+
+ jsonRpc = new JsonRpc(this);
+
+ static styles = css``;
+
+ static properties = {
+ _aclInfo: {state: true},
+ };
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.hotReload();
+ }
+
+ hotReload(){
+ this.jsonRpc.getAclInfo().then(jsonRpcResponse => {
+ this._aclInfo = jsonRpcResponse.result;
+ });
+ }
+
+ render() {
+ if(this._aclInfo){
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }else {
+ return html``;
+ }
+ }
+}
+
+customElements.define('qwc-kafka-access-control-list', QwcKafkaAccessControlList);
\ No newline at end of file
diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-add-message.js b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-add-message.js
new file mode 100644
index 0000000000000..e48bb62634ff5
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-add-message.js
@@ -0,0 +1,249 @@
+import { LitElement, html, css} from 'lit';
+import { JsonRpc } from 'jsonrpc';
+import '@vaadin/form-layout';
+import '@vaadin/text-field';
+import '@vaadin/combo-box';
+import '@vaadin/text-area';
+import '@vaadin/button';
+
+/**
+ * This component shows the Add Message screen
+ */
+export class QwcKafkaAddMessage extends LitElement {
+
+ static styles = css`
+
+ :root {
+ display: flex;
+ flex-direction: column;
+ }
+
+ `;
+
+ static properties = {
+ partitionsCount: {type: Number},
+ topicName: {type: String},
+ extensionName: {type: String}, // TODO: Add 'pane' concept in router to register internal extension pages.
+ _targetPartitions: {state: false},
+ _types: {state: false},
+ _newMessage: {state: true},
+ _newMessageHeaders: {state: true, type: Array},
+ };
+
+ constructor() {
+ super();
+ this.partitionsCount = 0;
+ this.topicName = null;
+ this._targetPartitions = [];
+ this._types = [];
+ this._reset();
+ this.responsiveSteps = [
+ { minWidth: 0, columns: 1 },
+ { minWidth: '320px', columns: 2 },
+ ];
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.jsonRpc = new JsonRpc(this.extensionName);
+
+ this._targetPartitions.push({name: "Any", value: "any"});
+ for (var i = 0; i < this.partitionsCount; i++) {
+ this._targetPartitions.push({name: i.toString(), value: i});
+ }
+
+ this._types.push({name: "Text", value: "text"});
+ this._types.push({name: "None (Tombstone)", value: "none"});
+ this._types.push({name: "JSON", value: "json"});
+ this._types.push({name: "Binary", value: "binary"});
+ }
+
+ _reset(){
+ this._newMessage = new Object();
+ this._newMessage.partition = 'any';
+ this._newMessage.type = 'text';
+ this._newMessage.key = null;
+ this._newMessage.value = null;
+ this._newMessageHeaders = null;
+ }
+
+ _cancel(){
+ this._reset();
+ const canceled = new CustomEvent("kafka-message-added-canceled", {
+ detail: {},
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ this.dispatchEvent(canceled);
+ }
+
+ render() {
+
+ return html`
+ this._createMessagePartitionChanged(e)}">
+
+
+ this._createMessageTypeChanged(e)}">
+
+
+ this._createMessageKeyChanged(e)}"
+ required
+ clear-button-visible>
+
+
+ this._createMessageValueChanged(e)}"
+ required>
+
+
+
+ Headers
+
+
+
+
+ this._newMessageAddHeader(e)}>
+
+
+
+
+
+
+ ${this._renderAddedMessageHeaders()}
+
+ ${this._renderCreateMessageButtons()}
+ `;
+ }
+
+ _renderAddedMessageHeaders(){
+ if(this._newMessageHeaders && this._newMessageHeaders.length > 0){
+ return html``;
+ }
+ }
+
+ _renderCreateMessageButtons(){
+ return html`
+ Create
+ Cancel
+
`;
+ }
+
+ _submitCreateMessageForm(){
+ if (this._newMessage.partition === 'any') this._newMessage.partition = -1;
+
+ let headers = new Object();
+ if(this._newMessageHeaders && this._newMessageHeaders.length > 0){
+ this._newMessageHeaders.forEach(function (h) {
+ headers[h.key] = h.value;
+ });
+ }
+
+ this.jsonRpc.createMessage({topicName:this.topicName,
+ partition: Number(this._newMessage.partition),
+ key:this._newMessage.key,
+ value:this._newMessage.value,
+ headers: headers
+ }).then(jsonRpcResponse => {
+ this._reset();
+ const success = new CustomEvent("kafka-message-added-success", {
+ detail: {result: jsonRpcResponse.result},
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+
+ this.dispatchEvent(success);
+
+ });
+
+
+ }
+
+ _createMessagePartitionChanged(e){
+ this._newMessage.partition = e.detail.value;
+ }
+
+ _createMessageTypeChanged(e){
+ this._newMessage.type = e.detail.value.trim();
+ }
+
+ _createMessageKeyChanged(e){
+ this._newMessage.key = e.detail.value.trim();
+ }
+
+ _createMessageValueChanged(e){
+ this._newMessage.value = e.detail.value.trim();
+ }
+
+ _newMessageAddHeader(e){
+ let target = e.target;
+ let parent = null;
+ if(target.nodeName.toLowerCase() === "vaadin-icon"){
+ parent = target.parentElement.parentElement;
+ }else{
+ parent = target.parentElement;
+ }
+
+ let h = new Object();
+
+ var children = parent.children;
+ for (var i = 0; i < children.length; i++) {
+ var child = children[i];
+ if(child.nodeName.toLowerCase() === "vaadin-text-field"){
+ h[child.id] = child.value;
+ child.value = '';
+ }
+ }
+ this._addToHeaders(h);
+ }
+
+ _addToHeaders(h){
+ if(!this._newMessageHeaders){
+ this._newMessageHeaders = [h];
+ } else {
+ this._newMessageHeaders = [
+ ...this._newMessageHeaders,
+ h
+ ];
+ }
+ }
+
+}
+
+customElements.define('qwc-kafka-add-message', QwcKafkaAddMessage);
\ No newline at end of file
diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-add-topic.js b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-add-topic.js
new file mode 100644
index 0000000000000..ad8ca2104ee67
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-add-topic.js
@@ -0,0 +1,124 @@
+import { LitElement, html, css} from 'lit';
+import { JsonRpc } from 'jsonrpc';
+
+import '@vaadin/text-field';
+import '@vaadin/integer-field';
+import '@vaadin/button';
+
+/**
+ * This component shows the add Topics Screen
+ */
+export class QwcKafkaAddTopic extends LitElement {
+ jsonRpc = new JsonRpc(this);
+
+ static styles = css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ }
+ `;
+
+ static properties = {
+ extensionName: {type: String}, // TODO: Add 'pane' concept in router to register internal extension pages.
+ _newTopic: {state: true},
+ };
+
+ constructor() {
+ super();
+ this._reset();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this._reset();
+ this.jsonRpc = new JsonRpc(this.extensionName);
+ }
+
+ render(){
+ return html` this._nameChanged(e)}"
+ required
+ clear-button-visible>
+
+ this._partitionsChanged(e)}"
+ min="0"
+ max="99">
+
+ this._replicationsChanged(e)}"
+ min="0"
+ max="99">
+
+ ${this._renderButtons()}`;
+ }
+
+ _renderButtons(){
+ return html`
+ Create
+ Cancel
+
`;
+ }
+
+ _reset(){
+ this._newTopic = new Object();
+ this._newTopic.name = '';
+ this._newTopic.partitions = 1;
+ this._newTopic.replications = 1;
+ }
+
+ _cancel(){
+ this._reset();
+ const canceled = new CustomEvent("kafka-topic-added-canceled", {
+ detail: {},
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ this.dispatchEvent(canceled);
+ }
+
+ _submit(){
+ if(this._newTopic.name.trim() !== ''){
+
+ this.jsonRpc.createTopic({
+ topicName: this._newTopic.name,
+ partitions: parseInt(this._newTopic.partitions),
+ replications: parseInt(this._newTopic.replications)
+ }).then(jsonRpcResponse => {
+ this._reset();
+ const success = new CustomEvent("kafka-topic-added-success", {
+ detail: {result: jsonRpcResponse.result},
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+
+ this.dispatchEvent(success);
+ });
+ }
+ }
+
+ _nameChanged(e){
+ this._newTopic.name = e.detail.value.trim();
+ }
+
+ _partitionsChanged(e){
+ this._newTopic.partitions = e.detail.value;
+ }
+
+ _replicationsChanged(e){
+ this._newTopic.replications = e.detail.value;
+ }
+}
+
+customElements.define('qwc-kafka-add-topic', QwcKafkaAddTopic);
\ No newline at end of file
diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-consumer-groups.js b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-consumer-groups.js
new file mode 100644
index 0000000000000..d7f719f86d780
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-consumer-groups.js
@@ -0,0 +1,194 @@
+import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';
+import { JsonRpc } from 'jsonrpc';
+import '@vaadin/progress-bar';
+import '@vaadin/grid';
+import '@vaadin/grid/vaadin-grid-sort-column.js';
+import { columnBodyRenderer, gridRowDetailsRenderer } from '@vaadin/grid/lit.js';
+
+/**
+ * This component shows the Kafka Consumer Groups
+ */
+export class QwcKafkaConsumerGroups extends QwcHotReloadElement {
+ jsonRpc = new JsonRpc(this);
+
+ static styles = css`
+ .kafka {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+
+ .table {
+ height: 100%;
+ }
+ .top-bar {
+ display: flex;
+ align-items: baseline;
+ gap: 20px;
+ }
+ `;
+
+ static properties = {
+ _consumerGroups: {state: true},
+ _selectedConsumerGroups: {state: true, type: Array},
+ _memberDetailOpenedItem: {state: true, type: Array},
+ };
+
+ constructor() {
+ super();
+ this._consumerGroups = null;
+ this._selectedConsumerGroups = [];
+ this._memberDetailOpenedItem = [];
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.hotReload();
+ }
+
+ hotReload(){
+ this.jsonRpc.getInfo().then(jsonRpcResponse => {
+ this._consumerGroups = jsonRpcResponse.result.consumerGroups;
+ });
+ }
+
+ render() {
+ if(this._consumerGroups){
+ return html`
+ ${this._renderConsumerGroups()}
+ ${this._renderSelectedConsumerGroup()}
+
+
`;
+ }else {
+ return html``;
+ }
+ }
+
+ _renderConsumerGroups(){
+ if(this._selectedConsumerGroups.length === 0){
+ return html`
+ {
+ const item = e.detail.value;
+ this._selectedConsumerGroups = item ? [item] : [];
+ }}">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+ }
+
+ _renderSelectedConsumerGroup(){
+ if(this._selectedConsumerGroups.length > 0){
+ let name = this._selectedConsumerGroups[0].name;
+ let members = this._selectedConsumerGroups[0].members;
+ return html`
+ {this._selectedConsumerGroups = []}}">
+
+ Back
+
+
${name}
+
+ {
+ const prop = event.detail.value;
+ this._memberDetailOpenedItem = prop ? [prop] : [];
+ }}"
+ ${gridRowDetailsRenderer(this._memberDetailRenderer, [])}>
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+ }
+
+
+
+
+ _membersRenderer(consumerGroup) {
+ return html`${consumerGroup.members.length}`;
+ }
+
+ _memberPartitionsRenderer(member){
+ return html`${member.partitions.length}`;
+ }
+
+ _memberDetailRenderer(member) {
+ if(member.partitions && member.partitions.length > 0){
+ return html`
+
+
+
+ `;
+ }
+ }
+
+ _topicHeaderRenderer(){
+ return html`Topic`;
+ }
+ _partitionHeaderRenderer(){
+ return html`Partition`;
+ }
+ _topicHeade_lagHeaderRendererrRenderer(){
+ return html`Lag`;
+ }
+}
+
+customElements.define('qwc-kafka-consumer-groups', QwcKafkaConsumerGroups);
\ No newline at end of file
diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-messages.js b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-messages.js
new file mode 100644
index 0000000000000..b6de375425ff0
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-messages.js
@@ -0,0 +1,255 @@
+import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';
+import { JsonRpc } from 'jsonrpc';
+import '@vaadin/progress-bar';
+import '@vaadin/grid';
+import '@vaadin/grid/vaadin-grid-sort-column.js';
+import { columnBodyRenderer, gridRowDetailsRenderer } from '@vaadin/grid/lit.js';
+import '@vaadin/dialog';
+import { dialogRenderer } from '@vaadin/dialog/lit.js';
+import '@vaadin/button';
+import 'qui-code-block';
+import '@vaadin/split-layout';
+import './qwc-kafka-add-message.js';
+
+/**
+ * This component shows the Kafka Messages for a certain topic
+ */
+export class QwcKafkaMessages extends QwcHotReloadElement {
+
+ static styles = css`
+ .kafka {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+ .top-bar {
+ display: flex;
+ align-items: baseline;
+ gap: 20px;
+ }
+ .detail-block {
+ width: 50%;
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ }
+ .header-grid, .code-block {
+ padding: 10px;
+ }
+ .bottom {
+ display: flex;
+ flex-direction: row-reverse;
+ }
+
+ .create-button {
+ margin-right: 30px;
+ margin-bottom: 10px;
+ font-size: xx-large;
+ cursor: pointer;
+ }
+ `;
+
+ static properties = {
+ partitionsCount: {type: Number},
+ topicName: {type: String},
+ extensionName: {type: String}, // TODO: Add 'pane' concept in router to register internal extension pages.
+ _messages: {state: false, type: Array},
+ _createMessageDialogOpened: {state: true},
+ _messagesDetailOpenedItem: {state: false, type: Array}
+ };
+
+ constructor() {
+ super();
+ this._messages = null;
+ this._createMessageDialogOpened = false;
+ this._messagesDetailOpenedItem = [];
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.jsonRpc = new JsonRpc(this.extensionName);
+ this.hotReload();
+ }
+
+ hotReload(){
+ this.jsonRpc.topicMessages({topicName: this.topicName}).then(jsonRpcResponse => {
+ this._messages = jsonRpcResponse.result.messages;
+ });
+ }
+
+ render() {
+ if(this._messages){
+ return html`
+ ${this._renderCreateMessageDialog()}
+ ${this._renderTopBar()}
+ ${this._renderMessages()}
+ ${this._renderAddMessagesPlusButton()}
+
`;
+ } else {
+ return html``;
+ }
+ }
+
+ _renderTopBar(){
+ return html`
+
+
+
+ Back
+
+
${this.topicName}
+ `;
+ }
+
+ _backAction(){
+ const back = new CustomEvent("kafka-messages-back", {
+ detail: {},
+ bubbles: true,
+ cancelable: true,
+ composed: false,
+ });
+ this.dispatchEvent(back);
+ }
+
+ _renderMessages(){
+
+ return html` {
+ const prop = event.detail.value;
+ this._messagesDetailOpenedItem = prop ? [prop] : [];
+ }}"
+ ${gridRowDetailsRenderer(this._messagesDetailRenderer, [])}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ _messagesDetailRenderer(message) {
+ let headers = [];
+ for (const [key, value] of Object.entries(message.headers)) {
+ headers.push({key:key, value: value});
+ }
+
+ return html`
+
+ Message value:
+
+
+
+
+
+
+ Message headers:
+
+
+ `;
+ }
+
+ _timestampRenderer(message){
+ return html`${this._timestampToFormattedString(message.timestamp)}`;
+ }
+
+ _timestampToFormattedString(UNIX_timestamp) {
+ const a = new Date(UNIX_timestamp);
+ const year = a.getFullYear();
+ const month = this._addTrailingZero(a.getMonth());
+ const date = this._addTrailingZero(a.getDate());
+ const hour = this._addTrailingZero(a.getHours());
+ const min = this._addTrailingZero(a.getMinutes());
+ const sec = this._addTrailingZero(a.getSeconds());
+ return date + '/' + month + '/' + year + ' ' + hour + ':' + min + ':' + sec;
+ }
+
+ _addTrailingZero(data) {
+ if (data < 10) {
+ return "0" + data;
+ }
+ return data;
+ }
+
+ _renderAddMessagesPlusButton(){
+ if(this._messages){
+ return html`
+ {this._createMessageDialogOpened = true}}" title="Create message">
+
`;
+ }
+ }
+
+ _renderCreateMessageDialog(){
+ return html` (this._createMessageDialogOpened = e.detail.value)}"
+ ${dialogRenderer(() => this._renderCreateMessageDialogForm(), "Add new message to ${this.topicName}")}
+ >`;
+ }
+
+ _renderCreateMessageDialogForm(){
+ return html`
+ `;
+ }
+
+ _messageAddedCanceled(){
+ this._createMessageDialogOpened = false;
+ }
+
+ _messageAdded(e){
+ this._messages = e.detail.result.messages;
+ this._createMessageDialogOpened = false;
+ }
+}
+
+customElements.define('qwc-kafka-messages', QwcKafkaMessages);
\ No newline at end of file
diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-nodes.js b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-nodes.js
new file mode 100644
index 0000000000000..16b7f64b7a85d
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-nodes.js
@@ -0,0 +1,85 @@
+import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';
+import { JsonRpc } from 'jsonrpc';
+import '@vaadin/progress-bar';
+import '@vaadin/grid';
+import { columnBodyRenderer, columnHeaderRenderer } from '@vaadin/grid/lit.js';
+
+/**
+ * This component shows the Kafka Nodes
+ */
+export class QwcKafkaNodes extends QwcHotReloadElement {
+ jsonRpc = new JsonRpc(this);
+
+ static styles = css`
+ .noGridHeader::part(header-cell){
+ display: none;
+ }
+ .header, .nodes {
+ padding-right: 30px;
+ padding-left: 30px;
+ }
+ `;
+
+ static properties = {
+ _info: {state: true}
+ };
+
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.hotReload();
+ }
+
+ hotReload(){
+ this.jsonRpc.getInfo().then(jsonRpcResponse => {
+ this._info = jsonRpcResponse.result;
+ });
+ }
+
+ render() {
+ if(this._info){
+ let header = [];
+ header.push({key:"Kafka cluster id", value: this._info.clusterInfo.id});
+ header.push({key:"Controller node (broker)", value: this._info.broker});
+ header.push({key:"ACL operations", value: this._info.clusterInfo.aclOperations});
+
+ return html`
+
+
Cluster Nodes
+
+
+
+
+
+
+ `;
+ } else {
+ return html``;
+ }
+
+ }
+
+ _keyRenderer(info){
+ return html`${info.key}`;
+ }
+
+ _idHeaderRenderer(){
+ return html`ID`;
+ }
+ _hostHeaderRenderer(){
+ return html`Host`;
+ }
+ _portHeaderRenderer(){
+ return html`Port`;
+ }
+}
+
+customElements.define('qwc-kafka-nodes', QwcKafkaNodes);
\ No newline at end of file
diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-schema-registry.js b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-schema-registry.js
new file mode 100644
index 0000000000000..1fc62d09cab13
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-schema-registry.js
@@ -0,0 +1,23 @@
+import { LitElement, html, css} from 'lit';
+
+/**
+ * This component shows the Kafka Scheme Registry
+ */
+export class QwcKafkaSchemeRegistry extends LitElement {
+
+ static styles = css``;
+
+ static properties = {
+
+ };
+
+ constructor() {
+ super();
+ }
+
+ render() {
+ return html` TODO: Scheme Registry`;
+ }
+}
+
+customElements.define('qwc-kafka-schema-registry', QwcKafkaSchemeRegistry);
\ No newline at end of file
diff --git a/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-topics.js b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-topics.js
new file mode 100644
index 0000000000000..eed4ffc417e65
--- /dev/null
+++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-topics.js
@@ -0,0 +1,253 @@
+import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';
+import { JsonRpc } from 'jsonrpc';
+import '@vaadin/progress-bar';
+import '@vaadin/grid';
+import '@vaadin/grid/vaadin-grid-sort-column.js';
+import { columnBodyRenderer } from '@vaadin/grid/lit.js';
+import '@vaadin/dialog';
+import { dialogRenderer } from '@vaadin/dialog/lit.js';
+import '@vaadin/text-field';
+import '@vaadin/integer-field';
+import '@vaadin/button';
+import './qwc-kafka-messages.js';
+import './qwc-kafka-add-topic.js';
+
+
+/**
+ * This component shows the Kafka Topics
+ */
+export class QwcKafkaTopics extends QwcHotReloadElement {
+ jsonRpc = new JsonRpc(this);
+
+ static styles = css`
+ .kafka {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+
+ .bottom {
+ display: flex;
+ flex-direction: row-reverse;
+ }
+
+ .create-button {
+ margin-right: 30px;
+ margin-bottom: 10px;
+ font-size: xx-large;
+ cursor: pointer;
+ }
+
+ .delete-button {
+ font-size: small;
+ color: var(--lumo-error-text-color);
+ cursor: pointer;
+ }
+
+ .clickableCell {
+ display: block;
+ width: 100%;
+ cursor: pointer;
+ }
+ `;
+
+ static properties = {
+ _topics: {status: true, type: Array},
+ _selectedTopic: {state: true},
+ _createTopicDialogOpened: {state: true},
+
+ _deleteTopicDialogOpened: {state: true},
+ _deleteTopicName: {state: false}
+ };
+
+ constructor() {
+ super();
+ this._topics = null;
+ this._selectedTopic = null;
+
+ this._createTopicDialogOpened = false;
+ this._deleteTopicName = '';
+ this._deleteTopicDialogOpened = false;
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.hotReload();
+ }
+
+ hotReload(){
+ this.jsonRpc.getTopics().then(jsonRpcResponse => {
+ this._topics = jsonRpcResponse.result;
+ });
+ }
+
+ render() {
+ if(this._topics && this._selectedTopic === null){
+ return this._renderTopicList();
+ } else if(this._topics && this._selectedTopic!=null){
+ return html``
+ } else {
+ return html``;
+ }
+ }
+
+ _showTopicList(){
+ this._selectedTopic = null;
+ }
+
+ _renderTopicList(){
+ return html`
+ ${this._renderCreateTopicDialog()}
+ ${this._renderConfirmDeleteDialog()}
+ ${this._renderTopicGrid()}
+
+
+ {this._createTopicDialogOpened = true}}" title="Create topic">
+
+
`;
+ }
+
+ _renderTopicGrid(){
+ return html`
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+ `;
+ }
+
+ _renderCreateTopicDialog(){
+ return html` (this._createTopicDialogOpened = e.detail.value)}"
+ ${dialogRenderer(() => this._renderCreateTopicDialogForm(), "Create topic")}
+ >`;
+ }
+
+ _renderConfirmDeleteDialog(){
+
+ return html` (this._deleteTopicDialogOpened = e.detail.value)}"
+ ${dialogRenderer(() => this._renderDeleteTopicDialogForm(), "Confirm delete")}
+ >`;
+ }
+
+ _renderDeleteTopicDialogForm(){
+ return html`
+ Are you sure you want to delete topic ${this._deleteTopicName}
?
+ ${this._renderDeleteTopicButtons()}
+ `;
+ }
+
+ _openConfirmDeleteDialog(e){
+ this._deleteTopicName = e.target.dataset.topicName;
+ this._deleteTopicDialogOpened = true;
+ }
+
+ _nameRenderer(topic){
+ return this._clickableCell(topic, topic.name);
+ }
+
+ _idRenderer(topic){
+ return this._clickableCell(topic, topic.topicId);
+ }
+
+ _partitionsCountRenderer(topic){
+ return this._clickableCell(topic, topic.partitionsCount);
+ }
+
+ _nmsgRenderer(topic){
+ return this._clickableCell(topic, topic.nmsg);
+ }
+
+ _clickableCell(topic, val){
+ return html` this._selectTopic(topic)}>${val}`;
+ }
+
+ _selectTopic(topic){
+ this._selectedTopic = topic;
+ }
+
+ _deleteActionRenderer(topic){
+ return html``;
+ }
+
+ _renderCreateTopicDialogForm(){
+ return html`
+ `;
+ }
+
+ _topicAdded(e){
+ this._topics = e.detail.result;
+ this._createTopicDialogOpened = false;
+ }
+
+ _topicAddedCanceled(e){
+ this._createTopicDialogOpened = false;
+ }
+
+ _renderDeleteTopicButtons(){
+ return html`
+ Delete
+ Cancel
+
`;
+ }
+
+ _resetDeleteTopicForm(){
+ this._deleteTopicName = '';
+ this._deleteTopicDialogOpened = false;
+ }
+
+ _submitDeleteTopicForm(){
+ this.jsonRpc.deleteTopic({
+ topicName: this._deleteTopicName
+ }).then(jsonRpcResponse => {
+ this._topics = jsonRpcResponse.result;
+ });
+ this._resetDeleteTopicForm();
+ }
+}
+
+customElements.define('qwc-kafka-topics', QwcKafkaTopics);
\ No newline at end of file
diff --git a/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/devui/KafkaJsonRPCService.java b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/devui/KafkaJsonRPCService.java
new file mode 100644
index 0000000000000..c1d7a28fc801f
--- /dev/null
+++ b/extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/devui/KafkaJsonRPCService.java
@@ -0,0 +1,87 @@
+package io.quarkus.kafka.client.runtime.devui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import jakarta.inject.Inject;
+
+import io.quarkus.kafka.client.runtime.KafkaAdminClient;
+import io.quarkus.kafka.client.runtime.ui.KafkaUiUtils;
+import io.quarkus.kafka.client.runtime.ui.model.Order;
+import io.quarkus.kafka.client.runtime.ui.model.request.KafkaCreateTopicRequest;
+import io.quarkus.kafka.client.runtime.ui.model.request.KafkaMessageCreateRequest;
+import io.quarkus.kafka.client.runtime.ui.model.request.KafkaMessagesRequest;
+import io.quarkus.kafka.client.runtime.ui.model.request.KafkaOffsetRequest;
+import io.quarkus.kafka.client.runtime.ui.model.response.KafkaAclInfo;
+import io.quarkus.kafka.client.runtime.ui.model.response.KafkaInfo;
+import io.quarkus.kafka.client.runtime.ui.model.response.KafkaMessagePage;
+import io.quarkus.kafka.client.runtime.ui.model.response.KafkaTopic;
+
+public class KafkaJsonRPCService {
+
+ @Inject
+ KafkaUiUtils kafkaUiUtils;
+
+ @Inject
+ KafkaAdminClient kafkaAdminClient;
+
+ public List getTopics() throws InterruptedException, ExecutionException {
+ return kafkaUiUtils.getTopics();
+ }
+
+ public List createTopic(String topicName, int partitions, int replications)
+ throws InterruptedException, ExecutionException {
+
+ KafkaCreateTopicRequest createTopicRequest = new KafkaCreateTopicRequest(topicName, partitions, (short) replications);
+ boolean created = kafkaAdminClient.createTopic(createTopicRequest);
+ if (created) {
+ return kafkaUiUtils.getTopics();
+ }
+ throw new RuntimeException("Topic [" + topicName + "] not created");
+ }
+
+ public List deleteTopic(String topicName) throws InterruptedException, ExecutionException {
+ boolean deleted = kafkaAdminClient.deleteTopic(topicName);
+ if (deleted) {
+ return kafkaUiUtils.getTopics();
+ }
+ throw new RuntimeException("Topic [" + topicName + "] not deleted");
+ }
+
+ public KafkaMessagePage topicMessages(String topicName) throws ExecutionException, InterruptedException {
+ List partitions = getPartitions(topicName);
+ KafkaOffsetRequest offsetRequest = new KafkaOffsetRequest(topicName, partitions, Order.NEW_FIRST);
+ Map offset = kafkaUiUtils.getOffset(offsetRequest);
+ KafkaMessagesRequest request = new KafkaMessagesRequest(topicName, Order.NEW_FIRST, 20, offset);
+ return kafkaUiUtils.getMessages(request);
+ }
+
+ public KafkaMessagePage createMessage(String topicName, Integer partition, String key, String value,
+ Map headers)
+ throws ExecutionException, InterruptedException {
+
+ if (partition < 0)
+ partition = null;
+
+ KafkaMessageCreateRequest request = new KafkaMessageCreateRequest(topicName, partition, value, key, headers);
+
+ kafkaUiUtils.createMessage(request);
+
+ return topicMessages(topicName);
+ }
+
+ public List getPartitions(String topicName) throws ExecutionException, InterruptedException {
+ return new ArrayList<>(kafkaUiUtils.partitions(topicName));
+ }
+
+ public KafkaInfo getInfo() throws ExecutionException, InterruptedException {
+ return kafkaUiUtils.getKafkaInfo();
+ }
+
+ public KafkaAclInfo getAclInfo() throws InterruptedException, ExecutionException {
+ return kafkaUiUtils.getAclInfo();
+ }
+
+}
diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/jsonrpc.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/jsonrpc.js
index 8b6dccac69953..f17647f35c340 100644
--- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/jsonrpc.js
+++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/jsonrpc.js
@@ -206,6 +206,10 @@ export class JsonRpc {
}
}
+ getExtensionName(){
+ return this._extensionName;
+ }
+
static sendJsonRPCMessage(jsonrpcpayload, log=true) {
if (JsonRpc.webSocket.readyState !== WebSocket.OPEN) {
JsonRpc.initQueue.push(jsonrpcpayload);