From 7720cc2653d3c46b97954060afd225d71fe6296c Mon Sep 17 00:00:00 2001 From: Octavian Ciubotaru Date: Thu, 1 Aug 2024 20:49:49 +0300 Subject: [PATCH 1/6] BE: KC: Fix connector listing with STOPPED state (#511) --- contract/src/main/resources/swagger/kafbat-ui-api.yaml | 1 + contract/src/main/resources/swagger/kafka-connect-api.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index 19be2abaa..7ca62831f 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -3472,6 +3472,7 @@ components: - UNASSIGNED - TASK_FAILED - RESTARTING + - STOPPED ConnectorAction: type: string diff --git a/contract/src/main/resources/swagger/kafka-connect-api.yaml b/contract/src/main/resources/swagger/kafka-connect-api.yaml index dd8d85db4..e014d5529 100644 --- a/contract/src/main/resources/swagger/kafka-connect-api.yaml +++ b/contract/src/main/resources/swagger/kafka-connect-api.yaml @@ -448,6 +448,7 @@ components: - PAUSED - UNASSIGNED - RESTARTING + - STOPPED worker_id: type: string trace: From 42b3ae86779d3dd548e6cc5b907004bba67e19e5 Mon Sep 17 00:00:00 2001 From: Scott Busche Date: Thu, 1 Aug 2024 12:51:44 -0500 Subject: [PATCH 2/6] BE: Fix KafkaConsumerGroupTests on Windows (#261) Co-authored-by: Roman Zabaluev --- .../io/kafbat/ui/KafkaConsumerGroupTests.java | 67 +++++++++---------- .../io/kafbat/ui/service/acl/AclCsvTest.java | 29 +++++--- .../ui/service/acl/AclsServiceTest.java | 4 +- 3 files changed, 53 insertions(+), 47 deletions(-) diff --git a/api/src/test/java/io/kafbat/ui/KafkaConsumerGroupTests.java b/api/src/test/java/io/kafbat/ui/KafkaConsumerGroupTests.java index b7bd2dcb3..5f97317f2 100644 --- a/api/src/test/java/io/kafbat/ui/KafkaConsumerGroupTests.java +++ b/api/src/test/java/io/kafbat/ui/KafkaConsumerGroupTests.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.Properties; import java.util.UUID; -import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -32,7 +31,6 @@ public class KafkaConsumerGroupTests extends AbstractIntegrationTest { @Test void shouldNotFoundWhenNoSuchConsumerGroupId() { String groupId = "groupA"; - String expError = "The group id does not exist"; webTestClient .delete() .uri("/api/clusters/{clusterName}/consumer-groups/{groupId}", LOCAL, groupId) @@ -47,12 +45,13 @@ void shouldOkWhenConsumerGroupIsNotActive() { //Create a consumer and subscribe to the topic String groupId = UUID.randomUUID().toString(); - val consumer = createTestConsumerWithGroupId(groupId); - consumer.subscribe(List.of(topicName)); - consumer.poll(Duration.ofMillis(100)); + try (val consumer = createTestConsumerWithGroupId(groupId)) { + consumer.subscribe(List.of(topicName)); + consumer.poll(Duration.ofMillis(100)); - //Unsubscribe from all topics to be able to delete this consumer - consumer.unsubscribe(); + //Unsubscribe from all topics to be able to delete this consumer + consumer.unsubscribe(); + } //Delete the consumer when it's INACTIVE and check webTestClient @@ -69,24 +68,24 @@ void shouldBeBadRequestWhenConsumerGroupIsActive() { //Create a consumer and subscribe to the topic String groupId = UUID.randomUUID().toString(); - val consumer = createTestConsumerWithGroupId(groupId); - consumer.subscribe(List.of(topicName)); - consumer.poll(Duration.ofMillis(100)); + try (val consumer = createTestConsumerWithGroupId(groupId)) { + consumer.subscribe(List.of(topicName)); + consumer.poll(Duration.ofMillis(100)); - //Try to delete the consumer when it's ACTIVE - String expError = "The group is not empty"; - webTestClient - .delete() - .uri("/api/clusters/{clusterName}/consumer-groups/{groupId}", LOCAL, groupId) - .exchange() - .expectStatus() - .isBadRequest(); + //Try to delete the consumer when it's ACTIVE + webTestClient + .delete() + .uri("/api/clusters/{clusterName}/consumer-groups/{groupId}", LOCAL, groupId) + .exchange() + .expectStatus() + .isBadRequest(); + } } @Test void shouldReturnConsumerGroupsWithPagination() throws Exception { - try (var groups1 = startConsumerGroups(3, "cgPageTest1"); - var groups2 = startConsumerGroups(2, "cgPageTest2")) { + try (var ignored = startConsumerGroups(3, "cgPageTest1"); + var ignored1 = startConsumerGroups(2, "cgPageTest2")) { webTestClient .get() .uri("/api/clusters/{clusterName}/consumer-groups/paged?perPage=3&search=cgPageTest", LOCAL) @@ -114,19 +113,19 @@ void shouldReturnConsumerGroupsWithPagination() throws Exception { }); webTestClient - .get() - .uri("/api/clusters/{clusterName}/consumer-groups/paged?perPage=10&&search" - + "=cgPageTest&orderBy=NAME&sortOrder=DESC", LOCAL) - .exchange() - .expectStatus() - .isOk() - .expectBody(ConsumerGroupsPageResponseDTO.class) - .value(page -> { - assertThat(page.getPageCount()).isEqualTo(1); - assertThat(page.getConsumerGroups().size()).isEqualTo(5); - assertThat(page.getConsumerGroups()) - .isSortedAccordingTo(Comparator.comparing(ConsumerGroupDTO::getGroupId).reversed()); - }); + .get() + .uri("/api/clusters/{clusterName}/consumer-groups/paged?perPage=10&&search" + + "=cgPageTest&orderBy=NAME&sortOrder=DESC", LOCAL) + .exchange() + .expectStatus() + .isOk() + .expectBody(ConsumerGroupsPageResponseDTO.class) + .value(page -> { + assertThat(page.getPageCount()).isEqualTo(1); + assertThat(page.getConsumerGroups().size()).isEqualTo(5); + assertThat(page.getConsumerGroups()) + .isSortedAccordingTo(Comparator.comparing(ConsumerGroupDTO::getGroupId).reversed()); + }); webTestClient .get() @@ -156,7 +155,7 @@ private Closeable startConsumerGroups(int count, String consumerGroupPrefix) { return consumer; }) .limit(count) - .collect(Collectors.toList()); + .toList(); return () -> { consumers.forEach(KafkaConsumer::close); deleteTopic(topicName); diff --git a/api/src/test/java/io/kafbat/ui/service/acl/AclCsvTest.java b/api/src/test/java/io/kafbat/ui/service/acl/AclCsvTest.java index c6b725283..a9648f11c 100644 --- a/api/src/test/java/io/kafbat/ui/service/acl/AclCsvTest.java +++ b/api/src/test/java/io/kafbat/ui/service/acl/AclCsvTest.java @@ -6,6 +6,7 @@ import io.kafbat.ui.exception.ValidationException; import java.util.Collection; import java.util.List; +import java.util.stream.Stream; import org.apache.kafka.common.acl.AccessControlEntry; import org.apache.kafka.common.acl.AclBinding; import org.apache.kafka.common.acl.AclOperation; @@ -15,6 +16,8 @@ import org.apache.kafka.common.resource.ResourceType; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; class AclCsvTest { @@ -29,22 +32,26 @@ class AclCsvTest { ); @ParameterizedTest - @ValueSource(strings = { - "Principal,ResourceType, PatternType, ResourceName,Operation,PermissionType,Host\n" - + "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*\n" - + "User:test2,GROUP,PREFIXED,group1,DESCRIBE,DENY,localhost", - - //without header - "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*\n" - + "\n" - + "User:test2,GROUP,PREFIXED,group1,DESCRIBE,DENY,localhost" - + "\n" - }) + @MethodSource void parsesValidInputCsv(String csvString) { Collection parsed = AclCsv.parseCsv(csvString); assertThat(parsed).containsExactlyInAnyOrderElementsOf(TEST_BINDINGS); } + private static Stream parsesValidInputCsv() { + return Stream.of( + Arguments.of( + "Principal,ResourceType, PatternType, ResourceName,Operation,PermissionType,Host" + System.lineSeparator() + + "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*" + System.lineSeparator() + + "User:test2,GROUP,PREFIXED,group1,DESCRIBE,DENY,localhost"), + Arguments.of( + //without header + "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*" + System.lineSeparator() + + System.lineSeparator() + + "User:test2,GROUP,PREFIXED,group1,DESCRIBE,DENY,localhost" + + System.lineSeparator())); + } + @ParameterizedTest @ValueSource(strings = { // columns > 7 diff --git a/api/src/test/java/io/kafbat/ui/service/acl/AclsServiceTest.java b/api/src/test/java/io/kafbat/ui/service/acl/AclsServiceTest.java index 5f43f51cd..189e7c060 100644 --- a/api/src/test/java/io/kafbat/ui/service/acl/AclsServiceTest.java +++ b/api/src/test/java/io/kafbat/ui/service/acl/AclsServiceTest.java @@ -68,8 +68,8 @@ void testSyncAclWithAclCsv() { aclsService.syncAclWithAclCsv( CLUSTER, - "Principal,ResourceType, PatternType, ResourceName,Operation,PermissionType,Host\n" - + "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*\n" + "Principal,ResourceType, PatternType, ResourceName,Operation,PermissionType,Host" + System.lineSeparator() + + "User:test1,TOPIC,LITERAL,*,READ,ALLOW,*" + System.lineSeparator() + "User:test3,GROUP,PREFIXED,groupNew,DESCRIBE,DENY,localhost" ).block(); From 654978b3eef32a6dda38bd69b5ee53bd294ddfd9 Mon Sep 17 00:00:00 2001 From: Renat Kalimulin <103274228+Nilumilak@users.noreply.github.com> Date: Thu, 1 Aug 2024 20:56:42 +0300 Subject: [PATCH 3/6] FE: UX: Fix header opacity (#505) --- frontend/src/theme/theme.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/theme/theme.ts b/frontend/src/theme/theme.ts index 6c8284afd..f6cd2bacc 100644 --- a/frontend/src/theme/theme.ts +++ b/frontend/src/theme/theme.ts @@ -501,7 +501,7 @@ export const theme = { menu: { primary: { backgroundColor: { - normal: hexToRgba(Colors.brand[95], 0), + normal: Colors.brand[0], hover: hexToRgba(Colors.brand[95], 0.03), active: hexToRgba(Colors.brand[95], 0.05), }, @@ -985,7 +985,7 @@ export const darkTheme: ThemeType = { menu: { primary: { backgroundColor: { - normal: hexToRgba(Colors.brand[0], 0), + normal: Colors.brand[90], hover: hexToRgba(Colors.brand[0], 0.05), active: hexToRgba(Colors.brand[0], 0.1), }, From 053118698df54711d9a3d1d4221e3fe620138285 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Wed, 7 Aug 2024 22:08:28 +0300 Subject: [PATCH 4/6] Infra: Fix e2e compose run (#519) --- .github/workflows/e2e-run.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-run.yml b/.github/workflows/e2e-run.yml index a0bb764e1..b79e07897 100644 --- a/.github/workflows/e2e-run.yml +++ b/.github/workflows/e2e-run.yml @@ -103,8 +103,8 @@ jobs: run: | mkdir -p ./e2e-tests/target/selenoid-results/video mkdir -p ./e2e-tests/target/selenoid-results/logs - docker-compose -f ./e2e-tests/selenoid/selenoid-ci.yaml up -d - docker-compose -f ./documentation/compose/e2e-tests.yaml up -d + docker compose -f ./e2e-tests/selenoid/selenoid-ci.yaml up -d + docker compose -f ./documentation/compose/e2e-tests.yaml up -d - name: Dump Docker logs on failure if: failure() From 2bb9d5cfab3107e7e1032ec82690d83453aa7218 Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Thu, 8 Aug 2024 00:31:28 +0500 Subject: [PATCH 5/6] BE: Chore: Use dto builders in controller package (#504) Co-authored-by: Roman Zabaluev --- .../ui/controller/AccessController.java | 39 ++++++++++--------- .../kafbat/ui/controller/KsqlController.java | 1 + .../ui/controller/TopicsController.java | 20 ++++------ .../kafbat/ui/emitter/MessageFiltersTest.java | 6 ++- contract/pom.xml | 11 ++++++ 5 files changed, 44 insertions(+), 33 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/controller/AccessController.java b/api/src/main/java/io/kafbat/ui/controller/AccessController.java index 5833f2e3c..e5b1ea438 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AccessController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AccessController.java @@ -45,33 +45,34 @@ public Mono> getUserAuthInfo(ServerWebExch .map(SecurityContext::getAuthentication) .map(Principal::getName); + var builder = AuthenticationInfoDTO.builder() + .rbacEnabled(accessControlService.isRbacEnabled()); + return userName .zipWith(permissions) - .map(data -> { - var dto = new AuthenticationInfoDTO(accessControlService.isRbacEnabled()); - dto.setUserInfo(new UserInfoDTO(data.getT1(), data.getT2())); - return dto; - }) - .switchIfEmpty(Mono.just(new AuthenticationInfoDTO(accessControlService.isRbacEnabled()))) + .map(data -> (AuthenticationInfoDTO) builder + .userInfo(new UserInfoDTO(data.getT1(), data.getT2())) + .build() + ) + .switchIfEmpty(Mono.just(builder.build())) .map(ResponseEntity::ok); } private List mapPermissions(List permissions, List clusters) { return permissions .stream() - .map(permission -> { - UserPermissionDTO dto = new UserPermissionDTO(); - dto.setClusters(clusters); - dto.setResource(ResourceTypeDTO.fromValue(permission.getResource().toString().toUpperCase())); - dto.setValue(permission.getValue()); - dto.setActions(permission.getParsedActions() - .stream() - .map(p -> p.name().toUpperCase()) - .map(this::mapAction) - .filter(Objects::nonNull) - .toList()); - return dto; - }) + .map(permission -> (UserPermissionDTO) UserPermissionDTO.builder() + .clusters(clusters) + .resource(ResourceTypeDTO.fromValue(permission.getResource().toString().toUpperCase())) + .value(permission.getValue()) + .actions(permission.getParsedActions() + .stream() + .map(p -> p.name().toUpperCase()) + .map(this::mapAction) + .filter(Objects::nonNull) + .toList()) + .build() + ) .toList(); } diff --git a/api/src/main/java/io/kafbat/ui/controller/KsqlController.java b/api/src/main/java/io/kafbat/ui/controller/KsqlController.java index 6c633ee2c..d8b3203a9 100644 --- a/api/src/main/java/io/kafbat/ui/controller/KsqlController.java +++ b/api/src/main/java/io/kafbat/ui/controller/KsqlController.java @@ -53,6 +53,7 @@ public Mono> executeKsql(String cluster } @Override + @SuppressWarnings("unchecked") public Mono>> openKsqlResponsePipe(String clusterName, String pipeId, ServerWebExchange exchange) { diff --git a/api/src/main/java/io/kafbat/ui/controller/TopicsController.java b/api/src/main/java/io/kafbat/ui/controller/TopicsController.java index 53e9fc8cd..6ccfd18fd 100644 --- a/api/src/main/java/io/kafbat/ui/controller/TopicsController.java +++ b/api/src/main/java/io/kafbat/ui/controller/TopicsController.java @@ -350,18 +350,12 @@ private Comparator getComparatorForTopic( if (orderBy == null) { return defaultComparator; } - switch (orderBy) { - case TOTAL_PARTITIONS: - return Comparator.comparing(InternalTopic::getPartitionCount); - case OUT_OF_SYNC_REPLICAS: - return Comparator.comparing(t -> t.getReplicas() - t.getInSyncReplicas()); - case REPLICATION_FACTOR: - return Comparator.comparing(InternalTopic::getReplicationFactor); - case SIZE: - return Comparator.comparing(InternalTopic::getSegmentSize); - case NAME: - default: - return defaultComparator; - } + return switch (orderBy) { + case TOTAL_PARTITIONS -> Comparator.comparing(InternalTopic::getPartitionCount); + case OUT_OF_SYNC_REPLICAS -> Comparator.comparing(t -> t.getReplicas() - t.getInSyncReplicas()); + case REPLICATION_FACTOR -> Comparator.comparing(InternalTopic::getReplicationFactor); + case SIZE -> Comparator.comparing(InternalTopic::getSegmentSize); + default -> defaultComparator; + }; } } diff --git a/api/src/test/java/io/kafbat/ui/emitter/MessageFiltersTest.java b/api/src/test/java/io/kafbat/ui/emitter/MessageFiltersTest.java index cae8629eb..617cfb0c1 100644 --- a/api/src/test/java/io/kafbat/ui/emitter/MessageFiltersTest.java +++ b/api/src/test/java/io/kafbat/ui/emitter/MessageFiltersTest.java @@ -199,6 +199,10 @@ void testBase64DecodingWorks() { } private TopicMessageDTO msg() { - return new TopicMessageDTO(1, -1L, OffsetDateTime.now()); + return TopicMessageDTO.builder() + .partition(1) + .offset(-1L) + .timestamp(OffsetDateTime.now()) + .build(); } } diff --git a/contract/pom.xml b/contract/pom.xml index 994185c96..8d7e76cea 100644 --- a/contract/pom.xml +++ b/contract/pom.xml @@ -46,6 +46,11 @@ javax.annotation-api 1.3.2 + + org.projectlombok + lombok + ${org.projectlombok.version} + @@ -100,6 +105,12 @@ true true java8 + false + + @lombok.experimental.SuperBuilder + @lombok.NoArgsConstructor + @lombok.AllArgsConstructor + From 273e64cd1b7096769fad294918017a4db13a1fb4 Mon Sep 17 00:00:00 2001 From: bachmanity1 <81428651+bachmanity1@users.noreply.github.com> Date: Sun, 11 Aug 2024 00:05:08 +0900 Subject: [PATCH 6/6] BE: RBAC: Impl separate permissions for topic analysis (#513) Co-authored-by: Roman Zabaluev --- .../java/io/kafbat/ui/controller/TopicsController.java | 9 +++++---- .../io/kafbat/ui/model/rbac/permission/TopicAction.java | 2 ++ .../src/components/Topics/Topic/Statistics/Metrics.tsx | 4 ++-- .../components/Topics/Topic/Statistics/Statistics.tsx | 2 +- frontend/src/components/Topics/Topic/Topic.tsx | 9 +++++++-- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/controller/TopicsController.java b/api/src/main/java/io/kafbat/ui/controller/TopicsController.java index 6ccfd18fd..c230f4751 100644 --- a/api/src/main/java/io/kafbat/ui/controller/TopicsController.java +++ b/api/src/main/java/io/kafbat/ui/controller/TopicsController.java @@ -1,9 +1,10 @@ package io.kafbat.ui.controller; +import static io.kafbat.ui.model.rbac.permission.TopicAction.ANALYSIS_RUN; +import static io.kafbat.ui.model.rbac.permission.TopicAction.ANALYSIS_VIEW; import static io.kafbat.ui.model.rbac.permission.TopicAction.CREATE; import static io.kafbat.ui.model.rbac.permission.TopicAction.DELETE; import static io.kafbat.ui.model.rbac.permission.TopicAction.EDIT; -import static io.kafbat.ui.model.rbac.permission.TopicAction.MESSAGES_READ; import static io.kafbat.ui.model.rbac.permission.TopicAction.VIEW; import static java.util.stream.Collectors.toList; @@ -272,7 +273,7 @@ public Mono> analyzeTopic(String clusterName, String topicN var context = AccessContext.builder() .cluster(clusterName) - .topicActions(topicName, MESSAGES_READ) + .topicActions(topicName, ANALYSIS_RUN) .operationName("analyzeTopic") .build(); @@ -288,7 +289,7 @@ public Mono> cancelTopicAnalysis(String clusterName, String ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .topicActions(topicName, MESSAGES_READ) + .topicActions(topicName, ANALYSIS_RUN) .operationName("cancelTopicAnalysis") .build(); @@ -306,7 +307,7 @@ public Mono> getTopicAnalysis(String clusterNam var context = AccessContext.builder() .cluster(clusterName) - .topicActions(topicName, MESSAGES_READ) + .topicActions(topicName, ANALYSIS_VIEW) .operationName("getTopicAnalysis") .build(); diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/TopicAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/TopicAction.java index 8efbc6fe0..c1b0aeb16 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/TopicAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/TopicAction.java @@ -13,6 +13,8 @@ public enum TopicAction implements PermissibleAction { MESSAGES_READ(VIEW), MESSAGES_PRODUCE(VIEW), MESSAGES_DELETE(VIEW, EDIT), + ANALYSIS_VIEW(VIEW), + ANALYSIS_RUN(VIEW, ANALYSIS_VIEW), ; diff --git a/frontend/src/components/Topics/Topic/Statistics/Metrics.tsx b/frontend/src/components/Topics/Topic/Statistics/Metrics.tsx index f24d6bf5e..aec1b53bb 100644 --- a/frontend/src/components/Topics/Topic/Statistics/Metrics.tsx +++ b/frontend/src/components/Topics/Topic/Statistics/Metrics.tsx @@ -60,7 +60,7 @@ const Metrics: React.FC = () => { buttonSize="M" permission={{ resource: ResourceType.TOPIC, - action: Action.MESSAGES_READ, + action: Action.ANALYSIS_RUN, value: params.topicName, }} > @@ -110,7 +110,7 @@ const Metrics: React.FC = () => { buttonSize="S" permission={{ resource: ResourceType.TOPIC, - action: Action.MESSAGES_READ, + action: Action.ANALYSIS_RUN, value: params.topicName, }} > diff --git a/frontend/src/components/Topics/Topic/Statistics/Statistics.tsx b/frontend/src/components/Topics/Topic/Statistics/Statistics.tsx index fd275028b..2088cd46b 100644 --- a/frontend/src/components/Topics/Topic/Statistics/Statistics.tsx +++ b/frontend/src/components/Topics/Topic/Statistics/Statistics.tsx @@ -31,7 +31,7 @@ const Statistics: React.FC = () => { buttonSize="M" permission={{ resource: ResourceType.TOPIC, - action: Action.MESSAGES_READ, + action: Action.ANALYSIS_RUN, value: params.topicName, }} > diff --git a/frontend/src/components/Topics/Topic/Topic.tsx b/frontend/src/components/Topics/Topic/Topic.tsx index b5bcf8d52..a40bcfc12 100644 --- a/frontend/src/components/Topics/Topic/Topic.tsx +++ b/frontend/src/components/Topics/Topic/Topic.tsx @@ -194,12 +194,17 @@ const Topic: React.FC = () => { > Settings - (isActive ? 'is-active' : '')} + permission={{ + resource: ResourceType.TOPIC, + action: Action.ANALYSIS_VIEW, + value: topicName, + }} > Statistics - + }>