diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..4ec791ebb9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,92 @@ +name: "\U0001F41E Bug report" +description: File a bug report +labels: ["status/triage", "type/bug"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Hi, thanks for raising the issue(-s), all contributions really matter! + Please, note that we'll close the issue without further explanation if you don't follow + this template and don't provide the information requested within this template. + + - type: checkboxes + id: terms + attributes: + label: Issue submitter TODO list + description: By you checking these checkboxes we can be sure you've done the essential things. + options: + - label: I've looked up my issue in [FAQ](https://docs.kafka-ui.provectus.io/faq/common-problems) + required: true + - label: I've searched for an already existing issues [here](https://github.com/provectus/kafka-ui/issues) + required: true + - label: I've tried running `master`-labeled docker image and the issue still persists there + required: true + - label: I'm running a supported version of the application which is listed [here](https://github.com/provectus/kafka-ui/blob/master/SECURITY.md) + required: true + + - type: textarea + attributes: + label: Describe the bug (actual behavior) + description: A clear and concise description of what the bug is. Use a list, if there is more than one problem + validations: + required: true + + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen + validations: + required: false + + - type: textarea + attributes: + label: Your installation details + description: | + How do you run the app? Please provide as much info as possible: + 1. App version (commit hash in the top left corner of the UI) + 2. Helm chart version, if you use one + 3. Your application config. Please remove the sensitive info like passwords or API keys. + 4. Any IAAC configs + validations: + required: true + + - type: textarea + attributes: + label: Steps to reproduce + description: | + Please write down the order of the actions required to reproduce the issue. + For the advanced setups/complicated issue, we might need you to provide + a minimal [reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). + validations: + required: true + + - type: textarea + attributes: + label: Screenshots + description: | + If applicable, add screenshots to help explain your problem + validations: + required: false + + - type: textarea + attributes: + label: Logs + description: | + If applicable, *upload* screenshots to help explain your problem + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: | + Add any other context about the problem here. E.G.: + 1. Are there any alternative scenarios (different data/methods/configuration/setup) you have tried? + Were they successful or the same issue occurred? Please provide steps as well. + 2. Related issues (if there are any). + 3. Logs (if available) + 4. Is there any serious impact or behaviour on the end-user because of this issue, that can be overlooked? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index b6bbcda11a..0000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -name: "\U0001F41E Bug report" -about: Create a bug report -title: '' -labels: status/triage, type/bug -assignees: '' - ---- - - - - - -**Describe the bug** (Actual behavior) - - -**Expected behavior** - - -**Set up** - - - -**Steps to Reproduce** - - -1. - -**Screenshots** - - - -**Additional context** - diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000000..e52c2b7ae9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,66 @@ +name: "\U0001F680 Feature request" +description: Propose a new feature +labels: ["status/triage", "type/feature"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Hi, thanks for raising the issue(-s), all contributions really matter! + Please, note that we'll close the issue without further explanation if you don't follow + this template and don't provide the information requested within this template. + + - type: checkboxes + id: terms + attributes: + label: Issue submitter TODO list + description: By you checking these checkboxes we can be sure you've done the essential things. + options: + - label: I've searched for an already existing issues [here](https://github.com/provectus/kafka-ui/issues) + required: true + - label: I'm running a supported version of the application which is listed [here](https://github.com/provectus/kafka-ui/blob/master/SECURITY.md) and the feature is not present there + required: true + + - type: textarea + attributes: + label: Is your proposal related to a problem? + description: | + Provide a clear and concise description of what the problem is. + For example, "I'm always frustrated when..." + validations: + required: false + + - type: textarea + attributes: + label: Describe the feature you're interested in + description: | + Provide a clear and concise description of what you want to happen. + validations: + required: true + + - type: textarea + attributes: + label: Describe alternatives you've considered + description: | + Let us know about other solutions you've tried or researched. + validations: + required: false + + - type: input + attributes: + label: Version you're running + description: | + Please provide the app version you're currently running: + 1. App version (commit hash in the top left corner of the UI) + validations: + required: true + + - type: textarea + attributes: + label: Additional context + description: | + Is there anything else you can add about the proposal? + You might want to link to related issues here, if you haven't already. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 49a07ae978..0000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: "\U0001F680 Feature request" -about: Propose a new feature -title: '' -labels: status/triage, type/feature -assignees: '' - ---- - - - -### Which version of the app are you running? - - -### Is your proposal related to a problem? - - - -### Describe the solution you'd like - - - -### Describe alternatives you've considered - - - -### Additional context - - - diff --git a/.github/ISSUE_TEMPLATE/helm.yml b/.github/ISSUE_TEMPLATE/helm.yml new file mode 100644 index 0000000000..b36733504e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/helm.yml @@ -0,0 +1,92 @@ +name: "⎈ K8s/Helm problem report" +description: "Report a problem with k8s/helm charts/etc" +labels: ["status/triage", "scope/k8s"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Hi, thanks for raising the issue(-s), all contributions really matter! + Please, note that we'll close the issue without further explanation if you don't follow + this template and don't provide the information requested within this template. + + - type: checkboxes + id: terms + attributes: + label: Issue submitter TODO list + description: By you checking these checkboxes we can be sure you've done the essential things. + options: + - label: I've looked up my issue in [FAQ](https://docs.kafka-ui.provectus.io/faq/common-problems) + required: true + - label: I've searched for an already existing issues [here](https://github.com/provectus/kafka-ui/issues) + required: true + - label: I've tried running `master`-labeled docker image and the issue still persists there + required: true + - label: I'm running a supported version of the application which is listed [here](https://github.com/provectus/kafka-ui/blob/master/SECURITY.md) + required: true + + - type: textarea + attributes: + label: Describe the bug (actual behavior) + description: A clear and concise description of what the bug is. Use a list, if there is more than one problem + validations: + required: true + + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen + validations: + required: false + + - type: textarea + attributes: + label: Your installation details + description: | + How do you run the app? Please provide as much info as possible: + 1. App version (commit hash in the top left corner of the UI) + 2. Helm chart version + 3. Your application config. Please remove the sensitive info like passwords or API keys. + 4. Any IAAC configs + validations: + required: true + + - type: textarea + attributes: + label: Steps to reproduce + description: | + Please write down the order of the actions required to reproduce the issue. + For the advanced setups/complicated issue, we might need you to provide + a minimal [reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). + validations: + required: true + + - type: textarea + attributes: + label: Screenshots + description: | + If applicable, add screenshots to help explain your problem + validations: + required: false + + - type: textarea + attributes: + label: Logs + description: | + If applicable, *upload* screenshots to help explain your problem + validations: + required: false + + - type: textarea + attributes: + label: Additional context + description: | + Add any other context about the problem here. E.G.: + 1. Are there any alternative scenarios (different data/methods/configuration/setup) you have tried? + Were they successful or the same issue occurred? Please provide steps as well. + 2. Related issues (if there are any). + 3. Logs (if available) + 4. Is there any serious impact or behaviour on the end-user because of this issue, that can be overlooked? + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/k8s.md b/.github/ISSUE_TEMPLATE/k8s.md deleted file mode 100644 index 5f4eb8ca75..0000000000 --- a/.github/ISSUE_TEMPLATE/k8s.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: "⎈ K8s/Helm problem report" -about: Report a problem with k8s/helm charts/etc -title: '' -labels: scope/k8s, status/triage -assignees: azatsafin - ---- - - - -**Describe the bug** - - - -**Set up** - - - -**Steps to Reproduce** -Steps to reproduce the behavior: - -1. - -**Expected behavior** - - -**Screenshots** - - - -**Additional context** - diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 75ea103c31..7e8552962a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,8 +8,6 @@ updates: timezone: Europe/Moscow reviewers: - "Haarolean" - assignees: - - "Haarolean" labels: - "scope/backend" - "type/dependencies" @@ -99,8 +97,6 @@ updates: timezone: Europe/Moscow reviewers: - "Haarolean" - assignees: - - "Haarolean" labels: - "scope/infrastructure" - "type/dependencies" diff --git a/kafka-ui-api/pom.xml b/kafka-ui-api/pom.xml index e61827fd73..7f2d4c16be 100644 --- a/kafka-ui-api/pom.xml +++ b/kafka-ui-api/pom.xml @@ -21,6 +21,12 @@ + + + org.springframework + spring-core + 6.0.8 + org.springframework.boot spring-boot-starter-webflux @@ -109,6 +115,12 @@ io.projectreactor.addons reactor-extra + + + org.json + json + ${org.json.version} + org.springframework.boot diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBroker.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBroker.java index edab9a8aeb..4a0d1ba0dd 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBroker.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalBroker.java @@ -1,6 +1,7 @@ package com.provectus.kafka.ui.model; import java.math.BigDecimal; +import javax.annotation.Nullable; import lombok.Data; import org.apache.kafka.common.Node; @@ -10,15 +11,27 @@ public class InternalBroker { private final Integer id; private final String host; private final Integer port; - private final BigDecimal bytesInPerSec; - private final BigDecimal bytesOutPerSec; + private final @Nullable BigDecimal bytesInPerSec; + private final @Nullable BigDecimal bytesOutPerSec; + private final @Nullable Integer partitionsLeader; + private final @Nullable Integer partitions; + private final @Nullable Integer inSyncPartitions; + private final @Nullable BigDecimal leadersSkew; + private final @Nullable BigDecimal partitionsSkew; - public InternalBroker(Node node, Statistics statistics) { + public InternalBroker(Node node, + PartitionDistributionStats partitionDistribution, + Statistics statistics) { this.id = node.id(); this.host = node.host(); this.port = node.port(); this.bytesInPerSec = statistics.getMetrics().getBrokerBytesInPerSec().get(node.id()); this.bytesOutPerSec = statistics.getMetrics().getBrokerBytesOutPerSec().get(node.id()); + this.partitionsLeader = partitionDistribution.getPartitionLeaders().get(node); + this.partitions = partitionDistribution.getPartitionsCount().get(node); + this.inSyncPartitions = partitionDistribution.getInSyncPartitions().get(node); + this.leadersSkew = partitionDistribution.leadersSkew(node); + this.partitionsSkew = partitionDistribution.partitionsSkew(node); } } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java new file mode 100644 index 0000000000..b625533d1d --- /dev/null +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/PartitionDistributionStats.java @@ -0,0 +1,93 @@ +package com.provectus.kafka.ui.model; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.common.Node; +import org.apache.kafka.common.TopicPartitionInfo; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +@Slf4j +public class PartitionDistributionStats { + + // avg skew will show unuseful results on low number of partitions + private static final int MIN_PARTITIONS_FOR_SKEW_CALCULATION = 50; + + private static final MathContext ROUNDING_MATH_CTX = new MathContext(3); + + private final Map partitionLeaders; + private final Map partitionsCount; + private final Map inSyncPartitions; + private final double avgLeadersCntPerBroker; + private final double avgPartitionsPerBroker; + private final boolean skewCanBeCalculated; + + public static PartitionDistributionStats create(Statistics stats) { + return create(stats, MIN_PARTITIONS_FOR_SKEW_CALCULATION); + } + + static PartitionDistributionStats create(Statistics stats, int minPartitionsForSkewCalculation) { + var partitionLeaders = new HashMap(); + var partitionsReplicated = new HashMap(); + var isr = new HashMap(); + int partitionsCnt = 0; + for (TopicDescription td : stats.getTopicDescriptions().values()) { + for (TopicPartitionInfo tp : td.partitions()) { + partitionsCnt++; + tp.replicas().forEach(r -> incr(partitionsReplicated, r)); + tp.isr().forEach(r -> incr(isr, r)); + if (tp.leader() != null) { + incr(partitionLeaders, tp.leader()); + } + } + } + int nodesWithPartitions = partitionsReplicated.size(); + int partitionReplications = partitionsReplicated.values().stream().mapToInt(i -> i).sum(); + var avgPartitionsPerBroker = nodesWithPartitions == 0 ? 0 : ((double) partitionReplications) / nodesWithPartitions; + + int nodesWithLeaders = partitionLeaders.size(); + int leadersCnt = partitionLeaders.values().stream().mapToInt(i -> i).sum(); + var avgLeadersCntPerBroker = nodesWithLeaders == 0 ? 0 : ((double) leadersCnt) / nodesWithLeaders; + + return new PartitionDistributionStats( + partitionLeaders, + partitionsReplicated, + isr, + avgLeadersCntPerBroker, + avgPartitionsPerBroker, + partitionsCnt >= minPartitionsForSkewCalculation + ); + } + + private static void incr(Map map, Node n) { + map.compute(n, (k, c) -> c == null ? 1 : ++c); + } + + @Nullable + public BigDecimal partitionsSkew(Node node) { + return calculateAvgSkew(partitionsCount.get(node), avgPartitionsPerBroker); + } + + @Nullable + public BigDecimal leadersSkew(Node node) { + return calculateAvgSkew(partitionLeaders.get(node), avgLeadersCntPerBroker); + } + + // Returns difference (in percents) from average value, null if it can't be calculated + @Nullable + private BigDecimal calculateAvgSkew(@Nullable Integer value, double avgValue) { + if (avgValue == 0 || !skewCanBeCalculated) { + return null; + } + value = value == null ? 0 : value; + return new BigDecimal((value - avgValue) / avgValue * 100.0).round(ROUNDING_MATH_CTX); + } +} diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java index 8c7a3024ed..f5b7018034 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java @@ -123,11 +123,11 @@ public class ConsumerRecordDeserializer { } private static Long getKeySize(ConsumerRecord consumerRecord) { - return consumerRecord.key() != null ? (long) consumerRecord.key().get().length : null; + return consumerRecord.key() != null ? (long) consumerRecord.serializedKeySize() : null; } private static Long getValueSize(ConsumerRecord consumerRecord) { - return consumerRecord.value() != null ? (long) consumerRecord.value().get().length : null; + return consumerRecord.value() != null ? (long) consumerRecord.serializedValueSize() : null; } private static int headerSize(Header header) { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java index 40ea320b2e..66692894a6 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java @@ -122,8 +122,6 @@ public class SerdesInitializer { registeredSerdes, Optional.ofNullable(clusterProperties.getDefaultKeySerde()) .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default key serde not found")) - .or(() -> Optional.ofNullable(registeredSerdes.get(SchemaRegistrySerde.name()))) - .or(() -> Optional.ofNullable(registeredSerdes.get(ProtobufFileSerde.name()))) .orElse(null), Optional.ofNullable(clusterProperties.getDefaultValueSerde()) .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default value serde not found")) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/BrokerService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/BrokerService.java index 720642157b..8a2ac1a63e 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/BrokerService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/BrokerService.java @@ -10,6 +10,7 @@ import com.provectus.kafka.ui.model.BrokersLogdirsDTO; import com.provectus.kafka.ui.model.InternalBroker; import com.provectus.kafka.ui.model.InternalBrokerConfig; import com.provectus.kafka.ui.model.KafkaCluster; +import com.provectus.kafka.ui.model.PartitionDistributionStats; import com.provectus.kafka.ui.service.metrics.RawMetric; import java.util.Collections; import java.util.HashMap; @@ -64,11 +65,13 @@ public class BrokerService { } public Flux getBrokers(KafkaCluster cluster) { + var stats = statisticsCache.get(cluster); + var partitionsDistribution = PartitionDistributionStats.create(stats); return adminClientService .get(cluster) .flatMap(ReactiveAdminClient::describeCluster) .map(description -> description.getNodes().stream() - .map(node -> new InternalBroker(node, statisticsCache.get(cluster))) + .map(node -> new InternalBroker(node, partitionsDistribution, stats)) .collect(Collectors.toList())) .flatMapMany(Flux::fromIterable); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java index d07ef7ed2d..98b61541c5 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java @@ -109,6 +109,7 @@ public class KafkaConnectService { private Stream getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) { return Stream.of( fullConnectorInfo.getName(), + fullConnectorInfo.getConnect(), fullConnectorInfo.getStatus().getState().getValue(), fullConnectorInfo.getType().getValue()); } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java index d5b4400807..f36d3bec4d 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java @@ -43,8 +43,7 @@ class TopicAnalysisStats { Long max; final UpdateDoublesSketch sizeSketch = DoublesSketch.builder().build(); - void apply(byte[] bytes) { - int len = bytes.length; + void apply(int len) { sum += len; min = minNullable(min, len); max = maxNullable(max, len); @@ -98,7 +97,7 @@ class TopicAnalysisStats { if (rec.key() != null) { byte[] keyBytes = rec.key().get(); - keysSize.apply(keyBytes); + keysSize.apply(rec.serializedKeySize()); uniqKeys.update(keyBytes); } else { nullKeys++; @@ -106,7 +105,7 @@ class TopicAnalysisStats { if (rec.value() != null) { byte[] valueBytes = rec.value().get(); - valuesSize.apply(valueBytes); + valuesSize.apply(rec.serializedValueSize()); uniqValues.update(valueBytes); } else { nullValues++; diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/PartitionDistributionStatsTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/PartitionDistributionStatsTest.java new file mode 100644 index 0000000000..c83c4f5cd8 --- /dev/null +++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/model/PartitionDistributionStatsTest.java @@ -0,0 +1,83 @@ +package com.provectus.kafka.ui.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.provectus.kafka.ui.service.ReactiveAdminClient; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.common.Node; +import org.apache.kafka.common.TopicPartitionInfo; +import org.assertj.core.data.Percentage; +import org.junit.jupiter.api.Test; + +class PartitionDistributionStatsTest { + + @Test + void skewCalculatedBasedOnPartitionsCounts() { + Node n1 = new Node(1, "n1", 9092); + Node n2 = new Node(2, "n2", 9092); + Node n3 = new Node(3, "n3", 9092); + Node n4 = new Node(4, "n4", 9092); + + var stats = PartitionDistributionStats.create( + Statistics.builder() + .clusterDescription( + new ReactiveAdminClient.ClusterDescription(null, "test", Set.of(n1, n2, n3), null)) + .topicDescriptions( + Map.of( + "t1", new TopicDescription( + "t1", false, + List.of( + new TopicPartitionInfo(0, n1, List.of(n1, n2), List.of(n1, n2)), + new TopicPartitionInfo(1, n2, List.of(n2, n3), List.of(n2, n3)) + ) + ), + "t2", new TopicDescription( + "t2", false, + List.of( + new TopicPartitionInfo(0, n1, List.of(n1, n2), List.of(n1, n2)), + new TopicPartitionInfo(1, null, List.of(n2, n1), List.of(n1)) + ) + ) + ) + ) + .build(), 4 + ); + + assertThat(stats.getPartitionLeaders()) + .containsExactlyInAnyOrderEntriesOf(Map.of(n1, 2, n2, 1)); + assertThat(stats.getPartitionsCount()) + .containsExactlyInAnyOrderEntriesOf(Map.of(n1, 3, n2, 4, n3, 1)); + assertThat(stats.getInSyncPartitions()) + .containsExactlyInAnyOrderEntriesOf(Map.of(n1, 3, n2, 3, n3, 1)); + + // Node(partitions): n1(3), n2(4), n3(1), n4(0) + // average partitions cnt = (3+4+1) / 3 = 2.666 (counting only nodes with partitions!) + assertThat(stats.getAvgPartitionsPerBroker()) + .isCloseTo(2.666, Percentage.withPercentage(1)); + + assertThat(stats.partitionsSkew(n1)) + .isCloseTo(BigDecimal.valueOf(12.5), Percentage.withPercentage(1)); + assertThat(stats.partitionsSkew(n2)) + .isCloseTo(BigDecimal.valueOf(50), Percentage.withPercentage(1)); + assertThat(stats.partitionsSkew(n3)) + .isCloseTo(BigDecimal.valueOf(-62.5), Percentage.withPercentage(1)); + assertThat(stats.partitionsSkew(n4)) + .isCloseTo(BigDecimal.valueOf(-100), Percentage.withPercentage(1)); + + // Node(leaders): n1(2), n2(1), n3(0), n4(0) + // average leaders cnt = (2+1) / 2 = 1.5 (counting only nodes with leaders!) + assertThat(stats.leadersSkew(n1)) + .isCloseTo(BigDecimal.valueOf(33.33), Percentage.withPercentage(1)); + assertThat(stats.leadersSkew(n2)) + .isCloseTo(BigDecimal.valueOf(-33.33), Percentage.withPercentage(1)); + assertThat(stats.leadersSkew(n3)) + .isCloseTo(BigDecimal.valueOf(-100), Percentage.withPercentage(1)); + assertThat(stats.leadersSkew(n4)) + .isCloseTo(BigDecimal.valueOf(-100), Percentage.withPercentage(1)); + } + +} diff --git a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml index 4bd3d2207c..78c7cf3bf5 100644 --- a/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml +++ b/kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml @@ -2375,6 +2375,16 @@ components: type: number bytesOutPerSec: type: number + partitionsLeader: + type: integer + partitions: + type: integer + inSyncPartitions: + type: integer + partitionsSkew: + type: number + leadersSkew: + type: number required: - id diff --git a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java index a041defc93..b4cc54a38f 100644 --- a/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java +++ b/kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/services/ApiService.java @@ -36,29 +36,31 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti @Slf4j public class ApiService extends BaseSource { + private final ApiClient apiClient = new ApiClient().setBasePath(BASE_API_URL); + @SneakyThrows private TopicsApi topicApi() { - return new TopicsApi(new ApiClient().setBasePath(BASE_API_URL)); + return new TopicsApi(apiClient); } @SneakyThrows private SchemasApi schemaApi() { - return new SchemasApi(new ApiClient().setBasePath(BASE_API_URL)); + return new SchemasApi(apiClient); } @SneakyThrows private KafkaConnectApi connectorApi() { - return new KafkaConnectApi(new ApiClient().setBasePath(BASE_API_URL)); + return new KafkaConnectApi(apiClient); } @SneakyThrows private MessagesApi messageApi() { - return new MessagesApi(new ApiClient().setBasePath(BASE_API_URL)); + return new MessagesApi(apiClient); } @SneakyThrows private KsqlApi ksqlApi() { - return new KsqlApi(new ApiClient().setBasePath(BASE_API_URL)); + return new KsqlApi(apiClient); } @SneakyThrows diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java index d96bbb7f3a..3ce086ee7b 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java @@ -2,6 +2,7 @@ package com.provectus.kafka.ui.manualsuite.backlog; import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.BROKERS_SUITE_ID; import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.KSQL_DB_SUITE_ID; +import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.SCHEMAS_SUITE_ID; import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_PROFILE_SUITE_ID; import static com.provectus.kafka.ui.utilities.qase.enums.State.TO_BE_AUTOMATED; @@ -35,37 +36,65 @@ public class SmokeBacklog extends BaseManualTest { } @Automation(state = TO_BE_AUTOMATED) - @Suite(id = KSQL_DB_SUITE_ID) - @QaseId(284) + @Suite(id = BROKERS_SUITE_ID) + @QaseId(331) @Test public void testCaseD() { } - @Automation(state = TO_BE_AUTOMATED) - @Suite(id = BROKERS_SUITE_ID) - @QaseId(331) - @Test - public void testCaseE() { - } - @Automation(state = TO_BE_AUTOMATED) @Suite(id = BROKERS_SUITE_ID) @QaseId(332) @Test - public void testCaseF() { + public void testCaseE() { } @Automation(state = TO_BE_AUTOMATED) @Suite(id = TOPICS_PROFILE_SUITE_ID) @QaseId(335) @Test - public void testCaseG() { + public void testCaseF() { } @Automation(state = TO_BE_AUTOMATED) @Suite(id = TOPICS_PROFILE_SUITE_ID) @QaseId(336) @Test + public void testCaseG() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = TOPICS_PROFILE_SUITE_ID) + @QaseId(343) + @Test public void testCaseH() { } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = KSQL_DB_SUITE_ID) + @QaseId(344) + @Test + public void testCaseI() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = SCHEMAS_SUITE_ID) + @QaseId(345) + @Test + public void testCaseJ() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = SCHEMAS_SUITE_ID) + @QaseId(346) + @Test + public void testCaseK() { + } + + @Automation(state = TO_BE_AUTOMATED) + @Suite(id = TOPICS_PROFILE_SUITE_ID) + @QaseId(347) + @Test + public void testCaseL() { + } } diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java index 76f8506deb..758827e21b 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java @@ -92,4 +92,28 @@ public class TopicsTest extends BaseManualTest { @Test public void testCaseN() { } + + @Automation(state = NOT_AUTOMATED) + @QaseId(337) + @Test + public void testCaseO() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(339) + @Test + public void testCaseP() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(341) + @Test + public void testCaseQ() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(342) + @Test + public void testCaseR() { + } } diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java index 9621104b1a..c74c1ba6f0 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/WizardTest.java @@ -14,4 +14,16 @@ public class WizardTest extends BaseManualTest { @Test public void testCaseA() { } + + @Automation(state = NOT_AUTOMATED) + @QaseId(338) + @Test + public void testCaseB() { + } + + @Automation(state = NOT_AUTOMATED) + @QaseId(340) + @Test + public void testCaseC() { + } } diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java index c4bbe0def4..22ef931bf1 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java @@ -1,5 +1,6 @@ package com.provectus.kafka.ui.smokesuite.ksqldb; +import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs.STREAMS; import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES; import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB; import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; @@ -39,17 +40,21 @@ public class KsqlDbTest extends BaseTest { FIRST_TABLE.getName(), SECOND_TABLE.getName())); } - @QaseId(86) + @QaseId(284) @Test(priority = 1) - public void clearResultsForExecutedRequest() { - navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery()); + public void streamsAndTablesVisibilityCheck() { + naviSideBar + .openSideMenu(KSQL_DB); + ksqlDbList + .waitUntilScreenReady(); SoftAssert softly = new SoftAssert(); - softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); - softly.assertAll(); - ksqlQueryForm - .clickClearResultsBtn(); - softly.assertFalse(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertTrue(ksqlDbList.getTableByName(FIRST_TABLE.getName()).isVisible(), "getTableByName()"); + softly.assertTrue(ksqlDbList.getTableByName(SECOND_TABLE.getName()).isVisible(), "getTableByName()"); softly.assertAll(); + ksqlDbList + .openDetailsTab(STREAMS) + .waitUntilScreenReady(); + Assert.assertTrue(ksqlDbList.getStreamByName(DEFAULT_STREAM.getName()).isVisible(), "getStreamByName()"); } @QaseId(276) @@ -68,11 +73,31 @@ public class KsqlDbTest extends BaseTest { navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery()); SoftAssert softly = new SoftAssert(); softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); - softly.assertTrue(ksqlQueryForm.getItemByName(FIRST_TABLE.getName()).isVisible(), "getItemByName()"); - softly.assertTrue(ksqlQueryForm.getItemByName(SECOND_TABLE.getName()).isVisible(), "getItemByName()"); + softly.assertTrue(ksqlQueryForm.getItemByName(FIRST_TABLE.getName()).isVisible(), + String.format("getItemByName(%s)", FIRST_TABLE.getName())); + softly.assertTrue(ksqlQueryForm.getItemByName(SECOND_TABLE.getName()).isVisible(), + String.format("getItemByName(%s)", SECOND_TABLE.getName())); softly.assertAll(); } + @QaseId(86) + @Test(priority = 4) + public void clearResultsForExecutedRequest() { + navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery()); + SoftAssert softly = new SoftAssert(); + softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertAll(); + ksqlQueryForm + .clickClearResultsBtn(); + softly.assertFalse(ksqlQueryForm.areResultsVisible(), "areResultsVisible()"); + softly.assertAll(); + } + + @AfterClass(alwaysRun = true) + public void afterClass() { + TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName)); + } + @Step private void navigateToKsqlDbAndExecuteRequest(String query) { naviSideBar @@ -85,9 +110,4 @@ public class KsqlDbTest extends BaseTest { .setQuery(query) .clickExecuteBtn(query); } - - @AfterClass(alwaysRun = true) - public void afterClass() { - TOPIC_NAMES_LIST.forEach(topicName -> apiService.deleteTopic(topicName)); - } } diff --git a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java index ad20f595a4..bad6a9fcde 100644 --- a/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java +++ b/kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/topics/TopicsTest.java @@ -486,11 +486,7 @@ public class TopicsTest extends BaseTest { topicDetails .waitUntilScreenReady(); TOPIC_LIST.add(topicToCopy); - SoftAssert softly = new SoftAssert(); - softly.assertTrue(topicDetails.isAlertWithMessageVisible(SUCCESS, "Topic successfully created."), - "isAlertWithMessageVisible()"); - softly.assertTrue(topicDetails.isTopicHeaderVisible(topicToCopy.getName()), "isTopicHeaderVisible()"); - softly.assertAll(); + Assert.assertTrue(topicDetails.isTopicHeaderVisible(topicToCopy.getName()), "isTopicHeaderVisible()"); } @AfterClass(alwaysRun = true) diff --git a/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx b/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx index 30b3df8a56..5b3a24cdb7 100644 --- a/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx +++ b/kafka-ui-react-app/src/components/Connect/List/ActionsCell.tsx @@ -1,26 +1,41 @@ import React from 'react'; -import { FullConnectorInfo } from 'generated-sources'; +import { + Action, + ConnectorAction, + ConnectorState, + FullConnectorInfo, + ResourceType, +} from 'generated-sources'; import { CellContext } from '@tanstack/react-table'; import { ClusterNameRoute } from 'lib/paths'; import useAppParams from 'lib/hooks/useAppParams'; import { Dropdown, DropdownItem } from 'components/common/Dropdown'; -import { useDeleteConnector } from 'lib/hooks/api/kafkaConnect'; +import { + useDeleteConnector, + useUpdateConnectorState, +} from 'lib/hooks/api/kafkaConnect'; import { useConfirm } from 'lib/hooks/useConfirm'; +import { useIsMutating } from '@tanstack/react-query'; +import { ActionDropdownItem } from 'components/common/ActionComponent'; const ActionsCell: React.FC> = ({ row, }) => { - const { connect, name } = row.original; - + const { connect, name, status } = row.original; const { clusterName } = useAppParams(); - + const mutationsNumber = useIsMutating(); + const isMutating = mutationsNumber > 0; const confirm = useConfirm(); const deleteMutation = useDeleteConnector({ clusterName, connectName: connect, connectorName: name, }); - + const stateMutation = useUpdateConnectorState({ + clusterName, + connectName: connect, + connectorName: name, + }); const handleDelete = () => { confirm( <> @@ -31,8 +46,66 @@ const ActionsCell: React.FC> = ({ } ); }; + // const stateMutation = useUpdateConnectorState(routerProps); + const resumeConnectorHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESUME); + const restartConnectorHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESTART); + + const restartAllTasksHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESTART_ALL_TASKS); + + const restartFailedTasksHandler = () => + stateMutation.mutateAsync(ConnectorAction.RESTART_FAILED_TASKS); + return ( + {status.state === ConnectorState.PAUSED && ( + + Resume + + )} + + Restart Connector + + + Restart All Tasks + + + Restart Failed Tasks + Remove Connector diff --git a/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx index 9de28f38ff..82b4aab212 100644 --- a/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Connect/List/__tests__/List.spec.tsx @@ -9,7 +9,11 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render, WithRoute } from 'lib/testHelpers'; import { clusterConnectConnectorPath, clusterConnectorsPath } from 'lib/paths'; -import { useConnectors, useDeleteConnector } from 'lib/hooks/api/kafkaConnect'; +import { + useConnectors, + useDeleteConnector, + useUpdateConnectorState, +} from 'lib/hooks/api/kafkaConnect'; const mockedUsedNavigate = jest.fn(); const mockDelete = jest.fn(); @@ -22,6 +26,7 @@ jest.mock('react-router-dom', () => ({ jest.mock('lib/hooks/api/kafkaConnect', () => ({ useConnectors: jest.fn(), useDeleteConnector: jest.fn(), + useUpdateConnectorState: jest.fn(), })); const clusterName = 'local'; @@ -42,6 +47,10 @@ describe('Connectors List', () => { (useConnectors as jest.Mock).mockImplementation(() => ({ data: connectors, })); + const restartConnector = jest.fn(); + (useUpdateConnectorState as jest.Mock).mockImplementation(() => ({ + mutateAsync: restartConnector, + })); }); it('renders', async () => { diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx index fb4e258cca..dd5cfae748 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx @@ -142,6 +142,8 @@ const Message: React.FC = ({ timestampType={timestampType} keySize={keySize} contentSize={valueSize} + keySerde={keySerde} + valueSerde={valueSerde} /> )} diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.tsx index 93616ca432..d1237ba0d4 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.tsx @@ -3,7 +3,6 @@ import EditorViewer from 'components/common/EditorViewer/EditorViewer'; import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; import { SchemaType, TopicMessageTimestampTypeEnum } from 'generated-sources'; import { formatTimestamp } from 'lib/dateTimeHelpers'; -import { useSearchParams } from 'react-router-dom'; import * as S from './MessageContent.styled'; @@ -17,6 +16,8 @@ export interface MessageContentProps { timestampType?: TopicMessageTimestampTypeEnum; keySize?: number; contentSize?: number; + keySerde?: string; + valueSerde?: string; } const MessageContent: React.FC = ({ @@ -27,12 +28,10 @@ const MessageContent: React.FC = ({ timestampType, keySize, contentSize, + keySerde, + valueSerde, }) => { const [activeTab, setActiveTab] = React.useState('content'); - const [searchParams] = useSearchParams(); - const keyFormat = searchParams.get('keySerde') || ''; - const valueFormat = searchParams.get('valueSerde') || ''; - const activeTabContent = () => { switch (activeTab) { case 'content': @@ -110,7 +109,7 @@ const MessageContent: React.FC = ({ Key Serde - {keyFormat} + {keySerde} Size: @@ -120,7 +119,7 @@ const MessageContent: React.FC = ({ Value Serde - {valueFormat} + {valueSerde} Size: diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/__tests__/MessageContent.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/__tests__/MessageContent.spec.tsx index 91310a30e4..d76455242c 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/__tests__/MessageContent.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/__tests__/MessageContent.spec.tsx @@ -20,6 +20,8 @@ const setupWrapper = (props?: Partial) => { headers={{ header: 'test' }} timestamp={new Date(0)} timestampType={TopicMessageTimestampTypeEnum.CREATE_TIME} + keySerde="SchemaRegistry" + valueSerde="Avro" {...props} /> @@ -27,42 +29,20 @@ const setupWrapper = (props?: Partial) => { ); }; -const proto = - 'syntax = "proto3";\npackage com.provectus;\n\nmessage TestProtoRecord {\n string f1 = 1;\n int32 f2 = 2;\n}\n'; - global.TextEncoder = TextEncoder; -const searchParamsContentAVRO = new URLSearchParams({ - keySerde: 'SchemaRegistry', - valueSerde: 'AVRO', - limit: '100', -}); - -const searchParamsContentJSON = new URLSearchParams({ - keySerde: 'SchemaRegistry', - valueSerde: 'JSON', - limit: '100', -}); - -const searchParamsContentPROTOBUF = new URLSearchParams({ - keySerde: 'SchemaRegistry', - valueSerde: 'PROTOBUF', - limit: '100', -}); describe('MessageContent screen', () => { beforeEach(() => { - render(setupWrapper(), { - initialEntries: [`/messages?${searchParamsContentAVRO}`], - }); + render(setupWrapper()); }); - describe('renders', () => { - it('key format in document', () => { + describe('Checking keySerde and valueSerde', () => { + it('keySerde in document', () => { expect(screen.getByText('SchemaRegistry')).toBeInTheDocument(); }); - it('content format in document', () => { - expect(screen.getByText('AVRO')).toBeInTheDocument(); + it('valueSerde in document', () => { + expect(screen.getByText('Avro')).toBeInTheDocument(); }); }); @@ -98,42 +78,3 @@ describe('MessageContent screen', () => { }); }); }); - -describe('checking content type depend on message type', () => { - it('renders component with message having JSON type', () => { - render( - setupWrapper({ - messageContent: '{"data": "test"}', - }), - { initialEntries: [`/messages?${searchParamsContentJSON}`] } - ); - expect(screen.getByText('JSON')).toBeInTheDocument(); - }); - it('renders component with message having AVRO type', () => { - render( - setupWrapper({ - messageContent: '{"data": "test"}', - }), - { initialEntries: [`/messages?${searchParamsContentAVRO}`] } - ); - expect(screen.getByText('AVRO')).toBeInTheDocument(); - }); - it('renders component with message having PROTOBUF type', () => { - render( - setupWrapper({ - messageContent: proto, - }), - { initialEntries: [`/messages?${searchParamsContentPROTOBUF}`] } - ); - expect(screen.getByText('PROTOBUF')).toBeInTheDocument(); - }); - it('renders component with message having no type which is equal to having PROTOBUF type', () => { - render( - setupWrapper({ - messageContent: '', - }), - { initialEntries: [`/messages?${searchParamsContentPROTOBUF}`] } - ); - expect(screen.getByText('PROTOBUF')).toBeInTheDocument(); - }); -}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.styled.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.styled.tsx index 483c41d053..d2750abf7d 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.styled.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.styled.tsx @@ -8,15 +8,29 @@ export const Wrapper = styled.div` export const Columns = styled.div` margin: -0.75rem; margin-bottom: 0.75rem; + display: flex; + flex-direction: column; + padding: 0.75rem; + gap: 8px; @media screen and (min-width: 769px) { display: flex; } `; - -export const Column = styled.div` - flex-basis: 0; - flex-grow: 1; - flex-shrink: 1; - padding: 0.75rem; +export const Flex = styled.div` + display: flex; + flex-direction: row; + gap: 8px; + @media screen and (max-width: 1200px) { + flex-direction: column; + } +`; +export const FlexItem = styled.div` + width: 18rem; + @media screen and (max-width: 1450px) { + width: 50%; + } + @media screen and (max-width: 1200px) { + width: 100%; + } `; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx index 9450e512ad..bacfa76c93 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/SendMessage.tsx @@ -4,6 +4,7 @@ import { RouteParamsClusterTopic } from 'lib/paths'; import { Button } from 'components/common/Button/Button'; import Editor from 'components/common/Editor/Editor'; import Select, { SelectOption } from 'components/common/Select/Select'; +import Switch from 'components/common/Switch/Switch'; import useAppParams from 'lib/hooks/useAppParams'; import { showAlert } from 'lib/errorHandling'; import { useSendMessage, useTopicDetails } from 'lib/hooks/api/topics'; @@ -26,9 +27,12 @@ interface FormType { partition: number; keySerde: string; valueSerde: string; + keepContents: boolean; } -const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => { +const SendMessage: React.FC<{ closeSidebar: () => void }> = ({ + closeSidebar, +}) => { const { clusterName, topicName } = useAppParams(); const { data: topic } = useTopicDetails({ clusterName, topicName }); const { data: serdes = {} } = useSerdes({ @@ -47,11 +51,13 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => { handleSubmit, formState: { isSubmitting }, control, + setValue, } = useForm({ mode: 'onChange', defaultValues: { ...defaultValues, partition: Number(partitionOptions[0].value), + keepContents: false, }, }); @@ -62,6 +68,7 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => { content, headers, partition, + keepContents, }: FormType) => { let errors: string[] = []; @@ -110,7 +117,11 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => { keySerde, valueSerde, }); - onSubmit(); + if (!keepContents) { + setValue('key', ''); + setValue('content', ''); + closeSidebar(); + } } catch (e) { // do nothing } @@ -120,7 +131,7 @@ const SendMessage: React.FC<{ onSubmit: () => void }> = ({ onSubmit }) => {
- + Partition void }> = ({ onSubmit }) => { /> )} /> - - - Key Serde + + + + Key Serde + ( + + )} + /> + + +
( - - )} - /> - + Keep contents +
- - +
Key void }> = ({ onSubmit }) => { /> )} /> - - +
+
Value void }> = ({ onSubmit }) => { /> )} /> - +
- +
Headers void }> = ({ onSubmit }) => { /> )} /> - +