Browse Source

Merge branch 'master' into checks/626-update-delete-topics

Anna Antipova 4 years ago
parent
commit
d47c7b1649
42 changed files with 740 additions and 189 deletions
  1. 2 2
      .github/workflows/pr-checks.yaml
  2. 1 0
      README.md
  3. 5 0
      docker/jaas/schema_registry.jaas
  4. 1 0
      docker/jaas/schema_registry.password
  5. 66 0
      docker/kafka-cluster-sr-auth.yaml
  6. 1 1
      docker/kafka-ui.yaml
  7. 1 0
      docker/message.json
  8. 41 0
      guides/AWS_IAM.md
  9. 6 0
      kafka-ui-api/pom.xml
  10. 13 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicMetadataException.java
  11. 29 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/CleanupPolicy.java
  12. 1 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterMetrics.java
  13. 1 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopic.java
  14. 27 15
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaService.java
  15. 60 0
      kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaTopicCreateTests.java
  16. 9 0
      kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
  17. 67 36
      kafka-ui-e2e-checks/pom.xml
  18. 4 0
      kafka-ui-react-app/src/components/Brokers/Brokers.tsx
  19. 2 0
      kafka-ui-react-app/src/components/Brokers/BrokersContainer.ts
  20. 2 0
      kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx
  21. 48 0
      kafka-ui-react-app/src/components/Brokers/__test__/__snapshots__/Brokers.spec.tsx.snap
  22. 8 3
      kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx
  23. 1 1
      kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx
  24. 8 0
      kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/DetailsContainer.spec.tsx
  25. 43 42
      kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx
  26. 46 0
      kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx
  27. 8 0
      kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListContainer.spec.tsx
  28. 33 0
      kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListItem.spec.tsx
  29. 5 0
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx
  30. 12 0
      kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/__snapshots__/ClusterWidget.spec.tsx.snap
  31. 5 0
      kafka-ui-react-app/src/components/Schemas/Details/Details.tsx
  32. 7 0
      kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx
  33. 18 2
      kafka-ui-react-app/src/components/Schemas/Details/__test__/__snapshots__/Details.spec.tsx.snap
  34. 35 31
      kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups.tsx
  35. 2 4
      kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/__test__/TopicConsumerGroups.spec.tsx
  36. 42 43
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesTable.tsx
  37. 3 3
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx
  38. 51 3
      kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap
  39. 5 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx
  40. 15 0
      kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx
  41. 1 3
      kafka-ui-react-app/src/redux/actions/thunks/topics.ts
  42. 5 0
      kafka-ui-react-app/src/redux/reducers/brokers/selectors.ts

+ 2 - 2
.github/workflows/pr-checks.yaml

@@ -10,6 +10,6 @@ jobs:
       - uses: kentaro-m/task-completed-checker-action@v0.1.0
       - uses: kentaro-m/task-completed-checker-action@v0.1.0
         with:
         with:
           repo-token: "${{ secrets.GITHUB_TOKEN }}"
           repo-token: "${{ secrets.GITHUB_TOKEN }}"
-      - uses: derkinderfietsen/pr-description-enforcer@v1
+      - uses: dekinderfiets/pr-description-enforcer@v0.0.1
         with:
         with:
-          repo-token: '${{ secrets.GITHUB_TOKEN }}'
+          repo-token: '${{ secrets.GITHUB_TOKEN }}'

+ 1 - 0
README.md

@@ -109,6 +109,7 @@ To read more please follow to [chart documentation](charts/kafka-ui/README.md)
 # Guides
 # Guides
 
 
 - [SSO configuration](guides/SSO.md)
 - [SSO configuration](guides/SSO.md)
+- [AWS IAM configuration](guides/AWS_IAM.md)
 
 
 ## Connecting to a Secure Broker
 ## Connecting to a Secure Broker
 
 

+ 5 - 0
docker/jaas/schema_registry.jaas

@@ -0,0 +1,5 @@
+SchemaRegistryProps {
+  org.eclipse.jetty.jaas.spi.PropertyFileLoginModule required
+  file="/conf/schema_registry.password"
+  debug="false";
+};

+ 1 - 0
docker/jaas/schema_registry.password

@@ -0,0 +1 @@
+admin: OBF:1w8t1tvf1w261w8v1w1c1tvn1w8x,admin

+ 66 - 0
docker/kafka-cluster-sr-auth.yaml

@@ -0,0 +1,66 @@
+---
+version: '2'
+services:
+
+  zookeeper1:
+    image: confluentinc/cp-zookeeper:5.2.4
+    environment:
+      ZOOKEEPER_CLIENT_PORT: 2181
+      ZOOKEEPER_TICK_TIME: 2000
+    ports:
+      - 2182:2181
+
+  kafka1:
+    image: confluentinc/cp-kafka:5.2.4
+    depends_on:
+      - zookeeper1
+    environment:
+      KAFKA_BROKER_ID: 1
+      KAFKA_ZOOKEEPER_CONNECT: zookeeper1:2181
+      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:29092,PLAINTEXT_HOST://localhost:9093
+      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
+      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
+      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+      JMX_PORT: 9998
+      KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=localhost -Dcom.sun.management.jmxremote.rmi.port=9998
+    ports:
+      - 9093:9093
+      - 9998:9998 
+
+  schemaregistry1:
+    image: confluentinc/cp-schema-registry:5.5.0
+    ports:
+      - 18085:8085
+    depends_on:
+      - zookeeper1
+      - kafka1
+    volumes:
+      - ./jaas:/conf
+    environment:
+      SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: PLAINTEXT://kafka1:29092
+      SCHEMA_REGISTRY_KAFKASTORE_CONNECTION_URL: zookeeper1:2181
+      SCHEMA_REGISTRY_KAFKASTORE_SECURITY_PROTOCOL: PLAINTEXT
+      SCHEMA_REGISTRY_HOST_NAME: schemaregistry1
+      SCHEMA_REGISTRY_LISTENERS: http://schemaregistry1:8085
+
+      # Default credentials: admin/letmein
+      SCHEMA_REGISTRY_AUTHENTICATION_METHOD: BASIC
+      SCHEMA_REGISTRY_AUTHENTICATION_REALM: SchemaRegistryProps
+      SCHEMA_REGISTRY_AUTHENTICATION_ROLES: admin
+      SCHEMA_REGISTRY_OPTS: -Djava.security.auth.login.config=/conf/schema_registry.jaas
+
+      SCHEMA_REGISTRY_SCHEMA_REGISTRY_INTER_INSTANCE_PROTOCOL: "http"
+      SCHEMA_REGISTRY_LOG4J_ROOT_LOGLEVEL: INFO
+      SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas
+
+  kafka-init-topics:
+    image: confluentinc/cp-kafka:5.2.4
+    volumes:
+       - ./message.json:/data/message.json
+    depends_on:
+      - kafka1
+    command: "bash -c 'echo Waiting for Kafka to be ready... && \
+               cub kafka-ready -b kafka1:29092 1 30 && \
+               kafka-topics --create --topic second.users --partitions 3 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \
+               kafka-topics --create --topic second.messages --partitions 2 --replication-factor 1 --if-not-exists --zookeeper zookeeper1:2181 && \
+               kafka-console-producer --broker-list kafka1:29092 -topic second.users < /data/message.json'"

+ 1 - 1
docker/kafka-ui.yaml

@@ -26,7 +26,7 @@ services:
       KAFKA_CLUSTERS_1_BOOTSTRAPSERVERS: kafka1:29092
       KAFKA_CLUSTERS_1_BOOTSTRAPSERVERS: kafka1:29092
       KAFKA_CLUSTERS_1_ZOOKEEPER: zookeeper1:2181
       KAFKA_CLUSTERS_1_ZOOKEEPER: zookeeper1:2181
       KAFKA_CLUSTERS_1_JMXPORT: 9998
       KAFKA_CLUSTERS_1_JMXPORT: 9998
-      KAFKA_CLUSTERS_1_SCHEMAREGISTRY: http://schemaregistry0:8085
+      KAFKA_CLUSTERS_1_SCHEMAREGISTRY: http://schemaregistry1:8085
       KAFKA_CLUSTERS_1_KAFKACONNECT_0_NAME: first
       KAFKA_CLUSTERS_1_KAFKACONNECT_0_NAME: first
       KAFKA_CLUSTERS_1_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083
       KAFKA_CLUSTERS_1_KAFKACONNECT_0_ADDRESS: http://kafka-connect0:8083
 
 

+ 1 - 0
docker/message.json

@@ -0,0 +1 @@
+{}

+ 41 - 0
guides/AWS_IAM.md

@@ -0,0 +1,41 @@
+# How to configure AWS IAM Authentication
+
+UI for Apache Kafka comes with built-in [aws-msk-iam-auth](https://github.com/aws/aws-msk-iam-auth) library.
+
+You could pass sasl configs in properties section for each cluster.
+
+More details could be found here: [aws-msk-iam-auth](https://github.com/aws/aws-msk-iam-auth)
+ 
+## Examples: 
+
+Please replace 
+* <KAFKA_URL> with broker list
+* <PROFILE_NAME> with your aws profile
+
+
+### Running From Docker Image
+
+```sh
+docker run -p 8080:8080 \
+    -e KAFKA_CLUSTERS_0_NAME=local \
+    -e KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=<KAFKA_URL> \
+    -e KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL=SASL_SSL \
+    -e KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM=AWS_MSK_IAM \
+    -e KAFKA_CLUSTERS_0_PROPERTIES_SASL_CLIENT_CALLBACK_HANDLER_CLASS=software.amazon.msk.auth.iam.IAMClientCallbackHandler \
+    -e KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG=software.amazon.msk.auth.iam.IAMLoginModule required awsProfileName="<PROFILE_NAME>"; \
+    -d provectuslabs/kafka-ui:latest 
+```
+
+### Configuring by application.yaml
+
+```yaml
+kafka:
+  clusters:
+    - name: local
+      bootstrapServers: <KAFKA_URL>
+      properties:
+        security.protocol: SASL_SSL
+        sasl.mechanism: AWS_MSK_IAM
+        sasl.client.callback.handler.class: software.amazon.msk.auth.iam.IAMClientCallbackHandler
+        sasl.jaas.config: software.amazon.msk.auth.iam.IAMLoginModule required awsProfileName="<PROFILE_NAME>";
+```

+ 6 - 0
kafka-ui-api/pom.xml

@@ -97,6 +97,12 @@
             <version>${confluent.version}</version>
             <version>${confluent.version}</version>
         </dependency>
         </dependency>
 
 
+        <dependency>
+            <groupId>software.amazon.msk</groupId>
+            <artifactId>aws-msk-iam-auth</artifactId>
+            <version>1.1.0</version>
+        </dependency>
+
         <dependency>
         <dependency>
             <groupId>org.apache.avro</groupId>
             <groupId>org.apache.avro</groupId>
             <artifactId>avro</artifactId>
             <artifactId>avro</artifactId>

+ 13 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/TopicMetadataException.java

@@ -0,0 +1,13 @@
+package com.provectus.kafka.ui.exception;
+
+public class TopicMetadataException extends CustomBaseException {
+
+  public TopicMetadataException(String message) {
+    super(message);
+  }
+
+  @Override
+  public ErrorCode getErrorCode() {
+    return ErrorCode.INVALID_ENTITY_STATE;
+  }
+}

+ 29 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/CleanupPolicy.java

@@ -0,0 +1,29 @@
+package com.provectus.kafka.ui.model;
+
+import com.provectus.kafka.ui.exception.IllegalEntityStateException;
+import java.util.Arrays;
+
+public enum CleanupPolicy {
+  DELETE("delete"),
+  COMPACT("compact"),
+  COMPACT_DELETE("compact, delete"),
+  UNKNOWN("unknown");
+
+  private final String cleanUpPolicy;
+
+  CleanupPolicy(String cleanUpPolicy) {
+    this.cleanUpPolicy = cleanUpPolicy;
+  }
+
+  public String getCleanUpPolicy() {
+    return cleanUpPolicy;
+  }
+
+  public static CleanupPolicy fromString(String string) {
+    return Arrays.stream(CleanupPolicy.values())
+        .filter(v -> v.cleanUpPolicy.equals(string))
+        .findFirst()
+        .orElseThrow(() ->
+            new IllegalEntityStateException("Unknown cleanup policy value: " + string));
+  }
+}

+ 1 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalClusterMetrics.java

@@ -27,4 +27,5 @@ public class InternalClusterMetrics {
   private final Map<Integer, InternalBrokerMetrics> internalBrokerMetrics;
   private final Map<Integer, InternalBrokerMetrics> internalBrokerMetrics;
   private final List<Metric> metrics;
   private final List<Metric> metrics;
   private final int zooKeeperStatus;
   private final int zooKeeperStatus;
+  private final String version;
 }
 }

+ 1 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalTopic.java

@@ -14,6 +14,7 @@ public class InternalTopic {
   private final Map<Integer, InternalPartition> partitions;
   private final Map<Integer, InternalPartition> partitions;
   private final List<InternalTopicConfig> topicConfigs;
   private final List<InternalTopicConfig> topicConfigs;
 
 
+  private final CleanupPolicy cleanUpPolicy;
   private final int replicas;
   private final int replicas;
   private final int partitionCount;
   private final int partitionCount;
   private final int inSyncReplicas;
   private final int inSyncReplicas;

+ 27 - 15
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaService.java

@@ -1,6 +1,8 @@
 package com.provectus.kafka.ui.service;
 package com.provectus.kafka.ui.service;
 
 
+import com.provectus.kafka.ui.exception.TopicMetadataException;
 import com.provectus.kafka.ui.exception.ValidationException;
 import com.provectus.kafka.ui.exception.ValidationException;
+import com.provectus.kafka.ui.model.CleanupPolicy;
 import com.provectus.kafka.ui.model.CreateTopicMessage;
 import com.provectus.kafka.ui.model.CreateTopicMessage;
 import com.provectus.kafka.ui.model.ExtendedAdminClient;
 import com.provectus.kafka.ui.model.ExtendedAdminClient;
 import com.provectus.kafka.ui.model.InternalBrokerDiskUsage;
 import com.provectus.kafka.ui.model.InternalBrokerDiskUsage;
@@ -159,6 +161,7 @@ public class KafkaService {
         .onlinePartitionCount(topicsMetrics.getOnlinePartitionCount())
         .onlinePartitionCount(topicsMetrics.getOnlinePartitionCount())
         .offlinePartitionCount(topicsMetrics.getOfflinePartitionCount())
         .offlinePartitionCount(topicsMetrics.getOfflinePartitionCount())
         .zooKeeperStatus(ClusterUtil.convertToIntServerStatus(zookeeperStatus))
         .zooKeeperStatus(ClusterUtil.convertToIntServerStatus(zookeeperStatus))
+        .version(version)
         .build();
         .build();
 
 
     return currentCluster.toBuilder()
     return currentCluster.toBuilder()
@@ -205,12 +208,18 @@ public class KafkaService {
 
 
   private Map<String, InternalTopic> mergeWithConfigs(
   private Map<String, InternalTopic> mergeWithConfigs(
       List<InternalTopic> topics, Map<String, List<InternalTopicConfig>> configs) {
       List<InternalTopic> topics, Map<String, List<InternalTopicConfig>> configs) {
-    return topics.stream().map(
-        t -> t.toBuilder().topicConfigs(configs.get(t.getName())).build()
-    ).collect(Collectors.toMap(
-        InternalTopic::getName,
-        e -> e
-    ));
+    return topics.stream()
+        .map(t -> t.toBuilder().topicConfigs(configs.get(t.getName())).build())
+        .map(t -> t.toBuilder().cleanUpPolicy(
+            CleanupPolicy.fromString(t.getTopicConfigs().stream()
+                .filter(config -> config.getName().equals("cleanup.policy"))
+                .findFirst()
+                .orElseGet(() -> InternalTopicConfig.builder().value("unknown").build())
+                .getValue())).build())
+        .collect(Collectors.toMap(
+            InternalTopic::getName,
+            e -> e
+        ));
   }
   }
 
 
   @SneakyThrows
   @SneakyThrows
@@ -223,11 +232,12 @@ public class KafkaService {
     final Mono<Map<String, List<InternalTopicConfig>>> configsMono =
     final Mono<Map<String, List<InternalTopicConfig>>> configsMono =
         loadTopicsConfig(adminClient, topics);
         loadTopicsConfig(adminClient, topics);
 
 
-    return ClusterUtil.toMono(adminClient.describeTopics(topics).all()).map(
-        m -> m.values().stream().map(ClusterUtil::mapToInternalTopic).collect(Collectors.toList())
-    ).flatMap(internalTopics -> configsMono.map(configs ->
-        mergeWithConfigs(internalTopics, configs).values()
-    )).flatMapMany(Flux::fromIterable);
+    return ClusterUtil.toMono(adminClient.describeTopics(topics).all())
+        .map(m -> m.values().stream()
+            .map(ClusterUtil::mapToInternalTopic).collect(Collectors.toList()))
+        .flatMap(internalTopics -> configsMono
+            .map(configs -> mergeWithConfigs(internalTopics, configs).values()))
+        .flatMapMany(Flux::fromIterable);
   }
   }
 
 
 
 
@@ -260,10 +270,12 @@ public class KafkaService {
               topicData.getReplicationFactor().shortValue());
               topicData.getReplicationFactor().shortValue());
           newTopic.configs(topicData.getConfigs());
           newTopic.configs(topicData.getConfigs());
           return createTopic(adminClient, newTopic).map(v -> topicData);
           return createTopic(adminClient, newTopic).map(v -> topicData);
-        }).flatMap(
-          topicData ->
-              getTopicsData(adminClient, Collections.singleton(topicData.getName()))
-                  .next()
+        })
+        .onErrorResume(t -> Mono.error(new TopicMetadataException(t.getMessage())))
+        .flatMap(
+            topicData ->
+                getTopicsData(adminClient, Collections.singleton(topicData.getName()))
+                    .next()
         ).switchIfEmpty(Mono.error(new RuntimeException("Can't find created topic")))
         ).switchIfEmpty(Mono.error(new RuntimeException("Can't find created topic")))
         .flatMap(t ->
         .flatMap(t ->
             loadTopicsConfig(adminClient, Collections.singletonList(t.getName()))
             loadTopicsConfig(adminClient, Collections.singletonList(t.getName()))

+ 60 - 0
kafka-ui-api/src/test/java/com/provectus/kafka/ui/KafkaTopicCreateTests.java

@@ -0,0 +1,60 @@
+package com.provectus.kafka.ui;
+
+import com.provectus.kafka.ui.model.TopicCreation;
+import java.util.UUID;
+import lombok.extern.log4j.Log4j2;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+@ContextConfiguration(initializers = {AbstractBaseTest.Initializer.class})
+@Log4j2
+@AutoConfigureWebTestClient(timeout = "10000")
+public class KafkaTopicCreateTests extends AbstractBaseTest {
+  @Autowired
+  private WebTestClient webTestClient;
+  private TopicCreation topicCreation;
+
+  @BeforeEach
+  public void setUpBefore() {
+    this.topicCreation = new TopicCreation()
+        .replicationFactor(1)
+        .partitions(3)
+        .name(UUID.randomUUID().toString());
+  }
+
+  @Test
+  void shouldCreateNewTopicSuccessfully() {
+    webTestClient.post()
+        .uri("/api/clusters/{clusterName}/topics", LOCAL)
+        .bodyValue(topicCreation)
+        .exchange()
+        .expectStatus()
+        .isOk();
+  }
+
+  @Test
+  void shouldReturn400IfTopicAlreadyExists() {
+    TopicCreation topicCreation = new TopicCreation()
+        .replicationFactor(1)
+        .partitions(3)
+        .name(UUID.randomUUID().toString());
+
+    webTestClient.post()
+        .uri("/api/clusters/{clusterName}/topics", LOCAL)
+        .bodyValue(topicCreation)
+        .exchange()
+        .expectStatus()
+        .isOk();
+
+    webTestClient.post()
+        .uri("/api/clusters/{clusterName}/topics", LOCAL)
+        .bodyValue(topicCreation)
+        .exchange()
+        .expectStatus()
+        .isBadRequest();
+  }
+}

+ 9 - 0
kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml

@@ -1417,6 +1417,8 @@ components:
           type: array
           type: array
           items:
           items:
             $ref: '#/components/schemas/BrokerDiskUsage'
             $ref: '#/components/schemas/BrokerDiskUsage'
+        version:
+          type: string
 
 
     BrokerDiskUsage:
     BrokerDiskUsage:
       type: object
       type: object
@@ -1484,6 +1486,13 @@ components:
           type: integer
           type: integer
         underReplicatedPartitions:
         underReplicatedPartitions:
           type: integer
           type: integer
+        cleanUpPolicy:
+          type: string
+          enum:
+            - DELETE
+            - COMPACT
+            - COMPACT_DELETE
+            - UNKNOWN
         partitions:
         partitions:
           type: array
           type: array
           items:
           items:

+ 67 - 36
kafka-ui-e2e-checks/pom.xml

@@ -15,7 +15,7 @@
         <junit.version>5.7.0</junit.version>
         <junit.version>5.7.0</junit.version>
         <aspectj.version>1.9.6</aspectj.version>
         <aspectj.version>1.9.6</aspectj.version>
         <allure.version>2.13.7</allure.version>
         <allure.version>2.13.7</allure.version>
-        <json-smart.version>1.1.1</json-smart.version>
+        <json-smart.version>1.3.2</json-smart.version>
         <testcontainers.version>1.15.2</testcontainers.version>
         <testcontainers.version>1.15.2</testcontainers.version>
         <selenide.version>5.16.2</selenide.version>
         <selenide.version>5.16.2</selenide.version>
         <assertj.version>3.17.1</assertj.version>
         <assertj.version>3.17.1</assertj.version>
@@ -155,40 +155,71 @@
             <scope>test</scope>
             <scope>test</scope>
         </dependency>
         </dependency>
     </dependencies>
     </dependencies>
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-surefire-plugin</artifactId>
-                <version>${maven.surefire-plugin.version}</version>
-                <configuration>
-                    <argLine>
-                        -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
-                    </argLine>
-                </configuration>
-                <dependencies>
-                    <dependency>
-                        <groupId>org.aspectj</groupId>
-                        <artifactId>aspectjweaver</artifactId>
-                        <version>${aspectj.version}</version>
-                    </dependency>
-                </dependencies>
 
 
-            </plugin>
-            <plugin>
-                <groupId>io.qameta.allure</groupId>
-                <artifactId>allure-maven</artifactId>
-                <version>${allure-maven.version}</version>
-            </plugin>
-
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <configuration>
-                    <source>${maven.compiler.source}</source>
-                    <target>${maven.compiler.target}</target>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
+    <profiles>
+        <profile>
+            <id>local</id>
+            <!-- Disabling e2e tests by default (for local dev envs) since complex setup is needed for UI tests -->
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-surefire-plugin</artifactId>
+                        <version>${maven.surefire-plugin.version}</version>
+                        <configuration>
+                            <skipTests>true</skipTests>
+                        </configuration>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-compiler-plugin</artifactId>
+                        <configuration>
+                            <source>${maven.compiler.source}</source>
+                            <target>${maven.compiler.target}</target>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+        <profile>
+            <id>prod</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-surefire-plugin</artifactId>
+                        <version>${maven.surefire-plugin.version}</version>
+                        <configuration>
+                            <argLine>
+                                -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
+                            </argLine>
+                        </configuration>
+                        <dependencies>
+                            <dependency>
+                                <groupId>org.aspectj</groupId>
+                                <artifactId>aspectjweaver</artifactId>
+                                <version>${aspectj.version}</version>
+                            </dependency>
+                        </dependencies>
+                    </plugin>
+                    <plugin>
+                        <groupId>io.qameta.allure</groupId>
+                        <artifactId>allure-maven</artifactId>
+                        <version>${allure-maven.version}</version>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-compiler-plugin</artifactId>
+                        <configuration>
+                            <source>${maven.compiler.source}</source>
+                            <target>${maven.compiler.target}</target>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
 </project>
 </project>

+ 4 - 0
kafka-ui-react-app/src/components/Brokers/Brokers.tsx

@@ -27,6 +27,7 @@ const Brokers: React.FC<Props> = ({
   diskUsage,
   diskUsage,
   fetchClusterStats,
   fetchClusterStats,
   fetchBrokers,
   fetchBrokers,
+  version,
 }) => {
 }) => {
   const { clusterName } = useParams<{ clusterName: ClusterName }>();
   const { clusterName } = useParams<{ clusterName: ClusterName }>();
 
 
@@ -56,6 +57,9 @@ const Brokers: React.FC<Props> = ({
             {zkOnline ? 'Online' : 'Offline'}
             {zkOnline ? 'Online' : 'Offline'}
           </span>
           </span>
         </Indicator>
         </Indicator>
+        <Indicator className="is-one-third" label="Version">
+          {version}
+        </Indicator>
       </MetricsWrapper>
       </MetricsWrapper>
       <MetricsWrapper title="Partitions">
       <MetricsWrapper title="Partitions">
         <Indicator label="Online">
         <Indicator label="Online">

+ 2 - 0
kafka-ui-react-app/src/components/Brokers/BrokersContainer.ts

@@ -12,6 +12,7 @@ import {
   getOutOfSyncReplicasCount,
   getOutOfSyncReplicasCount,
   getUnderReplicatedPartitionCount,
   getUnderReplicatedPartitionCount,
   getDiskUsage,
   getDiskUsage,
+  getVersion,
 } from 'redux/reducers/brokers/selectors';
 } from 'redux/reducers/brokers/selectors';
 import Brokers from 'components/Brokers/Brokers';
 import Brokers from 'components/Brokers/Brokers';
 
 
@@ -26,6 +27,7 @@ const mapStateToProps = (state: RootState) => ({
   outOfSyncReplicasCount: getOutOfSyncReplicasCount(state),
   outOfSyncReplicasCount: getOutOfSyncReplicasCount(state),
   underReplicatedPartitionCount: getUnderReplicatedPartitionCount(state),
   underReplicatedPartitionCount: getUnderReplicatedPartitionCount(state),
   diskUsage: getDiskUsage(state),
   diskUsage: getDiskUsage(state),
+  version: getVersion(state),
 });
 });
 
 
 const mapDispatchToProps = {
 const mapDispatchToProps = {

+ 2 - 0
kafka-ui-react-app/src/components/Brokers/__test__/Brokers.spec.tsx

@@ -26,6 +26,7 @@ describe('Brokers Component', () => {
           inSyncReplicasCount={0}
           inSyncReplicasCount={0}
           outOfSyncReplicasCount={0}
           outOfSyncReplicasCount={0}
           underReplicatedPartitionCount={0}
           underReplicatedPartitionCount={0}
+          version="1"
           fetchClusterStats={jest.fn()}
           fetchClusterStats={jest.fn()}
           fetchBrokers={jest.fn()}
           fetchBrokers={jest.fn()}
           diskUsage={undefined}
           diskUsage={undefined}
@@ -61,6 +62,7 @@ describe('Brokers Component', () => {
           inSyncReplicasCount={64}
           inSyncReplicasCount={64}
           outOfSyncReplicasCount={0}
           outOfSyncReplicasCount={0}
           underReplicatedPartitionCount={0}
           underReplicatedPartitionCount={0}
+          version="1"
           fetchClusterStats={jest.fn()}
           fetchClusterStats={jest.fn()}
           fetchBrokers={jest.fn()}
           fetchBrokers={jest.fn()}
           diskUsage={[
           diskUsage={[

+ 48 - 0
kafka-ui-react-app/src/components/Brokers/__test__/__snapshots__/Brokers.spec.tsx.snap

@@ -69,6 +69,7 @@ exports[`Brokers Component Brokers Empty matches Brokers Empty snapshot 1`] = `
       onlinePartitionCount={0}
       onlinePartitionCount={0}
       outOfSyncReplicasCount={0}
       outOfSyncReplicasCount={0}
       underReplicatedPartitionCount={0}
       underReplicatedPartitionCount={0}
+      version="1"
       zooKeeperStatus={0}
       zooKeeperStatus={0}
     >
     >
       <div
       <div
@@ -179,6 +180,29 @@ exports[`Brokers Component Brokers Empty matches Brokers Empty snapshot 1`] = `
                   </div>
                   </div>
                 </div>
                 </div>
               </Indicator>
               </Indicator>
+              <Indicator
+                className="is-one-third"
+                label="Version"
+              >
+                <div
+                  className="level-item is-one-third"
+                >
+                  <div
+                    title="Version"
+                  >
+                    <p
+                      className="heading"
+                    >
+                      Version
+                    </p>
+                    <p
+                      className="title has-text-centered"
+                    >
+                      1
+                    </p>
+                  </div>
+                </div>
+              </Indicator>
             </div>
             </div>
           </div>
           </div>
         </MetricsWrapper>
         </MetricsWrapper>
@@ -400,6 +424,7 @@ exports[`Brokers Component Brokers matches snapshot 1`] = `
       onlinePartitionCount={64}
       onlinePartitionCount={64}
       outOfSyncReplicasCount={0}
       outOfSyncReplicasCount={0}
       underReplicatedPartitionCount={0}
       underReplicatedPartitionCount={0}
+      version="1"
       zooKeeperStatus={1}
       zooKeeperStatus={1}
     >
     >
       <div
       <div
@@ -510,6 +535,29 @@ exports[`Brokers Component Brokers matches snapshot 1`] = `
                   </div>
                   </div>
                 </div>
                 </div>
               </Indicator>
               </Indicator>
+              <Indicator
+                className="is-one-third"
+                label="Version"
+              >
+                <div
+                  className="level-item is-one-third"
+                >
+                  <div
+                    title="Version"
+                  >
+                    <p
+                      className="heading"
+                    >
+                      Version
+                    </p>
+                    <p
+                      className="title has-text-centered"
+                    >
+                      1
+                    </p>
+                  </div>
+                </div>
+              </Indicator>
             </div>
             </div>
           </div>
           </div>
         </MetricsWrapper>
         </MetricsWrapper>

+ 8 - 3
kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx

@@ -16,7 +16,7 @@ import ListItem from './ListItem';
 
 
 export interface Props extends ConsumerGroup, ConsumerGroupDetails {
 export interface Props extends ConsumerGroup, ConsumerGroupDetails {
   clusterName: ClusterName;
   clusterName: ClusterName;
-  consumers?: ConsumerGroupTopicPartition[];
+  partitions?: ConsumerGroupTopicPartition[];
   isFetched: boolean;
   isFetched: boolean;
   isDeleted: boolean;
   isDeleted: boolean;
   fetchConsumerGroupDetails: (
   fetchConsumerGroupDetails: (
@@ -29,7 +29,7 @@ export interface Props extends ConsumerGroup, ConsumerGroupDetails {
 const Details: React.FC<Props> = ({
 const Details: React.FC<Props> = ({
   clusterName,
   clusterName,
   groupId,
   groupId,
-  consumers,
+  partitions,
   isFetched,
   isFetched,
   isDeleted,
   isDeleted,
   fetchConsumerGroupDetails,
   fetchConsumerGroupDetails,
@@ -38,7 +38,7 @@ const Details: React.FC<Props> = ({
   React.useEffect(() => {
   React.useEffect(() => {
     fetchConsumerGroupDetails(clusterName, groupId);
     fetchConsumerGroupDetails(clusterName, groupId);
   }, [fetchConsumerGroupDetails, clusterName, groupId]);
   }, [fetchConsumerGroupDetails, clusterName, groupId]);
-  const items = consumers || [];
+  const items = partitions || [];
   const [isConfirmationModelVisible, setIsConfirmationModelVisible] =
   const [isConfirmationModelVisible, setIsConfirmationModelVisible] =
     React.useState<boolean>(false);
     React.useState<boolean>(false);
   const history = useHistory();
   const history = useHistory();
@@ -96,6 +96,11 @@ const Details: React.FC<Props> = ({
               </tr>
               </tr>
             </thead>
             </thead>
             <tbody>
             <tbody>
+              {items.length === 0 && (
+                <tr>
+                  <td colSpan={10}>No active consumer groups</td>
+                </tr>
+              )}
               {items.map((consumer) => (
               {items.map((consumer) => (
                 <ListItem
                 <ListItem
                   key={consumer.consumerId}
                   key={consumer.consumerId}

+ 1 - 1
kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/Details.spec.tsx

@@ -20,7 +20,7 @@ describe('Details component', () => {
       isDeleted={false}
       isDeleted={false}
       fetchConsumerGroupDetails={jest.fn()}
       fetchConsumerGroupDetails={jest.fn()}
       deleteConsumerGroup={jest.fn()}
       deleteConsumerGroup={jest.fn()}
-      consumers={[
+      partitions={[
         {
         {
           consumerId:
           consumerId:
             'consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0',
             'consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0',

+ 8 - 0
kafka-ui-react-app/src/components/ConsumerGroups/Details/__tests__/DetailsContainer.spec.tsx

@@ -0,0 +1,8 @@
+import React from 'react';
+import { containerRendersView } from 'lib/testHelpers';
+import Details from 'components/ConsumerGroups/Details/Details';
+import DetailsContainer from 'components/ConsumerGroups/Details/DetailsContainer';
+
+describe('DetailsContainer', () => {
+  containerRendersView(<DetailsContainer />, Details);
+});

+ 43 - 42
kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx

@@ -22,51 +22,52 @@ const List: React.FC<Props> = ({ consumerGroups }) => {
       <Breadcrumb>All Consumer Groups</Breadcrumb>
       <Breadcrumb>All Consumer Groups</Breadcrumb>
 
 
       <div className="box">
       <div className="box">
-        {consumerGroups.length > 0 ? (
-          <div>
-            <div className="columns">
-              <div className="column is-half is-offset-half">
-                <input
-                  id="searchText"
-                  type="text"
-                  name="searchText"
-                  className="input"
-                  placeholder="Search"
-                  value={searchText}
-                  onChange={handleInputChange}
-                />
-              </div>
+        <div>
+          <div className="columns">
+            <div className="column is-half is-offset-half">
+              <input
+                id="searchText"
+                type="text"
+                name="searchText"
+                className="input"
+                placeholder="Search"
+                value={searchText}
+                onChange={handleInputChange}
+              />
             </div>
             </div>
-            <table className="table is-striped is-fullwidth is-hoverable">
-              <thead>
+          </div>
+          <table className="table is-striped is-fullwidth is-hoverable">
+            <thead>
+              <tr>
+                <th>Consumer group ID</th>
+                <th>Num of members</th>
+                <th>Num of topics</th>
+                <th>Messages behind</th>
+                <th>Coordinator</th>
+                <th>State</th>
+              </tr>
+            </thead>
+            <tbody>
+              {consumerGroups
+                .filter(
+                  (consumerGroup) =>
+                    !searchText ||
+                    consumerGroup?.groupId?.indexOf(searchText) >= 0
+                )
+                .map((consumerGroup) => (
+                  <ListItem
+                    key={consumerGroup.groupId}
+                    consumerGroup={consumerGroup}
+                  />
+                ))}
+              {consumerGroups.length === 0 && (
                 <tr>
                 <tr>
-                  <th>Consumer group ID</th>
-                  <th>Num of members</th>
-                  <th>Num of topics</th>
-                  <th>Messages behind</th>
-                  <th>Coordinator</th>
-                  <th>State</th>
+                  <td colSpan={10}>No active consumer groups</td>
                 </tr>
                 </tr>
-              </thead>
-              <tbody>
-                {consumerGroups
-                  .filter(
-                    (consumerGroup) =>
-                      !searchText ||
-                      consumerGroup?.groupId?.indexOf(searchText) >= 0
-                  )
-                  .map((consumerGroup) => (
-                    <ListItem
-                      key={consumerGroup.groupId}
-                      consumerGroup={consumerGroup}
-                    />
-                  ))}
-              </tbody>
-            </table>
-          </div>
-        ) : (
-          'No active consumer groups'
-        )}
+              )}
+            </tbody>
+          </table>
+        </div>
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 46 - 0
kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/List.spec.tsx

@@ -0,0 +1,46 @@
+import React from 'react';
+import { shallow, mount } from 'enzyme';
+import List from 'components/ConsumerGroups/List/List';
+
+describe('List', () => {
+  const mockConsumerGroups = [
+    {
+      groupId: 'groupId',
+      members: 0,
+      topics: 1,
+      simple: false,
+      partitionAssignor: '',
+      coordinator: {
+        id: 1,
+        host: 'host',
+      },
+      partitions: [
+        {
+          consumerId: null,
+          currentOffset: 0,
+          endOffset: 0,
+          host: null,
+          messagesBehind: 0,
+          partition: 1,
+          topic: 'topic',
+        },
+      ],
+    },
+  ];
+  const component = shallow(
+    <List consumerGroups={mockConsumerGroups} clusterName="cluster" />
+  );
+  const componentEmpty = mount(
+    <List consumerGroups={[]} clusterName="cluster" />
+  );
+
+  it('render empty List consumer Groups', () => {
+    expect(componentEmpty.find('td').text()).toEqual(
+      'No active consumer groups'
+    );
+  });
+
+  it('render List consumer Groups', () => {
+    expect(component.exists('.section')).toBeTruthy();
+  });
+});

+ 8 - 0
kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListContainer.spec.tsx

@@ -0,0 +1,8 @@
+import React from 'react';
+import { containerRendersView } from 'lib/testHelpers';
+import ListContainer from 'components/ConsumerGroups/List/ListContainer';
+import List from 'components/ConsumerGroups/List/List';
+
+describe('ListContainer', () => {
+  containerRendersView(<ListContainer />, List);
+});

+ 33 - 0
kafka-ui-react-app/src/components/ConsumerGroups/List/__test__/ListItem.spec.tsx

@@ -0,0 +1,33 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import ListItem from 'components/ConsumerGroups/List/ListItem';
+
+describe('List', () => {
+  const mockConsumerGroup = {
+    groupId: 'groupId',
+    members: 0,
+    topics: 1,
+    simple: false,
+    partitionAssignor: '',
+    coordinator: {
+      id: 1,
+      host: 'host',
+    },
+    partitions: [
+      {
+        consumerId: null,
+        currentOffset: 0,
+        endOffset: 0,
+        host: null,
+        messagesBehind: 0,
+        partition: 1,
+        topic: 'topic',
+      },
+    ],
+  };
+  const component = shallow(<ListItem consumerGroup={mockConsumerGroup} />);
+
+  it('render empty ListItem', () => {
+    expect(component.exists('.is-clickable')).toBeTruthy();
+  });
+});

+ 5 - 0
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/ClusterWidget.tsx

@@ -18,6 +18,7 @@ const ClusterWidget: React.FC<ClusterWidgetProps> = ({
     bytesOutPerSec,
     bytesOutPerSec,
     onlinePartitionCount,
     onlinePartitionCount,
     readOnly,
     readOnly,
+    version,
   },
   },
 }) => (
 }) => (
   <div className="column is-full-modile is-6">
   <div className="column is-full-modile is-6">
@@ -38,6 +39,10 @@ const ClusterWidget: React.FC<ClusterWidgetProps> = ({
 
 
       <table className="table is-fullwidth">
       <table className="table is-fullwidth">
         <tbody>
         <tbody>
+          <tr>
+            <th>Version</th>
+            <td>{version}</td>
+          </tr>
           <tr>
           <tr>
             <th>Brokers</th>
             <th>Brokers</th>
             <td>
             <td>

+ 12 - 0
kafka-ui-react-app/src/components/Dashboard/ClustersWidget/__test__/__snapshots__/ClusterWidget.spec.tsx.snap

@@ -21,6 +21,12 @@ exports[`ClusterWidget when cluster is offline matches snapshot 1`] = `
       className="table is-fullwidth"
       className="table is-fullwidth"
     >
     >
       <tbody>
       <tbody>
+        <tr>
+          <th>
+            Version
+          </th>
+          <td />
+        </tr>
         <tr>
         <tr>
           <th>
           <th>
             Brokers
             Brokers
@@ -100,6 +106,12 @@ exports[`ClusterWidget when cluster is online matches snapshot 1`] = `
       className="table is-fullwidth"
       className="table is-fullwidth"
     >
     >
       <tbody>
       <tbody>
+        <tr>
+          <th>
+            Version
+          </th>
+          <td />
+        </tr>
         <tr>
         <tr>
           <th>
           <th>
             Brokers
             Brokers

+ 5 - 0
kafka-ui-react-app/src/components/Schemas/Details/Details.tsx

@@ -124,6 +124,11 @@ const Details: React.FC<DetailsProps> = ({
                 {versions.map((version) => (
                 {versions.map((version) => (
                   <SchemaVersion key={version.id} version={version} />
                   <SchemaVersion key={version.id} version={version} />
                 ))}
                 ))}
+                {versions.length === 0 && (
+                  <tr>
+                    <td colSpan={10}>No active Schema</td>
+                  </tr>
+                )}
               </tbody>
               </tbody>
             </table>
             </table>
           </div>
           </div>

+ 7 - 0
kafka-ui-react-app/src/components/Schemas/Details/__test__/Details.spec.tsx

@@ -49,6 +49,13 @@ describe('Details', () => {
         {...props}
         {...props}
       />
       />
     );
     );
+    describe('empty table', () => {
+      it('render empty table', () => {
+        const component = shallow(setupWrapper());
+        expect(component.find('td').text()).toEqual('No active Schema');
+      });
+    });
+
     describe('Initial state', () => {
     describe('Initial state', () => {
       it('should call fetchSchemaVersions every render', () => {
       it('should call fetchSchemaVersions every render', () => {
         mount(
         mount(

+ 18 - 2
kafka-ui-react-app/src/components/Schemas/Details/__test__/__snapshots__/Details.spec.tsx.snap

@@ -110,7 +110,15 @@ exports[`Details View Initial state matches snapshot 1`] = `
           </th>
           </th>
         </tr>
         </tr>
       </thead>
       </thead>
-      <tbody />
+      <tbody>
+        <tr>
+          <td
+            colSpan={10}
+          >
+            No active Schema
+          </td>
+        </tr>
+      </tbody>
     </table>
     </table>
   </div>
   </div>
 </div>
 </div>
@@ -393,7 +401,15 @@ exports[`Details View when page with schema versions loaded when versions are em
           </th>
           </th>
         </tr>
         </tr>
       </thead>
       </thead>
-      <tbody />
+      <tbody>
+        <tr>
+          <td
+            colSpan={10}
+          >
+            No active Schema
+          </td>
+        </tr>
+      </tbody>
     </table>
     </table>
   </div>
   </div>
 </div>
 </div>

+ 35 - 31
kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/TopicConsumerGroups.tsx

@@ -3,6 +3,7 @@ import { Topic, TopicDetails, ConsumerGroup } from 'generated-sources';
 import { ClusterName, TopicName } from 'redux/interfaces';
 import { ClusterName, TopicName } from 'redux/interfaces';
 import ConsumerGroupStateTag from 'components/common/ConsumerGroupState/ConsumerGroupStateTag';
 import ConsumerGroupStateTag from 'components/common/ConsumerGroupState/ConsumerGroupStateTag';
 import { useHistory } from 'react-router';
 import { useHistory } from 'react-router';
+import { clusterConsumerGroupsPath } from 'lib/paths';
 
 
 interface Props extends Topic, TopicDetails {
 interface Props extends Topic, TopicDetails {
   clusterName: ClusterName;
   clusterName: ClusterName;
@@ -26,43 +27,46 @@ const TopicConsumerGroups: React.FC<Props> = ({
 
 
   const history = useHistory();
   const history = useHistory();
   function goToConsumerGroupDetails(consumer: ConsumerGroup) {
   function goToConsumerGroupDetails(consumer: ConsumerGroup) {
-    history.push(`consumer-groups/${consumer.groupId}`);
+    history.push(
+      `${clusterConsumerGroupsPath(clusterName)}/${consumer.groupId}`
+    );
   }
   }
 
 
   return (
   return (
     <div className="box">
     <div className="box">
-      {consumerGroups.length > 0 ? (
-        <table className="table is-striped is-fullwidth">
-          <thead>
+      <table className="table is-striped is-fullwidth">
+        <thead>
+          <tr>
+            <th>Consumer group ID</th>
+            <th>Num of members</th>
+            <th>Messages behind</th>
+            <th>Coordinator</th>
+            <th>State</th>
+          </tr>
+        </thead>
+        <tbody>
+          {consumerGroups.map((consumer) => (
+            <tr
+              key={consumer.groupId}
+              className="is-clickable"
+              onClick={() => goToConsumerGroupDetails(consumer)}
+            >
+              <td>{consumer.groupId}</td>
+              <td>{consumer.members}</td>
+              <td>{consumer.messagesBehind}</td>
+              <td>{consumer.coordinator?.id}</td>
+              <td>
+                <ConsumerGroupStateTag state={consumer.state} />
+              </td>
+            </tr>
+          ))}
+          {consumerGroups.length === 0 && (
             <tr>
             <tr>
-              <th>Consumer group ID</th>
-              <th>Num of members</th>
-              <th>Messages behind</th>
-              <th>Coordinator</th>
-              <th>State</th>
+              <td colSpan={10}>No active consumer groups</td>
             </tr>
             </tr>
-          </thead>
-          <tbody>
-            {consumerGroups.map((consumer) => (
-              <tr
-                key={consumer.groupId}
-                className="is-clickable"
-                onClick={() => goToConsumerGroupDetails(consumer)}
-              >
-                <td>{consumer.groupId}</td>
-                <td>{consumer.members}</td>
-                <td>{consumer.messagesBehind}</td>
-                <td>{consumer.coordinator?.id}</td>
-                <td>
-                  <ConsumerGroupStateTag state={consumer.state} />
-                </td>
-              </tr>
-            ))}
-          </tbody>
-        </table>
-      ) : (
-        'No active consumer groups'
-      )}
+          )}
+        </tbody>
+      </table>
     </div>
     </div>
   );
   );
 };
 };

+ 2 - 4
kafka-ui-react-app/src/components/Topics/Topic/Details/ConsumerGroups/__test__/TopicConsumerGroups.spec.tsx

@@ -40,8 +40,7 @@ describe('Details', () => {
         topicName={mockTopicName}
         topicName={mockTopicName}
       />
       />
     );
     );
-
-    expect(component.exists('.table')).toBeFalsy();
+    expect(component.find('td').text()).toEqual('No active consumer groups');
   });
   });
 
 
   it('render ConsumerGroups in Topic', () => {
   it('render ConsumerGroups in Topic', () => {
@@ -54,7 +53,6 @@ describe('Details', () => {
         topicName={mockTopicName}
         topicName={mockTopicName}
       />
       />
     );
     );
-
-    expect(component.exists('.table')).toBeTruthy();
+    expect(component.exists('tbody')).toBeTruthy();
   });
   });
 });
 });

+ 42 - 43
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/MessagesTable.tsx

@@ -9,51 +9,50 @@ export interface MessagesTableProp {
   onNext(event: React.MouseEvent<HTMLButtonElement>): void;
   onNext(event: React.MouseEvent<HTMLButtonElement>): void;
 }
 }
 
 
-const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => {
-  if (!messages.length) {
-    return <div>No messages at selected topic</div>;
-  }
-
-  return (
-    <>
-      <table className="table is-fullwidth is-narrow">
-        <thead>
+const MessagesTable: React.FC<MessagesTableProp> = ({ messages, onNext }) => (
+  <>
+    <table className="table is-fullwidth is-narrow">
+      <thead>
+        <tr>
+          <th>Timestamp</th>
+          <th>Key</th>
+          <th>Offset</th>
+          <th>Partition</th>
+          <th>Content</th>
+          <th> </th>
+        </tr>
+      </thead>
+      <tbody>
+        {messages.map(
+          ({ partition, offset, timestamp, content, key }: TopicMessage) => (
+            <MessageItem
+              key={`message-${timestamp.getTime()}-${offset}`}
+              partition={partition}
+              offset={offset}
+              timestamp={timestamp}
+              content={content}
+              messageKey={key}
+            />
+          )
+        )}
+        {messages.length === 0 && (
           <tr>
           <tr>
-            <th>Timestamp</th>
-            <th>Key</th>
-            <th>Offset</th>
-            <th>Partition</th>
-            <th>Content</th>
-            <th> </th>
+            <td colSpan={10}>No messages at selected topic</td>
           </tr>
           </tr>
-        </thead>
-        <tbody>
-          {messages.map(
-            ({ partition, offset, timestamp, content, key }: TopicMessage) => (
-              <MessageItem
-                key={`message-${timestamp.getTime()}-${offset}`}
-                partition={partition}
-                offset={offset}
-                timestamp={timestamp}
-                content={content}
-                messageKey={key}
-              />
-            )
-          )}
-        </tbody>
-      </table>
-      <div className="columns">
-        <div className="column is-full">
-          <CustomParamButton
-            className="is-link is-pulled-right"
-            type="fa-chevron-right"
-            onClick={onNext}
-            btnText="Next"
-          />
-        </div>
+        )}
+      </tbody>
+    </table>
+    <div className="columns">
+      <div className="column is-full">
+        <CustomParamButton
+          className="is-link is-pulled-right"
+          type="fa-chevron-right"
+          onClick={onNext}
+          btnText="Next"
+        />
       </div>
       </div>
-    </>
-  );
-};
+    </div>
+  </>
+);
 
 
 export default MessagesTable;
 export default MessagesTable;

+ 3 - 3
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx

@@ -18,9 +18,9 @@ describe('MessagesTable', () => {
   describe('when topic is empty', () => {
   describe('when topic is empty', () => {
     it('renders table row with JSONEditor', () => {
     it('renders table row with JSONEditor', () => {
       const wrapper = shallow(setupWrapper());
       const wrapper = shallow(setupWrapper());
-      expect(wrapper.exists('table')).toBeFalsy();
-      expect(wrapper.exists('CustomParamButton')).toBeFalsy();
-      expect(wrapper.text()).toEqual('No messages at selected topic');
+      expect(wrapper.find('td').text()).toEqual(
+        'No messages at selected topic'
+      );
     });
     });
 
 
     it('matches snapshot', () => {
     it('matches snapshot', () => {

+ 51 - 3
kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/__snapshots__/MessagesTable.spec.tsx.snap

@@ -63,7 +63,55 @@ exports[`MessagesTable when topic contains messages matches snapshot 1`] = `
 `;
 `;
 
 
 exports[`MessagesTable when topic is empty matches snapshot 1`] = `
 exports[`MessagesTable when topic is empty matches snapshot 1`] = `
-<div>
-  No messages at selected topic
-</div>
+<Fragment>
+  <table
+    className="table is-fullwidth is-narrow"
+  >
+    <thead>
+      <tr>
+        <th>
+          Timestamp
+        </th>
+        <th>
+          Key
+        </th>
+        <th>
+          Offset
+        </th>
+        <th>
+          Partition
+        </th>
+        <th>
+          Content
+        </th>
+        <th>
+           
+        </th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr>
+        <td
+          colSpan={10}
+        >
+          No messages at selected topic
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <div
+    className="columns"
+  >
+    <div
+      className="column is-full"
+    >
+      <CustomParamButton
+        btnText="Next"
+        className="is-link is-pulled-right"
+        onClick={[MockFunction]}
+        type="fa-chevron-right"
+      />
+    </div>
+  </div>
+</Fragment>
 `;
 `;

+ 5 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/Overview.tsx

@@ -102,6 +102,11 @@ const Overview: React.FC<Props> = ({
                 </td>
                 </td>
               </tr>
               </tr>
             ))}
             ))}
+            {partitions?.length === 0 && (
+              <tr>
+                <td colSpan={10}>No Partitions found</td>
+              </tr>
+            )}
           </tbody>
           </tbody>
         </table>
         </table>
       </div>
       </div>

+ 15 - 0
kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx

@@ -38,5 +38,20 @@ describe('Overview', () => {
 
 
       expect(component.exists('Dropdown')).toBeTruthy();
       expect(component.exists('Dropdown')).toBeTruthy();
     });
     });
+
+    it('does not render Partitions', () => {
+      const componentEmpty = shallow(
+        <Overview
+          name={mockTopicName}
+          partitions={[]}
+          internal={mockInternal}
+          clusterName={mockClusterName}
+          topicName={mockTopicName}
+          clearTopicMessages={mockClearTopicMessages}
+        />
+      );
+
+      expect(componentEmpty.find('td').text()).toEqual('No Partitions found');
+    });
   });
   });
 });
 });

+ 1 - 3
kafka-ui-react-app/src/redux/actions/thunks/topics.ts

@@ -332,9 +332,7 @@ export const fetchTopicConsumerGroups =
           ...state.byName,
           ...state.byName,
           [topicName]: {
           [topicName]: {
             ...state.byName[topicName],
             ...state.byName[topicName],
-            consumerGroups: {
-              ...consumerGroups,
-            },
+            consumerGroups,
           },
           },
         },
         },
       };
       };

+ 5 - 0
kafka-ui-react-app/src/redux/reducers/brokers/selectors.ts

@@ -48,3 +48,8 @@ export const getDiskUsage = createSelector(
   brokersState,
   brokersState,
   ({ diskUsage }) => diskUsage
   ({ diskUsage }) => diskUsage
 );
 );
+
+export const getVersion = createSelector(
+  brokersState,
+  ({ version }) => version
+);