From de8de37b3a4b266f542229c4a9bbac8f33a48713 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Thu, 20 Jul 2023 23:05:25 +1000 Subject: [PATCH] Dev UI: Migrate Kafka Client UI Signed-off-by: Phillip Kruger --- .../deployment/devui/KafkaDevUIProcessor.java | 55 ++++ .../dev-ui/qwc-kafka-access-control-list.js | 68 +++++ .../resources/dev-ui/qwc-kafka-add-message.js | 249 +++++++++++++++++ .../resources/dev-ui/qwc-kafka-add-topic.js | 124 +++++++++ .../dev-ui/qwc-kafka-consumer-groups.js | 194 +++++++++++++ .../resources/dev-ui/qwc-kafka-messages.js | 257 ++++++++++++++++++ .../main/resources/dev-ui/qwc-kafka-nodes.js | 85 ++++++ .../dev-ui/qwc-kafka-schema-registry.js | 23 ++ .../main/resources/dev-ui/qwc-kafka-topics.js | 255 +++++++++++++++++ .../runtime/devui/KafkaJsonRPCService.java | 87 ++++++ .../resources/dev-ui/controller/jsonrpc.js | 4 + 11 files changed, 1401 insertions(+) create mode 100644 extensions/kafka-client/deployment/src/main/java/io/quarkus/kafka/client/deployment/devui/KafkaDevUIProcessor.java create mode 100644 extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-access-control-list.js create mode 100644 extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-add-message.js create mode 100644 extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-add-topic.js create mode 100644 extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-consumer-groups.js create mode 100644 extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-messages.js create mode 100644 extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-nodes.js create mode 100644 extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-schema-registry.js create mode 100644 extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-topics.js create mode 100644 extensions/kafka-client/runtime/src/main/java/io/quarkus/kafka/client/runtime/devui/KafkaJsonRPCService.java 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` + + + + + + + + + + + + +
+ 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._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` + + + + + + + + + + + + + + + + + + + + `; + } + } + + _renderSelectedConsumerGroup(){ + if(this._selectedConsumerGroups.length > 0){ + let name = this._selectedConsumerGroups[0].name; + let members = this._selectedConsumerGroups[0].members; + return html`
+ + + Back + +

${name}

+
+ + + + + + + + + + + + `; + } + } + + + + + _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..aa1989b1bb79b --- /dev/null +++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-messages.js @@ -0,0 +1,257 @@ +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` + + + + + + + + + + + + + + + + `; + } + + _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`
+ +
`; + } + } + + _renderCreateMessageDialog(){ + if(this._createMessageDialogOpened){ + return html` 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..bd2cdc9d52b00 --- /dev/null +++ b/extensions/kafka-client/deployment/src/main/resources/dev-ui/qwc-kafka-topics.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 } 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()} + +
+ +
+
`; + } + + _renderTopicGrid(){ + return html` + + + + + + + + + + + + + + > + + `; + } + + _renderCreateTopicDialog(){ + if(this._createTopicDialogOpened){ + return html` this._renderCreateTopicDialogForm(), "Create topic")} + >`; + } + } + + _renderConfirmDeleteDialog(){ + + return html` 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._createTopicDialogOpened = false; + this._topics = e.detail.result; + } + + _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);