Merge branch 'master' into checks/626-update-delete-topics
This commit is contained in:
commit
d47c7b1649
42 changed files with 742 additions and 191 deletions
4
.github/workflows/pr-checks.yaml
vendored
4
.github/workflows/pr-checks.yaml
vendored
|
@ -10,6 +10,6 @@ jobs:
|
|||
- uses: kentaro-m/task-completed-checker-action@v0.1.0
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- uses: derkinderfietsen/pr-description-enforcer@v1
|
||||
- uses: dekinderfiets/pr-description-enforcer@v0.0.1
|
||||
with:
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
|
|
@ -109,6 +109,7 @@ To read more please follow to [chart documentation](charts/kafka-ui/README.md)
|
|||
# Guides
|
||||
|
||||
- [SSO configuration](guides/SSO.md)
|
||||
- [AWS IAM configuration](guides/AWS_IAM.md)
|
||||
|
||||
## Connecting to a Secure Broker
|
||||
|
||||
|
|
5
docker/jaas/schema_registry.jaas
Normal file
5
docker/jaas/schema_registry.jaas
Normal file
|
@ -0,0 +1,5 @@
|
|||
SchemaRegistryProps {
|
||||
org.eclipse.jetty.jaas.spi.PropertyFileLoginModule required
|
||||
file="/conf/schema_registry.password"
|
||||
debug="false";
|
||||
};
|
1
docker/jaas/schema_registry.password
Normal file
1
docker/jaas/schema_registry.password
Normal file
|
@ -0,0 +1 @@
|
|||
admin: OBF:1w8t1tvf1w261w8v1w1c1tvn1w8x,admin
|
66
docker/kafka-cluster-sr-auth.yaml
Normal file
66
docker/kafka-cluster-sr-auth.yaml
Normal file
|
@ -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'"
|
|
@ -26,7 +26,7 @@ services:
|
|||
KAFKA_CLUSTERS_1_BOOTSTRAPSERVERS: kafka1:29092
|
||||
KAFKA_CLUSTERS_1_ZOOKEEPER: zookeeper1:2181
|
||||
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_ADDRESS: http://kafka-connect0:8083
|
||||
|
||||
|
|
1
docker/message.json
Normal file
1
docker/message.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
41
guides/AWS_IAM.md
Normal file
41
guides/AWS_IAM.md
Normal file
|
@ -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>";
|
||||
```
|
|
@ -97,6 +97,12 @@
|
|||
<version>${confluent.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>software.amazon.msk</groupId>
|
||||
<artifactId>aws-msk-iam-auth</artifactId>
|
||||
<version>1.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.avro</groupId>
|
||||
<artifactId>avro</artifactId>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -27,4 +27,5 @@ public class InternalClusterMetrics {
|
|||
private final Map<Integer, InternalBrokerMetrics> internalBrokerMetrics;
|
||||
private final List<Metric> metrics;
|
||||
private final int zooKeeperStatus;
|
||||
private final String version;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ public class InternalTopic {
|
|||
private final Map<Integer, InternalPartition> partitions;
|
||||
private final List<InternalTopicConfig> topicConfigs;
|
||||
|
||||
private final CleanupPolicy cleanUpPolicy;
|
||||
private final int replicas;
|
||||
private final int partitionCount;
|
||||
private final int inSyncReplicas;
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
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.model.CleanupPolicy;
|
||||
import com.provectus.kafka.ui.model.CreateTopicMessage;
|
||||
import com.provectus.kafka.ui.model.ExtendedAdminClient;
|
||||
import com.provectus.kafka.ui.model.InternalBrokerDiskUsage;
|
||||
|
@ -159,6 +161,7 @@ public class KafkaService {
|
|||
.onlinePartitionCount(topicsMetrics.getOnlinePartitionCount())
|
||||
.offlinePartitionCount(topicsMetrics.getOfflinePartitionCount())
|
||||
.zooKeeperStatus(ClusterUtil.convertToIntServerStatus(zookeeperStatus))
|
||||
.version(version)
|
||||
.build();
|
||||
|
||||
return currentCluster.toBuilder()
|
||||
|
@ -205,12 +208,18 @@ public class KafkaService {
|
|||
|
||||
private Map<String, InternalTopic> mergeWithConfigs(
|
||||
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
|
||||
|
@ -223,11 +232,12 @@ public class KafkaService {
|
|||
final Mono<Map<String, List<InternalTopicConfig>>> configsMono =
|
||||
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());
|
||||
newTopic.configs(topicData.getConfigs());
|
||||
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")))
|
||||
.flatMap(t ->
|
||||
loadTopicsConfig(adminClient, Collections.singletonList(t.getName()))
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1417,6 +1417,8 @@ components:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BrokerDiskUsage'
|
||||
version:
|
||||
type: string
|
||||
|
||||
BrokerDiskUsage:
|
||||
type: object
|
||||
|
@ -1484,6 +1486,13 @@ components:
|
|||
type: integer
|
||||
underReplicatedPartitions:
|
||||
type: integer
|
||||
cleanUpPolicy:
|
||||
type: string
|
||||
enum:
|
||||
- DELETE
|
||||
- COMPACT
|
||||
- COMPACT_DELETE
|
||||
- UNKNOWN
|
||||
partitions:
|
||||
type: array
|
||||
items:
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<junit.version>5.7.0</junit.version>
|
||||
<aspectj.version>1.9.6</aspectj.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>
|
||||
<selenide.version>5.16.2</selenide.version>
|
||||
<assertj.version>3.17.1</assertj.version>
|
||||
|
@ -155,40 +155,71 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
</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>
|
||||
|
|
|
@ -27,6 +27,7 @@ const Brokers: React.FC<Props> = ({
|
|||
diskUsage,
|
||||
fetchClusterStats,
|
||||
fetchBrokers,
|
||||
version,
|
||||
}) => {
|
||||
const { clusterName } = useParams<{ clusterName: ClusterName }>();
|
||||
|
||||
|
@ -56,6 +57,9 @@ const Brokers: React.FC<Props> = ({
|
|||
{zkOnline ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</Indicator>
|
||||
<Indicator className="is-one-third" label="Version">
|
||||
{version}
|
||||
</Indicator>
|
||||
</MetricsWrapper>
|
||||
<MetricsWrapper title="Partitions">
|
||||
<Indicator label="Online">
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
getOutOfSyncReplicasCount,
|
||||
getUnderReplicatedPartitionCount,
|
||||
getDiskUsage,
|
||||
getVersion,
|
||||
} from 'redux/reducers/brokers/selectors';
|
||||
import Brokers from 'components/Brokers/Brokers';
|
||||
|
||||
|
@ -26,6 +27,7 @@ const mapStateToProps = (state: RootState) => ({
|
|||
outOfSyncReplicasCount: getOutOfSyncReplicasCount(state),
|
||||
underReplicatedPartitionCount: getUnderReplicatedPartitionCount(state),
|
||||
diskUsage: getDiskUsage(state),
|
||||
version: getVersion(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
|
|
|
@ -26,6 +26,7 @@ describe('Brokers Component', () => {
|
|||
inSyncReplicasCount={0}
|
||||
outOfSyncReplicasCount={0}
|
||||
underReplicatedPartitionCount={0}
|
||||
version="1"
|
||||
fetchClusterStats={jest.fn()}
|
||||
fetchBrokers={jest.fn()}
|
||||
diskUsage={undefined}
|
||||
|
@ -61,6 +62,7 @@ describe('Brokers Component', () => {
|
|||
inSyncReplicasCount={64}
|
||||
outOfSyncReplicasCount={0}
|
||||
underReplicatedPartitionCount={0}
|
||||
version="1"
|
||||
fetchClusterStats={jest.fn()}
|
||||
fetchBrokers={jest.fn()}
|
||||
diskUsage={[
|
||||
|
|
|
@ -69,6 +69,7 @@ exports[`Brokers Component Brokers Empty matches Brokers Empty snapshot 1`] = `
|
|||
onlinePartitionCount={0}
|
||||
outOfSyncReplicasCount={0}
|
||||
underReplicatedPartitionCount={0}
|
||||
version="1"
|
||||
zooKeeperStatus={0}
|
||||
>
|
||||
<div
|
||||
|
@ -179,6 +180,29 @@ exports[`Brokers Component Brokers Empty matches Brokers Empty snapshot 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</MetricsWrapper>
|
||||
|
@ -400,6 +424,7 @@ exports[`Brokers Component Brokers matches snapshot 1`] = `
|
|||
onlinePartitionCount={64}
|
||||
outOfSyncReplicasCount={0}
|
||||
underReplicatedPartitionCount={0}
|
||||
version="1"
|
||||
zooKeeperStatus={1}
|
||||
>
|
||||
<div
|
||||
|
@ -510,6 +535,29 @@ exports[`Brokers Component Brokers matches snapshot 1`] = `
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</MetricsWrapper>
|
||||
|
|
|
@ -16,7 +16,7 @@ import ListItem from './ListItem';
|
|||
|
||||
export interface Props extends ConsumerGroup, ConsumerGroupDetails {
|
||||
clusterName: ClusterName;
|
||||
consumers?: ConsumerGroupTopicPartition[];
|
||||
partitions?: ConsumerGroupTopicPartition[];
|
||||
isFetched: boolean;
|
||||
isDeleted: boolean;
|
||||
fetchConsumerGroupDetails: (
|
||||
|
@ -29,7 +29,7 @@ export interface Props extends ConsumerGroup, ConsumerGroupDetails {
|
|||
const Details: React.FC<Props> = ({
|
||||
clusterName,
|
||||
groupId,
|
||||
consumers,
|
||||
partitions,
|
||||
isFetched,
|
||||
isDeleted,
|
||||
fetchConsumerGroupDetails,
|
||||
|
@ -38,7 +38,7 @@ const Details: React.FC<Props> = ({
|
|||
React.useEffect(() => {
|
||||
fetchConsumerGroupDetails(clusterName, groupId);
|
||||
}, [fetchConsumerGroupDetails, clusterName, groupId]);
|
||||
const items = consumers || [];
|
||||
const items = partitions || [];
|
||||
const [isConfirmationModelVisible, setIsConfirmationModelVisible] =
|
||||
React.useState<boolean>(false);
|
||||
const history = useHistory();
|
||||
|
@ -96,6 +96,11 @@ const Details: React.FC<Props> = ({
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No active consumer groups</td>
|
||||
</tr>
|
||||
)}
|
||||
{items.map((consumer) => (
|
||||
<ListItem
|
||||
key={consumer.consumerId}
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('Details component', () => {
|
|||
isDeleted={false}
|
||||
fetchConsumerGroupDetails={jest.fn()}
|
||||
deleteConsumerGroup={jest.fn()}
|
||||
consumers={[
|
||||
partitions={[
|
||||
{
|
||||
consumerId:
|
||||
'consumer-messages-consumer-1-122fbf98-643b-491d-8aec-c0641d2513d0',
|
||||
|
|
|
@ -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);
|
||||
});
|
|
@ -22,51 +22,52 @@ const List: React.FC<Props> = ({ consumerGroups }) => {
|
|||
<Breadcrumb>All Consumer Groups</Breadcrumb>
|
||||
|
||||
<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>
|
||||
<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}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
'No active consumer groups'
|
||||
)}
|
||||
<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>
|
||||
<td colSpan={10}>No active consumer groups</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -18,6 +18,7 @@ const ClusterWidget: React.FC<ClusterWidgetProps> = ({
|
|||
bytesOutPerSec,
|
||||
onlinePartitionCount,
|
||||
readOnly,
|
||||
version,
|
||||
},
|
||||
}) => (
|
||||
<div className="column is-full-modile is-6">
|
||||
|
@ -38,6 +39,10 @@ const ClusterWidget: React.FC<ClusterWidgetProps> = ({
|
|||
|
||||
<table className="table is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<td>{version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Brokers</th>
|
||||
<td>
|
||||
|
|
|
@ -21,6 +21,12 @@ exports[`ClusterWidget when cluster is offline matches snapshot 1`] = `
|
|||
className="table is-fullwidth"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Version
|
||||
</th>
|
||||
<td />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Brokers
|
||||
|
@ -100,6 +106,12 @@ exports[`ClusterWidget when cluster is online matches snapshot 1`] = `
|
|||
className="table is-fullwidth"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
Version
|
||||
</th>
|
||||
<td />
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
Brokers
|
||||
|
|
|
@ -124,6 +124,11 @@ const Details: React.FC<DetailsProps> = ({
|
|||
{versions.map((version) => (
|
||||
<SchemaVersion key={version.id} version={version} />
|
||||
))}
|
||||
{versions.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No active Schema</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -49,6 +49,13 @@ describe('Details', () => {
|
|||
{...props}
|
||||
/>
|
||||
);
|
||||
describe('empty table', () => {
|
||||
it('render empty table', () => {
|
||||
const component = shallow(setupWrapper());
|
||||
expect(component.find('td').text()).toEqual('No active Schema');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Initial state', () => {
|
||||
it('should call fetchSchemaVersions every render', () => {
|
||||
mount(
|
||||
|
|
|
@ -110,7 +110,15 @@ exports[`Details View Initial state matches snapshot 1`] = `
|
|||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody />
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={10}
|
||||
>
|
||||
No active Schema
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -393,7 +401,15 @@ exports[`Details View when page with schema versions loaded when versions are em
|
|||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody />
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
colSpan={10}
|
||||
>
|
||||
No active Schema
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Topic, TopicDetails, ConsumerGroup } from 'generated-sources';
|
|||
import { ClusterName, TopicName } from 'redux/interfaces';
|
||||
import ConsumerGroupStateTag from 'components/common/ConsumerGroupState/ConsumerGroupStateTag';
|
||||
import { useHistory } from 'react-router';
|
||||
import { clusterConsumerGroupsPath } from 'lib/paths';
|
||||
|
||||
interface Props extends Topic, TopicDetails {
|
||||
clusterName: ClusterName;
|
||||
|
@ -26,43 +27,46 @@ const TopicConsumerGroups: React.FC<Props> = ({
|
|||
|
||||
const history = useHistory();
|
||||
function goToConsumerGroupDetails(consumer: ConsumerGroup) {
|
||||
history.push(`consumer-groups/${consumer.groupId}`);
|
||||
history.push(
|
||||
`${clusterConsumerGroupsPath(clusterName)}/${consumer.groupId}`
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="box">
|
||||
{consumerGroups.length > 0 ? (
|
||||
<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>
|
||||
<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>
|
||||
</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'
|
||||
)}
|
||||
))}
|
||||
{consumerGroups.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No active consumer groups</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -40,8 +40,7 @@ describe('Details', () => {
|
|||
topicName={mockTopicName}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.exists('.table')).toBeFalsy();
|
||||
expect(component.find('td').text()).toEqual('No active consumer groups');
|
||||
});
|
||||
|
||||
it('render ConsumerGroups in Topic', () => {
|
||||
|
@ -54,7 +53,6 @@ describe('Details', () => {
|
|||
topicName={mockTopicName}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.exists('.table')).toBeTruthy();
|
||||
expect(component.exists('tbody')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,51 +9,50 @@ export interface MessagesTableProp {
|
|||
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>
|
||||
<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>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
||||
export default MessagesTable;
|
||||
|
|
|
@ -18,9 +18,9 @@ describe('MessagesTable', () => {
|
|||
describe('when topic is empty', () => {
|
||||
it('renders table row with JSONEditor', () => {
|
||||
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', () => {
|
||||
|
|
|
@ -63,7 +63,55 @@ exports[`MessagesTable when topic contains messages 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>
|
||||
`;
|
||||
|
|
|
@ -102,6 +102,11 @@ const Overview: React.FC<Props> = ({
|
|||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{partitions?.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>No Partitions found</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -38,5 +38,20 @@ describe('Overview', () => {
|
|||
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -332,9 +332,7 @@ export const fetchTopicConsumerGroups =
|
|||
...state.byName,
|
||||
[topicName]: {
|
||||
...state.byName[topicName],
|
||||
consumerGroups: {
|
||||
...consumerGroups,
|
||||
},
|
||||
consumerGroups,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -48,3 +48,8 @@ export const getDiskUsage = createSelector(
|
|||
brokersState,
|
||||
({ diskUsage }) => diskUsage
|
||||
);
|
||||
|
||||
export const getVersion = createSelector(
|
||||
brokersState,
|
||||
({ version }) => version
|
||||
);
|
||||
|
|
Loading…
Add table
Reference in a new issue