From 0815739d790f10080a9e8474603338e553c0bb95 Mon Sep 17 00:00:00 2001 From: soffest Date: Thu, 21 May 2020 14:09:59 +0300 Subject: [PATCH] Consumer group details view. (#18) * Added concumer groups list with search. * Added consumer group details view. * Minor tweaks. * Backend group details (#19) * temp result commit * consumer group details api done * fixes * changed calls from loop into vars, changed foreach to map * changes * removed redundant import * Refactoring, fixed contract * Fixed useless group query * fix-consumer-groups-retaining-cluster-on-cluster-switch Co-authored-by: Roman Nedzvetskiy Co-authored-by: German Osin Co-authored-by: Azat Gataullin * Add loader, fix details * Add empty text for consumer groups Co-authored-by: Sofia Shnaidman Co-authored-by: Roman Nedzvetskiy Co-authored-by: Roman Nedzvetskiy Co-authored-by: German Osin Co-authored-by: Azat Gataullin Co-authored-by: Maxim Tereshin --- kafka-ui-api/pom.xml | 5 + .../ui/cluster/mapper/ClusterMapper.java | 1 + .../cluster/model/InternalClusterMetrics.java | 2 + .../ui/cluster/service/ClusterService.java | 61 +- .../kafka/ui/cluster/util/ClusterUtil.java | 34 +- .../kafka/ui/kafka/KafkaService.java | 7 +- .../kafka/ui/rest/MetricsRestController.java | 7 +- kafka-ui-react-app/.eslintrc.json | 7 +- kafka-ui-react-app/mock/index.js | 5 +- .../mock/payload/consumerGroupDetails.json | 29 + kafka-ui-react-app/package-lock.json | 584 ++++++++++++++++++ .../ConsumerGroups/ConsumerGroups.tsx | 7 +- .../ConsumerGroups/Details/Details.tsx | 86 +++ .../Details/DetailsContainer.ts | 29 + .../ConsumerGroups/Details/ListItem.tsx | 33 + .../components/ConsumerGroups/List/List.tsx | 72 ++- .../ConsumerGroups/List/ListItem.tsx | 29 +- .../src/components/Nav/ClusterMenu.tsx | 52 +- kafka-ui-react-app/src/components/Nav/Nav.tsx | 4 +- .../src/components/Topics/New/New.tsx | 13 +- kafka-ui-react-app/src/redux/actionType.ts | 4 + .../src/redux/actions/actions.ts | 8 +- .../src/redux/actions/thunks.ts | 11 + .../src/redux/api/consumerGroups.ts | 6 +- kafka-ui-react-app/src/redux/api/topics.ts | 10 +- .../src/redux/interfaces/consumerGroup.ts | 28 +- .../src/redux/interfaces/index.ts | 4 +- .../src/redux/interfaces/topic.ts | 2 +- .../src/redux/reducers/brokers/selectors.ts | 44 +- .../redux/reducers/consumerGroups/reducer.ts | 42 +- .../reducers/consumerGroups/selectors.ts | 35 +- kafka-ui-react-app/src/theme/index.scss | 4 + pom.xml | 1 + 33 files changed, 1156 insertions(+), 110 deletions(-) create mode 100644 kafka-ui-react-app/mock/payload/consumerGroupDetails.json create mode 100644 kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx create mode 100644 kafka-ui-react-app/src/components/ConsumerGroups/Details/DetailsContainer.ts create mode 100644 kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx diff --git a/kafka-ui-api/pom.xml b/kafka-ui-api/pom.xml index 2203c5a176..5aa24fcb2b 100644 --- a/kafka-ui-api/pom.xml +++ b/kafka-ui-api/pom.xml @@ -49,6 +49,11 @@ kafka-clients ${kafka-clients.version} + + org.apache.kafka + kafka_2.13 + ${kafka.version} + com.101tec zkclient diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/mapper/ClusterMapper.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/mapper/ClusterMapper.java index 63e727cecc..e3725a42fe 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/mapper/ClusterMapper.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/mapper/ClusterMapper.java @@ -8,6 +8,7 @@ import com.provectus.kafka.ui.cluster.model.KafkaCluster; import com.provectus.kafka.ui.model.*; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.mapstruct.ValueMapping; @Mapper(componentModel = "spring") public interface ClusterMapper { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/model/InternalClusterMetrics.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/model/InternalClusterMetrics.java index 1f878ac6e5..b7baa7c6e3 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/model/InternalClusterMetrics.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/model/InternalClusterMetrics.java @@ -1,5 +1,6 @@ package com.provectus.kafka.ui.cluster.model; +import com.provectus.kafka.ui.model.ServerStatus; import lombok.Builder; import lombok.Data; @@ -22,4 +23,5 @@ public class InternalClusterMetrics { //TODO: find way to fill private final int segmentSize; private final int segmentCount; + private final int zooKeeperStatus; } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/service/ClusterService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/service/ClusterService.java index 88f1c672dd..25e8496abc 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/service/ClusterService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/service/ClusterService.java @@ -8,14 +8,20 @@ import com.provectus.kafka.ui.kafka.KafkaService; import com.provectus.kafka.ui.model.*; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +import org.apache.kafka.clients.admin.ConsumerGroupListing; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.TopicPartition; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; @Service @RequiredArgsConstructor @@ -66,10 +72,57 @@ public class ClusterService { } @SneakyThrows - public Mono> getConsumerGroups(String clusterName) { + public Mono getConsumerGroupDetail(String clusterName, String consumerGroupId) { + var cluster = clustersStorage.getClusterByName(clusterName).orElseThrow(Throwable::new); + + return kafkaService.getOrCreateAdminClient(cluster).map(ac -> + ac.describeConsumerGroups(Collections.singletonList(consumerGroupId)).all() + ).flatMap(groups -> + groupMetadata(cluster, consumerGroupId) + .flatMap(offsets -> { + Map endOffsets = topicPartitionsEndOffsets(cluster, offsets.keySet()); + return ClusterUtil.toMono(groups).map(s -> s.get(consumerGroupId).members().stream() + .flatMap(c -> Stream.of(ClusterUtil.convertToConsumerTopicPartitionDetails(c, offsets, endOffsets))) + .collect(Collectors.toList()).stream().flatMap(t -> t.stream().flatMap(Stream::of)).collect(Collectors.toList())); + }) + ) + .map(c -> new ConsumerGroupDetails().consumers(c).consumerGroupId(consumerGroupId)); + + } + + public Mono> groupMetadata(KafkaCluster cluster, String consumerGroupId) { + return + kafkaService.getOrCreateAdminClient(cluster) + .map(ac -> ac.listConsumerGroupOffsets(consumerGroupId).partitionsToOffsetAndMetadata()) + .flatMap(ClusterUtil::toMono); + } + + public Map topicPartitionsEndOffsets(KafkaCluster cluster, Collection topicPartitions) { + Properties properties = new Properties(); + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.getBootstrapServers()); + properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + properties.put(ConsumerConfig.GROUP_ID_CONFIG, UUID.randomUUID().toString()); + + try (KafkaConsumer consumer = new KafkaConsumer<>(properties)) { + return consumer.endOffsets(topicPartitions); + } + } + + @SneakyThrows + public Mono> getConsumerGroups (String clusterName) { return clustersStorage.getClusterByName(clusterName) .map(kafkaService::getConsumerGroups) .orElse(Mono.empty()); + +// var cluster = clustersStorage.getClusterByName(clusterName).orElseThrow(Throwable::new); +// return kafkaService.getOrCreateAdminClient(cluster).map(ac -> ac.listConsumerGroups().all()) +// .flatMap(s -> +// kafkaService.getOrCreateAdminClient(cluster).flatMap(ac -> +// ClusterUtil.toMono(s).map(s1 -> s1.stream().map(ConsumerGroupListing::groupId).collect(Collectors.toList())).map(ac::describeConsumerGroups) +// )) +// .flatMap(s -> ClusterUtil.toMono(s.all()).map(details -> details.values().stream() +// .map(c -> ClusterUtil.convertToConsumerGroup(c, cluster)).collect(Collectors.toList()))); } public Flux getBrokers (String clusterName) { diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/util/ClusterUtil.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/util/ClusterUtil.java index adf1caa480..1f73a14851 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/util/ClusterUtil.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/cluster/util/ClusterUtil.java @@ -2,14 +2,21 @@ package com.provectus.kafka.ui.cluster.util; import com.provectus.kafka.ui.cluster.model.*; import com.provectus.kafka.ui.model.ConsumerGroup; +import com.provectus.kafka.ui.model.ConsumerTopicPartitionDetail; +import com.provectus.kafka.ui.model.ServerStatus; import org.apache.kafka.clients.admin.ConfigEntry; import org.apache.kafka.clients.admin.ConsumerGroupDescription; +import org.apache.kafka.clients.admin.MemberDescription; import org.apache.kafka.clients.admin.TopicDescription; +import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.common.KafkaFuture; +import org.apache.kafka.common.TopicPartition; import reactor.core.publisher.Mono; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import static com.provectus.kafka.ui.kafka.KafkaConstants.TOPIC_DEFAULT_CONFIGS; import static org.apache.kafka.common.config.TopicConfig.MESSAGE_FORMAT_VERSION_CONFIG; @@ -30,11 +37,32 @@ public class ClusterUtil { ConsumerGroup consumerGroup = new ConsumerGroup(); consumerGroup.setConsumerGroupId(c.groupId()); consumerGroup.setNumConsumers(c.members().size()); - int numTopics = c.members().stream().mapToInt( m -> m.assignment().topicPartitions().size()).sum(); + int numTopics = c.members().stream().flatMap(m -> m.assignment().topicPartitions().stream().flatMap(t -> Stream.of(t.topic()))).collect(Collectors.toSet()).size(); consumerGroup.setNumTopics(numTopics); return consumerGroup; } + public static List convertToConsumerTopicPartitionDetails( + MemberDescription consumer, + Map groupOffsets, + Map endOffsets + ) { + return consumer.assignment().topicPartitions().stream() + .map(tp -> { + Long currentOffset = groupOffsets.get(tp).offset(); + Long endOffset = endOffsets.get(tp); + ConsumerTopicPartitionDetail cd = new ConsumerTopicPartitionDetail(); + cd.setConsumerId(consumer.consumerId()); + cd.setTopic(tp.topic()); + cd.setPartition(tp.partition()); + cd.setCurrentOffset(currentOffset); + cd.setEndOffset(endOffset); + cd.setMessagesBehind(endOffset - currentOffset); + return cd; + }).collect(Collectors.toList()); + } + + public static InternalTopicConfig mapToInternalTopicConfig(ConfigEntry configEntry) { InternalTopicConfig.InternalTopicConfigBuilder builder = InternalTopicConfig.builder() .name(configEntry.name()) @@ -95,4 +123,8 @@ public class ClusterUtil { return topic.build(); } + public static int convertToIntServerStatus(ServerStatus serverStatus) { + return serverStatus.equals(ServerStatus.ONLINE) ? 1 : 0; + } + } diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/kafka/KafkaService.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/kafka/KafkaService.java index 2941c51d72..a4990b6f5d 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/kafka/KafkaService.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/kafka/KafkaService.java @@ -71,12 +71,15 @@ public class KafkaService { InternalClusterMetrics clusterMetrics = metricsBuilder .activeControllers(brokersMetrics.getActiveControllers()) + .topicCount(topicsMetrics.getTopicCount()) .brokerCount(brokersMetrics.getBrokerCount()) .underReplicatedPartitionCount(topicsMetrics.getUnderReplicatedPartitionCount()) .inSyncReplicasCount(topicsMetrics.getInSyncReplicasCount()) .outOfSyncReplicasCount(topicsMetrics.getOutOfSyncReplicasCount()) .onlinePartitionCount(topicsMetrics.getOnlinePartitionCount()) - .offlinePartitionCount(topicsMetrics.getOfflinePartitionCount()).build(); + .offlinePartitionCount(topicsMetrics.getOfflinePartitionCount()) + .zooKeeperStatus(ClusterUtil.convertToIntServerStatus(zookeeperStatus)) + .build(); return currentCluster.toBuilder() .status(ServerStatus.ONLINE) @@ -110,6 +113,7 @@ public class KafkaService { .outOfSyncReplicasCount(outOfSyncReplicasCount) .onlinePartitionCount(onlinePartitionCount) .offlinePartitionCount(offlinePartitionCount) + .topicCount(topics.size()) .build(); } @@ -138,7 +142,6 @@ public class KafkaService { builder.brokerCount(brokers.size()).activeControllers(c != null ? 1 : 0); // TODO: fill bytes in/out metrics List brokerIds = brokers.stream().map(Node::id).collect(Collectors.toList()); - return builder.build(); } ) diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/rest/MetricsRestController.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/rest/MetricsRestController.java index 7e814efd36..1e9d6e1083 100644 --- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/rest/MetricsRestController.java +++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/rest/MetricsRestController.java @@ -77,10 +77,15 @@ public class MetricsRestController implements ApiClustersApi { } @Override - public Mono>> getConsumerGroup(String clusterName, ServerWebExchange exchange) { + public Mono>> getConsumerGroups(String clusterName, ServerWebExchange exchange) { return clusterService.getConsumerGroups(clusterName) .map(Flux::fromIterable) .map(ResponseEntity::ok) .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); // TODO: check behaviour on cluster not found and empty groups list } + + @Override + public Mono> getConsumerGroup(String clusterName, String consumerGroupId, ServerWebExchange exchange) { + return clusterService.getConsumerGroupDetail(clusterName, consumerGroupId).map(ResponseEntity::ok); + } } diff --git a/kafka-ui-react-app/.eslintrc.json b/kafka-ui-react-app/.eslintrc.json index aa2aca13cd..6dc2d5b711 100644 --- a/kafka-ui-react-app/.eslintrc.json +++ b/kafka-ui-react-app/.eslintrc.json @@ -39,7 +39,9 @@ "react/jsx-filename-extension": [ 1, { "extensions": [".js", ".jsx", ".ts", ".tsx"] } - ] + ], + "jsx-a11y/label-has-associated-control": "off", + "no-param-reassign": [2, { "props": false }] }, "overrides": [ { @@ -52,7 +54,8 @@ "settings": { "import/resolver": { "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"] + "extensions": [".js", ".jsx", ".ts", ".tsx"], + "paths": ["src"] } } } diff --git a/kafka-ui-react-app/mock/index.js b/kafka-ui-react-app/mock/index.js index 6aa2454b33..56b2be1b1a 100644 --- a/kafka-ui-react-app/mock/index.js +++ b/kafka-ui-react-app/mock/index.js @@ -6,6 +6,7 @@ const topics = require('./payload/topics.json'); const topicDetails = require('./payload/topicDetails.json'); const topicConfigs = require('./payload/topicConfigs.json'); const consumerGroups = require('./payload/consumerGroups.json'); +const consumerGroupDetails = require('./payload/consumerGroupDetails.json'); const db = { clusters, @@ -14,7 +15,8 @@ const db = { topics: topics.map((topic) => ({...topic, id: topic.name})), topicDetails, topicConfigs, - consumerGroups: consumerGroups.map((group) => ({...group, id: group.consumerGroupId})) + consumerGroups: consumerGroups.map((group) => ({...group, id: group.consumerGroupId})), + consumerGroupDetails }; const server = jsonServer.create(); const router = jsonServer.router(db); @@ -34,6 +36,7 @@ server.use( '/clusters/:clusterName/metrics/broker': '/brokerMetrics/:clusterName', '/clusters/:clusterName/topics/:id': '/topicDetails', '/clusters/:clusterName/topics/:id/config': '/topicConfigs', + '/clusters/:clusterName/consumer-groups/:id': '/consumerGroupDetails', }) ); diff --git a/kafka-ui-react-app/mock/payload/consumerGroupDetails.json b/kafka-ui-react-app/mock/payload/consumerGroupDetails.json new file mode 100644 index 0000000000..b8ba26f6fb --- /dev/null +++ b/kafka-ui-react-app/mock/payload/consumerGroupDetails.json @@ -0,0 +1,29 @@ +{ + "consumerGroupId": "_fake.cluster.consumer_1", + "consumers": [ + { + "consumerId": "_fake.cluster.consumer_1-1-1", + "topic": "my-topic", + "partition": 0, + "messagesBehind": 1246, + "currentOffset": 2834, + "endOffset": 2835 + }, + { + "consumerId": "_fake.cluster.consumer_2-2-2", + "topic": "docker-connect-status", + "partition": 1, + "messagesBehind": 678, + "currentOffset": 234, + "endOffset": 246 + }, + { + "consumerId": "_fake.cluster.consumer_2-2-2", + "topic": "docker-connect-status", + "partition": 2, + "messagesBehind": 143, + "currentOffset": 123, + "endOffset": 134 + } + ] +} \ No newline at end of file diff --git a/kafka-ui-react-app/package-lock.json b/kafka-ui-react-app/package-lock.json index a1a0443295..d71589c948 100644 --- a/kafka-ui-react-app/package-lock.json +++ b/kafka-ui-react-app/package-lock.json @@ -1241,6 +1241,15 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" }, + "@samverschueren/stream-to-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", + "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==", + "dev": true, + "requires": { + "any-observable": "^0.3.0" + } + }, "@sheerun/mutationobserver-shim": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz", @@ -2046,6 +2055,12 @@ "color-convert": "^1.9.0" } }, + "any-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", + "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", + "dev": true + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -3870,6 +3885,60 @@ "restore-cursor": "^3.1.0" } }, + "cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + } + } + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", @@ -3907,6 +3976,12 @@ } } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, "clone-deep": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", @@ -4657,6 +4732,12 @@ "mimic-response": "^1.0.0" } }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, "deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", @@ -4689,6 +4770,15 @@ "ip-regex": "^2.1.0" } }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, "defer-to-connect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.1.tgz", @@ -5067,6 +5157,12 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.322.tgz", "integrity": "sha512-Tc8JQEfGQ1MzfSzI/bTlSr7btJv/FFO7Yh6tanqVmIWOuNCu6/D1MilIEgLtmWqIrsv+o4IjpLAhgMBr/ncNAA==" }, + "elegant-spinner": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-2.0.0.tgz", + "integrity": "sha512-5YRYHhvhYzV/FC4AiMdeSIg3jAYGq9xFvbhZMpPlJoBsfYgrw2DSCYeXfat6tYBu45PWiyRr3+flaCPPmviPaA==", + "dev": true + }, "elliptic": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", @@ -5147,6 +5243,15 @@ } } }, + "enquirer": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.5.tgz", + "integrity": "sha512-BNT1C08P9XD0vNg3J475yIUG+mVdp9T6towYFHUv897X0KoHBjB1shyrNmhmtHWKP17iSWgo7Gqh7BBuzLZMSA==", + "dev": true, + "requires": { + "ansi-colors": "^3.2.1" + } + }, "entities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz", @@ -7306,6 +7411,12 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, "husky": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", @@ -9424,6 +9535,352 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" }, + "lint-staged": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.2.tgz", + "integrity": "sha512-78kNqNdDeKrnqWsexAmkOU3Z5wi+1CsQmUmfCuYgMTE8E4rAIX8RHW7xgxwAZ+LAayb7Cca4uYX4P3LlevzjVg==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "commander": "^5.0.0", + "cosmiconfig": "^6.0.0", + "debug": "^4.1.1", + "dedent": "^0.7.0", + "execa": "^4.0.0", + "listr2": "1.3.8", + "log-symbols": "^3.0.0", + "micromatch": "^4.0.2", + "normalize-path": "^3.0.0", + "please-upgrade-node": "^3.2.0", + "string-argv": "0.3.1", + "stringify-object": "^3.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, + "cross-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", + "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.1.tgz", + "integrity": "sha512-SCjM/zlBdOK8Q5TIjOn6iEHZaPHFsMoTxXQ2nvUvtPnuohz3H2dIozSg+etNR98dGoYUp2ENSKLL/XaMmbxVgw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "listr2": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-1.3.8.tgz", + "integrity": "sha512-iRDRVTgSDz44tBeBBg/35TQz4W+EZBWsDUq7hPpqeUHm7yLPNll0rkwW3lIX9cPAK7l+x95mGWLpxjqxftNfZA==", + "dev": true, + "requires": { + "@samverschueren/stream-to-observable": "^0.3.0", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-truncate": "^2.1.0", + "elegant-spinner": "^2.0.0", + "enquirer": "^2.3.4", + "figures": "^3.2.0", + "indent-string": "^4.0.0", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "pad": "^3.2.0", + "rxjs": "^6.3.3", + "through": "^2.3.8", + "uuid": "^7.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "dev": true + } + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -9562,6 +10019,97 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2" + } + }, + "log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "loglevel": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.6.tgz", @@ -10959,6 +11507,15 @@ "semver": "^6.2.0" } }, + "pad": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pad/-/pad-3.2.0.tgz", + "integrity": "sha512-2u0TrjcGbOjBTJpyewEl4hBO3OeX5wWue7eIFPzQTg6wFSvoaHcBTTUY5m+n0hd04gmTCPuY0kCpVIVuw5etwg==", + "dev": true, + "requires": { + "wcwidth": "^1.0.1" + } + }, "pako": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", @@ -11128,6 +11685,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -14630,6 +15193,12 @@ "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=" }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", @@ -14930,6 +15499,12 @@ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -15833,6 +16408,15 @@ "minimalistic-assert": "^1.0.0" } }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, "weak-napi": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/weak-napi/-/weak-napi-1.0.3.tgz", diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx index 747cf998f2..91cf633f22 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/ConsumerGroups.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { ClusterName } from 'redux/interfaces'; import { Switch, Route } from 'react-router-dom'; import PageLoader from 'components/common/PageLoader/PageLoader'; -import ListContainer from './List/ListContainer'; +import DetailsContainer from 'components/ConsumerGroups/Details/DetailsContainer'; +import ListContainer from 'components/ConsumerGroups/List/ListContainer'; interface Props { clusterName: ClusterName; @@ -27,6 +28,10 @@ const ConsumerGroups: React.FC = ({ path="/ui/clusters/:clusterName/consumer-groups" component={ListContainer} /> + ); } diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx new file mode 100644 index 0000000000..39780bb6e8 --- /dev/null +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { ClusterName } from 'redux/interfaces'; +import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; +import { clusterConsumerGroupsPath } from 'lib/paths'; +import { + ConsumerGroupID, + ConsumerGroup, + ConsumerGroupDetails, + Consumer, +} from 'redux/interfaces/consumerGroup'; + +import PageLoader from 'components/common/PageLoader/PageLoader'; +import ListItem from './ListItem'; + +interface Props extends ConsumerGroup, ConsumerGroupDetails { + clusterName: ClusterName; + consumerGroupID: ConsumerGroupID; + consumers: Consumer[]; + isFetched: boolean; + fetchConsumerGroupDetails: ( + clusterName: ClusterName, + consumerGroupID: ConsumerGroupID + ) => void; +} + +const Details: React.FC = ({ + clusterName, + consumerGroupID, + consumers, + isFetched, + fetchConsumerGroupDetails, +}) => { + React.useEffect(() => { + fetchConsumerGroupDetails(clusterName, consumerGroupID); + }, [fetchConsumerGroupDetails, clusterName, consumerGroupID]); + const items = consumers || []; + + return ( +
+
+
+ + {consumerGroupID} + +
+
+ + {isFetched ? ( +
+ + + + + + + + + + + + + {items.map((consumer) => ( + + ))} + +
Consumer IDTopicPartitionMessages behindCurrent offsetEnd offset
+
+ ) : ( + + )} +
+ ); +}; + +export default Details; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/DetailsContainer.ts b/kafka-ui-react-app/src/components/ConsumerGroups/Details/DetailsContainer.ts new file mode 100644 index 0000000000..56418333be --- /dev/null +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/DetailsContainer.ts @@ -0,0 +1,29 @@ +import { connect } from 'react-redux'; +import Details from './Details'; +import {ClusterName, RootState} from 'redux/interfaces'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { getIsConsumerGroupDetailsFetched, getConsumerGroupByID } from 'redux/reducers/consumerGroups/selectors'; +import { ConsumerGroupID } from 'redux/interfaces/consumerGroup'; +import { fetchConsumerGroupDetails } from 'redux/actions/thunks'; + +interface RouteProps { + clusterName: ClusterName; + consumerGroupID: string; +} + +interface OwnProps extends RouteComponentProps { } + +const mapStateToProps = (state: RootState, { match: { params: { consumerGroupID, clusterName } } }: OwnProps) => ({ + clusterName, + consumerGroupID, + isFetched: getIsConsumerGroupDetailsFetched(state), + ...getConsumerGroupByID(state, consumerGroupID) +}); + +const mapDispatchToProps = { + fetchConsumerGroupDetails: (clusterName: ClusterName, consumerGroupID: ConsumerGroupID) => fetchConsumerGroupDetails(clusterName, consumerGroupID), +}; + +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(Details) +); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx new file mode 100644 index 0000000000..35a7b02a82 --- /dev/null +++ b/kafka-ui-react-app/src/components/ConsumerGroups/Details/ListItem.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Consumer } from 'redux/interfaces/consumerGroup'; +import { NavLink } from 'react-router-dom'; +import { ClusterName } from 'redux/interfaces/cluster'; + +interface Props { + clusterName: ClusterName; + consumer: Consumer; +} + +const ListItem: React.FC = ({ clusterName, consumer }) => { + return ( + + {consumer.consumerId} + + + {consumer.topic} + + + {consumer.partition} + {consumer.messagesBehind} + {consumer.currentOffset} + {consumer.endOffset} + + ); +}; + +export default ListItem; diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx index cdeaf0a855..4bf3707d10 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List/List.tsx @@ -1,33 +1,31 @@ -import React, { ChangeEvent } from 'react'; -import { ConsumerGroup, ClusterName } from 'redux/interfaces'; -import ListItem from './ListItem'; +import React from 'react'; +import { ClusterName, ConsumerGroup } from 'redux/interfaces'; import Breadcrumb from 'components/common/Breadcrumb/Breadcrumb'; +import ListItem from './ListItem'; interface Props { clusterName: ClusterName; - consumerGroups: (ConsumerGroup)[]; + consumerGroups: ConsumerGroup[]; } -const List: React.FC = ({ - consumerGroups, -}) => { - +const List: React.FC = ({ consumerGroups }) => { const [searchText, setSearchText] = React.useState(''); const handleInputChange = (event: React.ChangeEvent) => { setSearchText(event.target.value); }; - const items = consumerGroups; - return (
All Consumer Groups
-
-
- 0 ? ( +
+
+
+ = ({ value={searchText} onChange={handleInputChange} /> +
+
+ + + + + + + + + + {consumerGroups + .filter( + (consumerGroup) => + !searchText || + consumerGroup?.consumerGroupId?.indexOf(searchText) >= 0 + ) + .map((consumerGroup) => ( + + ))} + +
Consumer group IDNum of consumersNum of topics
-
- - - - - - - - - - {items - .filter( (consumerGroup) => !searchText || consumerGroup?.consumerGroupId?.indexOf(searchText) >= 0) - .map((consumerGroup, index) => ( - - ))} - -
Consumer group IDNum of consumersNum of topics
+ ) : ( + 'No active consumer groups' + )}
); diff --git a/kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx b/kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx index a6dfa45b33..6e5089aee3 100644 --- a/kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx +++ b/kafka-ui-react-app/src/components/ConsumerGroups/List/ListItem.tsx @@ -1,24 +1,23 @@ import React from 'react'; -import { NavLink } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { ConsumerGroup } from 'redux/interfaces'; -const ListItem: React.FC = ({ - consumerGroupId, - numConsumers, - numTopics, +const ListItem: React.FC<{ consumerGroup: ConsumerGroup }> = ({ + consumerGroup, }) => { + const history = useHistory(); + + function goToConsumerGroupDetails() { + history.push(`consumer-groups/${consumerGroup.consumerGroupId}`); + } + return ( - - {/* - - {consumerGroupId} - - */} - {consumerGroupId} - {numConsumers} - {numTopics} + + {consumerGroup.consumerGroupId} + {consumerGroup.numConsumers} + {consumerGroup.numTopics} ); -} +}; export default ListItem; diff --git a/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx b/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx index 28fb9b68da..cc3d417906 100644 --- a/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx +++ b/kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx @@ -1,9 +1,15 @@ import React, { CSSProperties } from 'react'; import { Cluster } from 'redux/interfaces'; import { NavLink } from 'react-router-dom'; -import { clusterBrokersPath, clusterTopicsPath, clusterConsumerGroupsPath } from 'lib/paths'; +import { + clusterBrokersPath, + clusterTopicsPath, + clusterConsumerGroupsPath, +} from 'lib/paths'; -interface Props extends Cluster {} +interface Props { + cluster: Cluster; +} const DefaultIcon: React.FC = () => { const style: CSSProperties = { @@ -15,29 +21,47 @@ const DefaultIcon: React.FC = () => { return ( - + - ) + ); }; -const ClusterMenu: React.FC = ({ - name, - defaultCluster, -}) => ( +const ClusterMenu: React.FC = ({ cluster }) => (
  • - - {defaultCluster && } - {name} + + {cluster.defaultCluster && } + {cluster.name}
      - + Brokers - + Topics - + Consumers
    diff --git a/kafka-ui-react-app/src/components/Nav/Nav.tsx b/kafka-ui-react-app/src/components/Nav/Nav.tsx index a6c4008789..950d6fa0b4 100644 --- a/kafka-ui-react-app/src/components/Nav/Nav.tsx +++ b/kafka-ui-react-app/src/components/Nav/Nav.tsx @@ -28,8 +28,8 @@ const Nav: React.FC = ({ {!isClusterListFetched &&
    } {isClusterListFetched && - clusters.map((cluster, index) => ( - + clusters.map((cluster) => ( + ))} ); diff --git a/kafka-ui-react-app/src/components/Topics/New/New.tsx b/kafka-ui-react-app/src/components/Topics/New/New.tsx index 34def6a1a1..ffb8af28fe 100644 --- a/kafka-ui-react-app/src/components/Topics/New/New.tsx +++ b/kafka-ui-react-app/src/components/Topics/New/New.tsx @@ -37,12 +37,12 @@ const New: React.FC = ({ redirectToTopicPath(clusterName, name); } }, [ - isSubmitting, - isTopicCreated, - redirectToTopicPath, - clusterName, - methods.getValues, - ]); + isSubmitting, + isTopicCreated, + redirectToTopicPath, + clusterName, + methods.getValues, + ]); const onSubmit = async (data: TopicFormData) => { // TODO: need to fix loader. After success loading the first time, we won't wait for creation any more, because state is @@ -68,6 +68,7 @@ const New: React.FC = ({
    + {/* eslint-disable react/jsx-props-no-spreading */}
    diff --git a/kafka-ui-react-app/src/redux/actionType.ts b/kafka-ui-react-app/src/redux/actionType.ts index 1b91c1915d..8fb8880da0 100644 --- a/kafka-ui-react-app/src/redux/actionType.ts +++ b/kafka-ui-react-app/src/redux/actionType.ts @@ -30,4 +30,8 @@ export enum ActionType { GET_CONSUMER_GROUPS__REQUEST = 'GET_CONSUMER_GROUPS__REQUEST', GET_CONSUMER_GROUPS__SUCCESS = 'GET_CONSUMER_GROUPS__SUCCESS', GET_CONSUMER_GROUPS__FAILURE = 'GET_CONSUMER_GROUPS__FAILURE', + + GET_CONSUMER_GROUP_DETAILS__REQUEST = 'GET_CONSUMER_GROUP_DETAILS__REQUEST', + GET_CONSUMER_GROUP_DETAILS__SUCCESS = 'GET_CONSUMER_GROUP_DETAILS__SUCCESS', + GET_CONSUMER_GROUP_DETAILS__FAILURE = 'GET_CONSUMER_GROUP_DETAILS__FAILURE', }; diff --git a/kafka-ui-react-app/src/redux/actions/actions.ts b/kafka-ui-react-app/src/redux/actions/actions.ts index 6ac3085b61..aca653c388 100644 --- a/kafka-ui-react-app/src/redux/actions/actions.ts +++ b/kafka-ui-react-app/src/redux/actions/actions.ts @@ -1,6 +1,6 @@ import { createAsyncAction } from 'typesafe-actions'; import { ActionType } from 'redux/actionType'; -import { ConsumerGroup } from '../interfaces/consumerGroup'; +import { ConsumerGroup, ConsumerGroupID, ConsumerGroupDetails } from '../interfaces/consumerGroup'; import { Broker, BrokerMetrics, @@ -58,3 +58,9 @@ export const fetchConsumerGroupsAction = createAsyncAction( ActionType.GET_CONSUMER_GROUPS__SUCCESS, ActionType.GET_CONSUMER_GROUPS__FAILURE, )(); + +export const fetchConsumerGroupDetailsAction = createAsyncAction( + ActionType.GET_CONSUMER_GROUP_DETAILS__REQUEST, + ActionType.GET_CONSUMER_GROUP_DETAILS__SUCCESS, + ActionType.GET_CONSUMER_GROUP_DETAILS__FAILURE, +)(); diff --git a/kafka-ui-react-app/src/redux/actions/thunks.ts b/kafka-ui-react-app/src/redux/actions/thunks.ts index da1ff10b95..ea1ad62cad 100644 --- a/kafka-ui-react-app/src/redux/actions/thunks.ts +++ b/kafka-ui-react-app/src/redux/actions/thunks.ts @@ -1,5 +1,6 @@ import * as api from 'redux/api'; import * as actions from './actions'; +import { ConsumerGroupID } from '../interfaces/consumerGroup'; import { PromiseThunk, Cluster, @@ -87,3 +88,13 @@ export const fetchConsumerGroupsList = (clusterName: ClusterName): PromiseThunk< dispatch(actions.fetchConsumerGroupsAction.failure()); } }; + +export const fetchConsumerGroupDetails = (clusterName: ClusterName, consumerGroupID: ConsumerGroupID): PromiseThunk => async (dispatch) => { + dispatch(actions.fetchConsumerGroupDetailsAction.request()); + try { + const consumerGroupDetails = await api.getConsumerGroupDetails(clusterName, consumerGroupID); + dispatch(actions.fetchConsumerGroupDetailsAction.success({ consumerGroupID, details: consumerGroupDetails })); + } catch (e) { + dispatch(actions.fetchConsumerGroupDetailsAction.failure()); + } +}; diff --git a/kafka-ui-react-app/src/redux/api/consumerGroups.ts b/kafka-ui-react-app/src/redux/api/consumerGroups.ts index 225b60479e..84ecf3758c 100644 --- a/kafka-ui-react-app/src/redux/api/consumerGroups.ts +++ b/kafka-ui-react-app/src/redux/api/consumerGroups.ts @@ -1,8 +1,12 @@ import { ClusterName } from '../interfaces/cluster'; -import { ConsumerGroup } from '../interfaces/consumerGroup'; +import { ConsumerGroup, ConsumerGroupID, ConsumerGroupDetails } from '../interfaces/consumerGroup'; import { BASE_PARAMS, BASE_URL } from '../../lib/constants'; export const getConsumerGroups = (clusterName: ClusterName): Promise => fetch(`${BASE_URL}/clusters/${clusterName}/consumerGroups`, { ...BASE_PARAMS }) .then(res => res.json()); + +export const getConsumerGroupDetails = (clusterName: ClusterName, consumerGroupID: ConsumerGroupID): Promise => + fetch(`${BASE_URL}/clusters/${clusterName}/consumer-groups/${consumerGroupID}`, { ...BASE_PARAMS }) + .then(res => res.json()); diff --git a/kafka-ui-react-app/src/redux/api/topics.ts b/kafka-ui-react-app/src/redux/api/topics.ts index ee0824e47a..1a95dca5ad 100644 --- a/kafka-ui-react-app/src/redux/api/topics.ts +++ b/kafka-ui-react-app/src/redux/api/topics.ts @@ -1,14 +1,14 @@ import { reduce } from 'lodash'; import { - TopicName, - Topic, ClusterName, - TopicDetails, + Topic, TopicConfig, - TopicFormData, + TopicDetails, TopicFormCustomParam, + TopicFormData, + TopicName, } from 'redux/interfaces'; -import { BASE_URL, BASE_PARAMS } from 'lib/constants'; +import { BASE_PARAMS, BASE_URL } from 'lib/constants'; export const getTopicConfig = ( clusterName: ClusterName, diff --git a/kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts b/kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts index 3008e5cf0f..b762ebbba8 100644 --- a/kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts +++ b/kafka-ui-react-app/src/redux/interfaces/consumerGroup.ts @@ -1,5 +1,31 @@ +export type ConsumerGroupID = string; + export interface ConsumerGroup { - consumerGroupId: string; + consumerGroupId: ConsumerGroupID; numConsumers: number; numTopics: number; +} + +export interface ConsumerGroupDetails { + consumerGroupId: ConsumerGroupID; + numConsumers: number; + numTopics: number; + consumers: Consumer[]; +} + +export interface Consumer { + consumerId: string; + topic: string; + partition: number; + messagesBehind: number; + currentOffset: number; + endOffset: number; +} + +export interface ConsumerGroupDetailedInfo extends ConsumerGroup, ConsumerGroupDetails { +} + +export interface ConsumerGroupsState { + byID: { [consumerGroupID: string]: ConsumerGroupDetailedInfo }, + allIDs: string[] } \ No newline at end of file diff --git a/kafka-ui-react-app/src/redux/interfaces/index.ts b/kafka-ui-react-app/src/redux/interfaces/index.ts index 304b0ea65a..5b53fdab0f 100644 --- a/kafka-ui-react-app/src/redux/interfaces/index.ts +++ b/kafka-ui-react-app/src/redux/interfaces/index.ts @@ -8,7 +8,7 @@ import { TopicsState } from './topic'; import { Cluster } from './cluster'; import { BrokersState } from './broker'; import { LoaderState } from './loader'; -import { ConsumerGroup } from './consumerGroup'; +import { ConsumerGroupsState } from './consumerGroup'; export * from './topic'; export * from './cluster'; @@ -27,7 +27,7 @@ export interface RootState { topics: TopicsState; clusters: Cluster[]; brokers: BrokersState; - consumerGroups: ConsumerGroup[]; + consumerGroups: ConsumerGroupsState; loader: LoaderState; } diff --git a/kafka-ui-react-app/src/redux/interfaces/topic.ts b/kafka-ui-react-app/src/redux/interfaces/topic.ts index 465c0522fb..cdbd15962d 100644 --- a/kafka-ui-react-app/src/redux/interfaces/topic.ts +++ b/kafka-ui-react-app/src/redux/interfaces/topic.ts @@ -75,4 +75,4 @@ export interface TopicFormData { customParams: { [index: string]: TopicFormCustomParam; }; -}; +} diff --git a/kafka-ui-react-app/src/redux/reducers/brokers/selectors.ts b/kafka-ui-react-app/src/redux/reducers/brokers/selectors.ts index 795b343b53..74b55c9501 100644 --- a/kafka-ui-react-app/src/redux/reducers/brokers/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/brokers/selectors.ts @@ -8,16 +8,38 @@ const getBrokerListFetchingStatus = createFetchingSelector('GET_BROKERS'); export const getIsBrokerListFetched = createSelector( getBrokerListFetchingStatus, - (status) => status === FetchStatus.fetched, + (status) => status === FetchStatus.fetched ); -const getBrokerList = createSelector(brokersState, ({ items }) => items); - -export const getBrokerCount = createSelector(brokersState, ({ brokerCount }) => brokerCount); -export const getZooKeeperStatus = createSelector(brokersState, ({ zooKeeperStatus }) => zooKeeperStatus); -export const getActiveControllers = createSelector(brokersState, ({ activeControllers }) => activeControllers); -export const getOnlinePartitionCount = createSelector(brokersState, ({ onlinePartitionCount }) => onlinePartitionCount); -export const getOfflinePartitionCount = createSelector(brokersState, ({ offlinePartitionCount }) => offlinePartitionCount); -export const getInSyncReplicasCount = createSelector(brokersState, ({ inSyncReplicasCount }) => inSyncReplicasCount); -export const getOutOfSyncReplicasCount = createSelector(brokersState, ({ outOfSyncReplicasCount }) => outOfSyncReplicasCount); -export const getUnderReplicatedPartitionCount = createSelector(brokersState, ({ underReplicatedPartitionCount }) => underReplicatedPartitionCount); +export const getBrokerCount = createSelector( + brokersState, + ({ brokerCount }) => brokerCount +); +export const getZooKeeperStatus = createSelector( + brokersState, + ({ zooKeeperStatus }) => zooKeeperStatus +); +export const getActiveControllers = createSelector( + brokersState, + ({ activeControllers }) => activeControllers +); +export const getOnlinePartitionCount = createSelector( + brokersState, + ({ onlinePartitionCount }) => onlinePartitionCount +); +export const getOfflinePartitionCount = createSelector( + brokersState, + ({ offlinePartitionCount }) => offlinePartitionCount +); +export const getInSyncReplicasCount = createSelector( + brokersState, + ({ inSyncReplicasCount }) => inSyncReplicasCount +); +export const getOutOfSyncReplicasCount = createSelector( + brokersState, + ({ outOfSyncReplicasCount }) => outOfSyncReplicasCount +); +export const getUnderReplicatedPartitionCount = createSelector( + brokersState, + ({ underReplicatedPartitionCount }) => underReplicatedPartitionCount +); diff --git a/kafka-ui-react-app/src/redux/reducers/consumerGroups/reducer.ts b/kafka-ui-react-app/src/redux/reducers/consumerGroups/reducer.ts index bd0e8ec71d..6bd01b7249 100644 --- a/kafka-ui-react-app/src/redux/reducers/consumerGroups/reducer.ts +++ b/kafka-ui-react-app/src/redux/reducers/consumerGroups/reducer.ts @@ -1,12 +1,48 @@ import { Action, ConsumerGroup } from 'redux/interfaces'; import { ActionType } from 'redux/actionType'; +import { ConsumerGroupsState } from '../../interfaces/consumerGroup'; -export const initialState: ConsumerGroup[] = []; +export const initialState: ConsumerGroupsState = { + byID: {}, + allIDs: [] +}; -const reducer = (state = initialState, action: Action): ConsumerGroup[] => { +const updateConsumerGroupsList = (state: ConsumerGroupsState, payload: ConsumerGroup[]): ConsumerGroupsState => { + const initialMemo: ConsumerGroupsState = { + ...state, + allIDs: [] + }; + + return payload.reduce( + (memo: ConsumerGroupsState, consumerGroup) => { + const {consumerGroupId} = consumerGroup; + memo.byID[consumerGroupId] = { + ...memo.byID[consumerGroupId], + ...consumerGroup, + }; + memo.allIDs.push(consumerGroupId); + + return memo; + }, + initialMemo, + ); +}; + +const reducer = (state = initialState, action: Action): ConsumerGroupsState => { switch (action.type) { case ActionType.GET_CONSUMER_GROUPS__SUCCESS: - return action.payload; + return updateConsumerGroupsList(state, action.payload); + case ActionType.GET_CONSUMER_GROUP_DETAILS__SUCCESS: + return { + ...state, + byID: { + ...state.byID, + [action.payload.consumerGroupID]: { + ...state.byID[action.payload.consumerGroupID], + ...action.payload.details, + } + } + }; default: return state; } diff --git a/kafka-ui-react-app/src/redux/reducers/consumerGroups/selectors.ts b/kafka-ui-react-app/src/redux/reducers/consumerGroups/selectors.ts index e471bc0afe..1abc5919b4 100644 --- a/kafka-ui-react-app/src/redux/reducers/consumerGroups/selectors.ts +++ b/kafka-ui-react-app/src/redux/reducers/consumerGroups/selectors.ts @@ -1,15 +1,44 @@ import { createSelector } from 'reselect'; -import { ConsumerGroup, RootState, FetchStatus } from 'redux/interfaces'; +import { RootState, FetchStatus } from 'redux/interfaces'; import { createFetchingSelector } from 'redux/reducers/loader/selectors'; +import { ConsumerGroupID, ConsumerGroupsState } from '../../interfaces/consumerGroup'; -const consumerGroupsState = ({ consumerGroups }: RootState): ConsumerGroup[] => consumerGroups; +const consumerGroupsState = ({ consumerGroups }: RootState): ConsumerGroupsState => consumerGroups; + +const getConsumerGroupsMap = (state: RootState) => consumerGroupsState(state).byID; +const getConsumerGroupsIDsList = (state: RootState) => consumerGroupsState(state).allIDs; const getConsumerGroupsListFetchingStatus = createFetchingSelector('GET_CONSUMER_GROUPS'); +const getConsumerGroupDetailsFetchingStatus = createFetchingSelector('GET_CONSUMER_GROUP_DETAILS'); export const getIsConsumerGroupsListFetched = createSelector( getConsumerGroupsListFetchingStatus, (status) => status === FetchStatus.fetched, ); -export const getConsumerGroupsList = createSelector(consumerGroupsState, (consumerGroups) => consumerGroups); \ No newline at end of file +export const getIsConsumerGroupDetailsFetched = createSelector( + getConsumerGroupDetailsFetchingStatus, + (status) => status === FetchStatus.fetched, +); + +export const getConsumerGroupsList = createSelector( + getIsConsumerGroupsListFetched, + getConsumerGroupsMap, + getConsumerGroupsIDsList, + (isFetched, byID, ids) => { + if (!isFetched) { + return []; + } + + return ids.map(key => byID[key]); + }, +); + +const getConsumerGroupID = (_: RootState, consumerGroupID: ConsumerGroupID) => consumerGroupID; + +export const getConsumerGroupByID = createSelector( + getConsumerGroupsMap, + getConsumerGroupID, + (consumerGroups, consumerGroupID) => consumerGroups[consumerGroupID], +); diff --git a/kafka-ui-react-app/src/theme/index.scss b/kafka-ui-react-app/src/theme/index.scss index bc30b825ab..a4811b1264 100644 --- a/kafka-ui-react-app/src/theme/index.scss +++ b/kafka-ui-react-app/src/theme/index.scss @@ -31,3 +31,7 @@ code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } + +.cursor-pointer { + cursor: pointer; +} diff --git a/pom.xml b/pom.xml index 60d8d0ecb0..7cfc6b5b0d 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,7 @@ 4.2.2 1.6.0 1.2.32 + 2.4.1 com.provectus