From 8c7012679d7ec0293e0f96a1740690bf0bfc994e Mon Sep 17 00:00:00 2001 From: William Chen Date: Sun, 21 Jul 2024 14:15:49 -0700 Subject: [PATCH 01/29] Docs: Remove missing compose links (#487) --- documentation/compose/DOCKER_COMPOSE.md | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/documentation/compose/DOCKER_COMPOSE.md b/documentation/compose/DOCKER_COMPOSE.md index fdb16db67..57a5cf4d0 100644 --- a/documentation/compose/DOCKER_COMPOSE.md +++ b/documentation/compose/DOCKER_COMPOSE.md @@ -2,15 +2,13 @@ 1. [kafka-ui.yaml](./kafbat-ui.yaml) - Default configuration with 2 kafka clusters with two nodes of Schema Registry, one kafka-connect and a few dummy topics. 2. [kafka-ui-arm64.yaml](../../.dev/dev_arm64.yaml) - Default configuration for ARM64(Mac M1) architecture with 1 kafka cluster without zookeeper with one node of Schema Registry, one kafka-connect and a few dummy topics. -3. [kafka-clusters-only.yaml](./kafka-clusters-only.yaml) - A configuration for development purposes, everything besides `kafka-ui` itself (to be run locally). -4. [kafka-ui-ssl.yml](./kafka-ssl.yml) - Connect to Kafka via TLS/SSL -5. [kafka-cluster-sr-auth.yaml](./cluster-sr-auth.yaml) - Schema registry with authentication. -6. [kafka-ui-auth-context.yaml](./auth-context.yaml) - Basic (username/password) authentication with custom path (URL) (issue 861). -7. [e2e-tests.yaml](./e2e-tests.yaml) - Configuration with different connectors (github-source, s3, sink-activities, source-activities) and Ksql functionality. -8. [kafka-ui-jmx-secured.yml](./ui-jmx-secured.yml) - Kafka’s JMX with SSL and authentication. -9. [kafka-ui-reverse-proxy.yaml](./nginx-proxy.yaml) - An example for using the app behind a proxy (like nginx). -10. [kafka-ui-sasl.yaml](./ui-sasl.yaml) - SASL auth for Kafka. -11. [kafka-ui-traefik-proxy.yaml](./traefik-proxy.yaml) - Traefik specific proxy configuration. -12. [oauth-cognito.yaml](./oauth-cognito.yaml) - OAuth2 with Cognito -13. [kafka-ui-with-jmx-exporter.yaml](./ui-with-jmx-exporter.yaml) - A configuration with 2 kafka clusters with enabled prometheus jmx exporters instead of jmx. -14. [kafka-with-zookeeper.yaml](./kafka-zookeeper.yaml) - An example for using kafka with zookeeper \ No newline at end of file +3. [kafka-ui-ssl.yml](./kafka-ssl.yml) - Connect to Kafka via TLS/SSL +4. [kafka-cluster-sr-auth.yaml](./cluster-sr-auth.yaml) - Schema registry with authentication. +5. [kafka-ui-auth-context.yaml](./auth-context.yaml) - Basic (username/password) authentication with custom path (URL) (issue 861). +6. [e2e-tests.yaml](./e2e-tests.yaml) - Configuration with different connectors (github-source, s3, sink-activities, source-activities) and Ksql functionality. +7. [kafka-ui-jmx-secured.yml](./ui-jmx-secured.yml) - Kafka’s JMX with SSL and authentication. +8. [kafka-ui-reverse-proxy.yaml](./nginx-proxy.yaml) - An example for using the app behind a proxy (like nginx). +9. [kafka-ui-sasl.yaml](./ui-sasl.yaml) - SASL auth for Kafka. +10. [kafka-ui-traefik-proxy.yaml](./traefik-proxy.yaml) - Traefik specific proxy configuration. +11. [kafka-ui-with-jmx-exporter.yaml](./ui-with-jmx-exporter.yaml) - A configuration with 2 kafka clusters with enabled prometheus jmx exporters instead of jmx. +12. [kafka-with-zookeeper.yaml](./kafka-zookeeper.yaml) - An example for using kafka with zookeeper \ No newline at end of file From 7720cc2653d3c46b97954060afd225d71fe6296c Mon Sep 17 00:00:00 2001 From: Octavian Ciubotaru Date: Thu, 1 Aug 2024 20:49:49 +0300 Subject: [PATCH 02/29] 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 03/29] 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 04/29] 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 05/29] 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 06/29] 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 07/29] 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 - + }> From 04d15b365c19b41c3fcc6e7c93465387b9b5794e Mon Sep 17 00:00:00 2001 From: tnewman-at-gm Date: Wed, 18 Sep 2024 19:16:00 -0400 Subject: [PATCH 08/29] Auth: Support Azure Entra (Event Hub with Kafka Protocol) (#530) --- api/pom.xml | 6 + .../azure/AzureEntraLoginCallbackHandler.java | 110 ++++++++++++ .../azure/AzureEntraOAuthBearerToken.java | 62 +++++++ .../AzureEntraLoginCallbackHandlerTest.java | 169 ++++++++++++++++++ .../azure/AzureEntraOAuthBearerTokenTest.java | 59 ++++++ frontend/src/lib/constants.ts | 1 + .../src/widgets/ClusterConfigForm/schema.ts | 3 + .../ClusterConfigForm/utils/getJaasConfig.ts | 2 + .../utils/transformFormDataToPayload.ts | 9 + 9 files changed, 421 insertions(+) create mode 100644 api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandler.java create mode 100644 api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerToken.java create mode 100644 api/src/test/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandlerTest.java create mode 100644 api/src/test/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerTokenTest.java diff --git a/api/pom.xml b/api/pom.xml index 70ab59c5c..17bae6b85 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -94,6 +94,12 @@ 2.1.0 + + com.azure + azure-identity + 1.13.0 + + org.apache.avro avro diff --git a/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandler.java b/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandler.java new file mode 100644 index 000000000..c6c08b6fd --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandler.java @@ -0,0 +1,110 @@ +package io.kafbat.ui.config.auth.azure; + +import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; + +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import com.azure.identity.DefaultAzureCredentialBuilder; +import java.net.URI; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.AppConfigurationEntry; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.common.security.auth.AuthenticateCallbackHandler; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback; + +@Slf4j +public class AzureEntraLoginCallbackHandler implements AuthenticateCallbackHandler { + + private static final Duration ACCESS_TOKEN_REQUEST_BLOCK_TIME = Duration.ofSeconds(10); + + private static final int ACCESS_TOKEN_REQUEST_MAX_RETRIES = 6; + + private static final String TOKEN_AUDIENCE_FORMAT = "%s://%s/.default"; + + static TokenCredential tokenCredential = new DefaultAzureCredentialBuilder().build(); + + private TokenRequestContext tokenRequestContext; + + @Override + public void configure( + Map configs, String mechanism, List jaasConfigEntries) { + tokenRequestContext = buildTokenRequestContext(configs); + } + + private TokenRequestContext buildTokenRequestContext(Map configs) { + URI uri = buildEventHubsServerUri(configs); + String tokenAudience = buildTokenAudience(uri); + + TokenRequestContext request = new TokenRequestContext(); + request.addScopes(tokenAudience); + return request; + } + + private URI buildEventHubsServerUri(Map configs) { + final List bootstrapServers = (List) configs.get(BOOTSTRAP_SERVERS_CONFIG); + + if (null == bootstrapServers) { + final String message = BOOTSTRAP_SERVERS_CONFIG + " is missing from the Kafka configuration."; + log.error(message); + throw new IllegalArgumentException(message); + } + + if (bootstrapServers.size() != 1) { + final String message = + BOOTSTRAP_SERVERS_CONFIG + + " contains multiple bootstrap servers. Only a single bootstrap server is supported."; + log.error(message); + throw new IllegalArgumentException(message); + } + + return URI.create("https://" + bootstrapServers.get(0)); + } + + private String buildTokenAudience(URI uri) { + return String.format(TOKEN_AUDIENCE_FORMAT, uri.getScheme(), uri.getHost()); + } + + @Override + public void handle(Callback[] callbacks) throws UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof OAuthBearerTokenCallback oauthCallback) { + handleOAuthCallback(oauthCallback); + } else { + throw new UnsupportedCallbackException(callback); + } + } + } + + private void handleOAuthCallback(OAuthBearerTokenCallback oauthCallback) { + try { + final OAuthBearerToken token = tokenCredential + .getToken(tokenRequestContext) + .map(AzureEntraOAuthBearerToken::new) + .timeout(ACCESS_TOKEN_REQUEST_BLOCK_TIME) + .doOnError(e -> log.warn("Failed to acquire Azure token for Event Hub Authentication. Retrying.", e)) + .retry(ACCESS_TOKEN_REQUEST_MAX_RETRIES) + .block(); + + oauthCallback.token(token); + } catch (final RuntimeException e) { + final String message = + "Failed to acquire Azure token for Event Hub Authentication. " + + "Please ensure valid Azure credentials are configured."; + log.error(message, e); + oauthCallback.error("invalid_grant", message, null); + } + } + + public void close() { + // NOOP + } + + void setTokenCredential(final TokenCredential tokenCredential) { + AzureEntraLoginCallbackHandler.tokenCredential = tokenCredential; + } +} diff --git a/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerToken.java b/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerToken.java new file mode 100644 index 000000000..e9c315940 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerToken.java @@ -0,0 +1,62 @@ +package io.kafbat.ui.config.auth.azure; + +import com.azure.core.credential.AccessToken; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.kafka.common.errors.SaslAuthenticationException; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken; + +public class AzureEntraOAuthBearerToken implements OAuthBearerToken { + + private final AccessToken accessToken; + + private final JWTClaimsSet claims; + + public AzureEntraOAuthBearerToken(AccessToken accessToken) { + this.accessToken = accessToken; + + try { + claims = JWTParser.parse(accessToken.getToken()).getJWTClaimsSet(); + } catch (ParseException exception) { + throw new SaslAuthenticationException("Unable to parse the access token", exception); + } + } + + @Override + public String value() { + return accessToken.getToken(); + } + + @Override + public Long startTimeMs() { + return claims.getIssueTime().getTime(); + } + + @Override + public long lifetimeMs() { + return claims.getExpirationTime().getTime(); + } + + @Override + public Set scope() { + // Referring to + // https://docs.microsoft.com/azure/active-directory/develop/access-tokens#payload-claims, the + // scp + // claim is a String which is presented as a space separated list. + return Arrays.stream(((String) claims.getClaim("scp")).split(" ")).collect(Collectors.toSet()); + } + + @Override + public String principalName() { + return (String) claims.getClaim("upn"); + } + + public boolean isExpired() { + return accessToken.isExpired(); + } +} diff --git a/api/src/test/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandlerTest.java b/api/src/test/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandlerTest.java new file mode 100644 index 000000000..d2e39ce75 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandlerTest.java @@ -0,0 +1,169 @@ +package io.kafbat.ui.config.auth.azure; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.azure.core.credential.AccessToken; +import com.azure.core.credential.TokenCredential; +import com.azure.core.credential.TokenRequestContext; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.UnsupportedCallbackException; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerToken; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; + +@ExtendWith(MockitoExtension.class) +public class AzureEntraLoginCallbackHandlerTest { + + // These are not real tokens. It was generated using fake values with an invalid signature, + // so it is safe to store here. + private static final String VALID_SAMPLE_TOKEN = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjlHbW55RlBraGMzaE91UjIybXZTdmduTG83WSIsImtpZCI6IjlHbW55" + + "RlBraGMzaE91UjIybXZTdmduTG83WSJ9.eyJhdWQiOiJodHRwczovL3NhbXBsZS5zZXJ2aWNlYnVzLndpbmRvd3MubmV0IiwiaX" + + "NzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvc2FtcGxlLyIsImlhdCI6MTY5ODQxNTkxMiwibmJmIjoxNjk4NDE1OTEzLCJleH" + + "AiOjE2OTg0MTU5MTQsImFjciI6IjEiLCJhaW8iOiJzYW1wbGUtYWlvIiwiYW1yIjpbXSwiYXBwaWQiOiJzYW1wbGUtYXBwLWlkIi" + + "wiYXBwaWRhY3IiOiIwIiwiZmFtaWx5X25hbWUiOiJTYW1wbGUiLCJnaXZlbl9uYW1lIjoiU2FtcGxlIiwiZ3JvdXBzIjpbXSwiaX" + + "BhZGRyIjoiMTI3LjAuMC4xIiwibmFtZSI6IlNhbXBsZSBOYW1lIiwib2lkIjoic2FtcGxlLW9pZCIsIm9ucHJlbV9zaWQiOiJzYW" + + "1wbGUtb25wcmVtX3NpZCIsInB1aWQiOiJzYW1wbGUtcHVpZCIsInJoIjoic2FtcGxlLXJoIiwic2NwIjoiZXZlbnRfaHViIHN0b3" + + "JhZ2VfYWNjb3VudCIsInN1YiI6IlNhbXBsZSBTdWJqZWN0IiwidGlkIjoic2FtcGxlLXRpZCIsInVuaXF1ZV9uYW1lIjoic2FtcG" + + "xlQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzYW1wbGVAbWljcm9zb2Z0LmNvbSIsInV0aSI6InNhbXBsZS11dGkiLCJ2ZXIiOiIxLj" + + "AiLCJ3aWRzIjpbXX0.DC_guYOsDlRc5GsXE39dn_zlBX54_Y8_mDTLXLgienl9dPMX5RE2X1QXGXA9ukZtptMzP_0wcoqDDjNrys" + + "GrNhztyeOr0YSeMMFq2NQ5vMBzLapwONwsnv55Hn0jOje9cqnMf43z1LHI6q6-rIIRz-SiTuoYUgOTxzFftpt-7FSqLjQpYEH7bL" + + "p-0yIU_aJUSb5HQTJbtYYOb54hsZ6VXpaiZ013qGtKODbHTG37kdoIw2MPn66CxanLZKeZM31IVxC-duAqxDgK4O2Ne6xRZRIPW1" + + "yt61QnZutWTJ4bAyhmplym3OWZ369cyiSJek0uyS5tibXeCYG4Kk8UQSFcsyfwgOsD0xvvcXcLexcUcEekoNBj6ixDhWssFzhC8T" + + "Npy8-QKNe_Tp6qHzJdI6OV71jpDkGvcmseLHC9GOxBWB0IdYbePTFK-rz2dkN3uMUiFwQJvEbORsq1IaQXj2esT0F7sMfqzWQF9h" + + "koVy4mJg_auvrZlnQkNPdLHfCacU33ZPwtuSS6b-0XolbxZ5DlJ4p1OJPeHl2xsi61qiHuCBsmnkLNtHmyxNTXGs7xc4dEQokaCK" + + "-FB_lzC3D4mkJMxKWopQGXnQtizaZjyclGpiUFs3mEauxC7RpsbanitxPFs7FK3mY0MQJk9JNVi1oM-8qfEp8nYT2DwFBhLcIp2z" + + "Q"; + + @Mock + private OAuthBearerTokenCallback oauthBearerTokenCallBack; + + @Mock + private OAuthBearerToken oauthBearerToken; + + @Mock + private TokenCredential tokenCredential; + + @Mock + private AccessToken accessToken; + + private AzureEntraLoginCallbackHandler azureEntraLoginCallbackHandler; + + @BeforeEach + public void beforeEach() { + azureEntraLoginCallbackHandler = new AzureEntraLoginCallbackHandler(); + azureEntraLoginCallbackHandler.setTokenCredential(tokenCredential); + } + + @Test + public void shouldProvideTokenToCallbackWithSuccessfulTokenRequest() + throws UnsupportedCallbackException { + final Map configs = new HashMap<>(); + configs.put( + "bootstrap.servers", + List.of("test-eh.servicebus.windows.net:9093")); + + when(tokenCredential.getToken(any(TokenRequestContext.class))).thenReturn(Mono.just(accessToken)); + when(accessToken.getToken()).thenReturn(VALID_SAMPLE_TOKEN); + + azureEntraLoginCallbackHandler.configure(configs, null, null); + azureEntraLoginCallbackHandler.handle(new Callback[] {oauthBearerTokenCallBack}); + + final ArgumentCaptor contextCaptor = + ArgumentCaptor.forClass(TokenRequestContext.class); + final ArgumentCaptor tokenCaptor = + ArgumentCaptor.forClass(OAuthBearerToken.class); + + verify(tokenCredential, times(1)).getToken(contextCaptor.capture()); + verify(oauthBearerTokenCallBack, times(0)).error(anyString(), anyString(), anyString()); + verify(oauthBearerTokenCallBack, times(1)).token(tokenCaptor.capture()); + + final TokenRequestContext tokenRequestContext = contextCaptor.getValue(); + assertThat(tokenRequestContext, is(notNullValue())); + assertThat( + tokenRequestContext.getScopes(), + is(List.of("https://test-eh.servicebus.windows.net/.default"))); + assertThat(tokenRequestContext.getClaims(), is(nullValue())); + assertThat(tokenRequestContext.getTenantId(), is(nullValue())); + assertFalse(tokenRequestContext.isCaeEnabled()); + + assertThat(tokenCaptor.getValue(), is(notNullValue())); + assertEquals(VALID_SAMPLE_TOKEN, tokenCaptor.getValue().value()); + } + + @Test + public void shouldProvideErrorToCallbackWithTokenError() throws UnsupportedCallbackException { + final Map configs = new HashMap<>(); + configs.put( + "bootstrap.servers", + List.of("test-eh.servicebus.windows.net:9093")); + + when(tokenCredential.getToken(any(TokenRequestContext.class))) + .thenThrow(new RuntimeException("failed to acquire token")); + + azureEntraLoginCallbackHandler.configure(configs, null, null); + azureEntraLoginCallbackHandler.handle(new Callback[] {oauthBearerTokenCallBack}); + + verify(oauthBearerTokenCallBack, times(1)) + .error( + "invalid_grant", + "Failed to acquire Azure token for Event Hub Authentication. " + + "Please ensure valid Azure credentials are configured.", + null); + verify(oauthBearerTokenCallBack, times(0)).token(any()); + } + + @Test + public void shouldThrowExceptionWithNullBootstrapServers() { + final Map configs = new HashMap<>(); + + assertThrows(IllegalArgumentException.class, () -> azureEntraLoginCallbackHandler.configure( + configs, null, null)); + } + + @Test + public void shouldThrowExceptionWithMultipleBootstrapServers() { + final Map configs = new HashMap<>(); + configs.put("bootstrap.servers", List.of("server1", "server2")); + + assertThrows(IllegalArgumentException.class, () -> azureEntraLoginCallbackHandler.configure( + configs, null, null)); + } + + @Test + public void shouldThrowExceptionWithUnsupportedCallback() { + assertThrows(UnsupportedCallbackException.class, () -> azureEntraLoginCallbackHandler.handle( + new Callback[] {mock(Callback.class)})); + } + + @Test + public void shouldDoNothingOnClose() { + azureEntraLoginCallbackHandler.close(); + } + + @Test + public void shouldSupportDefaultConstructor() { + new AzureEntraLoginCallbackHandler(); + } +} diff --git a/api/src/test/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerTokenTest.java b/api/src/test/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerTokenTest.java new file mode 100644 index 000000000..84ed3b1cd --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerTokenTest.java @@ -0,0 +1,59 @@ +package io.kafbat.ui.config.auth.azure; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.azure.core.credential.AccessToken; +import java.time.OffsetDateTime; +import java.util.Set; +import org.apache.kafka.common.errors.SaslAuthenticationException; +import org.junit.jupiter.api.Test; + +public class AzureEntraOAuthBearerTokenTest { + + // These are not real tokens. It was generated using fake values with an invalid signature, + // so it is safe to store here. + private static final String VALID_SAMPLE_TOKEN = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IjlHbW55RlBraGMzaE91UjIybXZTdmduTG83WSIsImtpZCI6IjlHbW55" + + "RlBraGMzaE91UjIybXZTdmduTG83WSJ9.eyJhdWQiOiJodHRwczovL3NhbXBsZS5zZXJ2aWNlYnVzLndpbmRvd3MubmV0IiwiaX" + + "NzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvc2FtcGxlLyIsImlhdCI6MTY5ODQxNTkxMiwibmJmIjoxNjk4NDE1OTEzLCJleH" + + "AiOjE2OTg0MTU5MTQsImFjciI6IjEiLCJhaW8iOiJzYW1wbGUtYWlvIiwiYW1yIjpbXSwiYXBwaWQiOiJzYW1wbGUtYXBwLWlkIi" + + "wiYXBwaWRhY3IiOiIwIiwiZmFtaWx5X25hbWUiOiJTYW1wbGUiLCJnaXZlbl9uYW1lIjoiU2FtcGxlIiwiZ3JvdXBzIjpbXSwiaX" + + "BhZGRyIjoiMTI3LjAuMC4xIiwibmFtZSI6IlNhbXBsZSBOYW1lIiwib2lkIjoic2FtcGxlLW9pZCIsIm9ucHJlbV9zaWQiOiJzYW" + + "1wbGUtb25wcmVtX3NpZCIsInB1aWQiOiJzYW1wbGUtcHVpZCIsInJoIjoic2FtcGxlLXJoIiwic2NwIjoiZXZlbnRfaHViIHN0b3" + + "JhZ2VfYWNjb3VudCIsInN1YiI6IlNhbXBsZSBTdWJqZWN0IiwidGlkIjoic2FtcGxlLXRpZCIsInVuaXF1ZV9uYW1lIjoic2FtcG" + + "xlQG1pY3Jvc29mdC5jb20iLCJ1cG4iOiJzYW1wbGVAbWljcm9zb2Z0LmNvbSIsInV0aSI6InNhbXBsZS11dGkiLCJ2ZXIiOiIxLj" + + "AiLCJ3aWRzIjpbXX0.DC_guYOsDlRc5GsXE39dn_zlBX54_Y8_mDTLXLgienl9dPMX5RE2X1QXGXA9ukZtptMzP_0wcoqDDjNrys" + + "GrNhztyeOr0YSeMMFq2NQ5vMBzLapwONwsnv55Hn0jOje9cqnMf43z1LHI6q6-rIIRz-SiTuoYUgOTxzFftpt-7FSqLjQpYEH7bL" + + "p-0yIU_aJUSb5HQTJbtYYOb54hsZ6VXpaiZ013qGtKODbHTG37kdoIw2MPn66CxanLZKeZM31IVxC-duAqxDgK4O2Ne6xRZRIPW1" + + "yt61QnZutWTJ4bAyhmplym3OWZ369cyiSJek0uyS5tibXeCYG4Kk8UQSFcsyfwgOsD0xvvcXcLexcUcEekoNBj6ixDhWssFzhC8T" + + "Npy8-QKNe_Tp6qHzJdI6OV71jpDkGvcmseLHC9GOxBWB0IdYbePTFK-rz2dkN3uMUiFwQJvEbORsq1IaQXj2esT0F7sMfqzWQF9h" + + "koVy4mJg_auvrZlnQkNPdLHfCacU33ZPwtuSS6b-0XolbxZ5DlJ4p1OJPeHl2xsi61qiHuCBsmnkLNtHmyxNTXGs7xc4dEQokaCK" + + "-FB_lzC3D4mkJMxKWopQGXnQtizaZjyclGpiUFs3mEauxC7RpsbanitxPFs7FK3mY0MQJk9JNVi1oM-8qfEp8nYT2DwFBhLcIp2z" + + "Q"; + + @Test + void constructorShouldParseToken() { + final AccessToken accessToken = new AccessToken(VALID_SAMPLE_TOKEN, OffsetDateTime.MIN); + + final AzureEntraOAuthBearerToken azureOAuthBearerToken = + new AzureEntraOAuthBearerToken(accessToken); + + assertThat(azureOAuthBearerToken, is(notNullValue())); + assertThat(azureOAuthBearerToken.value(), is(VALID_SAMPLE_TOKEN)); + assertThat(azureOAuthBearerToken.startTimeMs(), is(1698415912000L)); + assertThat(azureOAuthBearerToken.lifetimeMs(), is(1698415914000L)); + assertThat(azureOAuthBearerToken.scope(), is(Set.of("event_hub", "storage_account"))); + assertThat(azureOAuthBearerToken.principalName(), is("sample@microsoft.com")); + assertTrue(azureOAuthBearerToken.isExpired()); + } + + @Test + void constructorShouldRejectInvalidToken() { + assertThrows(SaslAuthenticationException.class, () -> new AzureEntraOAuthBearerToken( + new AccessToken("invalid", OffsetDateTime.MIN))); + } +} diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts index 102b79faa..3d765ee0a 100644 --- a/frontend/src/lib/constants.ts +++ b/frontend/src/lib/constants.ts @@ -84,6 +84,7 @@ export const AUTH_OPTIONS = [ { value: 'Delegation tokens', label: 'Delegation tokens' }, { value: 'SASL/LDAP', label: 'SASL/LDAP' }, { value: 'SASL/AWS IAM', label: 'SASL/AWS IAM' }, + { value: 'SASL/Azure Entra', label: 'SASL/Azure Entra' }, { value: 'mTLS', label: 'mTLS' }, ]; diff --git a/frontend/src/widgets/ClusterConfigForm/schema.ts b/frontend/src/widgets/ClusterConfigForm/schema.ts index 68ffaa743..3c49568b2 100644 --- a/frontend/src/widgets/ClusterConfigForm/schema.ts +++ b/frontend/src/widgets/ClusterConfigForm/schema.ts @@ -121,6 +121,7 @@ const authPropsSchema = lazy((_, { parent }) => { return object({ awsProfileName: string(), }); + case 'SASL/Azure Entra': case 'mTLS': default: return mixed().optional(); @@ -142,6 +143,7 @@ const authSchema = lazy((value) => { 'Delegation tokens', 'SASL/LDAP', 'SASL/AWS IAM', + 'SASL/Azure Entra', 'mTLS', ]), securityProtocol: string() @@ -157,6 +159,7 @@ const authSchema = lazy((value) => { 'SASL/SCRAM-512', 'SASL/LDAP', 'SASL/AWS IAM', + 'SASL/Azure Entra', ].includes(v); }, then: (schema) => schema.required('required field'), diff --git a/frontend/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts b/frontend/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts index 006f401fa..0a9b914ea 100644 --- a/frontend/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts +++ b/frontend/src/widgets/ClusterConfigForm/utils/getJaasConfig.ts @@ -9,6 +9,8 @@ const JAAS_CONFIGS = { 'org.apache.kafka.common.security.scram.ScramLoginModule', 'SASL/LDAP': 'org.apache.kafka.common.security.plain.PlainLoginModule', 'SASL/AWS IAM': 'software.amazon.msk.auth.iam.IAMLoginModule', + 'SASL/Azure Entra': + 'org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule', }; type MethodName = keyof typeof JAAS_CONFIGS; diff --git a/frontend/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts b/frontend/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts index 0dea696a1..fdc9d740e 100644 --- a/frontend/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts +++ b/frontend/src/widgets/ClusterConfigForm/utils/transformFormDataToPayload.ts @@ -215,6 +215,15 @@ export const transformFormDataToPayload = (data: ClusterConfigFormValues) => { }), }; break; + case 'SASL/Azure Entra': + config.properties = { + 'security.protocol': securityProtocol, + 'sasl.mechanism': 'OAUTHBEARER', + 'sasl.client.callback.handler.class': + 'io.kafbat.ui.sasl.azure.entra.AzureEntraLoginCallbackHandler', + 'sasl.jaas.config': getJaasConfig('SASL/Azure Entra', {}), + }; + break; case 'mTLS': config.properties = { 'security.protocol': 'SSL', From 3690d685b684f4fdaa42c3930882e616f5c4eaab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Sep 2024 02:38:54 +0400 Subject: [PATCH 09/29] Bump com.google.protobuf:protobuf-java from 3.23.3 to 3.25.5 in the maven group (#554) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0b31b04dd..679587ce6 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ 3.5.2 1.5.5.Final 1.18.32 - 3.23.3 + 3.25.5 2.13.9 2.2 3.1.9 From 025c41abdf38f94d65ba22c36df5ba4e7794b598 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Fri, 20 Sep 2024 02:39:17 +0400 Subject: [PATCH 10/29] BE: Chore: Polishing --- .../azure/AzureEntraLoginCallbackHandler.java | 19 ++++++++++--------- .../azure/AzureEntraOAuthBearerToken.java | 6 +++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandler.java b/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandler.java index c6c08b6fd..1f0a721e4 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandler.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraLoginCallbackHandler.java @@ -31,8 +31,9 @@ public class AzureEntraLoginCallbackHandler implements AuthenticateCallbackHandl private TokenRequestContext tokenRequestContext; @Override - public void configure( - Map configs, String mechanism, List jaasConfigEntries) { + public void configure(Map configs, + String mechanism, + List jaasConfigEntries) { tokenRequestContext = buildTokenRequestContext(configs); } @@ -45,16 +46,17 @@ private TokenRequestContext buildTokenRequestContext(Map configs) { return request; } + @SuppressWarnings("unchecked") private URI buildEventHubsServerUri(Map configs) { final List bootstrapServers = (List) configs.get(BOOTSTRAP_SERVERS_CONFIG); - if (null == bootstrapServers) { + if (bootstrapServers == null) { final String message = BOOTSTRAP_SERVERS_CONFIG + " is missing from the Kafka configuration."; log.error(message); throw new IllegalArgumentException(message); } - if (bootstrapServers.size() != 1) { + if (bootstrapServers.size() > 1) { final String message = BOOTSTRAP_SERVERS_CONFIG + " contains multiple bootstrap servers. Only a single bootstrap server is supported."; @@ -72,11 +74,10 @@ private String buildTokenAudience(URI uri) { @Override public void handle(Callback[] callbacks) throws UnsupportedCallbackException { for (Callback callback : callbacks) { - if (callback instanceof OAuthBearerTokenCallback oauthCallback) { - handleOAuthCallback(oauthCallback); - } else { + if (!(callback instanceof OAuthBearerTokenCallback oauthCallback)) { throw new UnsupportedCallbackException(callback); } + handleOAuthCallback(oauthCallback); } } @@ -91,7 +92,7 @@ private void handleOAuthCallback(OAuthBearerTokenCallback oauthCallback) { .block(); oauthCallback.token(token); - } catch (final RuntimeException e) { + } catch (RuntimeException e) { final String message = "Failed to acquire Azure token for Event Hub Authentication. " + "Please ensure valid Azure credentials are configured."; @@ -104,7 +105,7 @@ public void close() { // NOOP } - void setTokenCredential(final TokenCredential tokenCredential) { + void setTokenCredential(TokenCredential tokenCredential) { AzureEntraLoginCallbackHandler.tokenCredential = tokenCredential; } } diff --git a/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerToken.java b/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerToken.java index e9c315940..a53ac2552 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerToken.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/azure/AzureEntraOAuthBearerToken.java @@ -5,7 +5,6 @@ import com.nimbusds.jwt.JWTParser; import java.text.ParseException; import java.util.Arrays; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.apache.kafka.common.errors.SaslAuthenticationException; @@ -14,7 +13,6 @@ public class AzureEntraOAuthBearerToken implements OAuthBearerToken { private final AccessToken accessToken; - private final JWTClaimsSet claims; public AzureEntraOAuthBearerToken(AccessToken accessToken) { @@ -48,7 +46,9 @@ public Set scope() { // https://docs.microsoft.com/azure/active-directory/develop/access-tokens#payload-claims, the // scp // claim is a String which is presented as a space separated list. - return Arrays.stream(((String) claims.getClaim("scp")).split(" ")).collect(Collectors.toSet()); + return Arrays + .stream(((String) claims.getClaim("scp")).split(" ")) + .collect(Collectors.toSet()); } @Override From 126980339425e4bff3d98b020fe606778d3c45b1 Mon Sep 17 00:00:00 2001 From: Cole Smith <114085318+colesmith54@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:24:45 -0400 Subject: [PATCH 11/29] Fix: SIGSEV in docker container on ARM64 (#558) --- api/pom.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/pom.xml b/api/pom.xml index 17bae6b85..a892eba7f 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -97,7 +97,13 @@ com.azure azure-identity - 1.13.0 + 1.13.3 + + + io.netty + netty-tcnative-boringssl-static + + From 52451ec5ed599b9e77f358b3e9eda2ae33219cbd Mon Sep 17 00:00:00 2001 From: Mason Woodford <63433735+masonwoodford@users.noreply.github.com> Date: Wed, 25 Sep 2024 03:22:59 +0900 Subject: [PATCH 12/29] FE: Fix refetching data on window focus (#380) Co-authored-by: Roman Zabaluev --- frontend/src/components/Schemas/Edit/Edit.tsx | 12 ++++-- .../src/components/Topics/Topic/Edit/Edit.tsx | 15 +++++-- frontend/src/lib/constants.ts | 5 +++ .../lib/hooks/api/__tests__/schema.spec.ts | 11 +++++ .../lib/hooks/api/__tests__/topics.spec.ts | 41 ++++++++++++++----- frontend/src/lib/hooks/api/schemas.ts | 13 +++++- frontend/src/lib/hooks/api/topics.ts | 30 +++++++++++--- 7 files changed, 103 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/Schemas/Edit/Edit.tsx b/frontend/src/components/Schemas/Edit/Edit.tsx index 2e6892197..6e8c04230 100644 --- a/frontend/src/components/Schemas/Edit/Edit.tsx +++ b/frontend/src/components/Schemas/Edit/Edit.tsx @@ -4,6 +4,7 @@ import useAppParams from 'lib/hooks/useAppParams'; import PageLoader from 'components/common/PageLoader/PageLoader'; import { useNavigate } from 'react-router-dom'; import { useGetLatestSchema } from 'lib/hooks/api/schemas'; +import { QUERY_REFETCH_LIMITED_OPTIONS } from 'lib/constants'; import Form from './Form'; @@ -14,10 +15,13 @@ const Edit: React.FC = () => { isFetching, isError, data: schema, - } = useGetLatestSchema({ - clusterName, - subject, - }); + } = useGetLatestSchema( + { + clusterName, + subject, + }, + QUERY_REFETCH_LIMITED_OPTIONS + ); useEffect(() => { if (isError) { diff --git a/frontend/src/components/Topics/Topic/Edit/Edit.tsx b/frontend/src/components/Topics/Topic/Edit/Edit.tsx index 598ac5982..1d02bd598 100644 --- a/frontend/src/components/Topics/Topic/Edit/Edit.tsx +++ b/frontend/src/components/Topics/Topic/Edit/Edit.tsx @@ -7,7 +7,10 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { topicFormValidationSchema } from 'lib/yupExtended'; import useAppParams from 'lib/hooks/useAppParams'; import topicParamsTransformer from 'components/Topics/Topic/Edit/topicParamsTransformer'; -import { MILLISECONDS_IN_WEEK } from 'lib/constants'; +import { + MILLISECONDS_IN_WEEK, + QUERY_REFETCH_LIMITED_OPTIONS, +} from 'lib/constants'; import { useTopicConfig, useTopicDetails, @@ -30,8 +33,14 @@ export const TOPIC_EDIT_FORM_DEFAULT_PROPS = { const Edit: React.FC = () => { const { clusterName, topicName } = useAppParams(); - const { data: topic } = useTopicDetails({ clusterName, topicName }); - const { data: topicConfig } = useTopicConfig({ clusterName, topicName }); + const { data: topic } = useTopicDetails( + { clusterName, topicName }, + QUERY_REFETCH_LIMITED_OPTIONS + ); + const { data: topicConfig } = useTopicConfig( + { clusterName, topicName }, + QUERY_REFETCH_LIMITED_OPTIONS + ); const updateTopic = useUpdateTopic({ clusterName, topicName }); const defaultValues = topicParamsTransformer(topic, topicConfig); diff --git a/frontend/src/lib/constants.ts b/frontend/src/lib/constants.ts index 3d765ee0a..3e3925ec9 100644 --- a/frontend/src/lib/constants.ts +++ b/frontend/src/lib/constants.ts @@ -73,6 +73,11 @@ export const QUERY_REFETCH_OFF_OPTIONS = { refetchIntervalInBackground: false, }; +export const QUERY_REFETCH_LIMITED_OPTIONS = { + refetchOnWindowFocus: false, + refetchIntervalInBackground: false, +}; + // Cluster Form Constants export const AUTH_OPTIONS = [ { value: 'SASL/JAAS', label: 'SASL/JAAS' }, diff --git a/frontend/src/lib/hooks/api/__tests__/schema.spec.ts b/frontend/src/lib/hooks/api/__tests__/schema.spec.ts index eea7c94b8..7dbfc8700 100644 --- a/frontend/src/lib/hooks/api/__tests__/schema.spec.ts +++ b/frontend/src/lib/hooks/api/__tests__/schema.spec.ts @@ -5,6 +5,7 @@ import { } from 'lib/testHelpers'; import fetchMock from 'fetch-mock'; import * as hooks from 'lib/hooks/api/schemas'; +import { QUERY_REFETCH_LIMITED_OPTIONS } from 'lib/constants'; import { act } from 'react-dom/test-utils'; import { renderHook, waitFor } from '@testing-library/react'; import { CompatibilityLevelCompatibilityEnum } from 'generated-sources'; @@ -47,6 +48,16 @@ describe('Schema hooks', () => { ); await expectQueryWorks(mock, result); }); + it('returns the correct data with queryOptions', async () => { + const mock = fetchMock.getOnce(schemasAPILatestUrl, schemaVersion); + const { result } = renderQueryHook(() => + hooks.useGetLatestSchema( + { clusterName, subject }, + QUERY_REFETCH_LIMITED_OPTIONS + ) + ); + await expectQueryWorks(mock, result); + }); }); describe('useGetSchemasVersions', () => { diff --git a/frontend/src/lib/hooks/api/__tests__/topics.spec.ts b/frontend/src/lib/hooks/api/__tests__/topics.spec.ts index 6007f3356..9085014fd 100644 --- a/frontend/src/lib/hooks/api/__tests__/topics.spec.ts +++ b/frontend/src/lib/hooks/api/__tests__/topics.spec.ts @@ -9,6 +9,7 @@ import fetchMock from 'fetch-mock'; import { externalTopicPayload, topicConfigPayload } from 'lib/fixtures/topics'; import { CreateTopicMessage } from 'generated-sources'; import { TopicFormData, TopicFormDataRaw } from 'lib/interfaces/topic'; +import { QUERY_REFETCH_LIMITED_OPTIONS } from 'lib/constants'; const clusterName = 'test-cluster'; const topicName = 'test-topic'; @@ -30,17 +31,37 @@ describe('Topics hooks', () => { const { result } = renderQueryHook(() => hooks.useTopics({ clusterName })); await expectQueryWorks(mock, result); }); - it('handles useTopicDetails', async () => { - const mock = fetchMock.getOnce(topicPath, externalTopicPayload); - const { result } = renderQueryHook(() => - hooks.useTopicDetails(topicParams) - ); - await expectQueryWorks(mock, result); + describe('useTopicDetails', () => { + it('handles useTopicDetails', async () => { + const mock = fetchMock.getOnce(topicPath, externalTopicPayload); + const { result } = renderQueryHook(() => + hooks.useTopicDetails(topicParams) + ); + await expectQueryWorks(mock, result); + }); + it('handles useTopicDetails with queryOptions', async () => { + const mock = fetchMock.getOnce(topicPath, externalTopicPayload); + const { result } = renderQueryHook(() => + hooks.useTopicDetails(topicParams, QUERY_REFETCH_LIMITED_OPTIONS) + ); + await expectQueryWorks(mock, result); + }); }); - it('handles useTopicConfig', async () => { - const mock = fetchMock.getOnce(`${topicPath}/config`, topicConfigPayload); - const { result } = renderQueryHook(() => hooks.useTopicConfig(topicParams)); - await expectQueryWorks(mock, result); + describe('useTopicConfig', () => { + it('handles useTopicConfig', async () => { + const mock = fetchMock.getOnce(`${topicPath}/config`, topicConfigPayload); + const { result } = renderQueryHook(() => + hooks.useTopicConfig(topicParams) + ); + await expectQueryWorks(mock, result); + }); + it('handles useTopicConfig with queryOptions', async () => { + const mock = fetchMock.getOnce(`${topicPath}/config`, topicConfigPayload); + const { result } = renderQueryHook(() => + hooks.useTopicConfig(topicParams, QUERY_REFETCH_LIMITED_OPTIONS) + ); + await expectQueryWorks(mock, result); + }); }); it('handles useTopicConsumerGroups', async () => { const mock = fetchMock.getOnce(`${topicPath}/consumer-groups`, []); diff --git a/frontend/src/lib/hooks/api/schemas.ts b/frontend/src/lib/hooks/api/schemas.ts index 7082d180a..aeb1ed9a6 100644 --- a/frontend/src/lib/hooks/api/schemas.ts +++ b/frontend/src/lib/hooks/api/schemas.ts @@ -1,4 +1,9 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + useMutation, + useQuery, + useQueryClient, + UseQueryOptions, +} from '@tanstack/react-query'; import { GLOBAL_COMPATIBILITY_SCHEMAS_QUERY_KEY, LATEST_SCHEMA_QUERY_KEY, @@ -19,7 +24,10 @@ import { import { schemasApiClient } from 'lib/api'; import { ClusterName } from 'lib/interfaces/cluster'; -export function useGetLatestSchema(param: GetLatestSchemaRequest) { +export function useGetLatestSchema( + param: GetLatestSchemaRequest, + options?: UseQueryOptions +) { return useQuery({ queryKey: [ SCHEMA_QUERY_KEY, @@ -28,6 +36,7 @@ export function useGetLatestSchema(param: GetLatestSchemaRequest) { param.subject, ], queryFn: () => schemasApiClient.getLatestSchema(param), + ...options, }); } diff --git a/frontend/src/lib/hooks/api/topics.ts b/frontend/src/lib/hooks/api/topics.ts index b0aac312d..fb05ee219 100644 --- a/frontend/src/lib/hooks/api/topics.ts +++ b/frontend/src/lib/hooks/api/topics.ts @@ -4,7 +4,12 @@ import { consumerGroupsApiClient, messagesApiClient, } from 'lib/api'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + useMutation, + useQuery, + useQueryClient, + UseQueryOptions, +} from '@tanstack/react-query'; import { CreateTopicMessage, GetTopicDetailsRequest, @@ -12,6 +17,7 @@ import { Topic, TopicConfig, TopicCreation, + TopicDetails, TopicUpdate, } from 'generated-sources'; import { showServerError, showSuccessAlert } from 'lib/errorHandling'; @@ -49,11 +55,25 @@ export function useTopics(props: GetTopicsRequest) { { keepPreviousData: true } ); } -export function useTopicDetails(props: GetTopicDetailsRequest) { - return useQuery(topicKeys.details(props), () => api.getTopicDetails(props)); +export function useTopicDetails( + props: GetTopicDetailsRequest, + queryOptions?: UseQueryOptions +) { + return useQuery( + topicKeys.details(props), + () => api.getTopicDetails(props), + queryOptions + ); } -export function useTopicConfig(props: GetTopicDetailsRequest) { - return useQuery(topicKeys.config(props), () => api.getTopicConfigs(props)); +export function useTopicConfig( + props: GetTopicDetailsRequest, + queryOptions?: UseQueryOptions +) { + return useQuery( + topicKeys.config(props), + () => api.getTopicConfigs(props), + queryOptions + ); } export function useTopicConsumerGroups(props: GetTopicDetailsRequest) { return useQuery(topicKeys.consumerGroups(props), () => From 3f3ca4cfabe36acd57fddcec1589c39a1800f61f Mon Sep 17 00:00:00 2001 From: Dmitry Werner Date: Wed, 25 Sep 2024 23:22:19 +0500 Subject: [PATCH 13/29] BE: Make gh version check timeout configurable (#518) --- .../ui/service/ApplicationInfoService.java | 16 +++++++++++----- .../io/kafbat/ui/util/GithubReleaseInfo.java | 16 ++++++++++------ .../io/kafbat/ui/AbstractIntegrationTest.java | 3 +++ .../ui/service/ApplicationInfoServiceTest.java | 17 +++++++++++++++++ .../kafbat/ui/util/GithubReleaseInfoTest.java | 2 +- 5 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 api/src/test/java/io/kafbat/ui/service/ApplicationInfoServiceTest.java diff --git a/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java b/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java index 0a04ce9d6..7d380036c 100644 --- a/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java +++ b/api/src/main/java/io/kafbat/ui/service/ApplicationInfoService.java @@ -1,7 +1,9 @@ package io.kafbat.ui.service; import static io.kafbat.ui.model.ApplicationInfoDTO.EnabledFeaturesEnum; +import static io.kafbat.ui.util.GithubReleaseInfo.GITHUB_RELEASE_INFO_TIMEOUT; +import com.google.common.annotations.VisibleForTesting; import io.kafbat.ui.model.ApplicationInfoBuildDTO; import io.kafbat.ui.model.ApplicationInfoDTO; import io.kafbat.ui.model.ApplicationInfoLatestReleaseDTO; @@ -13,27 +15,27 @@ import java.util.Optional; import java.util.Properties; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.info.BuildProperties; import org.springframework.boot.info.GitProperties; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; -import reactor.core.publisher.Mono; @Service public class ApplicationInfoService { - - private final GithubReleaseInfo githubReleaseInfo = new GithubReleaseInfo(); - + private final GithubReleaseInfo githubReleaseInfo; private final DynamicConfigOperations dynamicConfigOperations; private final BuildProperties buildProperties; private final GitProperties gitProperties; public ApplicationInfoService(DynamicConfigOperations dynamicConfigOperations, @Autowired(required = false) BuildProperties buildProperties, - @Autowired(required = false) GitProperties gitProperties) { + @Autowired(required = false) GitProperties gitProperties, + @Value("${" + GITHUB_RELEASE_INFO_TIMEOUT + ":10}") int githubApiMaxWaitTime) { this.dynamicConfigOperations = dynamicConfigOperations; this.buildProperties = Optional.ofNullable(buildProperties).orElse(new BuildProperties(new Properties())); this.gitProperties = Optional.ofNullable(gitProperties).orElse(new GitProperties(new Properties())); + githubReleaseInfo = new GithubReleaseInfo(githubApiMaxWaitTime); } public ApplicationInfoDTO getApplicationInfo() { @@ -74,4 +76,8 @@ public void updateGithubReleaseInfo() { githubReleaseInfo.refresh().subscribe(); } + @VisibleForTesting + GithubReleaseInfo githubReleaseInfo() { + return githubReleaseInfo; + } } diff --git a/api/src/main/java/io/kafbat/ui/util/GithubReleaseInfo.java b/api/src/main/java/io/kafbat/ui/util/GithubReleaseInfo.java index e3f546ad8..ba767a65a 100644 --- a/api/src/main/java/io/kafbat/ui/util/GithubReleaseInfo.java +++ b/api/src/main/java/io/kafbat/ui/util/GithubReleaseInfo.java @@ -2,17 +2,17 @@ import com.google.common.annotations.VisibleForTesting; import java.time.Duration; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import reactor.core.publisher.Mono; @Slf4j public class GithubReleaseInfo { + public static final String GITHUB_RELEASE_INFO_TIMEOUT = "github.release.info.timeout"; private static final String GITHUB_LATEST_RELEASE_RETRIEVAL_URL = "https://api.github.com/repos/kafbat/kafka-ui/releases/latest"; - private static final Duration GITHUB_API_MAX_WAIT_TIME = Duration.ofSeconds(10); - public record GithubReleaseDto(String html_url, String tag_name, String published_at) { static GithubReleaseDto empty() { @@ -24,17 +24,21 @@ static GithubReleaseDto empty() { private final Mono refreshMono; - public GithubReleaseInfo() { - this(GITHUB_LATEST_RELEASE_RETRIEVAL_URL); + @Getter + private final int githubApiMaxWaitTime; + + public GithubReleaseInfo(int githubApiMaxWaitTime) { + this(GITHUB_LATEST_RELEASE_RETRIEVAL_URL, githubApiMaxWaitTime); } @VisibleForTesting - GithubReleaseInfo(String url) { + GithubReleaseInfo(String url, int githubApiMaxWaitTime) { + this.githubApiMaxWaitTime = githubApiMaxWaitTime; this.refreshMono = new WebClientConfigurator().build() .get() .uri(url) .exchangeToMono(resp -> resp.bodyToMono(GithubReleaseDto.class)) - .timeout(GITHUB_API_MAX_WAIT_TIME) + .timeout(Duration.ofSeconds(this.githubApiMaxWaitTime)) .doOnError(th -> log.trace("Error getting latest github release info", th)) .onErrorResume(th -> true, th -> Mono.just(GithubReleaseDto.empty())) .doOnNext(release -> this.release = release) diff --git a/api/src/test/java/io/kafbat/ui/AbstractIntegrationTest.java b/api/src/test/java/io/kafbat/ui/AbstractIntegrationTest.java index 5f7e6040c..554387a1a 100644 --- a/api/src/test/java/io/kafbat/ui/AbstractIntegrationTest.java +++ b/api/src/test/java/io/kafbat/ui/AbstractIntegrationTest.java @@ -1,5 +1,7 @@ package io.kafbat.ui; +import static io.kafbat.ui.util.GithubReleaseInfo.GITHUB_RELEASE_INFO_TIMEOUT; + import io.kafbat.ui.container.KafkaConnectContainer; import io.kafbat.ui.container.KsqlDbContainer; import io.kafbat.ui.container.SchemaRegistryContainer; @@ -98,6 +100,7 @@ public void initialize(@NotNull ConfigurableApplicationContext context) { System.setProperty("dynamic.config.enabled", "true"); System.setProperty("config.related.uploads.dir", tmpDir.toString()); + System.setProperty(GITHUB_RELEASE_INFO_TIMEOUT, String.valueOf(100)); } } diff --git a/api/src/test/java/io/kafbat/ui/service/ApplicationInfoServiceTest.java b/api/src/test/java/io/kafbat/ui/service/ApplicationInfoServiceTest.java new file mode 100644 index 000000000..64e6ed743 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/ApplicationInfoServiceTest.java @@ -0,0 +1,17 @@ +package io.kafbat.ui.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.kafbat.ui.AbstractIntegrationTest; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class ApplicationInfoServiceTest extends AbstractIntegrationTest { + @Autowired + private ApplicationInfoService service; + + @Test + public void testCustomGithubReleaseInfoTimeout() { + assertEquals(100, service.githubReleaseInfo().getGithubApiMaxWaitTime()); + } +} diff --git a/api/src/test/java/io/kafbat/ui/util/GithubReleaseInfoTest.java b/api/src/test/java/io/kafbat/ui/util/GithubReleaseInfoTest.java index 2f97d7a3a..8d78cedfc 100644 --- a/api/src/test/java/io/kafbat/ui/util/GithubReleaseInfoTest.java +++ b/api/src/test/java/io/kafbat/ui/util/GithubReleaseInfoTest.java @@ -37,7 +37,7 @@ void test() { """)); var url = mockWebServer.url("repos/kafbat/kafka-ui/releases/latest").toString(); - var infoHolder = new GithubReleaseInfo(url); + var infoHolder = new GithubReleaseInfo(url, 10); infoHolder.refresh().block(); var i = infoHolder.get(); From 8df11ac8e3e33784b40cf929284f7443615c73a5 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Fri, 4 Oct 2024 04:42:16 +0400 Subject: [PATCH 14/29] Infra: FE: Bump pnpm (#567) --- .github/workflows/frontend_tests.yml | 2 +- frontend/package.json | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/frontend_tests.yml b/.github/workflows/frontend_tests.yml index 9fa145337..6755b1b61 100644 --- a/.github/workflows/frontend_tests.yml +++ b/.github/workflows/frontend_tests.yml @@ -23,7 +23,7 @@ jobs: - uses: pnpm/action-setup@v4.0.0 with: - version: 9.1.2 + version: 9.11.0 - name: Install node uses: actions/setup-node@v4.0.2 diff --git a/frontend/package.json b/frontend/package.json index 37c344f01..aef4f7fea 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -104,7 +104,7 @@ }, "engines": { "node": "v18.17.1", - "pnpm": "v9.1.2" + "pnpm": "v9.11.0" }, "pnpm": { "overrides": { diff --git a/pom.xml b/pom.xml index 679587ce6..75cae5bfc 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ v18.17.1 - v9.1.2 + v9.11.0 0.42.1 From 941358d25f8b6c76951757375592c51c6935f910 Mon Sep 17 00:00:00 2001 From: Renat Kalimulin <103274228+Nilumilak@users.noreply.github.com> Date: Sun, 6 Oct 2024 23:49:56 +0300 Subject: [PATCH 15/29] UX: Allow searching inside code textareas (#578) --- frontend/src/components/common/Editor/Editor.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/common/Editor/Editor.tsx b/frontend/src/components/common/Editor/Editor.tsx index 05c91e355..42ee7c93b 100644 --- a/frontend/src/components/common/Editor/Editor.tsx +++ b/frontend/src/components/common/Editor/Editor.tsx @@ -1,4 +1,5 @@ import AceEditor, { IAceEditorProps } from 'react-ace'; +import 'ace-builds/src-noconflict/ext-searchbox'; import 'ace-builds/src-noconflict/mode-json5'; import 'ace-builds/src-noconflict/mode-protobuf'; import 'ace-builds/src-noconflict/theme-tomorrow'; From 07f0e0e75471a44be52782b06d0362c42820cba1 Mon Sep 17 00:00:00 2001 From: Guillaume Lhermenier Date: Mon, 7 Oct 2024 23:53:16 +0200 Subject: [PATCH 16/29] Infra: Add actions to publish to ECR & Docker Hub (#347) Co-authored-by: Roman Zabaluev --- .github/workflows/docker_build.yml | 87 +++++++++++++++++++++++ .github/workflows/docker_publish.yml | 100 +++++++++++++++++++++++++++ .github/workflows/main.yml | 67 ++++++++---------- .github/workflows/release.yml | 58 +++++----------- 4 files changed, 235 insertions(+), 77 deletions(-) create mode 100644 .github/workflows/docker_build.yml create mode 100644 .github/workflows/docker_publish.yml diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml new file mode 100644 index 000000000..2c4f513d5 --- /dev/null +++ b/.github/workflows/docker_build.yml @@ -0,0 +1,87 @@ +name: "Docker build" + +on: + workflow_call: + inputs: + sha: + required: true + type: string + version: + required: true + type: string + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + token: ${{ github.token }} + + - name: Download maven artifacts + uses: actions/download-artifact@v4 + with: + name: kafbat-ui-${{ inputs.version }} + path: api/target + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v4 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ inputs.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + # Build multi platform images and loading them at the same time is not possible with default container runtime : https://github.com/docker/buildx/issues/59 + # So let's use containerd instead as it supports this option + # Also containerd is one of the option to allow preserving provenance attestations :https://docs.docker.com/build/attestations/#creating-attestations + - name: Setup docker with containerd + uses: crazy-max/ghaction-setup-docker@v3 + with: + daemon-config: | + { + "features": { + "containerd-snapshotter": true + } + } + + - name: Build docker image + id: docker_build + uses: docker/build-push-action@v5 + with: + builder: ${{ steps.buildx.outputs.name }} + context: api + platforms: linux/amd64,linux/arm64 + provenance: mode=min + sbom: true + push: false + load: true + tags: | + kafka-ui:temp + build-args: | + JAR_FILE=api-${{ inputs.version }}.jar + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache + + - name: Dump docker image + run: | + docker image save kafka-ui:temp > /tmp/image.tar + + - name: Upload docker image + uses: actions/upload-artifact@v4 + with: + name: image + path: /tmp/image.tar + retention-days: 1 diff --git a/.github/workflows/docker_publish.yml b/.github/workflows/docker_publish.yml new file mode 100644 index 000000000..47ee65cd8 --- /dev/null +++ b/.github/workflows/docker_publish.yml @@ -0,0 +1,100 @@ +name: "Docker publish" + +on: + workflow_call: + inputs: + version: + required: true + type: string + generic_tag: + required: true + type: string + +permissions: + packages: write + id-token: write # Required to authenticate with OIDC for AWS + +jobs: + deploy: + continue-on-error: true + strategy: + fail-fast: false + matrix: + registry: [ 'docker.io', 'ghcr.io', 'ecr' ] + + runs-on: ubuntu-latest + steps: + + - name: Download docker image + uses: actions/download-artifact@v4 + with: + name: image + path: /tmp + + # setup containerd to preserve provenance attestations :https://docs.docker.com/build/attestations/#creating-attestations + - name: Setup docker with containerd + uses: crazy-max/ghaction-setup-docker@v3 + with: + daemon-config: | + { + "features": { + "containerd-snapshotter": true + } + } + + - name: Load docker image into daemon + run: | + docker load --input /tmp/image.tar + + - name: Login to docker.io + if: matrix.registry == 'docker.io' + uses: docker/login-action@v3 + with: + registry: ${{ matrix.registry }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io + if: matrix.registry == 'ghcr.io' + uses: docker/login-action@v3 + with: + registry: ${{ matrix.registry }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure AWS credentials + if: matrix.registry == 'ecr' + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: us-east-1 # This region only for public ECR + role-to-assume: ${{ secrets.AWS_ROLE }} + + - name: Login to public ECR + if: matrix.registry == 'ecr' + id: login-ecr-public + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + + - name: define env vars + run: | + if [ ${{matrix.registry }} == 'docker.io' ]; then + echo "REGISTRY=${{ matrix.registry }}" >> $GITHUB_ENV + echo "REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV + elif [ ${{ matrix.registry }} == 'ghcr.io' ]; then + echo "REGISTRY=${{ matrix.registry }}" >> $GITHUB_ENV + echo "REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV + elif [ ${{ matrix.registry }} == 'ecr' ]; then + echo "REGISTRY=${{ steps.login-ecr-public.outputs.registry }}" >> $GITHUB_ENV + echo "REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV + else + echo "REGISTRY=" >> $GITHUB_ENV + echo "REPOSITORY=notworking" >> $GITHUB_ENV + fi + + - name: Push images to ${{ matrix.registry }} + run: | + docker tag kafka-ui:temp ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ inputs.generic_tag }} + docker tag kafka-ui:temp ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ inputs.version }} + docker push ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ inputs.generic_tag }} + docker push ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ inputs.version }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d4e180086..7701b91e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,11 +9,14 @@ permissions: contents: read jobs: - build: + jar-build: runs-on: ubuntu-latest + permissions: contents: read - packages: write + + outputs: + version: ${{steps.build.outputs.version}} steps: - name: Checkout @@ -37,42 +40,30 @@ jobs: export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec) echo "version=${VERSION}" >> $GITHUB_OUTPUT - # docker images - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Cache Docker layers - uses: actions/cache@v4 + - name: Upload jar + uses: actions/upload-artifact@v4 with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- + name: kafbat-ui-${{ steps.build.outputs.version }} + path: api/target/api-${{ steps.build.outputs.version }}.jar + retention-days: 1 - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + docker-build: + needs: jar-build + permissions: + contents: read + uses: ./.github/workflows/docker_build.yml + secrets: inherit + with: + sha: ${{ github.sha }} + version: ${{ needs.jar-build.outputs.version }} - - name: Build & push docker image - id: docker_build_and_push - uses: docker/build-push-action@v5 - with: - builder: ${{ steps.buildx.outputs.name }} - context: api - platforms: linux/amd64,linux/arm64 - provenance: false - push: true - tags: | - ghcr.io/kafbat/kafka-ui:${{ steps.build.outputs.version }} - ghcr.io/kafbat/kafka-ui:main - build-args: | - JAR_FILE=api-${{ steps.build.outputs.version }}.jar - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache + docker-deploy: + needs: [ jar-build, docker-build ] + permissions: + packages: write + id-token: write # Required to authenticate with OIDC for AWS + uses: ./.github/workflows/docker_publish.yml + secrets: inherit + with: + version: ${{ needs.jar-build.outputs.version }} + generic_tag: main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c7fcce52..3a3c9de23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,47 +52,27 @@ jobs: with: name: kafbat-ui-${{ steps.build.outputs.version }} path: api/target/api-${{ steps.build.outputs.version }}.jar - ################# - # # - # Docker images # - # # - ################# - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Cache Docker layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- + docker-build: + needs: release + permissions: + contents: read + uses: ./.github/workflows/docker_build.yml + secrets: inherit + with: + sha: ${{ github.sha }} + version: ${{ needs.release.outputs.version }} - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push - id: docker_build_and_push - uses: docker/build-push-action@v5 - with: - builder: ${{ steps.buildx.outputs.name }} - context: api - platforms: linux/amd64,linux/arm64 - provenance: false - push: true - tags: | - ghcr.io/kafbat/kafka-ui:${{ steps.build.outputs.version }} - ghcr.io/kafbat/kafka-ui:latest - build-args: | - JAR_FILE=api-${{ steps.build.outputs.version }}.jar - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache + docker-deploy: + needs: [release, docker-build] + permissions: + packages: write + id-token: write # Required to authenticate with OIDC for AWS + uses: ./.github/workflows/docker_publish.yml + secrets: inherit + with: + version: ${{ needs.release.outputs.version }} + generic_tag: latest charts: runs-on: ubuntu-latest From 2c2b77dc9b4bc04281ab0dc3bcb6e38649df39c0 Mon Sep 17 00:00:00 2001 From: Azat Safin Date: Tue, 8 Oct 2024 15:19:01 +0300 Subject: [PATCH 17/29] Infra: Fix ECR push action (#586) --- .github/workflows/docker_publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_publish.yml b/.github/workflows/docker_publish.yml index 47ee65cd8..7fefcd3c6 100644 --- a/.github/workflows/docker_publish.yml +++ b/.github/workflows/docker_publish.yml @@ -85,7 +85,7 @@ jobs: echo "REGISTRY=${{ matrix.registry }}" >> $GITHUB_ENV echo "REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV elif [ ${{ matrix.registry }} == 'ecr' ]; then - echo "REGISTRY=${{ steps.login-ecr-public.outputs.registry }}" >> $GITHUB_ENV + echo "REGISTRY=${{ env.ECR_REGISTRY }}" >> $GITHUB_ENV echo "REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV else echo "REGISTRY=" >> $GITHUB_ENV From 91ed1670fdef51566ec2bac29c89d594becd3eab Mon Sep 17 00:00:00 2001 From: Azat Safin Date: Tue, 8 Oct 2024 15:35:33 +0300 Subject: [PATCH 18/29] Infra: Fix ECR push action (#588) --- .github/workflows/docker_publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_publish.yml b/.github/workflows/docker_publish.yml index 7fefcd3c6..e359ea740 100644 --- a/.github/workflows/docker_publish.yml +++ b/.github/workflows/docker_publish.yml @@ -85,7 +85,7 @@ jobs: echo "REGISTRY=${{ matrix.registry }}" >> $GITHUB_ENV echo "REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV elif [ ${{ matrix.registry }} == 'ecr' ]; then - echo "REGISTRY=${{ env.ECR_REGISTRY }}" >> $GITHUB_ENV + echo "REGISTRY=${{ vars.ECR_REGISTRY }}" >> $GITHUB_ENV echo "REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV else echo "REGISTRY=" >> $GITHUB_ENV From c336bbde60bf6c573b75bb81c9ac708ea17b42bc Mon Sep 17 00:00:00 2001 From: Renat Kalimulin <103274228+Nilumilak@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:59:17 +0300 Subject: [PATCH 19/29] FE: Fix unnecessary full page re-rendering (#594) --- frontend/src/components/PageContainer/PageContainer.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/PageContainer/PageContainer.tsx b/frontend/src/components/PageContainer/PageContainer.tsx index 9d209e032..4459de6a5 100644 --- a/frontend/src/components/PageContainer/PageContainer.tsx +++ b/frontend/src/components/PageContainer/PageContainer.tsx @@ -1,6 +1,7 @@ import React, { type FC, type PropsWithChildren, + Suspense, useEffect, useMemo, } from 'react'; @@ -15,6 +16,7 @@ import { useClusters } from 'lib/hooks/api/clusters'; import { ResourceType } from 'generated-sources'; import { useGetUserInfo } from 'lib/hooks/api/roles'; import { useScreenSize } from 'lib/hooks/useScreenSize'; +import PageLoader from 'components/common/PageLoader/PageLoader'; const PageContainer: FC = ({ children }) => { const { isLarge } = useScreenSize(); @@ -62,7 +64,7 @@ const PageContainer: FC = ({ children }) => { aria-hidden="true" aria-label="Overlay" /> - {children} + }>{children} ); From fbef485eb51a9cdc3e688a7d7b7c892269d3f37a Mon Sep 17 00:00:00 2001 From: Azat Safin Date: Wed, 9 Oct 2024 11:49:58 +0300 Subject: [PATCH 20/29] Infra: ECR Build minor fixes (#580) --- .github/workflows/build-public-image.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-public-image.yml b/.github/workflows/build-public-image.yml index ac48d29a0..3e364c992 100644 --- a/.github/workflows/build-public-image.yml +++ b/.github/workflows/build-public-image.yml @@ -6,7 +6,9 @@ on: types: ['labeled'] permissions: + id-token: write contents: read + pull-requests: write jobs: build: @@ -47,12 +49,11 @@ jobs: key: ${{ runner.os }}-buildx-${{ github.sha }} restore-keys: | ${{ runner.os }}-buildx- - - name: Configure AWS credentials for Kafka-UI account + - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v4 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: us-east-1 + role-to-assume: ${{ secrets.AWS_ROLE }} - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 @@ -65,7 +66,7 @@ jobs: builder: ${{ steps.buildx.outputs.name }} context: api push: true - tags: public.ecr.aws/kafbat/kafka-ui-custom-build:${{ steps.extract_branch.outputs.tag }} + tags: ${{ vars.ECR_REGISTRY }}/${{ github.repository }}:${{ steps.extract_branch.outputs.tag }} build-args: | JAR_FILE=api-${{ steps.build.outputs.version }}.jar cache-from: type=local,src=/tmp/.buildx-cache @@ -75,6 +76,6 @@ jobs: with: issue-number: ${{ github.event.pull_request.number }} body: | - Image published at public.ecr.aws/kafbat/kafka-ui-custom-build:${{ steps.extract_branch.outputs.tag }} + Image published at ${{ vars.ECR_REGISTRY }}/${{ github.repository }}:${{ steps.extract_branch.outputs.tag }} outputs: tag: ${{ steps.extract_branch.outputs.tag }} From f36c18d8092837b04a9f1aea4a930e1c4c47f77b Mon Sep 17 00:00:00 2001 From: p-eye <50516754+p-eye@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:55:15 +0900 Subject: [PATCH 21/29] Consumers: Unsubscribe topics from consumer group (#549) Co-authored-by: Roman Zabaluev --- .../controller/ConsumerGroupsController.java | 18 +++++ .../ui/service/ConsumerGroupService.java | 20 ++++-- .../ui/service/ReactiveAdminClient.java | 22 ++++++ .../io/kafbat/ui/KafkaConsumerGroupTests.java | 67 +++++++++++++++++++ .../main/resources/swagger/kafbat-ui-api.yaml | 26 +++++++ .../ConsumerGroups/Details/ListItem.tsx | 36 +++++++++- frontend/src/lib/hooks/api/consumers.ts | 27 ++++++++ 7 files changed, 208 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/controller/ConsumerGroupsController.java b/api/src/main/java/io/kafbat/ui/controller/ConsumerGroupsController.java index 248c6f7d7..32337bbcf 100644 --- a/api/src/main/java/io/kafbat/ui/controller/ConsumerGroupsController.java +++ b/api/src/main/java/io/kafbat/ui/controller/ConsumerGroupsController.java @@ -59,6 +59,24 @@ public Mono> deleteConsumerGroup(String clusterName, .thenReturn(ResponseEntity.ok().build()); } + @Override + public Mono> deleteConsumerGroupOffsets(String clusterName, + String groupId, + String topicName, + ServerWebExchange exchange) { + var context = AccessContext.builder() + .cluster(clusterName) + .consumerGroupActions(groupId, RESET_OFFSETS) + .topicActions(topicName, TopicAction.VIEW) + .operationName("deleteConsumerGroupOffsets") + .build(); + + return validateAccess(context) + .then(consumerGroupService.deleteConsumerGroupOffset(getCluster(clusterName), groupId, topicName)) + .doOnEach(sig -> audit(context, sig)) + .thenReturn(ResponseEntity.ok().build()); + } + @Override public Mono> getConsumerGroup(String clusterName, String consumerGroupId, diff --git a/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java b/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java index 452de4a59..27593cd6f 100644 --- a/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java +++ b/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java @@ -209,12 +209,13 @@ private Mono> describeConsumerGroups(ReactiveAdmi } - private Mono> loadDescriptionsByInternalConsumerGroups(ReactiveAdminClient ac, - List groups, - Comparator comparator, - int pageNum, - int perPage, - SortOrderDTO sortOrderDto) { + private Mono> loadDescriptionsByInternalConsumerGroups( + ReactiveAdminClient ac, + List groups, + Comparator comparator, + int pageNum, + int perPage, + SortOrderDTO sortOrderDto) { var groupNames = groups.stream().map(ConsumerGroupListing::groupId).toList(); return ac.describeConsumerGroups(groupNames) @@ -247,6 +248,13 @@ public Mono deleteConsumerGroupById(KafkaCluster cluster, .flatMap(adminClient -> adminClient.deleteConsumerGroups(List.of(groupId))); } + public Mono deleteConsumerGroupOffset(KafkaCluster cluster, + String groupId, + String topicName) { + return adminClientService.get(cluster) + .flatMap(adminClient -> adminClient.deleteConsumerGroupOffsets(groupId, topicName)); + } + public EnhancedConsumer createConsumer(KafkaCluster cluster) { return createConsumer(cluster, Map.of()); } diff --git a/api/src/main/java/io/kafbat/ui/service/ReactiveAdminClient.java b/api/src/main/java/io/kafbat/ui/service/ReactiveAdminClient.java index bb04f5527..651f6d531 100644 --- a/api/src/main/java/io/kafbat/ui/service/ReactiveAdminClient.java +++ b/api/src/main/java/io/kafbat/ui/service/ReactiveAdminClient.java @@ -74,6 +74,7 @@ import org.apache.kafka.common.errors.ClusterAuthorizationException; import org.apache.kafka.common.errors.GroupIdNotFoundException; import org.apache.kafka.common.errors.GroupNotEmptyException; +import org.apache.kafka.common.errors.GroupSubscribedToTopicException; import org.apache.kafka.common.errors.InvalidRequestException; import org.apache.kafka.common.errors.SecurityDisabledException; import org.apache.kafka.common.errors.TopicAuthorizationException; @@ -436,6 +437,27 @@ public Mono deleteConsumerGroups(Collection groupIds) { th -> Mono.error(new IllegalEntityStateException("The group is not empty"))); } + public Mono deleteConsumerGroupOffsets(String groupId, String topicName) { + return listConsumerGroupOffsets(List.of(groupId), null) + .flatMap(table -> { + // filter TopicPartitions by topicName + Set partitions = table.row(groupId).keySet().stream() + .filter(tp -> tp.topic().equals(topicName)) + .collect(Collectors.toSet()); + // check if partitions have no committed offsets + return partitions.isEmpty() + ? Mono.error(new NotFoundException("The topic or partition is unknown")) + // call deleteConsumerGroupOffsets + : toMono(client.deleteConsumerGroupOffsets(groupId, partitions).all()); + }) + .onErrorResume(GroupIdNotFoundException.class, + th -> Mono.error(new NotFoundException("The group id does not exist"))) + .onErrorResume(UnknownTopicOrPartitionException.class, + th -> Mono.error(new NotFoundException("The topic or partition is unknown"))) + .onErrorResume(GroupSubscribedToTopicException.class, + th -> Mono.error(new IllegalEntityStateException("The group is not empty"))); + } + public Mono createTopic(String name, int numPartitions, @Nullable Integer replicationFactor, diff --git a/api/src/test/java/io/kafbat/ui/KafkaConsumerGroupTests.java b/api/src/test/java/io/kafbat/ui/KafkaConsumerGroupTests.java index 5f97317f2..b1bf4baa7 100644 --- a/api/src/test/java/io/kafbat/ui/KafkaConsumerGroupTests.java +++ b/api/src/test/java/io/kafbat/ui/KafkaConsumerGroupTests.java @@ -4,6 +4,7 @@ import io.kafbat.ui.model.ConsumerGroupDTO; import io.kafbat.ui.model.ConsumerGroupsPageResponseDTO; +import io.kafbat.ui.producer.KafkaTestProducer; import java.io.Closeable; import java.time.Duration; import java.util.Comparator; @@ -22,6 +23,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; @Slf4j public class KafkaConsumerGroupTests extends AbstractIntegrationTest { @@ -31,12 +34,76 @@ public class KafkaConsumerGroupTests extends AbstractIntegrationTest { @Test void shouldNotFoundWhenNoSuchConsumerGroupId() { String groupId = "groupA"; + String topicName = "topicX"; + webTestClient .delete() .uri("/api/clusters/{clusterName}/consumer-groups/{groupId}", LOCAL, groupId) .exchange() .expectStatus() .isNotFound(); + + webTestClient + .delete() + .uri("/api/clusters/{clusterName}/consumer-groups/{groupId}/topics/{topicName}", LOCAL, groupId, topicName) + .exchange() + .expectStatus() + .isNotFound(); + } + + @Test + void shouldNotFoundWhenNoSuchTopic() { + String topicName = createTopicWithRandomName(); + String topicNameUnSubscribed = "topicX"; + + //Create a consumer and subscribe to the topic + String groupId = UUID.randomUUID().toString(); + try (val consumer = createTestConsumerWithGroupId(groupId)) { + consumer.subscribe(List.of(topicName)); + consumer.poll(Duration.ofMillis(100)); + + webTestClient + .delete() + .uri("/api/clusters/{clusterName}/consumer-groups/{groupId}/topics/{topicName}", LOCAL, groupId, + topicNameUnSubscribed) + .exchange() + .expectStatus() + .isNotFound(); + } + } + + @Test + void shouldOkWhenConsumerGroupIsNotActiveAndPartitionOffsetExists() { + String topicName = createTopicWithRandomName(); + + //Create a consumer and subscribe to the topic + String groupId = UUID.randomUUID().toString(); + + try (KafkaTestProducer producer = KafkaTestProducer.forKafka(kafka)) { + Flux.fromStream( + Stream.of("one", "two", "three", "four") + .map(value -> Mono.fromFuture(producer.send(topicName, value))) + ).blockLast(); + } catch (Throwable e) { + log.error("Error on sending", e); + throw new RuntimeException(e); + } + + try (val consumer = createTestConsumerWithGroupId(groupId)) { + consumer.subscribe(List.of(topicName)); + consumer.poll(Duration.ofMillis(100)); + + //Stop consumers to delete consumer offset from the topic + consumer.pause(consumer.assignment()); + } + + //Delete the consumer offset when it's INACTIVE and check + webTestClient + .delete() + .uri("/api/clusters/{clusterName}/consumer-groups/{groupId}/topics/{topicName}", LOCAL, groupId, topicName) + .exchange() + .expectStatus() + .isOk(); } @Test diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index 7ca62831f..5eede6cef 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -1048,6 +1048,32 @@ paths: 200: description: OK + /api/clusters/{clusterName}/consumer-groups/{id}/topics/{topicName}: + delete: + tags: + - Consumer Groups + summary: delete consumer group offsets + operationId: deleteConsumerGroupOffsets + parameters: + - name: clusterName + in: path + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + - name: topicName + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + /api/clusters/{clusterName}/schemas: post: tags: diff --git a/frontend/src/components/ConsumerGroups/Details/ListItem.tsx b/frontend/src/components/ConsumerGroups/Details/ListItem.tsx index 57b7cb133..21560e9ca 100644 --- a/frontend/src/components/ConsumerGroups/Details/ListItem.tsx +++ b/frontend/src/components/ConsumerGroups/Details/ListItem.tsx @@ -1,8 +1,16 @@ import React from 'react'; -import { ConsumerGroupTopicPartition } from 'generated-sources'; +import { + Action, + ConsumerGroupTopicPartition, + ResourceType, +} from 'generated-sources'; import { Link } from 'react-router-dom'; import { ClusterName } from 'lib/interfaces/cluster'; -import { clusterTopicPath } from 'lib/paths'; +import { ClusterGroupParam, clusterTopicPath } from 'lib/paths'; +import { useDeleteConsumerGroupOffsetsMutation } from 'lib/hooks/api/consumers'; +import useAppParams from 'lib/hooks/useAppParams'; +import { Dropdown } from 'components/common/Dropdown'; +import { ActionDropdownItem } from 'components/common/ActionComponent'; import MessageToggleIcon from 'components/common/Icons/MessageToggleIcon'; import IconButtonWrapper from 'components/common/Icons/IconButtonWrapper'; import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled'; @@ -18,6 +26,9 @@ interface Props { const ListItem: React.FC = ({ clusterName, name, consumers }) => { const [isOpen, setIsOpen] = React.useState(false); + const consumerProps = useAppParams(); + const deleteOffsetMutation = + useDeleteConsumerGroupOffsetsMutation(consumerProps); const getTotalconsumerLag = () => { let count = 0; @@ -27,6 +38,11 @@ const ListItem: React.FC = ({ clusterName, name, consumers }) => { return count; }; + const deleteOffsetHandler = (topicName?: string) => { + if (topicName === undefined) return; + deleteOffsetMutation.mutateAsync(topicName); + }; + return ( <> @@ -41,6 +57,22 @@ const ListItem: React.FC = ({ clusterName, name, consumers }) => { {getTotalconsumerLag()} + + + deleteOffsetHandler(name)} + danger + confirm="Are you sure you want to delete offsets from the topic?" + permission={{ + resource: ResourceType.CONSUMER, + action: Action.RESET_OFFSETS, + value: consumerProps.consumerGroupID, + }} + > + Delete offsets + + + {isOpen && } diff --git a/frontend/src/lib/hooks/api/consumers.ts b/frontend/src/lib/hooks/api/consumers.ts index 85ae048a9..f2373e989 100644 --- a/frontend/src/lib/hooks/api/consumers.ts +++ b/frontend/src/lib/hooks/api/consumers.ts @@ -90,3 +90,30 @@ export const useResetConsumerGroupOffsetsMutation = ({ } ); }; + +export const useDeleteConsumerGroupOffsetsMutation = ({ + clusterName, + consumerGroupID, +}: UseConsumerGroupDetailsProps) => { + const queryClient = useQueryClient(); + return useMutation( + (topicName: string) => + api.deleteConsumerGroupOffsets({ + clusterName, + id: consumerGroupID, + topicName, + }), + { + onSuccess: (_, topicName) => { + showSuccessAlert({ + message: `Consumer ${consumerGroupID} group offsets in topic ${topicName} deleted`, + }); + queryClient.invalidateQueries([ + 'clusters', + clusterName, + 'consumerGroups', + ]); + }, + } + ); +}; From 4e2cee76b210cf2dfdc9ba489ef378cd9821ac4f Mon Sep 17 00:00:00 2001 From: Poleg Kashti <46862163+polegkashti@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:56:52 +0300 Subject: [PATCH 22/29] FE: Fix display of ACLs with leading/trailing spaces in table (#591) Co-authored-by: Poleg Kashti --- frontend/src/components/common/NewTable/Table.styled.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/common/NewTable/Table.styled.ts b/frontend/src/components/common/NewTable/Table.styled.ts index 5db0ba806..3cb247654 100644 --- a/frontend/src/components/common/NewTable/Table.styled.ts +++ b/frontend/src/components/common/NewTable/Table.styled.ts @@ -152,6 +152,7 @@ export const Table = styled.table( color: ${table.td.color.normal}; vertical-align: middle; word-wrap: break-word; + white-space: pre; & a { color: ${table.td.color.normal}; From 1930d8bb911095400af4b82afc473f815f427d80 Mon Sep 17 00:00:00 2001 From: SIX Douglas Date: Wed, 16 Oct 2024 22:58:06 +0200 Subject: [PATCH 23/29] BE: Chore: Bump Spring Boot w/ dependencies (#606) --- api/pom.xml | 13 ------------- .../ui/serdes/PropertyResolverImplTest.java | 8 +------- .../ui/service/ksql/KsqlApiClientTest.java | 11 +++++------ pom.xml | 18 +++++++++--------- 4 files changed, 15 insertions(+), 35 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index a892eba7f..09297b840 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -266,18 +266,6 @@ cel - - ch.qos.logback - logback-classic - 1.4.12 - - - - ch.qos.logback - logback-core - 1.4.12 - - com.squareup.okhttp3 logging-interceptor @@ -289,7 +277,6 @@ commons-compress 1.26.0 - diff --git a/api/src/test/java/io/kafbat/ui/serdes/PropertyResolverImplTest.java b/api/src/test/java/io/kafbat/ui/serdes/PropertyResolverImplTest.java index 59d650ef7..97d592dc6 100644 --- a/api/src/test/java/io/kafbat/ui/serdes/PropertyResolverImplTest.java +++ b/api/src/test/java/io/kafbat/ui/serdes/PropertyResolverImplTest.java @@ -5,8 +5,6 @@ import java.util.List; import java.util.Map; -import lombok.AllArgsConstructor; -import lombok.Data; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.boot.context.properties.bind.BindException; @@ -21,11 +19,7 @@ class PropertyResolverImplTest { private final MockEnvironment env = new MockEnvironment(); - @Data - @AllArgsConstructor - public static class CustomPropertiesClass { - private String f1; - private Integer f2; + public record CustomPropertiesClass(String f1, Integer f2) { } @Test diff --git a/api/src/test/java/io/kafbat/ui/service/ksql/KsqlApiClientTest.java b/api/src/test/java/io/kafbat/ui/service/ksql/KsqlApiClientTest.java index 7bfb7c22a..90e549662 100644 --- a/api/src/test/java/io/kafbat/ui/service/ksql/KsqlApiClientTest.java +++ b/api/src/test/java/io/kafbat/ui/service/ksql/KsqlApiClientTest.java @@ -3,12 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.DecimalNode; +import com.fasterxml.jackson.databind.node.DoubleNode; import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.TextNode; import io.kafbat.ui.AbstractIntegrationTest; -import java.math.BigDecimal; import java.time.Duration; import java.util.Map; import org.junit.jupiter.api.AfterAll; @@ -80,11 +79,11 @@ private void assertLastKsqTutorialQueryResult(KsqlApiClient client) { assertThat(header.getValues()).isNull(); }) .assertNext(row -> { - var distance = (DecimalNode) row.getValues().get(0).get(0); + var distance = (DoubleNode) row.getValues().get(0).get(0); var riders = (ArrayNode) row.getValues().get(0).get(1); var count = (IntNode) row.getValues().get(0).get(2); - assertThat(distance).isEqualTo(new DecimalNode(new BigDecimal(0))); + assertThat(distance).isEqualTo(new DoubleNode(0D)); assertThat(riders).isEqualTo(new ArrayNode(JsonNodeFactory.instance) .add(new TextNode("4ab5cbad")) .add(new TextNode("8b6eae59")) @@ -92,11 +91,11 @@ private void assertLastKsqTutorialQueryResult(KsqlApiClient client) { assertThat(count).isEqualTo(new IntNode(3)); }) .assertNext(row -> { - var distance = (DecimalNode) row.getValues().get(0).get(0); + var distance = (DoubleNode) row.getValues().get(0).get(0); var riders = (ArrayNode) row.getValues().get(0).get(1); var count = (IntNode) row.getValues().get(0).get(2); - assertThat(distance).isEqualTo(new DecimalNode(new BigDecimal(10))); + assertThat(distance).isEqualTo(new DoubleNode(10D)); assertThat(riders).isEqualTo(new ArrayNode(JsonNodeFactory.instance) .add(new TextNode("18f4ea86"))); assertThat(count).isEqualTo(new IntNode(1)); diff --git a/pom.xml b/pom.xml index 75cae5bfc..e38658fbb 100644 --- a/pom.xml +++ b/pom.xml @@ -33,30 +33,30 @@ 4.12.0 2.12.0 3.25.3 - 1.11.3 + 1.11.4 1.12.19 7.4.4 3.1.0 3.0.13 2.14.0 3.5.2 - 1.5.5.Final - 1.18.32 + 1.6.2 + 1.18.34 3.25.5 2.13.9 - 2.2 - 3.1.9 + 2.3 + 3.3.4 1.0.0 0.1.17 0.1.39 - 20231013 + 20240303 0.3.0 - 33.0.0-jre + 33.3.1-jre - 5.9.1 + 5.11.2 5.11.0 4.12.0 - 1.19.5 + 1.20.2 v18.17.1 From 19b13ea52dc7e655305f3db2733952cd49d046bd Mon Sep 17 00:00:00 2001 From: Poleg Kashti <46862163+polegkashti@users.noreply.github.com> Date: Wed, 16 Oct 2024 23:59:22 +0300 Subject: [PATCH 24/29] UX: Messages: Use "Newest" as default mode (#579) Co-authored-by: Roman Zabaluev --- frontend/src/lib/hooks/filterUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/hooks/filterUtils.ts b/frontend/src/lib/hooks/filterUtils.ts index 9e9d23fc6..5b3bcd2f0 100644 --- a/frontend/src/lib/hooks/filterUtils.ts +++ b/frontend/src/lib/hooks/filterUtils.ts @@ -1,8 +1,8 @@ import { PollingMode } from 'generated-sources'; export const ModeOptions = [ - { value: PollingMode.EARLIEST, label: 'Oldest' }, { value: PollingMode.LATEST, label: 'Newest' }, + { value: PollingMode.EARLIEST, label: 'Oldest' }, { value: PollingMode.TAILING, label: 'Live' }, { value: PollingMode.FROM_OFFSET, label: 'From offset' }, { value: PollingMode.TO_OFFSET, label: 'To offset' }, From 8d742ba5aab9c8207da084c843490eef354c873f Mon Sep 17 00:00:00 2001 From: "H@di" Date: Thu, 17 Oct 2024 00:30:28 +0330 Subject: [PATCH 25/29] Brokers: Improve accessibility for r/o clusters (#556) Co-authored-by: Roman Zabaluev --- .../main/java/io/kafbat/ui/config/ReadOnlyModeFilter.java | 2 +- .../main/java/io/kafbat/ui/model/InternalBrokerConfig.java | 4 ++-- api/src/main/java/io/kafbat/ui/service/BrokerService.java | 2 +- .../TableComponents/InputCell/InputCellViewMode.tsx | 6 ++++-- .../Broker/Configs/TableComponents/InputCell/styled.ts | 7 ++++--- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/config/ReadOnlyModeFilter.java b/api/src/main/java/io/kafbat/ui/config/ReadOnlyModeFilter.java index 81165e988..ac7c6747f 100644 --- a/api/src/main/java/io/kafbat/ui/config/ReadOnlyModeFilter.java +++ b/api/src/main/java/io/kafbat/ui/config/ReadOnlyModeFilter.java @@ -25,7 +25,7 @@ public class ReadOnlyModeFilter implements WebFilter { Pattern.compile("/api/clusters/(?[^/]++)"); private static final Set SAFE_ENDPOINTS = Set.of( - Pattern.compile("/api/clusters/[^/]+/topics/[^/]+/(smartfilters)$") + Pattern.compile("/api/clusters/[^/]+/topics/[^/]+/(smartfilters|analysis)$") ); private final ClustersStorage clustersStorage; diff --git a/api/src/main/java/io/kafbat/ui/model/InternalBrokerConfig.java b/api/src/main/java/io/kafbat/ui/model/InternalBrokerConfig.java index 496c8bfe5..5f87b0487 100644 --- a/api/src/main/java/io/kafbat/ui/model/InternalBrokerConfig.java +++ b/api/src/main/java/io/kafbat/ui/model/InternalBrokerConfig.java @@ -16,12 +16,12 @@ public class InternalBrokerConfig { private final boolean isReadOnly; private final List synonyms; - public static InternalBrokerConfig from(ConfigEntry configEntry) { + public static InternalBrokerConfig from(ConfigEntry configEntry, boolean readOnlyCluster) { InternalBrokerConfig.InternalBrokerConfigBuilder builder = InternalBrokerConfig.builder() .name(configEntry.name()) .value(configEntry.value()) .source(configEntry.source()) - .isReadOnly(configEntry.isReadOnly()) + .isReadOnly(readOnlyCluster || configEntry.isReadOnly()) .isSensitive(configEntry.isSensitive()) .synonyms(configEntry.synonyms()); return builder.build(); diff --git a/api/src/main/java/io/kafbat/ui/service/BrokerService.java b/api/src/main/java/io/kafbat/ui/service/BrokerService.java index fd9c4c8ab..198685b93 100644 --- a/api/src/main/java/io/kafbat/ui/service/BrokerService.java +++ b/api/src/main/java/io/kafbat/ui/service/BrokerService.java @@ -59,7 +59,7 @@ private Flux getBrokersConfig(KafkaCluster cluster, Intege } return loadBrokersConfig(cluster, brokerId) .map(list -> list.stream() - .map(InternalBrokerConfig::from) + .map(configEntry -> InternalBrokerConfig.from(configEntry, cluster.isReadOnly())) .collect(Collectors.toList())) .flatMapMany(Flux::fromIterable); } diff --git a/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellViewMode.tsx b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellViewMode.tsx index 1ddfa2b03..0fd2bad12 100644 --- a/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellViewMode.tsx +++ b/frontend/src/components/Brokers/Broker/Configs/TableComponents/InputCell/InputCellViewMode.tsx @@ -31,8 +31,10 @@ const InputCellViewMode: FC = ({ ); return ( - - {displayValue} + + + {displayValue} + ` +export const ValueWrapper = styled.div` display: flex; justify-content: space-between; - font-weight: ${({ $isDynamic }) => ($isDynamic ? 600 : 400)}; + font-weight: 400; button { margin: 0 10px; } `; -export const Value = styled.span` +export const Value = styled.span<{ $isDynamic?: boolean }>` line-height: 24px; margin-right: 10px; text-overflow: ellipsis; max-width: 400px; overflow: hidden; white-space: nowrap; + font-weight: ${({ $isDynamic }) => ($isDynamic ? 600 : 400)}; `; export const ButtonsWrapper = styled.div` From b4ffd96b95536ebe5891c5be99b9ce6ba7cab241 Mon Sep 17 00:00:00 2001 From: Renat Kalimulin <103274228+Nilumilak@users.noreply.github.com> Date: Thu, 17 Oct 2024 00:07:44 +0300 Subject: [PATCH 26/29] FE: Bump datepicker (#605) --- frontend/package.json | 2 +- frontend/pnpm-lock.yaml | 301 +++++++++++++++++++++++----------------- 2 files changed, 171 insertions(+), 132 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index aef4f7fea..c4fef99db 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,7 +21,7 @@ "pretty-ms": "7.0.1", "react": "18.2.0", "react-ace": "11.0.1", - "react-datepicker": "6.9.0", + "react-datepicker": "7.4.0", "react-dom": "18.2.0", "react-error-boundary": "4.0.13", "react-hook-form": "7.51.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 7ad87cafd..a7ba33ed0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -21,28 +21,28 @@ importers: dependencies: '@floating-ui/react': specifier: 0.26.13 - version: 0.26.13(react-dom@18.2.0)(react@18.2.0) + version: 0.26.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@hookform/error-message': specifier: 2.0.1 - version: 2.0.1(react-dom@18.2.0)(react-hook-form@7.51.3)(react@18.2.0) + version: 2.0.1(react-dom@18.2.0(react@18.2.0))(react-hook-form@7.51.3(react@18.2.0))(react@18.2.0) '@hookform/resolvers': specifier: 2.7.1 - version: 2.7.1(react-hook-form@7.51.3) + version: 2.7.1(react-hook-form@7.51.3(react@18.2.0)) '@microsoft/fetch-event-source': specifier: 2.0.1 version: 2.0.1 '@szhsin/react-menu': specifier: 3.5.3 - version: 3.5.3(react-dom@18.2.0)(react@18.2.0) + version: 3.5.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@tanstack/react-query': specifier: 4.36.1 - version: 4.36.1(react-dom@18.2.0)(react@18.2.0) + version: 4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@tanstack/react-query-devtools': specifier: 4.36.1 - version: 4.36.1(@tanstack/react-query@4.36.1)(react-dom@18.2.0)(react@18.2.0) + version: 4.36.1(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@tanstack/react-table': specifier: 8.16.0 - version: 8.16.0(react-dom@18.2.0)(react@18.2.0) + version: 8.16.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) ace-builds: specifier: 1.33.0 version: 1.33.0 @@ -69,10 +69,10 @@ importers: version: 18.2.0 react-ace: specifier: 11.0.1 - version: 11.0.1(react-dom@18.2.0)(react@18.2.0) + version: 11.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-datepicker: - specifier: 6.9.0 - version: 6.9.0(react-dom@18.2.0)(react@18.2.0) + specifier: 7.4.0 + version: 7.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) @@ -84,22 +84,22 @@ importers: version: 7.51.3(react@18.2.0) react-hot-toast: specifier: 2.4.1 - version: 2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0) + version: 2.4.1(csstype@3.1.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-is: specifier: 18.2.0 version: 18.2.0 react-multi-select-component: specifier: 4.3.4 - version: 4.3.4(react-dom@18.2.0)(react@18.2.0) + version: 4.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react-router-dom: specifier: 6.23.0 - version: 6.23.0(react-dom@18.2.0)(react@18.2.0) + version: 6.23.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) sass: specifier: 1.66.1 version: 1.66.1 styled-components: specifier: 6.1.8 - version: 6.1.8(react-dom@18.2.0)(react@18.2.0) + version: 6.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) use-debounce: specifier: 10.0.0 version: 10.0.0(react@18.2.0) @@ -127,10 +127,10 @@ importers: version: 10.0.0 '@testing-library/jest-dom': specifier: 6.4.2 - version: 6.4.2(jest@29.7.0) + version: 6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3))) '@testing-library/react': specifier: 14.3.1 - version: 14.3.1(react-dom@18.2.0)(react@18.2.0) + version: 14.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@testing-library/user-event': specifier: 14.5.2 version: 14.5.2(@testing-library/dom@10.0.0) @@ -148,7 +148,7 @@ importers: version: 18.2.79 '@types/react-datepicker': specifier: 6.2.0 - version: 6.2.0(react-dom@18.2.0)(react@18.2.0) + version: 6.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/react-dom': specifier: 18.2.25 version: 18.2.25 @@ -163,13 +163,13 @@ importers: version: 5.14.9 '@typescript-eslint/eslint-plugin': specifier: 6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.3.3) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/parser': specifier: 6.21.0 version: 6.21.0(eslint@8.57.0)(typescript@5.3.3) '@vitejs/plugin-react-swc': specifier: 3.6.0 - version: 3.6.0(vite@5.2.10) + version: 3.6.0(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)) dotenv: specifier: 16.4.5 version: 16.4.5 @@ -178,10 +178,10 @@ importers: version: 8.57.0 eslint-config-airbnb: specifier: 19.0.4 - version: 19.0.4(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.34.1)(eslint@8.57.0) + version: 19.0.4(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.34.1(eslint@8.57.0))(eslint@8.57.0) eslint-config-airbnb-typescript: specifier: 18.0.0 - version: 18.0.0(@typescript-eslint/eslint-plugin@6.21.0)(@typescript-eslint/parser@6.21.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + version: 18.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) eslint-config-prettier: specifier: 9.1.0 version: 9.1.0(eslint@8.57.0) @@ -190,10 +190,10 @@ importers: version: 0.3.9 eslint-import-resolver-typescript: specifier: 3.6.1 - version: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + version: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-import: specifier: 2.29.1 - version: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + version: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jest-dom: specifier: 5.4.0 version: 5.4.0(@testing-library/dom@10.0.0)(eslint@8.57.0) @@ -202,7 +202,7 @@ importers: version: 6.8.0(eslint@8.57.0) eslint-plugin-prettier: specifier: 5.1.3 - version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) + version: 5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5) eslint-plugin-react: specifier: 7.34.1 version: 7.34.1(eslint@8.57.0) @@ -211,10 +211,10 @@ importers: version: 4.6.0(eslint@8.57.0) fetch-mock: specifier: 9.11.0 - version: 9.11.0 + version: 9.11.0(node-fetch@2.6.7) jest: specifier: 29.7.0 - version: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2) + version: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) jest-environment-jsdom: specifier: 29.7.0 version: 29.7.0 @@ -223,10 +223,10 @@ importers: version: 2.0.0 jest-styled-components: specifier: 7.1.1 - version: 7.1.1(styled-components@6.1.8) + version: 7.1.1(styled-components@6.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)) jest-watch-typeahead: specifier: 2.2.2 - version: 2.2.2(jest@29.7.0) + version: 2.2.2(jest@29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3))) prettier: specifier: 3.2.5 version: 3.2.5 @@ -247,13 +247,13 @@ importers: version: 5.2.10(@types/node@20.11.17)(sass@1.66.1) vite-plugin-checker: specifier: 0.6.4 - version: 0.6.4(eslint@8.57.0)(typescript@5.3.3)(vite@5.2.10) + version: 0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.3.3)(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)) vite-plugin-ejs: specifier: 1.7.0 - version: 1.7.0(vite@5.2.10) + version: 1.7.0(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)) vite-tsconfig-paths: specifier: 4.3.2 - version: 4.3.2(typescript@5.3.3)(vite@5.2.10) + version: 4.3.2(typescript@5.3.3)(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)) whatwg-fetch: specifier: 3.6.20 version: 3.6.20 @@ -627,15 +627,30 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/react@0.26.13': resolution: {integrity: sha512-kBa9wntpugzrZ8t/4yWelvSmEKZdeTXTJzrxqyrLmcU/n1SM4nvse8yQh2e1b37rJGvtu0EplV9+IkBrCJ1vkw==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' + '@floating-ui/react@0.26.24': + resolution: {integrity: sha512-2ly0pCkZIGEQUq5H8bBK0XJmc1xIK/RM3tvVzY3GBER7IOD1UgmC2Y2tjj4AuS+TC+vTE1KJv2053290jua0Sw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + '@floating-ui/utils@0.2.1': resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + '@floating-ui/utils@0.2.8': + resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + '@hookform/error-message@2.0.1': resolution: {integrity: sha512-U410sAr92xgxT1idlu9WWOVjndxLdgPUHEB8Schr27C9eh7/xUnITWpCMF93s+lGiG++D4JnbSnrb5A21AdSNg==} peerDependencies: @@ -1621,8 +1636,8 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - clsx@2.1.0: - resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} co@4.6.0: @@ -3323,8 +3338,8 @@ packages: react: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^0.13.0 || ^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-datepicker@6.9.0: - resolution: {integrity: sha512-QTxuzeem7BUfVFWv+g5WuvzT0c5BPo+XTCNbMTZKSZQLU+cMMwSUHwspaxuIcDlwNcOH0tiJ+bh1fJ2yxOGYWA==} + react-datepicker@7.4.0: + resolution: {integrity: sha512-vSSok4DTZ9/Os8O4HjZLxh4SZVFU6dQvoCX6mfbNdBqMsBBdzftrvMz0Nb4UUVVbgj9o8PfX84K3/31oPrTqmg==} peerDependencies: react: ^16.9.0 || ^17 || ^18 react-dom: ^16.9.0 || ^17 || ^18 @@ -3367,12 +3382,6 @@ packages: react: ^16 || ^17 || ^18 react-dom: ^16 || ^17 || ^18 - react-onclickoutside@6.13.0: - resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} - peerDependencies: - react: ^15.5.x || ^16.x || ^17.x || ^18.x - react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x - react-router-dom@6.23.0: resolution: {integrity: sha512-Q9YaSYvubwgbal2c9DJKfx6hTNoBp3iJDsl+Duva/DwxoJH+OTXkxGpql4iUK2sla/8z4RpjAm6EWx1qUDuopQ==} engines: {node: '>=14.0.0'} @@ -4548,31 +4557,47 @@ snapshots: '@floating-ui/dom@1.6.3': dependencies: '@floating-ui/core': 1.2.1 - '@floating-ui/utils': 0.2.1 + '@floating-ui/utils': 0.2.8 + + '@floating-ui/react-dom@2.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@floating-ui/dom': 1.6.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) - '@floating-ui/react-dom@2.0.8(react-dom@18.2.0)(react@18.2.0)': + '@floating-ui/react-dom@2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@floating-ui/dom': 1.6.3 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - '@floating-ui/react@0.26.13(react-dom@18.2.0)(react@18.2.0)': + '@floating-ui/react@0.26.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@floating-ui/utils': 0.2.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) tabbable: 6.1.1 + '@floating-ui/react@0.26.24(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@floating-ui/utils': 0.2.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tabbable: 6.1.1 + '@floating-ui/utils@0.2.1': {} - '@hookform/error-message@2.0.1(react-dom@18.2.0)(react-hook-form@7.51.3)(react@18.2.0)': + '@floating-ui/utils@0.2.8': {} + + '@hookform/error-message@2.0.1(react-dom@18.2.0(react@18.2.0))(react-hook-form@7.51.3(react@18.2.0))(react@18.2.0)': dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-hook-form: 7.51.3(react@18.2.0) - '@hookform/resolvers@2.7.1(react-hook-form@7.51.3)': + '@hookform/resolvers@2.7.1(react-hook-form@7.51.3(react@18.2.0))': dependencies: react-hook-form: 7.51.3(react@18.2.0) @@ -4625,7 +4650,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2)': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -4639,7 +4664,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -4820,7 +4845,7 @@ snapshots: '@microsoft/fetch-event-source@2.0.1': {} - '@nestjs/axios@3.0.2(@nestjs/common@10.3.0)(axios@1.6.8)(rxjs@7.8.1)': + '@nestjs/axios@3.0.2(@nestjs/common@10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1) axios: 1.6.8 @@ -4834,7 +4859,7 @@ snapshots: tslib: 2.6.2 uid: 2.0.2 - '@nestjs/core@10.3.0(@nestjs/common@10.3.0)(reflect-metadata@0.1.13)(rxjs@7.8.1)': + '@nestjs/core@10.3.0(@nestjs/common@10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1))(reflect-metadata@0.1.13)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 @@ -4870,9 +4895,9 @@ snapshots: '@openapitools/openapi-generator-cli@2.13.4': dependencies: - '@nestjs/axios': 3.0.2(@nestjs/common@10.3.0)(axios@1.6.8)(rxjs@7.8.1) + '@nestjs/axios': 3.0.2(@nestjs/common@10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1) '@nestjs/common': 10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.3.0(@nestjs/common@10.3.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.3.0(@nestjs/common@10.3.0(reflect-metadata@0.1.13)(rxjs@7.8.1))(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 axios: 1.6.8 chalk: 4.1.2 @@ -5022,12 +5047,12 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@szhsin/react-menu@3.5.3(react-dom@18.2.0)(react@18.2.0)': + '@szhsin/react-menu@3.5.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-transition-state: 1.1.5(react-dom@18.2.0)(react@18.2.0) + react-transition-state: 1.1.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@tanstack/match-sorter-utils@8.15.1': dependencies: @@ -5035,23 +5060,24 @@ snapshots: '@tanstack/query-core@4.36.1': {} - '@tanstack/react-query-devtools@4.36.1(@tanstack/react-query@4.36.1)(react-dom@18.2.0)(react@18.2.0)': + '@tanstack/react-query-devtools@4.36.1(@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@tanstack/match-sorter-utils': 8.15.1 - '@tanstack/react-query': 4.36.1(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-query': 4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) superjson: 1.13.3 use-sync-external-store: 1.2.0(react@18.2.0) - '@tanstack/react-query@4.36.1(react-dom@18.2.0)(react@18.2.0)': + '@tanstack/react-query@4.36.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@tanstack/query-core': 4.36.1 react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) use-sync-external-store: 1.2.0(react@18.2.0) + optionalDependencies: + react-dom: 18.2.0(react@18.2.0) - '@tanstack/react-table@8.16.0(react-dom@18.2.0)(react@18.2.0)': + '@tanstack/react-table@8.16.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@tanstack/table-core': 8.16.0 react: 18.2.0 @@ -5081,7 +5107,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.2(jest@29.7.0)': + '@testing-library/jest-dom@6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)))': dependencies: '@adobe/css-tools': 4.3.3 '@babel/runtime': 7.22.11 @@ -5089,11 +5115,14 @@ snapshots: chalk: 3.0.0 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 - jest: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2) lodash: 4.17.21 redent: 3.0.0 + optionalDependencies: + '@jest/globals': 29.7.0 + '@types/jest': 29.5.12 + jest: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) - '@testing-library/react@14.3.1(react-dom@18.2.0)(react@18.2.0)': + '@testing-library/react@14.3.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.22.11 '@testing-library/dom': 9.3.1 @@ -5195,9 +5224,9 @@ snapshots: '@types/prop-types@15.7.5': {} - '@types/react-datepicker@6.2.0(react-dom@18.2.0)(react@18.2.0)': + '@types/react-datepicker@6.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: - '@floating-ui/react': 0.26.13(react-dom@18.2.0)(react@18.2.0) + '@floating-ui/react': 0.26.13(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@types/react': 18.2.79 date-fns: 3.6.0 transitivePeerDependencies: @@ -5248,7 +5277,7 @@ snapshots: dependencies: '@types/yargs-parser': 20.2.0 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.8.0 '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3) @@ -5263,6 +5292,7 @@ snapshots: natural-compare: 1.4.0 semver: 7.5.4 ts-api-utils: 1.3.0(typescript@5.3.3) + optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -5275,6 +5305,7 @@ snapshots: '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4 eslint: 8.57.0 + optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -5291,6 +5322,7 @@ snapshots: debug: 4.3.4 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.3.3) + optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -5307,6 +5339,7 @@ snapshots: minimatch: 9.0.3 semver: 7.5.4 ts-api-utils: 1.3.0(typescript@5.3.3) + optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: - supports-color @@ -5332,7 +5365,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react-swc@3.6.0(vite@5.2.10)': + '@vitejs/plugin-react-swc@3.6.0(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1))': dependencies: '@swc/core': 1.3.107 vite: 5.2.10(@types/node@20.11.17)(sass@1.66.1) @@ -5373,7 +5406,7 @@ snapshots: - supports-color ajv-formats@2.1.1(ajv@8.8.2): - dependencies: + optionalDependencies: ajv: 8.8.2 ajv@6.12.6: @@ -5726,7 +5759,7 @@ snapshots: clone@1.0.4: {} - clsx@2.1.0: {} + clsx@2.1.1: {} co@4.6.0: {} @@ -5797,13 +5830,13 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - create-jest@29.7.0(@types/node@20.11.17)(ts-node@10.9.2): + create-jest@29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.10 - jest-config: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -6190,29 +6223,29 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) object.assign: 4.1.4 object.entries: 1.1.7 semver: 6.3.1 - eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@6.21.0)(@typescript-eslint/parser@6.21.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-config-airbnb-typescript@18.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.3.3) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint@8.57.0)(typescript@5.3.3) '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) transitivePeerDependencies: - eslint-plugin-import - eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.34.1)(eslint@8.57.0): + eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.34.1(eslint@8.57.0))(eslint@8.57.0): dependencies: eslint: 8.57.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-react: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.57.0) @@ -6231,13 +6264,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.15.0 eslint: 8.57.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.2 is-core-module: 2.13.1 @@ -6248,19 +6281,19 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3) debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 @@ -6269,7 +6302,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -6279,6 +6312,8 @@ snapshots: object.values: 1.1.7 semver: 6.3.1 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.3.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -6287,9 +6322,10 @@ snapshots: eslint-plugin-jest-dom@5.4.0(@testing-library/dom@10.0.0)(eslint@8.57.0): dependencies: '@babel/runtime': 7.22.11 - '@testing-library/dom': 10.0.0 eslint: 8.57.0 requireindex: 1.2.0 + optionalDependencies: + '@testing-library/dom': 10.0.0 eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0): dependencies: @@ -6311,13 +6347,14 @@ snapshots: object.entries: 1.1.7 object.fromentries: 2.0.7 - eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5): + eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5): dependencies: eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) prettier: 3.2.5 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 + optionalDependencies: + eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-plugin-react-hooks@4.6.0(eslint@8.57.0): dependencies: @@ -6469,7 +6506,7 @@ snapshots: dependencies: bser: 2.1.1 - fetch-mock@9.11.0: + fetch-mock@9.11.0(node-fetch@2.6.7): dependencies: '@babel/core': 7.18.9 '@babel/runtime': 7.22.11 @@ -6481,6 +6518,8 @@ snapshots: path-to-regexp: 2.4.0 querystring: 0.2.1 whatwg-url: 6.5.0 + optionalDependencies: + node-fetch: 2.6.7 transitivePeerDependencies: - supports-color @@ -7040,16 +7079,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.11.17)(ts-node@10.9.2): + jest-cli@29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2) + create-jest: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.5.1 @@ -7059,12 +7098,11 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.11.17)(ts-node@10.9.2): + jest-config@29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)): dependencies: '@babel/core': 7.18.9 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.11.17 babel-jest: 29.7.0(@babel/core@7.18.9) chalk: 4.1.2 ci-info: 3.3.1 @@ -7084,6 +7122,8 @@ snapshots: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.11.17 ts-node: 10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3) transitivePeerDependencies: - babel-plugin-macros @@ -7193,7 +7233,7 @@ snapshots: jest-util: 29.7.0 jest-pnp-resolver@1.2.2(jest-resolve@29.7.0): - dependencies: + optionalDependencies: jest-resolve: 29.7.0 jest-regex-util@29.6.3: {} @@ -7299,10 +7339,10 @@ snapshots: dependencies: xml: 1.0.1 - jest-styled-components@7.1.1(styled-components@6.1.8): + jest-styled-components@7.1.1(styled-components@6.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0)): dependencies: '@adobe/css-tools': 4.3.3 - styled-components: 6.1.8(react-dom@18.2.0)(react@18.2.0) + styled-components: 6.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0) jest-util@29.6.3: dependencies: @@ -7331,11 +7371,11 @@ snapshots: leven: 3.1.0 pretty-format: 29.7.0 - jest-watch-typeahead@2.2.2(jest@29.7.0): + jest-watch-typeahead@2.2.2(jest@29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3))): dependencies: ansi-escapes: 6.0.0 chalk: 5.2.0 - jest: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2) + jest: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) jest-regex-util: 29.6.3 jest-watcher: 29.6.4 slash: 5.0.0 @@ -7371,12 +7411,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.11.17)(ts-node@10.9.2): + jest@29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2) + jest-cli: 29.7.0(@types/node@20.11.17)(ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -7849,7 +7889,7 @@ snapshots: queue-microtask@1.2.3: {} - react-ace@11.0.1(react-dom@18.2.0)(react@18.2.0): + react-ace@11.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: ace-builds: 1.33.0 diff-match-patch: 1.0.5 @@ -7859,15 +7899,14 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-datepicker@6.9.0(react-dom@18.2.0)(react@18.2.0): + react-datepicker@7.4.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: - '@floating-ui/react': 0.26.13(react-dom@18.2.0)(react@18.2.0) - clsx: 2.1.0 + '@floating-ui/react': 0.26.24(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + clsx: 2.1.1 date-fns: 3.6.0 prop-types: 15.8.1 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0) react-dom@18.2.0(react@18.2.0): dependencies: @@ -7884,7 +7923,7 @@ snapshots: dependencies: react: 18.2.0 - react-hot-toast@2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0): + react-hot-toast@2.4.1(csstype@3.1.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: goober: 2.1.10(csstype@3.1.2) react: 18.2.0 @@ -7898,17 +7937,12 @@ snapshots: react-is@18.2.0: {} - react-multi-select-component@4.3.4(react-dom@18.2.0)(react@18.2.0): - dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - - react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0): + react-multi-select-component@4.3.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-router-dom@6.23.0(react-dom@18.2.0)(react@18.2.0): + react-router-dom@6.23.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@remix-run/router': 1.16.0 react: 18.2.0 @@ -7920,7 +7954,7 @@ snapshots: '@remix-run/router': 1.16.0 react: 18.2.0 - react-transition-state@1.1.5(react-dom@18.2.0)(react@18.2.0): + react-transition-state@1.1.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -8276,7 +8310,7 @@ snapshots: strip-json-comments@3.1.1: {} - styled-components@6.1.8(react-dom@18.2.0)(react@18.2.0): + styled-components@6.1.8(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@emotion/is-prop-valid': 1.2.1 '@emotion/unitless': 0.8.0 @@ -8382,7 +8416,6 @@ snapshots: ts-node@10.9.2(@swc/core@1.3.107)(@types/node@20.11.17)(typescript@5.3.3): dependencies: '@cspotcode/source-map-support': 0.8.1 - '@swc/core': 1.3.107 '@tsconfig/node10': 1.0.9 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 @@ -8397,6 +8430,8 @@ snapshots: typescript: 5.3.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.3.107 ts-prune@0.10.3: dependencies: @@ -8408,7 +8443,7 @@ snapshots: ts-morph: 13.0.3 tsconfck@3.0.3(typescript@5.3.3): - dependencies: + optionalDependencies: typescript: 5.3.3 tsconfig-paths@3.15.0: @@ -8547,37 +8582,40 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.3 convert-source-map: 1.7.0 - vite-plugin-checker@0.6.4(eslint@8.57.0)(typescript@5.3.3)(vite@5.2.10): + vite-plugin-checker@0.6.4(eslint@8.57.0)(optionator@0.9.3)(typescript@5.3.3)(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)): dependencies: '@babel/code-frame': 7.23.5 ansi-escapes: 4.3.2 chalk: 4.1.2 chokidar: 3.5.2 commander: 8.3.0 - eslint: 8.57.0 fast-glob: 3.3.2 fs-extra: 11.2.0 npm-run-path: 4.0.1 semver: 7.5.4 strip-ansi: 6.0.1 tiny-invariant: 1.3.3 - typescript: 5.3.3 vite: 5.2.10(@types/node@20.11.17)(sass@1.66.1) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 + optionalDependencies: + eslint: 8.57.0 + optionator: 0.9.3 + typescript: 5.3.3 - vite-plugin-ejs@1.7.0(vite@5.2.10): + vite-plugin-ejs@1.7.0(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)): dependencies: ejs: 3.1.10 vite: 5.2.10(@types/node@20.11.17)(sass@1.66.1) - vite-tsconfig-paths@4.3.2(typescript@5.3.3)(vite@5.2.10): + vite-tsconfig-paths@4.3.2(typescript@5.3.3)(vite@5.2.10(@types/node@20.11.17)(sass@1.66.1)): dependencies: debug: 4.3.4 globrex: 0.1.2 tsconfck: 3.0.3(typescript@5.3.3) + optionalDependencies: vite: 5.2.10(@types/node@20.11.17)(sass@1.66.1) transitivePeerDependencies: - supports-color @@ -8585,13 +8623,13 @@ snapshots: vite@5.2.10(@types/node@20.11.17)(sass@1.66.1): dependencies: - '@types/node': 20.11.17 esbuild: 0.20.2 postcss: 8.4.38 rollup: 4.16.1 - sass: 1.66.1 optionalDependencies: + '@types/node': 20.11.17 fsevents: 2.3.3 + sass: 1.66.1 vscode-jsonrpc@6.0.0: {} @@ -8790,6 +8828,7 @@ snapshots: zustand@4.5.2(@types/react@18.2.79)(react@18.2.0): dependencies: + use-sync-external-store: 1.2.0(react@18.2.0) + optionalDependencies: '@types/react': 18.2.79 react: 18.2.0 - use-sync-external-store: 1.2.0(react@18.2.0) From 7be3325170d373ea882129a079c966951a2bb603 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Thu, 17 Oct 2024 23:43:34 +0400 Subject: [PATCH 27/29] Chore: Deps: Bump aws sdk (#618) --- api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/pom.xml b/api/pom.xml index 09297b840..8452167ce 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -91,7 +91,7 @@ software.amazon.msk aws-msk-iam-auth - 2.1.0 + 2.2.0 From c8a875944b89ba6ef5bffe35b8a4a25f09cf2023 Mon Sep 17 00:00:00 2001 From: Poleg Kashti <46862163+polegkashti@users.noreply.github.com> Date: Fri, 18 Oct 2024 13:06:21 +0300 Subject: [PATCH 28/29] BE: ACL: Consumers preset now include DESCRIBE permission for CG (#593) Co-authored-by: Poleg Kashti Co-authored-by: Roman Zabaluev --- .../io/kafbat/ui/service/acl/AclsService.java | 4 ++-- .../ui/service/acl/AclsServiceTest.java | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/service/acl/AclsService.java b/api/src/main/java/io/kafbat/ui/service/acl/AclsService.java index 4ea82fe12..b3877a336 100644 --- a/api/src/main/java/io/kafbat/ui/service/acl/AclsService.java +++ b/api/src/main/java/io/kafbat/ui/service/acl/AclsService.java @@ -158,7 +158,7 @@ public Mono createConsumerAcl(KafkaCluster cluster, CreateConsumerAclDTO r .then(); } - //Read, Describe on topics, Read on consumerGroups + //Read, Describe on topics and consumerGroups private List createConsumerBindings(CreateConsumerAclDTO request) { List bindings = new ArrayList<>(); bindings.addAll( @@ -172,7 +172,7 @@ private List createConsumerBindings(CreateConsumerAclDTO request) { bindings.addAll( createAllowBindings( GROUP, - List.of(READ), + List.of(READ, DESCRIBE), request.getPrincipal(), request.getHost(), request.getConsumerGroupsPrefix(), 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 189e7c060..cfa46d1eb 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 @@ -103,10 +103,10 @@ void createsConsumerDependantAcls() { .topics(List.of("t1", "t2")) ).block(); - //Read, Describe on topics, Read on consumerGroups + //Read, Describe on topics and consumerGroups Collection createdBindings = createdCaptor.getValue(); assertThat(createdBindings) - .hasSize(6) + .hasSize(8) .contains(new AclBinding( new ResourcePattern(ResourceType.TOPIC, "t1", PatternType.LITERAL), new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) @@ -122,9 +122,15 @@ void createsConsumerDependantAcls() { .contains(new AclBinding( new ResourcePattern(ResourceType.GROUP, "cg1", PatternType.LITERAL), new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.GROUP, "cg1", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.GROUP, "cg2", PatternType.LITERAL), + new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) .contains(new AclBinding( new ResourcePattern(ResourceType.GROUP, "cg2", PatternType.LITERAL), - new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))); + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))); } @Test @@ -145,10 +151,10 @@ void createsConsumerDependantAclsWhenTopicsAndGroupsSpecifiedByPrefix() { .topicsPrefix("topicPref") ).block(); - //Read, Describe on topics, Read on consumerGroups + //Read, Describe on topics and consumerGroups Collection createdBindings = createdCaptor.getValue(); assertThat(createdBindings) - .hasSize(3) + .hasSize(4) .contains(new AclBinding( new ResourcePattern(ResourceType.TOPIC, "topicPref", PatternType.PREFIXED), new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) @@ -157,7 +163,10 @@ void createsConsumerDependantAclsWhenTopicsAndGroupsSpecifiedByPrefix() { new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))) .contains(new AclBinding( new ResourcePattern(ResourceType.GROUP, "cgPref", PatternType.PREFIXED), - new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))); + new AccessControlEntry(principal, host, AclOperation.READ, AclPermissionType.ALLOW))) + .contains(new AclBinding( + new ResourcePattern(ResourceType.GROUP, "cgPref", PatternType.PREFIXED), + new AccessControlEntry(principal, host, AclOperation.DESCRIBE, AclPermissionType.ALLOW))); } @Test From 0ad86953a24ea78bf3b61739e49559c73a44d6ed Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Thu, 24 Oct 2024 13:43:22 +0400 Subject: [PATCH 29/29] Chore: Update local compose (#632) --- .dev/dev_arm64.yaml | 15 +++++++-------- .../rbac/extractor/OauthAuthorityExtractor.java | 2 +- documentation/compose/scripts/clusterID | 1 - .../compose/scripts/create_cluster_id.sh | 1 - documentation/compose/scripts/update_run.sh | 11 ----------- .../compose/scripts/update_run_cluster.sh | 11 ----------- 6 files changed, 8 insertions(+), 33 deletions(-) delete mode 100644 documentation/compose/scripts/clusterID delete mode 100644 documentation/compose/scripts/create_cluster_id.sh delete mode 100755 documentation/compose/scripts/update_run.sh delete mode 100644 documentation/compose/scripts/update_run_cluster.sh diff --git a/.dev/dev_arm64.yaml b/.dev/dev_arm64.yaml index b43e73bce..220140d3d 100644 --- a/.dev/dev_arm64.yaml +++ b/.dev/dev_arm64.yaml @@ -32,7 +32,8 @@ services: KAFKA_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED: 'true' kafka0: - image: confluentinc/cp-kafka:7.2.1.arm64 + image: confluentinc/cp-kafka:7.6.0.arm64 + user: "0:0" hostname: kafka0 container_name: kafka0 ports: @@ -56,12 +57,10 @@ services: KAFKA_JMX_PORT: 9997 # KAFKA_JMX_HOSTNAME: localhost # uncomment this line and comment the next one if running with kafka-ui as a jar KAFKA_JMX_OPTS: -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=kafka0 -Dcom.sun.management.jmxremote.rmi.port=9997 - volumes: - - ../documentation/compose/scripts/update_run.sh:/tmp/update_run.sh - command: "bash -c 'if [ ! -f /tmp/update_run.sh ]; then echo \"ERROR: Did you forget the update_run.sh file that came with this docker-compose.yml file?\" && exit 1 ; else /tmp/update_run.sh && /etc/confluent/docker/run ; fi'" + CLUSTER_ID: 'MkU3OEVBNTcwNTJENDM2Qk' schema-registry0: - image: confluentinc/cp-schema-registry:7.2.1.arm64 + image: confluentinc/cp-schema-registry:7.6.0.arm64 ports: - 8085:8085 depends_on: @@ -77,7 +76,7 @@ services: SCHEMA_REGISTRY_KAFKASTORE_TOPIC: _schemas kafka-connect0: - image: confluentinc/cp-kafka-connect:7.2.1.arm64 + image: confluentinc/cp-kafka-connect:7.6.0.arm64 ports: - 8083:8083 depends_on: @@ -102,7 +101,7 @@ services: CONNECT_PLUGIN_PATH: "/usr/share/java,/usr/share/confluent-hub-components,/usr/local/share/kafka/plugins,/usr/share/filestream-connectors" ksqldb0: - image: confluentinc/ksqldb-server:0.18.0 + image: confluentinc/cp-ksqldb-server:7.6.0.arm64 depends_on: - kafka0 - kafka-connect0 @@ -120,7 +119,7 @@ services: KSQL_CACHE_MAX_BYTES_BUFFERING: 0 kafka-init-topics: - image: confluentinc/cp-kafka:7.2.1.arm64 + image: confluentinc/cp-kafka:7.6.0.arm64 volumes: - ../documentation/compose/data/message.json:/data/message.json depends_on: diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/OauthAuthorityExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/OauthAuthorityExtractor.java index a4458e64b..6d14ab870 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/OauthAuthorityExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/OauthAuthorityExtractor.java @@ -76,7 +76,7 @@ private Set extractRoles(AccessControlService acs, DefaultOAuth2User pri var rolesFieldName = provider.getCustomParams().get(ROLES_FIELD_PARAM_NAME); if (rolesFieldName == null) { - log.warn("Provider [{}] doesn't contain a roles field param name, won't map roles", provider); + log.warn("Provider [{}] doesn't contain a roles field param name, won't map roles", provider.getClientName()); return Collections.emptySet(); } diff --git a/documentation/compose/scripts/clusterID b/documentation/compose/scripts/clusterID deleted file mode 100644 index 4417a5a68..000000000 --- a/documentation/compose/scripts/clusterID +++ /dev/null @@ -1 +0,0 @@ -zlFiTJelTOuhnklFwLWixw \ No newline at end of file diff --git a/documentation/compose/scripts/create_cluster_id.sh b/documentation/compose/scripts/create_cluster_id.sh deleted file mode 100644 index e921e836c..000000000 --- a/documentation/compose/scripts/create_cluster_id.sh +++ /dev/null @@ -1 +0,0 @@ -kafka-storage random-uuid > /workspace/kafbat-ui/documentation/compose/clusterID diff --git a/documentation/compose/scripts/update_run.sh b/documentation/compose/scripts/update_run.sh deleted file mode 100755 index 023c832b4..000000000 --- a/documentation/compose/scripts/update_run.sh +++ /dev/null @@ -1,11 +0,0 @@ -# This script is required to run kafka cluster (without zookeeper) -#!/bin/sh - -# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter -sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure - -# Docker workaround: Ignore cub zk-ready -sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure - -# KRaft required step: Format the storage directory with a new cluster ID -echo "kafka-storage format --ignore-formatted -t $(kafka-storage random-uuid) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure \ No newline at end of file diff --git a/documentation/compose/scripts/update_run_cluster.sh b/documentation/compose/scripts/update_run_cluster.sh deleted file mode 100644 index 31da333aa..000000000 --- a/documentation/compose/scripts/update_run_cluster.sh +++ /dev/null @@ -1,11 +0,0 @@ -# This script is required to run kafka cluster (without zookeeper) -#!/bin/sh - -# Docker workaround: Remove check for KAFKA_ZOOKEEPER_CONNECT parameter -sed -i '/KAFKA_ZOOKEEPER_CONNECT/d' /etc/confluent/docker/configure - -# Docker workaround: Ignore cub zk-ready -sed -i 's/cub zk-ready/echo ignore zk-ready/' /etc/confluent/docker/ensure - -# KRaft required step: Format the storage directory with a new cluster ID -echo "kafka-storage format --ignore-formatted -t $(cat /tmp/clusterID) -c /etc/kafka/kafka.properties" >> /etc/confluent/docker/ensure \ No newline at end of file