From 8a03bfde88015353dae517b7b7aa225428ef9195 Mon Sep 17 00:00:00 2001 From: German Osin Date: Tue, 10 Jun 2025 00:15:50 +0200 Subject: [PATCH 01/36] Use typespec contracts --- .../kafbat/ui/mapper/DynamicConfigMapper.java | 1 + .../mcp/McpSpecificationGeneratorTest.java | 2 +- build.gradle | 1 + contract-typespec/api/acls.tsp | 123 ++ contract-typespec/api/auth.tsp | 42 + contract-typespec/api/brokers.tsp | 114 + contract-typespec/api/clusters.tsp | 95 + contract-typespec/api/config.tsp | 311 +++ contract-typespec/api/consumer-groups.tsp | 139 ++ contract-typespec/api/kafka-connect.tsp | 283 +++ contract-typespec/api/ksql.tsp | 70 + contract-typespec/api/main.tsp | 13 + contract-typespec/api/messages.tsp | 234 ++ contract-typespec/api/models.tsp | 37 + contract-typespec/api/package-lock.json | 1933 +++++++++++++++++ contract-typespec/api/package.json | 15 + contract-typespec/api/quotas.tsp | 30 + contract-typespec/api/responses.tsp | 70 + contract-typespec/api/schemas.tsp | 160 ++ contract-typespec/api/topics.tsp | 309 +++ contract-typespec/api/tspconfig.yaml | 6 + contract-typespec/build.gradle | 58 + contract/build.gradle | 14 +- .../main/resources/swagger/kafbat-ui-api.yaml | 1 + gradle/libs.versions.toml | 2 +- settings.gradle | 1 + 26 files changed, 4060 insertions(+), 4 deletions(-) create mode 100644 contract-typespec/api/acls.tsp create mode 100644 contract-typespec/api/auth.tsp create mode 100644 contract-typespec/api/brokers.tsp create mode 100644 contract-typespec/api/clusters.tsp create mode 100644 contract-typespec/api/config.tsp create mode 100644 contract-typespec/api/consumer-groups.tsp create mode 100644 contract-typespec/api/kafka-connect.tsp create mode 100644 contract-typespec/api/ksql.tsp create mode 100644 contract-typespec/api/main.tsp create mode 100644 contract-typespec/api/messages.tsp create mode 100644 contract-typespec/api/models.tsp create mode 100644 contract-typespec/api/package-lock.json create mode 100644 contract-typespec/api/package.json create mode 100644 contract-typespec/api/quotas.tsp create mode 100644 contract-typespec/api/responses.tsp create mode 100644 contract-typespec/api/schemas.tsp create mode 100644 contract-typespec/api/topics.tsp create mode 100644 contract-typespec/api/tspconfig.yaml create mode 100644 contract-typespec/build.gradle diff --git a/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java b/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java index 59861d49e..703b8a0dc 100644 --- a/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java +++ b/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java @@ -1,6 +1,7 @@ package io.kafbat.ui.mapper; import io.kafbat.ui.model.ActionDTO; +import io.kafbat.ui.model.ApplicationConfigPropertiesAuthOauth2ClientValueDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesAuthOauth2ResourceServerDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesAuthOauth2ResourceServerJwtDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesAuthOauth2ResourceServerOpaquetokenDTO; diff --git a/api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java b/api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java index 6156a0bfa..6cbcf96c5 100644 --- a/api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java +++ b/api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java @@ -87,7 +87,7 @@ void testConvertController() { "clusterName", Map.of("type", "string"), "topicName", Map.of("type", "string"), "topicUpdate", SCHEMA_GENERATOR.generateSchema(TopicUpdateDTO.class) - ), List.of("clusterName", "topicName"), false, null, null) + ), List.of("clusterName", "topicName", "topicUpdate"), false, null, null) ) ); assertThat(tools).allMatch(tool -> diff --git a/build.gradle b/build.gradle index f999f4baa..b7b4eb462 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,7 @@ ext { release = resolveBooleanProperty("release") includeFrontend = resolveBooleanProperty("include-frontend", release) buildDockerImages = resolveBooleanProperty("build-docker-images", release) + useTypeSpec = resolveBooleanProperty("typespec", false) runE2e = resolveBooleanProperty("run-e2e") } diff --git a/contract-typespec/api/acls.tsp b/contract-typespec/api/acls.tsp new file mode 100644 index 000000000..3bd84e2f5 --- /dev/null +++ b/contract-typespec/api/acls.tsp @@ -0,0 +1,123 @@ +import "@typespec/openapi"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/acls") +@tag("Acls") +interface AclApi { + @doc("listKafkaAcls") + @get + @operationId("listAcls") + listAcls( + @path clusterName: string, + @query resourceType?: KafkaAclResourceType, + @query resourceName?: string, + @query namePatternType?: KafkaAclNamePatternType, + ): KafkaAcl[]; + + @route("/csv") + @doc("getAclAsCsv") + @get + @operationId("getAclAsCsv") + getAclAsCsv(@path clusterName: string): string; + + @route("/csv") + @doc("syncAclsCsv") + @post + @operationId("syncAclsCsv") + syncAclsCsv(@path clusterName: string, @body content: string): void | ApiBadRequestResponse; + + @doc("createAcl") + @post + @operationId("createAcl") + createAcl(@path clusterName: string, @body acl: KafkaAcl): void | ApiBadRequestResponse; + + @doc("deleteAcl") + @delete + @operationId("deleteAcl") + deleteAcl( + @path clusterName: string, + @body acl: KafkaAcl, + ): void | ApiNotFoundResponse; + + @route("/consumer") + @doc("createConsumerAcl") + @post + @operationId("createConsumerAcl") + createConsumerAcl( + @path clusterName: string, + @body payload: CreateConsumerAcl, + ): void | ApiBadRequestResponse; + + @route("/producer") + @doc("createProducerAcl") + @operationId("createProducerAcl") + @post + createProducerAcl( + @path clusterName: string, + @body payload: CreateProducerAcl, + ): void | ApiBadRequestResponse; + + @route("/streamApp") + @doc("createStreamAppAcl") + @post + @operationId("createStreamAppAcl") + createStreamAppAcl( + @path clusterName: string, + @body payload: CreateStreamAppAcl, + ): void | ApiBadRequestResponse; +} + +model KafkaAcl { + resourceType: KafkaAclResourceType; + resourceName: string; // "*" if acl can be applied to any resource of given type + namePatternType: KafkaAclNamePatternType; + principal: string; + host: string; + operation: "UNKNOWN" | "ALL" | "READ" | "WRITE" | "CREATE" | "DELETE" | "ALTER" | "DESCRIBE" | "CLUSTER_ACTION" | "DESCRIBE_CONFIGS" | "ALTER_CONFIGS" | "IDEMPOTENT_WRITE" | "CREATE_TOKENS" | "DESCRIBE_TOKENS"; + permission: "ALLOW" | "DENY"; +} + +enum KafkaAclResourceType { + UNKNOWN, + TOPIC, + GROUP, + CLUSTER, + TRANSACTIONAL_ID, + DELEGATION_TOKEN, + USER, +} + +enum KafkaAclNamePatternType { + MATCH, + LITERAL, + PREFIXED, +} + +model CreateConsumerAcl { + principal: string; + host: string; + topics?: string[]; + topicsPrefix?: string; + consumerGroups?: string[]; + consumerGroupsPrefix?: string; +} + +model CreateProducerAcl { + principal: string; + host: string; + topics?: string[]; + topicsPrefix?: string; + transactionalId?: string; + transactionsIdPrefix?: string; + idempotent?: boolean = false; +} + +model CreateStreamAppAcl { + principal: string; + host: string; + inputTopics: string[]; + outputTopics: string[]; + applicationId: string; +} \ No newline at end of file diff --git a/contract-typespec/api/auth.tsp b/contract-typespec/api/auth.tsp new file mode 100644 index 000000000..dd48a305b --- /dev/null +++ b/contract-typespec/api/auth.tsp @@ -0,0 +1,42 @@ +import "@typespec/openapi"; + +using TypeSpec.Http; +using OpenAPI; + +@tag("Authorization") +interface AuthorizationApi { + @route("/api/authorization") + @doc("Get user authorization related info") + @operationId("getUserAuthInfo") + @get + getUserAuthInfo(): AuthenticationInfo; +} + +@route("/login") +@doc("Authenticate") +@operationId("authenticate") +@post +op authenticate(@body form: LoginForm): void | Http.Response<401>; + +model LoginForm { + username: string; + password: string; +} + +model AuthenticationInfo { + rbacEnabled: boolean; // true if role based access control is enabled and granular permission access is required + userInfo?: UserInfo; +} + +model UserInfo { + username: string; + permissions: UserPermission[]; +} + + +model UserPermission { + clusters: string[]; + resource: ResourceType; + value?: string; + actions: Action[]; +} diff --git a/contract-typespec/api/brokers.tsp b/contract-typespec/api/brokers.tsp new file mode 100644 index 000000000..36e1bb920 --- /dev/null +++ b/contract-typespec/api/brokers.tsp @@ -0,0 +1,114 @@ +import "@typespec/openapi"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/brokers") +@tag("Brokers") +interface BrokersApi { + @get + @operationId("getBrokers") + @doc("getBrokers") + getBrokers(@path clusterName: string): Broker[]; + + @get + @route("/{id}/configs") + @operationId("getBrokerConfig") + @doc("getBrokerConfig") + getBrokerConfig(@path clusterName: string, @path id: int32): BrokerConfig[]; + + @put + @route("/{id}/configs/{name}") + @operationId("updateBrokerConfigByName") + @doc("updateBrokerConfigByName") + updateBrokerConfigByName( + @path clusterName: string, + @path id: int32, + @path name: string, + @body config: BrokerConfigItem, + ): void | ApiBadRequestResponse; + + @get + @route("/{id}/metrics") + @operationId("getBrokersMetrics") + @doc("getBrokersMetrics") + getBrokersMetrics(@path clusterName: string, @path id: int32): BrokerMetrics; + + @get + @route("/logdirs") + @operationId("getAllBrokersLogdirs") + @doc("getAllBrokersLogdirs") + getAllBrokersLogdirs( + @path clusterName: string, + @query broker?: int32[], + ): BrokersLogdirs[]; + + @patch(#{implicitOptionality: true}) + @route("/{id}/logdirs") + @operationId("updateBrokerTopicPartitionLogDir") + @doc("updateBrokerTopicPartitionLogDir") + updateBrokerTopicPartitionLogDir( + @path clusterName: string, + @path id: int32, + @body update: BrokerLogdirUpdate, + ): void | ApiBadRequestResponse; +} + +model Broker { + id: int32; + host?: string; + port?: int32; + bytesInPerSec?: float64; + bytesOutPerSec?: float64; + partitionsLeader?: int32; + partitions?: int32; + inSyncPartitions?: int32; + partitionsSkew?: float64; + leadersSkew?: float64; +} + +model BrokerLogdirUpdate { + topic?: string; + partition?: int32; + logDir?: string; +} + +model BrokerConfig { + name: string; + value: string; + source: ConfigSource; + isSensitive: boolean; + isReadOnly: boolean; + synonyms?: ConfigSynonym[]; +} + +model BrokerConfigItem { + value?: string; +} + +model BrokerMetrics { + segmentSize: int64; + segmentCount: int32; + metrics: Metric[]; +} + +model BrokersLogdirs { + name: string; + error: string; + topics: BrokerTopicLogdirs[]; +} + +model BrokerTopicLogdirs { + name?: string; + partitions?: BrokerTopicPartitionLogdir[]; +} + +model BrokerTopicPartitionLogdir extends TopicPartitionLogdir { + broker?: int32; +} + +model TopicPartitionLogdir { + partition?: int32; + size?: int64; + offsetLag?: int64; +} \ No newline at end of file diff --git a/contract-typespec/api/clusters.tsp b/contract-typespec/api/clusters.tsp new file mode 100644 index 000000000..7ce03994d --- /dev/null +++ b/contract-typespec/api/clusters.tsp @@ -0,0 +1,95 @@ +import "@typespec/openapi"; +import "./responses.tsp"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters") +@tag("Clusters") +interface ClustersApi { + @get + @operationId("getClusters") + @doc("getClusters") + getClusters(): Cluster[]; + + @post + @route("/{clusterName}/cache") + @operationId("updateClusterInfo") + @doc("updateClusterInfo") + updateClusterInfo(@path clusterName: string): Cluster; + + @get + @route("/{clusterName}/metrics") + @operationId("getClusterMetrics") + @doc("getClusterMetrics") + getClusterMetrics(@path clusterName: string): ClusterMetrics; + + @get + @route("/{clusterName}/stats") + @operationId("getClusterStats") + @doc("getClusterStats") + getClusterStats(@path clusterName: string): ClusterStats; +} + +model Cluster { + name: string; + defaultCluster: boolean; + status: ServerStatus; + lastError?: MetricsCollectionError; + brokerCount: int32; + onlinePartitionCount: int32; + topicCount: int32; + bytesInPerSec?: float64; + bytesOutPerSec?: float64; + readOnly: boolean; + version: string; + features: ClusterFeature[]; +} + +alias ClusterFeature = + "SCHEMA_REGISTRY" + | "KAFKA_CONNECT" + | "KSQL_DB" + | "TOPIC_DELETION" + | "KAFKA_ACL_VIEW" + | "KAFKA_ACL_EDIT" + | "CLIENT_QUOTA_MANAGEMENT"; + +enum ServerStatus { + ONLINE, + OFFLINE, + INITIALIZING, +} + +model ClusterMetrics { + items: Metric[]; +} + +model ClusterStats { + brokerCount: int32; + #deprecated "Unused" + zooKeeperStatus?: int32; + + @doc("Id of broker which is cluster's controller. null, if controller not known yet.") + activeControllers: int32; + + onlinePartitionCount: int32; + offlinePartitionCount: int32; + inSyncReplicasCount: int32; + outOfSyncReplicasCount: int32; + underReplicatedPartitionCount: int32; + diskUsage: BrokerDiskUsage[]; + version: string; +} + +model MetricsCollectionError { + message?: string; + stackTrace?: string; +} + + +model BrokerDiskUsage { + brokerId: int32; + segmentSize: int64; + segmentCount: int32; +} \ No newline at end of file diff --git a/contract-typespec/api/config.tsp b/contract-typespec/api/config.tsp new file mode 100644 index 000000000..ecaef9fdd --- /dev/null +++ b/contract-typespec/api/config.tsp @@ -0,0 +1,311 @@ +import "@typespec/openapi"; +import "./responses.tsp"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/info") +@doc("Gets application info") +@get +@tag("ApplicationConfig") +op getApplicationInfo(): ApplicationInfo; + +@route("/api/config") +@tag("ApplicationConfig") +interface ApplicationConfigApi { + @doc("Gets current application configuration") + @get + @operationId("getCurrentConfig") + getCurrentConfig(): ApplicationConfig; + + @put + @doc("Restarts application with specified configuration") + @operationId("restartWithConfig") + restartWithConfig(@body config: RestartRequest): void | ApiBadRequestResponse; + + @put + @route("/validated") + @doc("Restarts application with specified configuration") + @operationId("validateConfig") + validateConfig(@body config: ApplicationConfig): ApplicationConfigValidation | ApiBadRequestResponse; + + @post + @route("/relatedfiles") + @doc("Upload config related file") + @operationId("uploadConfigRelatedFile") + uploadConfigRelatedFile( + @multipartBody body: { + file: HttpPart; + } + ): UploadedFileInfo; + + @get + @route("/authentication") + @doc("Get authentication methods enabled for the app and other related settings") + @operationId("getAuthenticationSettings") + getAuthenticationSettings(): AppAuthenticationSettings; +} + + +model ApplicationInfo { + enabledFeatures: ApplicationFeature[]; + build: { + commitId: string; + version: string; + buildTime: string; + isLatestRelease: boolean; + }; + latestRelease: { + versionTag: string; + publishedAt: string; + htmlUrl: string; + }; +} + +model ApplicationConfig { + properties: { + auth: { + type: string; + oauth2: { + client?: Record<{ + provider: string; + clientId: string; + clientSecret?: string; + clientName?: string; + redirectUri?: string; + authorizationGrantType?: string; + issuerUri?: string; + authorizationUri?: string; + tokenUri?: string; + userInfoUri?: string; + jwkSetUri?: string; + userNameAttribute?: string; + scope?: string[]; + customParams?: Record; + }>; + resourceServer?: { + jwt?: { + jwkSetUri?: string; + jwsAlgorithms?: string[]; + issuerUri?: string; + publicKeyLocation?: string; + audiences?: string[]; + authorityPrefix?: string; + authoritiesClaimDelimiter?: string; + authoritiesClaimName?: string; + principalClaimName?: string; + }; + opaquetoken?: { + clientId?: string; + clientSecret?: string; + introspectionUri?: string; + }; + }; + }; + }; + rbac: { + roles: { + name: string; + clusters: string[]; + subjects: { + provider: string; + type: string; + value: string; + regex?: boolean = false; + }[]; + permissions: { + resource: ResourceType; + value: string; + actions: Action[]; + }[]; + }[]; + }; + webclient: { + maxInMemoryBufferSize: string; + responseTimeoutMs: int32; + }; + kafka: { + polling?: { + pollTimeoutMs: int32; + maxPageSize: int32; + defaultPageSize: int32; + responseTimeoutMs: int32; + }; + adminClientTimeout?: int32; + internalTopicPrefix: string; + clusters: { + name: string; + bootstrapServers: string; + ssl?: { + truststoreLocation?: string; + truststorePassword?: string; + verifySsl: boolean = true; + }; + schemaRegistry?: string; + schemaRegistryAuth?: { + username: string; + password: string; + }; + schemaRegistrySsl?: { + keystoreLocation: string; + keystorePassword: string; + }; + ksqldbServer?: string; + ksqldbServerSsl?: { + keystoreLocation: string; + keystorePassword: string; + }; + ksqldbServerAuth?: { + username: string; + password: string; + }; + kafkaConnect?: { + name: string; + address: string; + username?: string; + password?: string; + keystoreLocation?: string; + keystorePassword?: string; + }[]; + metrics?: KafkaMetricsConfig; + properties?: Record; + consumerProperties?: Record; + producerProperties?: Record; + readOnly: boolean; + serde?: SerdeConfig[]; + defaultKeySerde?: string; + defaultValueSerde?: string; + masking?: MaskingConfig[]; + pollingThrottleRate?: int64; + audit?: KafkaAuditConfig; + }[]; + }; + }; +} + +model KafkaMetricsConfig { + type: string; + port: int32; + ssl?: boolean; + username?: string; + password?: string; + keystoreLocation?: string; + keystorePassword?: string; +} + +model SerdeConfig { + name: string; + className: string; + filePath?: string; + properties?: Record; + topicKeysPattern?: string; + topicValuesPattern?: string; +} + +model MaskingConfig { + type: MaskType; + fields?: string[]; + fieldsNamePattern?: string; + maskingCharsReplacement?: string[]; + replacement?: string; + topicKeysPattern?: string; + topicValuesPattern?: string; +} + +enum MaskType { + REMOVE, + MASK, + REPLACE, +} + +model KafkaAuditConfig { + level: AuditLevel; + topic?: string; + auditTopicsPartitions?: int32; + topicAuditEnabled?: boolean; + consoleAuditEnabled?: boolean; + auditTopicProperties?: Record; +} + +enum AuditLevel { + ALL, + ALTER_ONLY, +} + + +model ApplicationConfigValidation { + clusters?: Record; +} + +model ApplicationPropertyValidation { + error: boolean; + + @doc("Contains error message if error = true") + errorMessage?: string; +} + +model ClusterConfigValidation { + kafka: ApplicationPropertyValidation; + schemaRegistry?: ApplicationPropertyValidation; + kafkaConnects?: Record; + ksqldb?: ApplicationPropertyValidation; +} + +alias ApplicationFeature = + "DYNAMIC_CONFIG"; + +enum AuthType { + DISABLED, + OAUTH2, + LOGIN_FORM, + LDAP +} + +model AppAuthenticationSettings { + authType?: global.AuthType; + oAuthProviders?: OAuthProvider[]; +} + +model OAuthProvider { + clientName: string; + authorizationUri: string; +} + +model RestartRequest { + config?: ApplicationConfig; +} + +model UploadedFileInfo { + location: string; +} + +enum Action { + ALL, + VIEW, + EDIT, + CREATE, + DELETE, + RESET_OFFSETS, + EXECUTE, + MODIFY_GLOBAL_COMPATIBILITY, + ANALYSIS_VIEW, + ANALYSIS_RUN, + MESSAGES_READ, + MESSAGES_PRODUCE, + MESSAGES_DELETE, + OPERATE, + RESTART, +} + +enum ResourceType { + APPLICATIONCONFIG, + CLUSTERCONFIG, + TOPIC, + CONSUMER, + SCHEMA, + CONNECT, + KSQL, + ACL, + AUDIT, + CLIENT_QUOTAS, +} \ No newline at end of file diff --git a/contract-typespec/api/consumer-groups.tsp b/contract-typespec/api/consumer-groups.tsp new file mode 100644 index 000000000..af05a4c6c --- /dev/null +++ b/contract-typespec/api/consumer-groups.tsp @@ -0,0 +1,139 @@ +import "@typespec/openapi"; +import "./responses.tsp"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/consumer-groups") +@tag("Consumer Groups") +interface ConsumerGroupsApi { + @get + @route("/paged") + @operationId("getConsumerGroupsPage") + getConsumerGroupsPage( + @path clusterName: string, + @query page?: int32, + @query perPage?: int32, + @query search?: string, + @query orderBy?: ConsumerGroupOrdering, + @query sortOrder?: SortOrder, + ): ConsumerGroupsPageResponse; + + @get + @route("/{id}") + @operationId("getConsumerGroup") + getConsumerGroup( + @path clusterName: string, + @path id: string, + ): ConsumerGroupDetails; + + @delete + @route("/{id}") + @operationId("deleteConsumerGroup") + deleteConsumerGroup(@path clusterName: string, @path id: string): void; + + @post + @route("/{id}/offsets") + @operationId("resetConsumerGroupOffsets") + resetConsumerGroupOffsets( + @path clusterName: string, + @path id: string, + @body reset: ConsumerGroupOffsetsReset, + ): void; + + @delete + @route("/{id}/topics/{topicName}") + @operationId("deleteConsumerGroupOffsets") + deleteConsumerGroupOffsets( + @path clusterName: string, + @path id: string, + @path topicName: string, + ): void; +} + +@route("/api/clusters/{clusterName}/topics/{topicName}/consumer-groups") +@tag("Consumer Groups") +interface TopicConsumerGroupsApi { + @get + @operationId("getTopicConsumerGroups") + getTopicConsumerGroups( + @path clusterName: string, + @path topicName: string, + ): ConsumerGroup[]; +} + +enum ConsumerGroupState { + UNKNOWN, + PREPARING_REBALANCE, + COMPLETING_REBALANCE, + STABLE, + DEAD, + EMPTY, +} + +@discriminator("inherit") +model ConsumerGroup { + groupId: string; + members?: int32; + topics?: int32; + simple?: boolean; + partitionAssignor?: string; + state?: ConsumerGroupState; + coordinator?: Broker; + + @doc("null if consumer group has no offsets committed") + consumerLag?: int64; +} + + +model ConsumerGroupDetails extends ConsumerGroup { + @invisible(Lifecycle) + inherit: "deatils"; + partitions?: ConsumerGroupTopicPartition[]; +} + +enum ConsumerGroupOrdering { + NAME, + MEMBERS, + STATE, + MESSAGES_BEHIND, + TOPIC_NUM, +} + +model ConsumerGroupsPageResponse { + pageCount?: int32; + consumerGroups?: ConsumerGroup[]; +} + +model ConsumerGroupOffsetsReset { + topic: string; + resetType: ConsumerGroupOffsetsResetType; + partitions?: int32[]; + resetToTimestamp?: int64; + partitionsOffsets?: PartitionOffset[]; +} + +enum ConsumerGroupOffsetsResetType { + EARLIEST, + LATEST, + TIMESTAMP, + OFFSET, +} + +model ConsumerGroupTopicPartition { + topic: string; + partition: int32; + currentOffset?: int64; + endOffset?: int64; + consumerLag?: int64; // null if consumer group has no offsets committed + consumerId?: string; + host?: string; +} + + + +model PartitionOffset { + partition: int32; + offset?: int64; +} + diff --git a/contract-typespec/api/kafka-connect.tsp b/contract-typespec/api/kafka-connect.tsp new file mode 100644 index 000000000..1870855be --- /dev/null +++ b/contract-typespec/api/kafka-connect.tsp @@ -0,0 +1,283 @@ +import "@typespec/openapi"; +import "./responses.tsp"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/connects") +@tag("Kafka Connect") +interface ConnectInstancesApi { + @get + @operationId("getConnects") + getConnects(@path clusterName: string): Connect[]; + + @get + @route("/{connectName}/plugins") + @doc("get connector plugins") + @operationId("getConnectorPlugins") + getConnectorPlugins( + @path clusterName: string, + @path connectName: string, + ): ConnectorPlugin[]; + + @put + @route("/{connectName}/plugins/{pluginName}/config/validate") + @doc("validate connector plugin configuration") + @operationId("validateConnectorPluginConfig") + validateConnectorPluginConfig( + @path clusterName: string, + @path connectName: string, + @path pluginName: string, + @body config: ConnectorConfig, + ): ConnectorPluginConfigValidationResponse; +} + +// /api/clusters/{clusterName}/connectors +@route("/api/clusters/{clusterName}/connectors") +@tag("Kafka Connect") +interface ConnectorsApi { + @get + @operationId("getAllConnectors") + getAllConnectors( + @path clusterName: string, + @query search?: string, + @query orderBy?: ConnectorColumnsToSort, + @query sortOrder?: SortOrder, + ): FullConnectorInfo[]; +} + +// /api/clusters/{clusterName}/connects/{connectName}/connectors +@route("/api/clusters/{clusterName}/connects/{connectName}/connectors") +@tag("Kafka Connect") +interface KafkaConnectConnectorsApi { + @get + @operationId("getConnectors") + getConnectors(@path clusterName: string, @path connectName: string): string[]; + + @post + @operationId("createConnector") + createConnector( + @path clusterName: string, + @path connectName: string, + @body connector: NewConnector, + ): Connector | ApiRebalanceInProgressResponse; + + @get + @route("/{connectorName}") + @operationId("getConnector") + getConnector( + @path clusterName: string, + @path connectName: string, + @path connectorName: string, + ): Connector; + + @delete + @route("/{connectorName}") + @operationId("deleteConnector") + deleteConnector( + @path clusterName: string, + @path connectName: string, + @path connectorName: string, + ): void | ApiRebalanceInProgressResponse; + + @post + @route("/{connectorName}/action/{action}") + @operationId("updateConnectorState") + updateConnectorState( + @path clusterName: string, + @path connectName: string, + @path connectorName: string, + @path action: ConnectorAction, + ): void | ApiRebalanceInProgressResponse | ApiBadRequestResponse; + + @get + @route("/{connectorName}/config") + @operationId("getConnectorConfig") + getConnectorConfig( + @path clusterName: string, + @path connectName: string, + @path connectorName: string, + ): ConnectorConfig; + + @put + @route("/{connectorName}/config") + @operationId("setConnectorConfig") + setConnectorConfig( + @path clusterName: string, + @path connectName: string, + @path connectorName: string, + @body config: ConnectorConfig, + ): Connector | ApiRebalanceInProgressResponse | ApiBadRequestResponse; + + @get + @route("/{connectorName}/tasks") + @operationId("getConnectorTasks") + getConnectorTasks( + @path clusterName: string, + @path connectName: string, + @path connectorName: string, + ): Task[]; + + @post + @route("/{connectorName}/tasks/{taskId}/action/restart") + @operationId("restartConnectorTask") + restartConnectorTask( + @path clusterName: string, + @path connectName: string, + @path connectorName: string, + @path taskId: int32, + ): void | ApiBadRequestResponse; + + @delete + @route("/{connectorName}/offsets") + @operationId("resetConnectorOffsets") + resetConnectorOffsets( + @path clusterName: string, + @path connectName: string, + @path connectorName: string, + ): void | ApiBadRequestResponse; +} + + +model Connect { + name: string; + address?: string; +} + +model ConnectorConfig is Record; + +model TaskId { + connector?: string; + task?: int32; +} + +model TaskStatus { + id: int32; + state: ConnectorTaskStatus; + worker_id: string; + trace?: string; +} + +model Task { + id?: TaskId; + status: TaskStatus; + config?: ConnectorConfig; +} + +model NewConnector { + name: string; + config: ConnectorConfig; +} + +enum ConnectorType { + SOURCE, + SINK, +} + +enum ConnectorTaskStatus { + RUNNING, + FAILED, + PAUSED, + RESTARTING, + UNASSIGNED, +} + +enum ConnectorState { + RUNNING, + FAILED, + PAUSED, + UNASSIGNED, + TASK_FAILED, + RESTARTING, + STOPPED, +} + +model ConnectorStatus { + state: ConnectorState; + worker_id: string; +} + +model Connector extends NewConnector { + tasks?: TaskId[]; + type: ConnectorType; + status: ConnectorStatus; + connect: string; +} + +enum ConnectorAction { + RESTART, + RESTART_ALL_TASKS, + RESTART_FAILED_TASKS, + PAUSE, + RESUME, + STOP, +} + +enum TaskAction { + restart, +} + +model ConnectorPlugin { + class?: string; +} + +model ConnectorPluginConfigDefinition { + name?: string; + type?: + | "BOOLEAN" + | "CLASS" + | "DOUBLE" + | "INT" + | "LIST" + | "LONG" + | "PASSWORD" + | "SHORT" + | "STRING"; + required?: boolean; + default_value?: string; + importance?: "LOW" | "MEDIUM" | "HIGH"; + documentation?: string; + group?: string; + width?: "SHORT" | "MEDIUM" | "LONG" | "NONE"; + display_name?: string; + dependents?: string[]; + order?: int32; +} + +model ConnectorPluginConfigValue { + name?: string; + value?: string; + recommended_values?: string[]; + errors?: string[]; + visible?: boolean; +} + +model ConnectorPluginConfig { + definition?: ConnectorPluginConfigDefinition; + value?: ConnectorPluginConfigValue; +} + +model ConnectorPluginConfigValidationResponse { + name?: string; + error_count?: int32; + groups?: string[]; + configs?: ConnectorPluginConfig[]; +} + +model FullConnectorInfo { + connect: string; + name: string; + connector_class?: string; + type?: ConnectorType; + topics?: string[]; + status: ConnectorStatus; + tasks_count?: integer; + failed_tasks_count?: integer; +} + +enum ConnectorColumnsToSort { + NAME, + CONNECT, + TYPE, + STATUS, +} diff --git a/contract-typespec/api/ksql.tsp b/contract-typespec/api/ksql.tsp new file mode 100644 index 000000000..efa0af06e --- /dev/null +++ b/contract-typespec/api/ksql.tsp @@ -0,0 +1,70 @@ +import "@typespec/openapi"; +import "./responses.tsp"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/ksql") +@tag("Ksql") +interface KsqlApi { + @post + @route("/v2") + @operationId("executeKsql") + executeKsql( + @path clusterName: string, + @body command: KsqlCommandV2, + ): KsqlCommandV2Response | ApiBadRequestResponse; + + @get + @route("/tables") + @operationId("listTables") + listTables(@path clusterName: string): KsqlTableDescription[]; + + @get + @route("/streams") + @operationId("listStreams") + listStreams(@path clusterName: string): KsqlStreamDescription[]; + + @get + @route("/response") + @operationId("openKsqlResponsePipe") + openKsqlResponsePipe( + @path clusterName: string, + @query pipeId: string, + ): SseResponse; +} + + +model KsqlCommandV2 { + ksql: string; + streamsProperties?: Record; +} + +model KsqlCommandV2Response { + pipeId: string; +} + +model KsqlTableDescription { + name?: string; + topic?: string; + keyFormat?: string; + valueFormat?: string; + isWindowed?: boolean; +} + +model KsqlStreamDescription { + name?: string; + topic?: string; + keyFormat?: string; + valueFormat?: string; +} + +model KsqlTableResponse { + header?: string; + columnNames?: string[]; + values?: unknown[][]; +} + +model KsqlResponse { + table?: KsqlTableResponse; +} \ No newline at end of file diff --git a/contract-typespec/api/main.tsp b/contract-typespec/api/main.tsp new file mode 100644 index 000000000..a7d2ed347 --- /dev/null +++ b/contract-typespec/api/main.tsp @@ -0,0 +1,13 @@ +// APIs +import "./clusters.tsp"; +import "./brokers.tsp"; +import "./topics.tsp"; +import "./messages.tsp"; +import "./consumer-groups.tsp"; +import "./schemas.tsp"; +import "./kafka-connect.tsp"; +import "./ksql.tsp"; +import "./acls.tsp"; +import "./quotas.tsp"; +import "./auth.tsp"; +import "./config.tsp"; diff --git a/contract-typespec/api/messages.tsp b/contract-typespec/api/messages.tsp new file mode 100644 index 000000000..4c11353c9 --- /dev/null +++ b/contract-typespec/api/messages.tsp @@ -0,0 +1,234 @@ +import "@typespec/openapi"; +import "./models.tsp"; +import "./responses.tsp"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/topics/{topicName}") +@tag("Messages") +interface MessagesApi { + @get + @route("/serdes") + @operationId("getSerdes") + getSerdes( + @path clusterName: string, + @path topicName: string, + @query use: SerdeUsage, + ): TopicSerdeSuggestion; + + @get + @route("/messages") + @operationId("getTopicMessages") + getTopicMessages( + @path clusterName: string, + @path topicName: string, + @query seekType?: SeekType, + @query seekTo?: string[], + @query limit?: int32, + @query q?: string, + @query filterQueryType?: MessageFilterType, + @query seekDirection?: SeekDirection, + @query keySerde?: string, + @query valueSerde?: string, + ): SseResponse; + + @delete + @route("/messages") + @operationId("deleteTopicMessages") + deleteTopicMessages( + @path clusterName: string, + @path topicName: string, + @query partitions?: int32[], + ): void | ApiNotFoundResponse | ApiBadRequestResponse; + + @post + @route("/messages") + @operationId("sendTopicMessages") + sendTopicMessages( + @path clusterName: string, + @path topicName: string, + @body message: CreateTopicMessage, + ): void | ApiNotFoundResponse | ApiBadRequestResponse; + + @post + @route("/smartfilters") + @operationId("registerFilter") + registerFilter( + @path clusterName: string, + @path topicName: string, + @body registration: MessageFilterRegistration, + ): MessageFilterId | ApiBadRequestResponse; + + @get + @route("/messages/v2") + @operationId("getTopicMessagesV2") + getTopicMessagesV2( + @path clusterName: string, + @path topicName: string, + @query mode?: PollingMode, + @query partitions?: int32[], + @query limit?: int32, + @query stringFilter?: string, + @query smartFilterId?: string, + @query offset?: int64, + @query timestamp?: int64, + @query keySerde?: string, + @query valueSerde?: string, + @query cursor?: string, + ): SseResponse | ApiBadRequestResponse; +} + +@route("/api/smartfilters/testexecutions") +@tag("Messages") +interface SmartFiltersTestExecutionsApi { + @put + @operationId("executeSmartFilterTest") + executeSmartFilterTest( + @body input: SmartFilterTestExecution, + ): SmartFilterTestExecutionResult | ApiBadRequestResponse; +} + + + +model TopicSerdeSuggestion { + key: SerdeDescription[]; + value: SerdeDescription[]; +} + +enum SerdeUsage { + SERIALIZE, + DESERIALIZE, +} + +model TopicMessageEvent { + type?: "PHASE" | "MESSAGE" | "CONSUMING" | "DONE"; + message?: TopicMessage; + phase?: TopicMessagePhase; + consuming?: TopicMessageConsuming; + cursor?: TopicMessageNextPageCursor; +} + +model TopicMessagePhase { + name?: string; +} + +model TimeStampFormat { + timeStampFormat?: string; +} + +model TopicMessageConsuming { + bytesConsumed?: int64; + elapsedMs?: int64; + isCancelled?: boolean; + messagesConsumed?: int32; + filterApplyErrors?: int32; +} + +model TopicMessageNextPageCursor { + id?: string; +} + +model TopicMessage { + partition: int32; + offset: int64; + timestamp: offsetDateTime; + timestampType?: "NO_TIMESTAMP_TYPE" | "CREATE_TIME" | "LOG_APPEND_TIME"; + key?: string; + headers?: Record; + value?: string; + #deprecated "use 'keySerde' field instead" + keyFormat?: MessageFormat; + #deprecated "use 'valueSerde' field instead" + valueFormat?: MessageFormat; + keySize?: int64; + valueSize?: int64; + #deprecated "use 'keyDeserializeProperties' field instead" + keySchemaId?: string; + #deprecated "use 'valueDeserializeProperties' field instead" + valueSchemaId?: string; + headersSize?: int64; + keySerde?: string; + valueSerde?: string; + keyDeserializeProperties?: Record; + valueDeserializeProperties?: Record; +} + +enum SeekType { + BEGINNING, + OFFSET, + TIMESTAMP, + LATEST, +} + +model MessageFilterRegistration { + filterCode?: string; +} + +model MessageFilterId { + id?: string; +} + +enum PollingMode { + FROM_OFFSET, + TO_OFFSET, + FROM_TIMESTAMP, + TO_TIMESTAMP, + LATEST, + EARLIEST, + TAILING, +} + +enum MessageFilterType { + STRING_CONTAINS, + CEL_SCRIPT, +} + +enum SeekDirection { + FORWARD, + BACKWARD, + TAILING, +} + +model SmartFilterTestExecution { + filterCode: string; + key?: string; + value?: string; + headers?: Record; + partition?: int32; + offset?: int64; + timestampMs?: int64; +} + +model SmartFilterTestExecutionResult { + result?: boolean; + error?: string; +} + +model CreateTopicMessage { + partition: int32; + key?: string | null; + headers?: Record; + value?: string | null; + keySerde?: string | null; + valueSerde?: string | null; +} + +enum MessageFormat { + AVRO, + JSON, + PROTOBUF, + UNKNOWN, +} + + +model SerdeDescription { + name: string; + description?: string; + + @doc("This serde was automatically chosen by cluster config. This should be enabled in UI by default. Also it will be used for deserialization if no serdes passed.") + preferred?: boolean; + + schema?: string; + additionalProperties?: Record; +} \ No newline at end of file diff --git a/contract-typespec/api/models.tsp b/contract-typespec/api/models.tsp new file mode 100644 index 000000000..bccaa5c5a --- /dev/null +++ b/contract-typespec/api/models.tsp @@ -0,0 +1,37 @@ +model Metric { + name?: string; + labels?: Record; + value?: decimal; +} + +enum ConfigSource { + DYNAMIC_TOPIC_CONFIG, + DYNAMIC_BROKER_LOGGER_CONFIG, + DYNAMIC_BROKER_CONFIG, + DYNAMIC_DEFAULT_BROKER_CONFIG, + DYNAMIC_CLIENT_METRICS_CONFIG, + STATIC_BROKER_CONFIG, + DEFAULT_CONFIG, + UNKNOWN, +} + +enum SortOrder { + ASC, + DESC, +} + + + + + +// model BrokerLogdirs { +// name: string; +// error: string; +// topics: TopicLogdirs[]; +// } + +// model TopicLogdirs { +// name?: string; +// partitions?: TopicPartitionLogdir[]; +// } + diff --git a/contract-typespec/api/package-lock.json b/contract-typespec/api/package-lock.json new file mode 100644 index 000000000..0cc016a26 --- /dev/null +++ b/contract-typespec/api/package-lock.json @@ -0,0 +1,1933 @@ +{ + "name": "tsp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tsp", + "version": "0.1.0", + "dependencies": { + "@typespec/compiler": "^1.0.0", + "@typespec/http": "^1.0.1", + "@typespec/openapi": "^1.0.0", + "@typespec/openapi3": "^1.0.0", + "@typespec/rest": "^0.70.0", + "@typespec/sse": "^0.70.0", + "build": "^0.1.4" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.7.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", + "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", + "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "11.7.2", + "@apidevtools/openapi-schemas": "^2.1.0", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.2" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz", + "integrity": "sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.12.tgz", + "integrity": "sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.13.tgz", + "integrity": "sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.15.tgz", + "integrity": "sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.12.tgz", + "integrity": "sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.15.tgz", + "integrity": "sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.15.tgz", + "integrity": "sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.3.tgz", + "integrity": "sha512-8YL0WiV7J86hVAxrh3fE5mDCzcTDe1670unmJRz6ArDgN+DBK1a0+rbnNWp4DUB5rPMwqD5ZP6YHl9KK1mbZRg==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.1.8", + "@inquirer/confirm": "^5.1.12", + "@inquirer/editor": "^4.2.13", + "@inquirer/expand": "^4.0.15", + "@inquirer/input": "^4.1.12", + "@inquirer/number": "^3.0.15", + "@inquirer/password": "^4.0.15", + "@inquirer/rawlist": "^4.1.3", + "@inquirer/search": "^3.0.15", + "@inquirer/select": "^4.2.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.3.tgz", + "integrity": "sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz", + "integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.3.tgz", + "integrity": "sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@typespec/asset-emitter": { + "version": "0.70.1", + "resolved": "https://registry.npmjs.org/@typespec/asset-emitter/-/asset-emitter-0.70.1.tgz", + "integrity": "sha512-X8hRA7LLWkNIWqAkWaWoa84PzDMUvjj3qCLQKT29k5twS419nN1GGT7BQaDQnYfPTsSssEFxgRgugr0AAErEsA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.0.0" + } + }, + "node_modules/@typespec/compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@typespec/compiler/-/compiler-1.0.0.tgz", + "integrity": "sha512-QFy0otaB4xkN4kQmYyT17yu3OVhN0gti9+EKnZqs5JFylw2Xecx22BPwUE1Byj42pZYg5d9WlO+WwmY5ALtRDg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.26.2", + "@inquirer/prompts": "^7.4.0", + "ajv": "~8.17.1", + "change-case": "~5.4.4", + "env-paths": "^3.0.0", + "globby": "~14.1.0", + "is-unicode-supported": "^2.1.0", + "mustache": "~4.2.0", + "picocolors": "~1.1.1", + "prettier": "~3.5.3", + "semver": "^7.7.1", + "tar": "^7.4.3", + "temporal-polyfill": "^0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.12", + "yaml": "~2.7.0", + "yargs": "~17.7.2" + }, + "bin": { + "tsp": "cmd/tsp.js", + "tsp-server": "cmd/tsp-server.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@typespec/events": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@typespec/events/-/events-0.70.0.tgz", + "integrity": "sha512-qHW1N05n8PkNf2YQGNMdl/sAYqrJv+zQ1kny+3vg/20nzVj7sZpNFIKqUIc11z0GkT7k3Q9SPTymvq+K00sAUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.0.0" + } + }, + "node_modules/@typespec/http": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@typespec/http/-/http-1.0.1.tgz", + "integrity": "sha512-J5tqBWlmkvI/W+kJn4EFuN0laGxbY8qT68jzEQEiYeAXSfNyFGRSoCwn8Ex6dJphq4IozOMdVTNtOZWIJlwmfw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.0.0", + "@typespec/streams": "^0.70.0" + }, + "peerDependenciesMeta": { + "@typespec/streams": { + "optional": true + } + } + }, + "node_modules/@typespec/openapi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@typespec/openapi/-/openapi-1.0.0.tgz", + "integrity": "sha512-pONzKIdK4wHgD1vBfD9opUk66zDG55DlHbueKOldH2p1LVf5FnMiuKE4kW0pl1dokT/HBNR5OJciCzzVf44AgQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.0.0", + "@typespec/http": "^1.0.0" + } + }, + "node_modules/@typespec/openapi3": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@typespec/openapi3/-/openapi3-1.0.0.tgz", + "integrity": "sha512-cDsnNtJkQCx0R/+9AqXzqAKH6CgtwmnQGQMQHbkw0/Sxs5uk6hoiexx7vz0DUR7H4492MqPT2kE4351KZbDYMw==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "~10.1.1", + "@typespec/asset-emitter": "^0.70.0", + "openapi-types": "~12.1.3", + "yaml": "~2.7.0" + }, + "bin": { + "tsp-openapi3": "cmd/tsp-openapi3.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.0.0", + "@typespec/http": "^1.0.0", + "@typespec/json-schema": "^1.0.0", + "@typespec/openapi": "^1.0.0", + "@typespec/versioning": "^0.70.0" + }, + "peerDependenciesMeta": { + "@typespec/json-schema": { + "optional": true + }, + "@typespec/versioning": { + "optional": true + }, + "@typespec/xml": { + "optional": true + } + } + }, + "node_modules/@typespec/rest": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@typespec/rest/-/rest-0.70.0.tgz", + "integrity": "sha512-pn3roMQV6jBNT4bVA/hnrBAAHleXSyfWQqNO+DhI3+tLU4jCrJHmUZDi82nI9xBl+jkmy2WZFZOelZA9PSABeg==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.0.0", + "@typespec/http": "^1.0.0" + } + }, + "node_modules/@typespec/sse": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@typespec/sse/-/sse-0.70.0.tgz", + "integrity": "sha512-11VsIRqPuK+bIq7gHVghM5CAqvcfe9TmL9mZkxlPKuV6RRWju831k18KqlwXTOgeEMwVGA1Xbg1TTi1F4S1B+w==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.0.0", + "@typespec/events": "^0.70.0", + "@typespec/http": "^1.0.0", + "@typespec/streams": "^0.70.0" + } + }, + "node_modules/@typespec/streams": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@typespec/streams/-/streams-0.70.0.tgz", + "integrity": "sha512-WIixoZ7CCLq2INX4UkN+aXlj07Je+ntW0xbeFGmpfq6Z2xifKnL6/sPiztURMXd4Z1I+XXFCn2pw1r9q5i4Cmw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.0.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/build": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/build/-/build-0.1.4.tgz", + "integrity": "sha512-KwbDJ/zrsU8KZRRMfoURG14cKIAStUlS8D5jBDvtrZbwO5FEkYqc3oB8HIhRiyD64A48w1lc+sOmQ+mmBw5U/Q==", + "dependencies": { + "cssmin": "0.3.x", + "jsmin": "1.x", + "jxLoader": "*", + "moo-server": "*", + "promised-io": "*", + "timespan": "2.x", + "uglify-js": "1.x", + "walker": "1.x", + "winston": "*", + "wrench": "1.3.x" + }, + "engines": { + "node": ">v0.4.12" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/cssmin": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/cssmin/-/cssmin-0.3.2.tgz", + "integrity": "sha512-bynxGIAJ8ybrnFobjsQotIjA8HFDDgPwbeUWNXXXfR+B4f9kkxdcUyagJoQCSUOfMV+ZZ6bMn8bvbozlCzUGwQ==", + "bin": { + "cssmin": "bin/cssmin" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsmin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/jsmin/-/jsmin-1.0.1.tgz", + "integrity": "sha512-OPuL5X/bFKgVdMvEIX3hnpx3jbVpFCrEM8pKPXjFkZUqg521r41ijdyTz7vACOhW6o1neVlcLyd+wkbK5fNHRg==", + "license": "Doug Crockford's license that allows this module to be used for Good but not for Evil", + "bin": { + "jsmin": "bin/jsmin" + }, + "engines": { + "node": ">=0.1.93" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jxLoader": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jxLoader/-/jxLoader-0.1.1.tgz", + "integrity": "sha512-ClEvAj3K68y8uKhub3RgTmcRPo5DfIWvtxqrKQdDPyZ1UVHIIKvVvjrAsJFSVL5wjv0rt5iH9SMCZ0XRKNzeUA==", + "dependencies": { + "js-yaml": "0.3.x", + "moo-server": "1.3.x", + "promised-io": "*", + "walker": "1.x" + }, + "engines": { + "node": ">v0.4.10" + } + }, + "node_modules/jxLoader/node_modules/js-yaml": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-0.3.7.tgz", + "integrity": "sha512-/7PsVDNP2tVe2Z1cF9kTEkjamIwz4aooDpRKmN1+g/9eePCgcxsv4QDvEbxO0EH+gdDD7MLyDoR6BASo3hH51g==", + "license": "MIT", + "engines": { + "node": "> 0.4.11" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/moo-server": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/moo-server/-/moo-server-1.3.0.tgz", + "integrity": "sha512-9A8/eor2DXwpv1+a4pZAAydqLFVrWoKoO1fzdzqLUhYVXAO1Kgd1FR2gFZi7YdHzF0s4W8cDNwCfKJQrvLqxDw==", + "engines": { + "node": ">v0.4.10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/promised-io": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/promised-io/-/promised-io-0.3.6.tgz", + "integrity": "sha512-bNwZusuNIW4m0SPR8jooSyndD35ggirHlxVl/UhIaZD/F0OBv9ebfc6tNmbpZts3QXHggkjIBH8lvtnzhtcz0A==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/temporal-polyfill": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.3.0.tgz", + "integrity": "sha512-qNsTkX9K8hi+FHDfHmf22e/OGuXmfBm9RqNismxBrnSmZVJKegQ+HYYXT+R7Ha8F/YSm2Y34vmzD4cxMu2u95g==", + "license": "MIT", + "dependencies": { + "temporal-spec": "0.3.0" + } + }, + "node_modules/temporal-spec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.3.0.tgz", + "integrity": "sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ==", + "license": "ISC" + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/timespan": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/timespan/-/timespan-2.3.0.tgz", + "integrity": "sha512-0Jq9+58T2wbOyLth0EU+AUb6JMGCLaTWIykJFa7hyAybjVH9gpVMTfUAwo5fWAvtFt2Tjh/Elg8JtgNpnMnM8g==", + "engines": { + "node": ">= 0.2.0" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uglify-js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.3.5.tgz", + "integrity": "sha512-YPX1DjKtom8l9XslmPFQnqWzTBkvI4N0pbkzLuPZZ4QTyig0uQqvZz9NgUdfEV+qccJzi7fVcGWdESvRIjWptQ==", + "bin": { + "uglifyjs": "bin/uglifyjs" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrench": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/wrench/-/wrench-1.3.9.tgz", + "integrity": "sha512-srTJQmLTP5YtW+F5zDuqjMEZqLLr/eJOZfDI5ibfPfRMeDh3oBUefAscuH0q5wBKE339ptH/S/0D18ZkfOfmKQ==", + "deprecated": "wrench.js is deprecated! You should check out fs-extra (https://github.com/jprichardson/node-fs-extra) for any operations you were using wrench for. Thanks for all the usage over the years.", + "engines": { + "node": ">=0.1.97" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", + "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/contract-typespec/api/package.json b/contract-typespec/api/package.json new file mode 100644 index 000000000..afd10e844 --- /dev/null +++ b/contract-typespec/api/package.json @@ -0,0 +1,15 @@ +{ + "name": "tsp", + "version": "0.1.0", + "type": "module", + "dependencies": { + "@typespec/compiler": "^1.0.0", + "@typespec/http": "^1.0.1", + "@typespec/openapi": "^1.0.0", + "@typespec/openapi3": "^1.0.0", + "@typespec/rest": "^0.70.0", + "@typespec/sse": "^0.70.0", + "build": "^0.1.4" + }, + "private": true +} diff --git a/contract-typespec/api/quotas.tsp b/contract-typespec/api/quotas.tsp new file mode 100644 index 000000000..8dd7fa27a --- /dev/null +++ b/contract-typespec/api/quotas.tsp @@ -0,0 +1,30 @@ +import "@typespec/openapi"; +import "./models.tsp"; +import "./responses.tsp"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/clientquotas") +@tag("ClientQuotas") +interface QuoatsApi { + @get + @doc("listQuotas") + @operationId("listQuotas") + listQuotas(@path clusterName: string): ClientQuotas[]; + + @post + @doc("upsertClientQuotas") + @operationId("upsertClientQuotas") + upsertClientQuotas( + @path clusterName: string, + @body quota: ClientQuotas, + ): void | ApiBadRequestResponse; +} + +model ClientQuotas { + user?: string; + clientId?: string; + ip?: string; + quotas?: Record; +} diff --git a/contract-typespec/api/responses.tsp b/contract-typespec/api/responses.tsp new file mode 100644 index 000000000..88a1d11f0 --- /dev/null +++ b/contract-typespec/api/responses.tsp @@ -0,0 +1,70 @@ +import "@typespec/http"; +import "@typespec/http/streams"; + +using TypeSpec.Http; +using TypeSpec.Streams; + +model ApiCreatedResponse is Response<201> { + @body + body: Model; +} + +model SseResponse is Stream { + @header contentType: "text/event-stream"; + @body body: Model[]; +} + +// ----- Error Responses ----- +model ApiNotFoundResponse is Response<404> { + message: string; +} + +model ApiTimeoutResponse is Response<408> { + message: string; +} + +model ApiDuplicateResponse is Response<409> { + message: string; +} + +model ApiRebalanceInProgressResponse is Response<409> { + message: string; +} + +model ApiInvalidParametersResponse is Response<422> { + message: string; +} + +model ApiBadRequestResponse is Response<400> { + @body + message: ErrorResponse; +} + +model ApiUnauthorized is Response<401> { + message: string; +} + +model ErrorResponse { + @doc("Internal error code (can be used for message formatting & localization on UI)") + code: int32; + + @doc("Error message") + message: string; + + @doc("Response unix timestamp in ms") + timestamp: decimal; + + @doc("Unique server-defined request id for convenient debugging") + requestId: string; + + fieldsErrors?: FieldError[]; + stackTrace?: string; +} + +model FieldError { + @doc("Name of field that violated format") + fieldName: string; + + @doc("Field format violations description (ex. [\"size must be between 0 and 20\", \"must be a well-formed email address\"])") + restrictions?: string[]; +} diff --git a/contract-typespec/api/schemas.tsp b/contract-typespec/api/schemas.tsp new file mode 100644 index 000000000..d7b29b409 --- /dev/null +++ b/contract-typespec/api/schemas.tsp @@ -0,0 +1,160 @@ +import "@typespec/openapi"; +import "./responses.tsp"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/schemas") +@tag("Schemas") +interface SchemasApi { + @post + @operationId("createNewSchema") + createNewSchema(@path clusterName: string, @body input: NewSchemaSubject): + | SchemaSubject + | ApiBadRequestResponse + | ApiDuplicateResponse + | ApiInvalidParametersResponse; + + @get + @operationId("getSchemas") + getSchemas( + @path clusterName: string, + @query page?: int32, + @query perPage?: int32, + @query search?: string, + ): SchemaSubjectsResponse; + + @delete + @route("/{subject}") + @operationId("deleteSchema") + deleteSchema( + @path clusterName: string, + @path subject: string, + ): void | ApiNotFoundResponse; + + @get + @route("/{subject}/versions") + @operationId("getAllVersionsBySubject") + getAllVersionsBySubject( + @path clusterName: string, + @path subject: string, + ): SchemaSubject[]; + + @get + @route("/{subject}/latest") + @operationId("getLatestSchema") + getLatestSchema( + @path clusterName: string, + @path subject: string, + ): SchemaSubject; + + @delete + @route("/{subject}/latest") + @operationId("deleteLatestSchema") + deleteLatestSchema( + @path clusterName: string, + @path subject: string, + ): void | ApiNotFoundResponse; + + @get + @route("/{subject}/versions/{version}") + @operationId("getSchemaByVersion") + getSchemaByVersion( + @path clusterName: string, + @path subject: string, + @path version: int32, + ): SchemaSubject; + + @delete + @route("/{subject}/versions/{version}") + @operationId("deleteSchemaByVersion") + deleteSchemaByVersion( + @path clusterName: string, + @path subject: string, + @path version: int32, + ): void | ApiNotFoundResponse; + + @get + @route("/compatibility") + @operationId("getGlobalSchemaCompatibilityLevel") + getGlobalSchemaCompatibilityLevel( + @path clusterName: string, + ): CompatibilityLevel; + + @put + @route("/compatibility") + @operationId("updateGlobalSchemaCompatibilityLevel") + updateGlobalSchemaCompatibilityLevel( + @path clusterName: string, + @body level: CompatibilityLevel, + ): void | ApiNotFoundResponse | ApiBadRequestResponse; + + @put + @route("/{subject}/compatibility") + @operationId("updateSchemaCompatibilityLevel") + updateSchemaCompatibilityLevel( + @path clusterName: string, + @path subject: string, + @body level: CompatibilityLevel, + ): void | ApiNotFoundResponse | ApiBadRequestResponse; + + @post + @route("/{subject}/check") + @operationId("checkSchemaCompatibility") + checkSchemaCompatibility( + @path clusterName: string, + @path subject: string, + @body input: NewSchemaSubject, + ): CompatibilityCheckResponse | ApiNotFoundResponse; +} + +model SchemaReference { + name: string; + subject: string; + version: int32; +} + +enum SchemaType { + AVRO, + JSON, + PROTOBUF, +} + +model SchemaSubject { + subject: string; + version: string; + id: int32; + schema: string; + compatibilityLevel: string; + schemaType: SchemaType; + references?: SchemaReference[]; +} + +model NewSchemaSubject { + @doc("should be set for creating/updating schema subject") + subject: string; + + schema: string; + schemaType: SchemaType; + references?: SchemaReference[]; +} + +model CompatibilityLevel { + compatibility: + | "BACKWARD" + | "BACKWARD_TRANSITIVE" + | "FORWARD" + | "FORWARD_TRANSITIVE" + | "FULL" + | "FULL_TRANSITIVE" + | "NONE"; +} + +model CompatibilityCheckResponse { + isCompatible: boolean; +} + +model SchemaSubjectsResponse { + pageCount?: int32; + schemas?: SchemaSubject[]; +} \ No newline at end of file diff --git a/contract-typespec/api/topics.tsp b/contract-typespec/api/topics.tsp new file mode 100644 index 000000000..9af5f75c1 --- /dev/null +++ b/contract-typespec/api/topics.tsp @@ -0,0 +1,309 @@ +import "@typespec/openapi"; +import "./responses.tsp"; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/topics") +@tag("Topics") +interface TopicsApi { + @get + @operationId("getTopics") + getTopics( + @path clusterName: string, + @query page?: int32, + @query perPage?: int32, + @query showInternal?: boolean, + @query search?: string, + @query orderBy?: TopicColumnsToSort, + @query sortOrder?: SortOrder, + ): TopicsResponse; + + @post + @operationId("createTopic") + createTopic( + @path clusterName: string, + @body topic: TopicCreation, + ): ApiCreatedResponse; + + @post + @route("/{topicName}/clone") + @operationId("cloneTopic") + cloneTopic( + @path clusterName: string, + @path topicName: string, + @query newTopicName: string, + ): ApiCreatedResponse | ApiNotFoundResponse; + + @get + @route("/{topicName}/analysis") + @operationId("getTopicAnalysis") + getTopicAnalysis( + @path clusterName: string, + @path topicName: string, + ): TopicAnalysis | ApiNotFoundResponse; + + @post + @route("/{topicName}/analysis") + @operationId("analyzeTopic") + analyzeTopic( + @path clusterName: string, + @path topicName: string, + ): void | ApiNotFoundResponse; + + @delete + @route("/{topicName}/analysis") + @operationId("cancelTopicAnalysis") + cancelTopicAnalysis( + @path clusterName: string, + @path topicName: string, + ): void | ApiNotFoundResponse; + + @get + @route("/{topicName}") + @operationId("getTopicDetails") + getTopicDetails( + @path clusterName: string, + @path topicName: string, + ): TopicDetails; + + @post + @route("/{topicName}") + @operationId("recreateTopic") + recreateTopic(@path clusterName: string, @path topicName: string): + | ApiCreatedResponse + | ApiTimeoutResponse + | ApiNotFoundResponse + | ApiBadRequestResponse; + + @patch(#{implicitOptionality: true}) + @route("/{topicName}") + @operationId("updateTopic") + updateTopic( + @path clusterName: string, + @path topicName: string, + @body update: TopicUpdate, + ): Topic; + + @delete + @route("/{topicName}") + @operationId("deleteTopic") + deleteTopic( + @path clusterName: string, + @path topicName: string, + ): void | NotFoundResponse; + + @get + @route("/{topicName}/config") + @operationId("getTopicConfigs") + getTopicConfigs( + @path clusterName: string, + @path topicName: string, + ): TopicConfig[]; + + @patch(#{ implicitOptionality: true }) + @route("/{topicName}/replications") + @operationId("changeReplicationFactor") + changeReplicationFactor( + @path clusterName: string, + @path topicName: string, + @body change: ReplicationFactorChange, + ): ReplicationFactorChangeResponse | ApiNotFoundResponse | ApiBadRequestResponse; + + @get + @route("/{topicName}/activeproducers") + @operationId("getActiveProducerStates") + getActiveProducerStates( + @path clusterName: string, + @path topicName: string, + ): TopicProducerState[]; + + @patch(#{ implicitOptionality: true }) + @route("/{topicName}/partitions") + @doc("increaseTopicPartitions") + @operationId("increaseTopicPartitions") + increaseTopicPartitions( + @path clusterName: string, + @path topicName: string, + @body partitionsIncrease: PartitionsIncrease, + ): PartitionsIncreaseResponse | ApiNotFoundResponse; +} + +model TopicsResponse { + pageCount: int32; + topics: Topic[]; +} + +enum TopicColumnsToSort { + NAME, + OUT_OF_SYNC_REPLICAS, + TOTAL_PARTITIONS, + REPLICATION_FACTOR, + SIZE, +} + +model Topic { + name: string; + internal: boolean; + partitionCount: int32; + replicationFactor: int32; + replicas: int32; + inSyncReplicas: int32; + segmentSize: int64; + segmentCount: int32; + bytesInPerSec: float64; + bytesOutPerSec: float64; + underReplicatedPartitions: int32; + cleanUpPolicy: CleanUpPolicy; + partitions: Partition[]; +} + +model TopicUpdate { + configs: Record; +} + +model TopicAnalysis { + progress?: TopicAnalysisProgress; + result?: TopicAnalysisResult; +} + +model TopicAnalysisProgress { + startedAt: int64; + completenessPercent: decimal; + msgsScanned: int64; + bytesScanned: int64; +} + +model TopicAnalysisResult { + startedAt: int64; + finishedAt: int64; + error: string; + totalStats: TopicAnalysisStats; + partitionStats: TopicAnalysisStats[]; +} + +model TopicAnalysisStats { + @doc("null if this is total stats") + partition?: int32; + + totalMsgs: int64; + minOffset: int64; + maxOffset: int64; + minTimestamp: int64; + maxTimestamp: int64; + nullKeys: int64; + nullValues: int64; + approxUniqKeys: int64; + approxUniqValues: int64; + keySize: TopicAnalysisSizeStats; + valueSize: TopicAnalysisSizeStats; + hourlyMsgCounts: { + hourStart: int64; + count: int64; + }[]; +} + +model TopicAnalysisSizeStats { + sum: int64; + min: int64; + max: int64; + avg: int64; + prctl50: int64; + prctl75: int64; + prctl95: int64; + prctl99: int64; + prctl999: int64; +} + +model TopicProducerState { + partition?: int32; + producerId?: int64; + producerEpoch?: int32; + lastSequence?: int32; + lastTimestampMs?: int64; + coordinatorEpoch?: int32; + currentTransactionStartOffset?: int64; +} + +model TopicDetails { + name: string; + internal: boolean; + partitions: Partition[]; + partitionCount: int32; + replicationFactor: int32; + replicas: int32; + inSyncReplicas: int32; + bytesInPerSec: float64; + bytesOutPerSec: float64; + segmentSize: int64; + segmentCount: int32; + underReplicatedPartitions: int32; + cleanUpPolicy: CleanUpPolicy; + keySerde: string; + valueSerde: string; +} + +model TopicConfig { + name: string; + value: string; + defaultValue: string; + source: ConfigSource; + isSensitive: boolean; + isReadOnly: boolean; + synonyms: ConfigSynonym[]; + doc: string; +} + +model TopicCreation { + name: string; + partitions: int32; + replicationFactor: int32; + configs: Record; +} + +enum CleanUpPolicy { + DELETE, + COMPACT, + COMPACT_DELETE, + UNKNOWN, +} + +model ConfigSynonym { + name?: string; + value?: string; + source?: ConfigSource; +} + +model Partition { + partition: int32; + leader?: int32; + replicas?: Replica[]; + offsetMax: int64; + offsetMin: int64; +} + +model PartitionsIncrease { + @minValue(1) + totalPartitionsCount: integer; +} + +model PartitionsIncreaseResponse { + totalPartitionsCount: integer; + topicName: string; +} + + +model ReplicationFactorChange { + totalReplicationFactor: integer; +} + +model ReplicationFactorChangeResponse { + totalReplicationFactor: integer; + topicName: string; +} + +model Replica { + broker: int32; + leader: boolean; + inSync: boolean; +} diff --git a/contract-typespec/api/tspconfig.yaml b/contract-typespec/api/tspconfig.yaml new file mode 100644 index 000000000..e84c6b273 --- /dev/null +++ b/contract-typespec/api/tspconfig.yaml @@ -0,0 +1,6 @@ +emit: + - "@typespec/openapi3" +options: + "@typespec/openapi3": + emitter-output-dir: "{output-dir}" + output-file: openapi.yaml diff --git a/contract-typespec/build.gradle b/contract-typespec/build.gradle new file mode 100644 index 000000000..6b5f018d4 --- /dev/null +++ b/contract-typespec/build.gradle @@ -0,0 +1,58 @@ +plugins { + id "java" + alias(libs.plugins.node.gradle) +} + + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + + +node { + download = "true" != project.property("local_node") + version = project.property("node_version").toString() + pnpmVersion = project.property("pnpm_version").toString() +} + +def typeSpecVersion = "1.0.0" + +sourceSets { + main { + resources { + srcDirs += ['build/tsp'] + } + } +} + + + +def typeSpecs = ["api"] + +typeSpecs.each { spec -> + + def tspPath = "\\tsp --output-dir=${project.layout.buildDirectory.get()}/tsp/${spec} compile .\\" + + tasks.register("installDependencies_${spec}", NpmTask) { + group = 'build' + npmCommand = ['install'] + workingDir = project.layout.projectDirectory.dir(spec).asFile + } + + tasks.register("generateOpenApi_${spec}", NpmTask) { + group = 'build' + dependsOn "installDependencies_${spec}" + npmCommand = ['exec'] + workingDir = project.layout.projectDirectory.dir(spec).asFile + args = ["--package=@typespec/compiler@${typeSpecVersion}", '-c', tspPath] + } + + tasks.register("cleanDependencies_${spec}", Delete) { + delete layout.projectDirectory.dir(spec).dir("node_modules") + } + + clean.dependsOn("cleanDependencies_${spec}") + processResources.dependsOn("generateOpenApi_${spec}") + build.dependsOn("generateOpenApi_${spec}") +} diff --git a/contract/build.gradle b/contract/build.gradle index d6d662970..d95af81b8 100644 --- a/contract/build.gradle +++ b/contract/build.gradle @@ -21,7 +21,12 @@ dependencies { tasks.register('generateUiClient', GenerateTask) { generatorName = "java" - inputSpec = specDir.file("kafbat-ui-api.yaml").asFile.absolutePath + if (useTypeSpec) { + dependsOn ":contract-typespec:build" + inputSpec = project(":contract-typespec").layout.buildDirectory.dir("tsp/api/openapi.yaml").get().asFile.absolutePath + } else { + inputSpec = specDir.file("kafbat-ui-api.yaml").asFile.absolutePath + } outputDir = targetDir.dir("kafbat-ui-client").asFile.absolutePath apiPackage = "io.kafbat.ui.api.api" invokerPackage = "io.kafbat.ui.api" @@ -36,7 +41,12 @@ tasks.register('generateUiClient', GenerateTask) { tasks.register('generateBackendApi', GenerateTask) { generatorName = "spring" - inputSpec = specDir.file("kafbat-ui-api.yaml").asFile.absolutePath + if (useTypeSpec) { + dependsOn ":contract-typespec:build" + inputSpec = project(":contract-typespec").layout.buildDirectory.dir("tsp/api/openapi.yaml").get().asFile.absolutePath + } else { + inputSpec = specDir.file("kafbat-ui-api.yaml").asFile.absolutePath + } outputDir = targetDir.dir("api").asFile.absolutePath apiPackage = "io.kafbat.ui.api" invokerPackage = "io.kafbat.ui.api" diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index 7b4c65744..928a7dc34 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -498,6 +498,7 @@ paths: schema: type: string requestBody: + required: true content: application/json: schema: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index edb5cd99d..48390b1ea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,7 @@ netty = '4.1.119.Final' spring-boot = { id = 'org.springframework.boot', version.ref = 'spring-boot' } spring-dependency-management = { id = 'io.spring.dependency-management', version = '1.1.3' } git-properties = { id = 'com.gorylenko.gradle-git-properties', version = '2.4.2' } -openapi-generator = { id = 'org.openapi.generator', version = '7.9.0' } +openapi-generator = { id = 'org.openapi.generator', version = '7.13.0' } allure = { id = 'io.qameta.allure', version = '2.10.0' } nexus-publish-plugin = { id = 'io.github.gradle-nexus.publish-plugin', version = '1.1.0' } node-gradle = { id = 'com.github.node-gradle.node', version = '7.1.0' } diff --git a/settings.gradle b/settings.gradle index a9be51116..fa0dcb978 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,7 @@ pluginManagement { rootProject.name = "kafbat-ui" include "contract" +include "contract-typespec" include "serde-api" include "api" include "frontend" From b8b91fc8d422f94a507805c4d945e156f00dae5d Mon Sep 17 00:00:00 2001 From: German Osin Date: Tue, 10 Jun 2025 08:39:13 +0200 Subject: [PATCH 02/36] Fixed styling --- contract-typespec/api/acls.tsp | 20 ++++++++++++++++++-- contract-typespec/api/messages.tsp | 4 +--- contract-typespec/api/models.tsp | 14 -------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/contract-typespec/api/acls.tsp b/contract-typespec/api/acls.tsp index 3bd84e2f5..1062c10f0 100644 --- a/contract-typespec/api/acls.tsp +++ b/contract-typespec/api/acls.tsp @@ -75,10 +75,26 @@ model KafkaAcl { namePatternType: KafkaAclNamePatternType; principal: string; host: string; - operation: "UNKNOWN" | "ALL" | "READ" | "WRITE" | "CREATE" | "DELETE" | "ALTER" | "DESCRIBE" | "CLUSTER_ACTION" | "DESCRIBE_CONFIGS" | "ALTER_CONFIGS" | "IDEMPOTENT_WRITE" | "CREATE_TOKENS" | "DESCRIBE_TOKENS"; + operation: KafkaAclOpeations; permission: "ALLOW" | "DENY"; } +alias KafkaAclOpeations = + UNKNOWN + | ALL + | READ + | WRITE + | CREATE + | DELETE + | ALTER + | DESCRIBE + | CLUSTER_ACTION + | DESCRIBE_CONFIGS + | ALTER_CONFIGS + | IDEMPOTENT_WRITE + | CREATE_TOKENS + | DESCRIBE_TOKENS; + enum KafkaAclResourceType { UNKNOWN, TOPIC, @@ -120,4 +136,4 @@ model CreateStreamAppAcl { inputTopics: string[]; outputTopics: string[]; applicationId: string; -} \ No newline at end of file +} diff --git a/contract-typespec/api/messages.tsp b/contract-typespec/api/messages.tsp index 4c11353c9..8baee52e9 100644 --- a/contract-typespec/api/messages.tsp +++ b/contract-typespec/api/messages.tsp @@ -89,8 +89,6 @@ interface SmartFiltersTestExecutionsApi { ): SmartFilterTestExecutionResult | ApiBadRequestResponse; } - - model TopicSerdeSuggestion { key: SerdeDescription[]; value: SerdeDescription[]; @@ -231,4 +229,4 @@ model SerdeDescription { schema?: string; additionalProperties?: Record; -} \ No newline at end of file +} diff --git a/contract-typespec/api/models.tsp b/contract-typespec/api/models.tsp index bccaa5c5a..e2df81cdd 100644 --- a/contract-typespec/api/models.tsp +++ b/contract-typespec/api/models.tsp @@ -21,17 +21,3 @@ enum SortOrder { } - - - -// model BrokerLogdirs { -// name: string; -// error: string; -// topics: TopicLogdirs[]; -// } - -// model TopicLogdirs { -// name?: string; -// partitions?: TopicPartitionLogdir[]; -// } - From a2175a149d8de43b38c8da44306f29b5b7595afa Mon Sep 17 00:00:00 2001 From: German Osin Date: Tue, 10 Jun 2025 10:12:02 +0200 Subject: [PATCH 03/36] Fixed styling --- contract-typespec/api/acls.tsp | 28 +++++++++++------------ contract-typespec/api/consumer-groups.tsp | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/contract-typespec/api/acls.tsp b/contract-typespec/api/acls.tsp index 1062c10f0..d22cd8a71 100644 --- a/contract-typespec/api/acls.tsp +++ b/contract-typespec/api/acls.tsp @@ -80,20 +80,20 @@ model KafkaAcl { } alias KafkaAclOpeations = - UNKNOWN - | ALL - | READ - | WRITE - | CREATE - | DELETE - | ALTER - | DESCRIBE - | CLUSTER_ACTION - | DESCRIBE_CONFIGS - | ALTER_CONFIGS - | IDEMPOTENT_WRITE - | CREATE_TOKENS - | DESCRIBE_TOKENS; + "UNKNOWN" + | "ALL" + | "READ" + | "WRITE" + | "CREATE" + | "DELETE" + | "ALTER" + | "DESCRIBE" + | "CLUSTER_ACTION" + | "DESCRIBE_CONFIGS" + | "ALTER_CONFIGS" + | "IDEMPOTENT_WRITE" + | "CREATE_TOKENS" + | "DESCRIBE_TOKENS"; enum KafkaAclResourceType { UNKNOWN, diff --git a/contract-typespec/api/consumer-groups.tsp b/contract-typespec/api/consumer-groups.tsp index af05a4c6c..29c727d45 100644 --- a/contract-typespec/api/consumer-groups.tsp +++ b/contract-typespec/api/consumer-groups.tsp @@ -88,7 +88,7 @@ model ConsumerGroup { model ConsumerGroupDetails extends ConsumerGroup { @invisible(Lifecycle) - inherit: "deatils"; + inherit: "details"; partitions?: ConsumerGroupTopicPartition[]; } From 5d4f4192b3a00bc7a970c67e10de0898d22cf0a8 Mon Sep 17 00:00:00 2001 From: German Osin Date: Wed, 11 Jun 2025 14:59:36 +0200 Subject: [PATCH 04/36] enable typespec by default --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b7b4eb462..deb45bcf0 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ ext { release = resolveBooleanProperty("release") includeFrontend = resolveBooleanProperty("include-frontend", release) buildDockerImages = resolveBooleanProperty("build-docker-images", release) - useTypeSpec = resolveBooleanProperty("typespec", false) + useTypeSpec = resolveBooleanProperty("typespec", true) runE2e = resolveBooleanProperty("run-e2e") } From 8cd0a0a67230b55420bc8a8b4b56eb7f95dec091 Mon Sep 17 00:00:00 2001 From: German Osin Date: Wed, 11 Jun 2025 15:26:26 +0200 Subject: [PATCH 05/36] Added info --- contract-typespec/api/acls.tsp | 2 ++ contract-typespec/api/auth.tsp | 2 ++ contract-typespec/api/brokers.tsp | 4 ++- contract-typespec/api/clusters.tsp | 4 ++- contract-typespec/api/config.tsp | 38 ++++++++++++----------- contract-typespec/api/consumer-groups.tsp | 2 ++ contract-typespec/api/kafka-connect.tsp | 2 ++ contract-typespec/api/ksql.tsp | 4 ++- contract-typespec/api/main.tsp | 12 +++++++ contract-typespec/api/messages.tsp | 2 ++ contract-typespec/api/models.tsp | 2 ++ contract-typespec/api/quotas.tsp | 2 ++ contract-typespec/api/responses.tsp | 2 ++ contract-typespec/api/schemas.tsp | 4 ++- contract-typespec/api/topics.tsp | 2 ++ 15 files changed, 62 insertions(+), 22 deletions(-) diff --git a/contract-typespec/api/acls.tsp b/contract-typespec/api/acls.tsp index d22cd8a71..0f9e57220 100644 --- a/contract-typespec/api/acls.tsp +++ b/contract-typespec/api/acls.tsp @@ -1,5 +1,7 @@ import "@typespec/openapi"; +namespace Api; + using TypeSpec.Http; using OpenAPI; diff --git a/contract-typespec/api/auth.tsp b/contract-typespec/api/auth.tsp index dd48a305b..f30d0a5af 100644 --- a/contract-typespec/api/auth.tsp +++ b/contract-typespec/api/auth.tsp @@ -1,5 +1,7 @@ import "@typespec/openapi"; +namespace Api; + using TypeSpec.Http; using OpenAPI; diff --git a/contract-typespec/api/brokers.tsp b/contract-typespec/api/brokers.tsp index 36e1bb920..74f7545d7 100644 --- a/contract-typespec/api/brokers.tsp +++ b/contract-typespec/api/brokers.tsp @@ -1,5 +1,7 @@ import "@typespec/openapi"; +namespace Api; + using TypeSpec.Http; using OpenAPI; @@ -111,4 +113,4 @@ model TopicPartitionLogdir { partition?: int32; size?: int64; offsetLag?: int64; -} \ No newline at end of file +} diff --git a/contract-typespec/api/clusters.tsp b/contract-typespec/api/clusters.tsp index 7ce03994d..f88146e98 100644 --- a/contract-typespec/api/clusters.tsp +++ b/contract-typespec/api/clusters.tsp @@ -1,6 +1,8 @@ import "@typespec/openapi"; import "./responses.tsp"; +namespace Api; + using TypeSpec.Http; using OpenAPI; @@ -92,4 +94,4 @@ model BrokerDiskUsage { brokerId: int32; segmentSize: int64; segmentCount: int32; -} \ No newline at end of file +} diff --git a/contract-typespec/api/config.tsp b/contract-typespec/api/config.tsp index ecaef9fdd..c01fb8929 100644 --- a/contract-typespec/api/config.tsp +++ b/contract-typespec/api/config.tsp @@ -1,6 +1,8 @@ import "@typespec/openapi"; import "./responses.tsp"; +namespace Api; + using TypeSpec.Http; using OpenAPI; @@ -81,10 +83,10 @@ model ApplicationConfig { jwkSetUri?: string; userNameAttribute?: string; scope?: string[]; - customParams?: Record; + customParams?: Record; }>; resourceServer?: { - jwt?: { + jwt?: { jwkSetUri?: string; jwsAlgorithms?: string[]; issuerUri?: string; @@ -93,15 +95,15 @@ model ApplicationConfig { authorityPrefix?: string; authoritiesClaimDelimiter?: string; authoritiesClaimName?: string; - principalClaimName?: string; + principalClaimName?: string; }; opaquetoken?: { clientId?: string; clientSecret?: string; - introspectionUri?: string; - }; - }; - }; + introspectionUri?: string; + }; + }; + }; }; rbac: { roles: { @@ -116,7 +118,7 @@ model ApplicationConfig { permissions: { resource: ResourceType; value: string; - actions: Action[]; + actions: Action[]; }[]; }[]; }; @@ -129,7 +131,7 @@ model ApplicationConfig { pollTimeoutMs: int32; maxPageSize: int32; defaultPageSize: int32; - responseTimeoutMs: int32; + responseTimeoutMs: int32; }; adminClientTimeout?: int32; internalTopicPrefix: string; @@ -139,7 +141,7 @@ model ApplicationConfig { ssl?: { truststoreLocation?: string; truststorePassword?: string; - verifySsl: boolean = true; + verifySsl: boolean = true; }; schemaRegistry?: string; schemaRegistryAuth?: { @@ -148,16 +150,16 @@ model ApplicationConfig { }; schemaRegistrySsl?: { keystoreLocation: string; - keystorePassword: string; + keystorePassword: string; }; ksqldbServer?: string; ksqldbServerSsl?: { keystoreLocation: string; - keystorePassword: string; + keystorePassword: string; }; ksqldbServerAuth?: { username: string; - password: string; + password: string; }; kafkaConnect?: { name: string; @@ -165,7 +167,7 @@ model ApplicationConfig { username?: string; password?: string; keystoreLocation?: string; - keystorePassword?: string; + keystorePassword?: string; }[]; metrics?: KafkaMetricsConfig; properties?: Record; @@ -177,9 +179,9 @@ model ApplicationConfig { defaultValueSerde?: string; masking?: MaskingConfig[]; pollingThrottleRate?: int64; - audit?: KafkaAuditConfig; + audit?: KafkaAuditConfig; }[]; - }; + }; }; } @@ -262,7 +264,7 @@ enum AuthType { } model AppAuthenticationSettings { - authType?: global.AuthType; + authType?: Api.AuthType; oAuthProviders?: OAuthProvider[]; } @@ -308,4 +310,4 @@ enum ResourceType { ACL, AUDIT, CLIENT_QUOTAS, -} \ No newline at end of file +} diff --git a/contract-typespec/api/consumer-groups.tsp b/contract-typespec/api/consumer-groups.tsp index 29c727d45..e8929f721 100644 --- a/contract-typespec/api/consumer-groups.tsp +++ b/contract-typespec/api/consumer-groups.tsp @@ -1,6 +1,8 @@ import "@typespec/openapi"; import "./responses.tsp"; +namespace Api; + using TypeSpec.Http; using OpenAPI; diff --git a/contract-typespec/api/kafka-connect.tsp b/contract-typespec/api/kafka-connect.tsp index 1870855be..1c21d9234 100644 --- a/contract-typespec/api/kafka-connect.tsp +++ b/contract-typespec/api/kafka-connect.tsp @@ -1,6 +1,8 @@ import "@typespec/openapi"; import "./responses.tsp"; +namespace Api; + using TypeSpec.Http; using OpenAPI; diff --git a/contract-typespec/api/ksql.tsp b/contract-typespec/api/ksql.tsp index efa0af06e..09375e881 100644 --- a/contract-typespec/api/ksql.tsp +++ b/contract-typespec/api/ksql.tsp @@ -1,6 +1,8 @@ import "@typespec/openapi"; import "./responses.tsp"; +namespace Api; + using TypeSpec.Http; using OpenAPI; @@ -67,4 +69,4 @@ model KsqlTableResponse { model KsqlResponse { table?: KsqlTableResponse; -} \ No newline at end of file +} diff --git a/contract-typespec/api/main.tsp b/contract-typespec/api/main.tsp index a7d2ed347..440ec454b 100644 --- a/contract-typespec/api/main.tsp +++ b/contract-typespec/api/main.tsp @@ -11,3 +11,15 @@ import "./acls.tsp"; import "./quotas.tsp"; import "./auth.tsp"; import "./config.tsp"; +import "@typespec/openapi"; + +using TypeSpec.OpenAPI; + +@service(#{ title: "Kafbat UI Api Service" }) +@info(#{ + contact: #{ name: "API Support", email: "support@kafbat.io" }, + license: #{ name: "Apache 2.0", url: "https://www.apache.org/licenses/LICENSE-2.0.html" }, + version: "0.2.0" +}) +namespace Api; + diff --git a/contract-typespec/api/messages.tsp b/contract-typespec/api/messages.tsp index 8baee52e9..e26a8952d 100644 --- a/contract-typespec/api/messages.tsp +++ b/contract-typespec/api/messages.tsp @@ -2,6 +2,8 @@ import "@typespec/openapi"; import "./models.tsp"; import "./responses.tsp"; +namespace Api; + using TypeSpec.Http; using OpenAPI; diff --git a/contract-typespec/api/models.tsp b/contract-typespec/api/models.tsp index e2df81cdd..ab4b590e7 100644 --- a/contract-typespec/api/models.tsp +++ b/contract-typespec/api/models.tsp @@ -1,3 +1,5 @@ +namespace Api; + model Metric { name?: string; labels?: Record; diff --git a/contract-typespec/api/quotas.tsp b/contract-typespec/api/quotas.tsp index 8dd7fa27a..ba94c4e5b 100644 --- a/contract-typespec/api/quotas.tsp +++ b/contract-typespec/api/quotas.tsp @@ -2,6 +2,8 @@ import "@typespec/openapi"; import "./models.tsp"; import "./responses.tsp"; +namespace Api; + using TypeSpec.Http; using OpenAPI; diff --git a/contract-typespec/api/responses.tsp b/contract-typespec/api/responses.tsp index 88a1d11f0..c784aff70 100644 --- a/contract-typespec/api/responses.tsp +++ b/contract-typespec/api/responses.tsp @@ -1,6 +1,8 @@ import "@typespec/http"; import "@typespec/http/streams"; +namespace Api; + using TypeSpec.Http; using TypeSpec.Streams; diff --git a/contract-typespec/api/schemas.tsp b/contract-typespec/api/schemas.tsp index d7b29b409..0b7922703 100644 --- a/contract-typespec/api/schemas.tsp +++ b/contract-typespec/api/schemas.tsp @@ -1,6 +1,8 @@ import "@typespec/openapi"; import "./responses.tsp"; +namespace Api; + using TypeSpec.Http; using OpenAPI; @@ -157,4 +159,4 @@ model CompatibilityCheckResponse { model SchemaSubjectsResponse { pageCount?: int32; schemas?: SchemaSubject[]; -} \ No newline at end of file +} diff --git a/contract-typespec/api/topics.tsp b/contract-typespec/api/topics.tsp index 9af5f75c1..8c0439ee5 100644 --- a/contract-typespec/api/topics.tsp +++ b/contract-typespec/api/topics.tsp @@ -1,6 +1,8 @@ import "@typespec/openapi"; import "./responses.tsp"; +namespace Api; + using TypeSpec.Http; using OpenAPI; From 6ce97f8b8b78c3942cf3c3c2da1b30faa3032165 Mon Sep 17 00:00:00 2001 From: German Osin Date: Wed, 11 Jun 2025 15:42:48 +0200 Subject: [PATCH 06/36] Frontend use new contracts --- frontend/build.gradle | 6 ++++++ frontend/openapitools.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/build.gradle b/frontend/build.gradle index dc982ad73..17e0d74c1 100644 --- a/frontend/build.gradle +++ b/frontend/build.gradle @@ -23,6 +23,12 @@ tasks.named("pnpmInstall") { tasks.register('generateContract', PnpmTask) { dependsOn pnpmInstall + if (useTypeSpec) { + dependsOn ":contract-typespec:build" + inputs.files(project(":contract-typespec").layout.buildDirectory.dir("tsp/api/openapi.yaml")) + } else { + inputs.files(fileTree("../contract/src/main/resources")) + } inputs.files(fileTree("../contract/src/main/resources")) outputs.dir(project.layout.projectDirectory.dir("src/generated-sources")) args = ['gen:sources'] diff --git a/frontend/openapitools.json b/frontend/openapitools.json index 4762ba082..4f82eb54d 100644 --- a/frontend/openapitools.json +++ b/frontend/openapitools.json @@ -7,7 +7,7 @@ "fetch": { "generatorName": "typescript-fetch", "output": "src/generated-sources", - "glob": "../contract/src/main/resources/swagger/kafbat-ui-api.yaml", + "glob": "../contract-typespec/build/tsp/api/openapi.yaml", "additionalProperties": { "enumPropertyNaming": "UPPERCASE", "typescriptThreePlus": true, From a7a59d80b87453a0f1ee8a151d4e4d5a201b5ea1 Mon Sep 17 00:00:00 2001 From: German Osin Date: Wed, 11 Jun 2025 18:19:35 +0200 Subject: [PATCH 07/36] Fixes in contracts for backward compatibility --- contract-typespec/api/acls.tsp | 18 +-- contract-typespec/api/auth.tsp | 8 +- contract-typespec/api/brokers.tsp | 26 +++- contract-typespec/api/clusters.tsp | 38 ++--- contract-typespec/api/config.tsp | 182 ++++++++++------------ contract-typespec/api/consumer-groups.tsp | 4 +- contract-typespec/api/kafka-connect.tsp | 5 +- contract-typespec/api/main.tsp | 6 + contract-typespec/api/messages.tsp | 6 +- contract-typespec/api/schemas.tsp | 2 +- contract-typespec/api/topics.tsp | 144 ++++++++--------- frontend/src/lib/fixtures/topics.ts | 2 + 12 files changed, 222 insertions(+), 219 deletions(-) diff --git a/contract-typespec/api/acls.tsp b/contract-typespec/api/acls.tsp index 0f9e57220..6c009edd6 100644 --- a/contract-typespec/api/acls.tsp +++ b/contract-typespec/api/acls.tsp @@ -114,8 +114,8 @@ enum KafkaAclNamePatternType { } model CreateConsumerAcl { - principal: string; - host: string; + principal?: string; + host?: string; topics?: string[]; topicsPrefix?: string; consumerGroups?: string[]; @@ -123,8 +123,8 @@ model CreateConsumerAcl { } model CreateProducerAcl { - principal: string; - host: string; + principal?: string; + host?: string; topics?: string[]; topicsPrefix?: string; transactionalId?: string; @@ -133,9 +133,9 @@ model CreateProducerAcl { } model CreateStreamAppAcl { - principal: string; - host: string; - inputTopics: string[]; - outputTopics: string[]; - applicationId: string; + principal?: string; + host?: string; + inputTopics?: string[]; + outputTopics?: string[]; + applicationId?: string; } diff --git a/contract-typespec/api/auth.tsp b/contract-typespec/api/auth.tsp index f30d0a5af..599403839 100644 --- a/contract-typespec/api/auth.tsp +++ b/contract-typespec/api/auth.tsp @@ -18,11 +18,13 @@ interface AuthorizationApi { @doc("Authenticate") @operationId("authenticate") @post -op authenticate(@body form: LoginForm): void | Http.Response<401>; +@tag("Unmapped") +op authenticate(@header contentType: "application/x-www-form-urlencoded", @body form: LoginForm): void | Http.Response<401>; + model LoginForm { - username: string; - password: string; + username?: string; + password?: string; } model AuthenticationInfo { diff --git a/contract-typespec/api/brokers.tsp b/contract-typespec/api/brokers.tsp index 74f7545d7..0f7442c1e 100644 --- a/contract-typespec/api/brokers.tsp +++ b/contract-typespec/api/brokers.tsp @@ -88,16 +88,27 @@ model BrokerConfigItem { value?: string; } +model BrokerLogdirs { + name?: string; + error?: string; + topics?: TopicLogdirs[]; +} + +model TopicLogdirs { + name?: string; + partitions?: TopicPartitionLogdir[]; +} + model BrokerMetrics { - segmentSize: int64; - segmentCount: int32; - metrics: Metric[]; + segmentSize?: int64; + segmentCount?: int32; + metrics?: Metric[]; } model BrokersLogdirs { - name: string; - error: string; - topics: BrokerTopicLogdirs[]; + name?: string; + error?: string; + topics?: BrokerTopicLogdirs[]; } model BrokerTopicLogdirs { @@ -105,7 +116,8 @@ model BrokerTopicLogdirs { partitions?: BrokerTopicPartitionLogdir[]; } -model BrokerTopicPartitionLogdir extends TopicPartitionLogdir { +model BrokerTopicPartitionLogdir { + ...TopicPartitionLogdir; broker?: int32; } diff --git a/contract-typespec/api/clusters.tsp b/contract-typespec/api/clusters.tsp index f88146e98..de7b95880 100644 --- a/contract-typespec/api/clusters.tsp +++ b/contract-typespec/api/clusters.tsp @@ -35,17 +35,17 @@ interface ClustersApi { model Cluster { name: string; - defaultCluster: boolean; + defaultCluster?: boolean; status: ServerStatus; lastError?: MetricsCollectionError; - brokerCount: int32; - onlinePartitionCount: int32; - topicCount: int32; + brokerCount?: int32; + onlinePartitionCount?: int32; + topicCount?: int32; bytesInPerSec?: float64; bytesOutPerSec?: float64; - readOnly: boolean; - version: string; - features: ClusterFeature[]; + readOnly?: boolean; + version?: string; + features?: ClusterFeature[]; } alias ClusterFeature = @@ -68,20 +68,20 @@ model ClusterMetrics { } model ClusterStats { - brokerCount: int32; + brokerCount?: int32; #deprecated "Unused" zooKeeperStatus?: int32; @doc("Id of broker which is cluster's controller. null, if controller not known yet.") - activeControllers: int32; - - onlinePartitionCount: int32; - offlinePartitionCount: int32; - inSyncReplicasCount: int32; - outOfSyncReplicasCount: int32; - underReplicatedPartitionCount: int32; - diskUsage: BrokerDiskUsage[]; - version: string; + activeControllers?: int32; + + onlinePartitionCount?: int32; + offlinePartitionCount?: int32; + inSyncReplicasCount?: int32; + outOfSyncReplicasCount?: int32; + underReplicatedPartitionCount?: int32; + diskUsage?: BrokerDiskUsage[]; + version?: string; } model MetricsCollectionError { @@ -92,6 +92,6 @@ model MetricsCollectionError { model BrokerDiskUsage { brokerId: int32; - segmentSize: int64; - segmentCount: int32; + segmentSize?: int64; + segmentCount?: int32; } diff --git a/contract-typespec/api/config.tsp b/contract-typespec/api/config.tsp index c01fb8929..a228a810c 100644 --- a/contract-typespec/api/config.tsp +++ b/contract-typespec/api/config.tsp @@ -50,23 +50,23 @@ interface ApplicationConfigApi { model ApplicationInfo { - enabledFeatures: ApplicationFeature[]; - build: { - commitId: string; - version: string; - buildTime: string; - isLatestRelease: boolean; + enabledFeatures?: ApplicationFeature[]; + build?: { + commitId?: string; + version?: string; + buildTime?: string; + isLatestRelease?: boolean; }; - latestRelease: { - versionTag: string; - publishedAt: string; - htmlUrl: string; + latestRelease?: { + versionTag?: string; + publishedAt?: string; + htmlUrl?: string; }; } model ApplicationConfig { properties: { - auth: { + auth?: { type: string; oauth2: { client?: Record<{ @@ -105,136 +105,116 @@ model ApplicationConfig { }; }; }; - rbac: { - roles: { - name: string; - clusters: string[]; - subjects: { - provider: string; - type: string; - value: string; + rbac?: { + roles?: { + name?: string; + clusters?: string[]; + subjects?: { + provider?: string; + type?: string; + value?: string; regex?: boolean = false; }[]; - permissions: { - resource: ResourceType; - value: string; - actions: Action[]; + permissions?: { + resource?: ResourceType; + value?: string; + actions?: Action[]; }[]; }[]; }; - webclient: { - maxInMemoryBufferSize: string; - responseTimeoutMs: int32; + webclient?: { + maxInMemoryBufferSize?: string; + responseTimeoutMs?: int32; }; - kafka: { + kafka?: { polling?: { - pollTimeoutMs: int32; - maxPageSize: int32; - defaultPageSize: int32; - responseTimeoutMs: int32; + pollTimeoutMs?: int32; + maxPageSize?: int32; + defaultPageSize?: int32; + responseTimeoutMs?: int32; }; adminClientTimeout?: int32; - internalTopicPrefix: string; - clusters: { - name: string; - bootstrapServers: string; + internalTopicPrefix?: string; + clusters?: { + name?: string; + bootstrapServers?: string; ssl?: { truststoreLocation?: string; truststorePassword?: string; - verifySsl: boolean = true; + verifySsl?: boolean = true; }; schemaRegistry?: string; schemaRegistryAuth?: { - username: string; - password: string; + username?: string; + password?: string; }; schemaRegistrySsl?: { - keystoreLocation: string; - keystorePassword: string; + keystoreLocation?: string; + keystorePassword?: string; }; ksqldbServer?: string; ksqldbServerSsl?: { - keystoreLocation: string; - keystorePassword: string; + keystoreLocation?: string; + keystorePassword?: string; }; ksqldbServerAuth?: { - username: string; - password: string; + username?: string; + password?: string; }; kafkaConnect?: { - name: string; - address: string; + name?: string; + address?: string; username?: string; password?: string; keystoreLocation?: string; keystorePassword?: string; }[]; - metrics?: KafkaMetricsConfig; + metrics?: { + type?: string; + port?: int32; + ssl?: boolean; + username?: string; + password?: string; + keystoreLocation?: string; + keystorePassword?: string; + }; properties?: Record; consumerProperties?: Record; producerProperties?: Record; - readOnly: boolean; - serde?: SerdeConfig[]; + readOnly?: boolean; + serde?: { + name?: string; + className?: string; + filePath?: string; + properties?: Record; + topicKeysPattern?: string; + topicValuesPattern?: string; + }[]; defaultKeySerde?: string; defaultValueSerde?: string; - masking?: MaskingConfig[]; + masking?: { + type?: "REMOVE" | "MASK" | "REPLACE"; + fields?: string[]; + fieldsNamePattern?: string; + maskingCharsReplacement?: string[]; + replacement?: string; + topicKeysPattern?: string; + topicValuesPattern?: string; + }[]; pollingThrottleRate?: int64; - audit?: KafkaAuditConfig; + audit?: { + level?: "ALL" | "ALTER_ONLY"; + topic?: string; + auditTopicsPartitions?: int32; + topicAuditEnabled?: boolean; + consoleAuditEnabled?: boolean; + auditTopicProperties?: Record; + }; }[]; }; }; } -model KafkaMetricsConfig { - type: string; - port: int32; - ssl?: boolean; - username?: string; - password?: string; - keystoreLocation?: string; - keystorePassword?: string; -} - -model SerdeConfig { - name: string; - className: string; - filePath?: string; - properties?: Record; - topicKeysPattern?: string; - topicValuesPattern?: string; -} - -model MaskingConfig { - type: MaskType; - fields?: string[]; - fieldsNamePattern?: string; - maskingCharsReplacement?: string[]; - replacement?: string; - topicKeysPattern?: string; - topicValuesPattern?: string; -} - -enum MaskType { - REMOVE, - MASK, - REPLACE, -} - -model KafkaAuditConfig { - level: AuditLevel; - topic?: string; - auditTopicsPartitions?: int32; - topicAuditEnabled?: boolean; - consoleAuditEnabled?: boolean; - auditTopicProperties?: Record; -} - -enum AuditLevel { - ALL, - ALTER_ONLY, -} - - model ApplicationConfigValidation { clusters?: Record; } diff --git a/contract-typespec/api/consumer-groups.tsp b/contract-typespec/api/consumer-groups.tsp index e8929f721..49cb13284 100644 --- a/contract-typespec/api/consumer-groups.tsp +++ b/contract-typespec/api/consumer-groups.tsp @@ -75,6 +75,8 @@ enum ConsumerGroupState { @discriminator("inherit") model ConsumerGroup { + @invisible(Lifecycle) + inherit: string; groupId: string; members?: int32; topics?: int32; @@ -132,8 +134,6 @@ model ConsumerGroupTopicPartition { host?: string; } - - model PartitionOffset { partition: int32; offset?: int64; diff --git a/contract-typespec/api/kafka-connect.tsp b/contract-typespec/api/kafka-connect.tsp index 1c21d9234..d64219b82 100644 --- a/contract-typespec/api/kafka-connect.tsp +++ b/contract-typespec/api/kafka-connect.tsp @@ -196,10 +196,11 @@ enum ConnectorState { model ConnectorStatus { state: ConnectorState; - worker_id: string; + worker_id?: string; } -model Connector extends NewConnector { +model Connector { + ...NewConnector; tasks?: TaskId[]; type: ConnectorType; status: ConnectorStatus; diff --git a/contract-typespec/api/main.tsp b/contract-typespec/api/main.tsp index 440ec454b..c0e566153 100644 --- a/contract-typespec/api/main.tsp +++ b/contract-typespec/api/main.tsp @@ -11,9 +11,14 @@ import "./acls.tsp"; import "./quotas.tsp"; import "./auth.tsp"; import "./config.tsp"; + +import "@typespec/http"; +import "@typespec/rest"; import "@typespec/openapi"; using TypeSpec.OpenAPI; +using Http; +using Rest; @service(#{ title: "Kafbat UI Api Service" }) @info(#{ @@ -21,5 +26,6 @@ using TypeSpec.OpenAPI; license: #{ name: "Apache 2.0", url: "https://www.apache.org/licenses/LICENSE-2.0.html" }, version: "0.2.0" }) +@server("http://localhost:8080", "Default endpoint for Kafbat UI API") namespace Api; diff --git a/contract-typespec/api/messages.tsp b/contract-typespec/api/messages.tsp index e26a8952d..59dc23a84 100644 --- a/contract-typespec/api/messages.tsp +++ b/contract-typespec/api/messages.tsp @@ -92,8 +92,8 @@ interface SmartFiltersTestExecutionsApi { } model TopicSerdeSuggestion { - key: SerdeDescription[]; - value: SerdeDescription[]; + key?: SerdeDescription[]; + value?: SerdeDescription[]; } enum SerdeUsage { @@ -223,7 +223,7 @@ enum MessageFormat { model SerdeDescription { - name: string; + name?: string; description?: string; @doc("This serde was automatically chosen by cluster config. This should be enabled in UI by default. Also it will be used for deserialization if no serdes passed.") diff --git a/contract-typespec/api/schemas.tsp b/contract-typespec/api/schemas.tsp index 0b7922703..a935edddc 100644 --- a/contract-typespec/api/schemas.tsp +++ b/contract-typespec/api/schemas.tsp @@ -123,9 +123,9 @@ enum SchemaType { } model SchemaSubject { + id: int32; subject: string; version: string; - id: int32; schema: string; compatibilityLevel: string; schemaType: SchemaType; diff --git a/contract-typespec/api/topics.tsp b/contract-typespec/api/topics.tsp index 8c0439ee5..8a50e46f6 100644 --- a/contract-typespec/api/topics.tsp +++ b/contract-typespec/api/topics.tsp @@ -132,8 +132,8 @@ interface TopicsApi { } model TopicsResponse { - pageCount: int32; - topics: Topic[]; + pageCount?: int32; + topics?: Topic[]; } enum TopicColumnsToSort { @@ -146,18 +146,18 @@ enum TopicColumnsToSort { model Topic { name: string; - internal: boolean; - partitionCount: int32; - replicationFactor: int32; - replicas: int32; - inSyncReplicas: int32; - segmentSize: int64; - segmentCount: int32; - bytesInPerSec: float64; - bytesOutPerSec: float64; - underReplicatedPartitions: int32; - cleanUpPolicy: CleanUpPolicy; - partitions: Partition[]; + internal?: boolean; + partitionCount?: int32; + replicationFactor?: int32; + replicas?: int32; + inSyncReplicas?: int32; + segmentSize?: int64; + segmentCount?: int32; + bytesInPerSec?: float64; + bytesOutPerSec?: float64; + underReplicatedPartitions?: int32; + cleanUpPolicy?: CleanUpPolicy; + partitions?: Partition[]; } model TopicUpdate { @@ -170,51 +170,51 @@ model TopicAnalysis { } model TopicAnalysisProgress { - startedAt: int64; - completenessPercent: decimal; - msgsScanned: int64; - bytesScanned: int64; + startedAt?: int64; + completenessPercent?: decimal; + msgsScanned?: int64; + bytesScanned?: int64; } model TopicAnalysisResult { - startedAt: int64; - finishedAt: int64; - error: string; - totalStats: TopicAnalysisStats; - partitionStats: TopicAnalysisStats[]; + startedAt?: int64; + finishedAt?: int64; + error?: string; + totalStats?: TopicAnalysisStats; + partitionStats?: TopicAnalysisStats[]; } model TopicAnalysisStats { @doc("null if this is total stats") partition?: int32; - totalMsgs: int64; - minOffset: int64; - maxOffset: int64; - minTimestamp: int64; - maxTimestamp: int64; - nullKeys: int64; - nullValues: int64; - approxUniqKeys: int64; - approxUniqValues: int64; - keySize: TopicAnalysisSizeStats; - valueSize: TopicAnalysisSizeStats; - hourlyMsgCounts: { - hourStart: int64; - count: int64; + totalMsgs?: int64; + minOffset?: int64; + maxOffset?: int64; + minTimestamp?: int64; + maxTimestamp?: int64; + nullKeys?: int64; + nullValues?: int64; + approxUniqKeys?: int64; + approxUniqValues?: int64; + keySize?: TopicAnalysisSizeStats; + valueSize?: TopicAnalysisSizeStats; + hourlyMsgCounts?: { + hourStart?: int64; + count?: int64; }[]; } model TopicAnalysisSizeStats { - sum: int64; - min: int64; - max: int64; - avg: int64; - prctl50: int64; - prctl75: int64; - prctl95: int64; - prctl99: int64; - prctl999: int64; + sum?: int64; + min?: int64; + max?: int64; + avg?: int64; + prctl50?: int64; + prctl75?: int64; + prctl95?: int64; + prctl99?: int64; + prctl999?: int64; } model TopicProducerState { @@ -229,38 +229,38 @@ model TopicProducerState { model TopicDetails { name: string; - internal: boolean; - partitions: Partition[]; - partitionCount: int32; - replicationFactor: int32; - replicas: int32; - inSyncReplicas: int32; - bytesInPerSec: float64; - bytesOutPerSec: float64; - segmentSize: int64; - segmentCount: int32; - underReplicatedPartitions: int32; - cleanUpPolicy: CleanUpPolicy; - keySerde: string; - valueSerde: string; + internal?: boolean; + partitions?: Partition[]; + partitionCount?: int32; + replicationFactor?: int32; + replicas?: int32; + inSyncReplicas?: int32; + bytesInPerSec?: float64; + bytesOutPerSec?: float64; + segmentSize?: int64; + segmentCount?: int32; + underReplicatedPartitions?: int32; + cleanUpPolicy?: CleanUpPolicy; + keySerde?: string; + valueSerde?: string; } model TopicConfig { name: string; - value: string; - defaultValue: string; - source: ConfigSource; - isSensitive: boolean; - isReadOnly: boolean; - synonyms: ConfigSynonym[]; - doc: string; + value?: string; + defaultValue?: string; + source?: ConfigSource; + isSensitive?: boolean; + isReadOnly?: boolean; + synonyms?: ConfigSynonym[]; + doc?: string; } model TopicCreation { name: string; partitions: int32; - replicationFactor: int32; - configs: Record; + replicationFactor?: int32; + configs?: Record; } enum CleanUpPolicy { @@ -305,7 +305,7 @@ model ReplicationFactorChangeResponse { } model Replica { - broker: int32; - leader: boolean; - inSync: boolean; + broker?: int32; + leader?: boolean; + inSync?: boolean; } diff --git a/frontend/src/lib/fixtures/topics.ts b/frontend/src/lib/fixtures/topics.ts index cff119077..f4b50ec39 100644 --- a/frontend/src/lib/fixtures/topics.ts +++ b/frontend/src/lib/fixtures/topics.ts @@ -64,6 +64,7 @@ export const topicConsumerGroups: ConsumerGroup[] = [ state: ConsumerGroupState.UNKNOWN, coordinator: { id: 1 }, consumerLag: 9, + inherit: "" }, { groupId: 'amazon.msk.canary.group.broker-4', @@ -74,6 +75,7 @@ export const topicConsumerGroups: ConsumerGroup[] = [ state: ConsumerGroupState.COMPLETING_REBALANCE, coordinator: { id: 1 }, consumerLag: 9, + inherit: "" }, ]; From d0ef634077cfd5fe5f1a085705973a4efd9f9d2c Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 12 Jun 2025 13:41:55 +0200 Subject: [PATCH 08/36] use typespec build --- contract-typespec/api/package.json | 3 +++ frontend/package.json | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/contract-typespec/api/package.json b/contract-typespec/api/package.json index afd10e844..d67b17407 100644 --- a/contract-typespec/api/package.json +++ b/contract-typespec/api/package.json @@ -11,5 +11,8 @@ "@typespec/sse": "^0.70.0", "build": "^0.1.4" }, + "scripts": { + "build": "tsp compile ." + }, "private": true } diff --git a/frontend/package.json b/frontend/package.json index b150467db..ff7d7bfb3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,8 +40,8 @@ "start": "vite", "dev": "vite", "compile": "pnpm gen:sources && tsc --noEmit", - "gen:sources": "rimraf ./src/generated-sources && openapi-generator-cli generate", - "build": "rimraf ./src/generated-sources && openapi-generator-cli generate && tsc --noEmit && vite build", + "gen:sources": "rimraf ./src/generated-sources && cd ../contract-typespec/api && pnpm build && cd ../../frontend && openapi-generator-cli generate", + "build": "pnpm gen:sources && tsc --noEmit && vite build", "preview": "vite preview", "lint": "eslint --ext .tsx,.ts src/", "lint:fix": "eslint --ext .tsx,.ts src/ --fix", From c29091f973b972955b31d9cfbd2731454041e64e Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 12 Jun 2025 13:55:09 +0200 Subject: [PATCH 09/36] Install pnpm dependencies --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index ff7d7bfb3..0a0d722a6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,7 +40,7 @@ "start": "vite", "dev": "vite", "compile": "pnpm gen:sources && tsc --noEmit", - "gen:sources": "rimraf ./src/generated-sources && cd ../contract-typespec/api && pnpm build && cd ../../frontend && openapi-generator-cli generate", + "gen:sources": "rimraf ./src/generated-sources && cd ../contract-typespec/api && pnpm install && pnpm build && cd ../../frontend && openapi-generator-cli generate", "build": "pnpm gen:sources && tsc --noEmit && vite build", "preview": "vite preview", "lint": "eslint --ext .tsx,.ts src/", From f114d4493c0969e4beb9edc474826fb91b09e891 Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 15:50:09 +0300 Subject: [PATCH 10/36] Actualize typespec --- contract-typespec/api/acls.tsp | 1 + contract-typespec/api/kafka-connect.tsp | 29 +++++++++++++++---------- gradle/libs.versions.toml | 3 +++ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/contract-typespec/api/acls.tsp b/contract-typespec/api/acls.tsp index 6c009edd6..aaeccf4a4 100644 --- a/contract-typespec/api/acls.tsp +++ b/contract-typespec/api/acls.tsp @@ -16,6 +16,7 @@ interface AclApi { @query resourceType?: KafkaAclResourceType, @query resourceName?: string, @query namePatternType?: KafkaAclNamePatternType, + @query search?: string ): KafkaAcl[]; @route("/csv") diff --git a/contract-typespec/api/kafka-connect.tsp b/contract-typespec/api/kafka-connect.tsp index d64219b82..ad0dc9e49 100644 --- a/contract-typespec/api/kafka-connect.tsp +++ b/contract-typespec/api/kafka-connect.tsp @@ -11,7 +11,7 @@ using OpenAPI; interface ConnectInstancesApi { @get @operationId("getConnects") - getConnects(@path clusterName: string): Connect[]; + getConnects(@path clusterName: string, @query withStats: boolean): Connect[]; @get @route("/{connectName}/plugins") @@ -144,6 +144,13 @@ interface KafkaConnectConnectorsApi { model Connect { name: string; address?: string; + connectorsCount?: int32 | null; + failedConnectorsCount?: int32 | null; + tasksCount?: int32 | null; + failedTasksCount?: int32 | null; + version?: string | null; + commit?: string | null; + clusterId?: string | null; } model ConnectorConfig is Record; @@ -156,7 +163,7 @@ model TaskId { model TaskStatus { id: int32; state: ConnectorTaskStatus; - worker_id: string; + workerId: string; trace?: string; } @@ -196,7 +203,7 @@ enum ConnectorState { model ConnectorStatus { state: ConnectorState; - worker_id?: string; + workerId?: string; } model Connector { @@ -213,7 +220,7 @@ enum ConnectorAction { RESTART_FAILED_TASKS, PAUSE, RESUME, - STOP, + STOP } enum TaskAction { @@ -237,12 +244,12 @@ model ConnectorPluginConfigDefinition { | "SHORT" | "STRING"; required?: boolean; - default_value?: string; + defaultValue?: string; importance?: "LOW" | "MEDIUM" | "HIGH"; documentation?: string; group?: string; width?: "SHORT" | "MEDIUM" | "LONG" | "NONE"; - display_name?: string; + displayName?: string; dependents?: string[]; order?: int32; } @@ -250,7 +257,7 @@ model ConnectorPluginConfigDefinition { model ConnectorPluginConfigValue { name?: string; value?: string; - recommended_values?: string[]; + recommendedValues?: string[]; errors?: string[]; visible?: boolean; } @@ -262,7 +269,7 @@ model ConnectorPluginConfig { model ConnectorPluginConfigValidationResponse { name?: string; - error_count?: int32; + errorCount?: int32; groups?: string[]; configs?: ConnectorPluginConfig[]; } @@ -270,12 +277,12 @@ model ConnectorPluginConfigValidationResponse { model FullConnectorInfo { connect: string; name: string; - connector_class?: string; + connectorClass?: string; type?: ConnectorType; topics?: string[]; status: ConnectorStatus; - tasks_count?: integer; - failed_tasks_count?: integer; + tasksCount?: integer; + failedTasksCount?: integer; } enum ConnectorColumnsToSort { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 728ea00f6..a4ceaece5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -140,3 +140,6 @@ google-oauth-client = { module = 'com.google.oauth-client:google-oauth-client', modelcontextprotocol-spring-webflux = {module = 'io.modelcontextprotocol.sdk:mcp-spring-webflux', version = '0.10.0'} victools-jsonschema-generator = {module = 'com.github.victools:jsonschema-generator', version = '4.38.0'} + +# CVE fixes +reactor-netty-http = {module = 'io.projectreactor.netty:reactor-netty-http', version = '1.2.8'} From 4279d6ce22222d5d575d638e239550658233c5e1 Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 15:53:03 +0300 Subject: [PATCH 11/36] Actualize typespec --- contract-typespec/api/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/contract-typespec/api/package.json b/contract-typespec/api/package.json index d67b17407..e6b91b178 100644 --- a/contract-typespec/api/package.json +++ b/contract-typespec/api/package.json @@ -9,6 +9,7 @@ "@typespec/openapi3": "^1.0.0", "@typespec/rest": "^0.70.0", "@typespec/sse": "^0.70.0", + "@typespec/streams": "^0.70.0", "build": "^0.1.4" }, "scripts": { From a1c261b2d58ba85a0695c8d24a5c89570800e0d2 Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 16:05:22 +0300 Subject: [PATCH 12/36] fixed frontend build --- contract-typespec/api/package.json | 17 +++++++++-------- contract-typespec/api/tspconfig.yaml | 2 ++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/contract-typespec/api/package.json b/contract-typespec/api/package.json index e6b91b178..f865cd1e9 100644 --- a/contract-typespec/api/package.json +++ b/contract-typespec/api/package.json @@ -3,17 +3,18 @@ "version": "0.1.0", "type": "module", "dependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.1", - "@typespec/openapi": "^1.0.0", - "@typespec/openapi3": "^1.0.0", - "@typespec/rest": "^0.70.0", - "@typespec/sse": "^0.70.0", - "@typespec/streams": "^0.70.0", + "@typespec/compiler": "^1.3.0", + "@typespec/http": "^1.3.1", + "@typespec/openapi": "^1.3.0", + "@typespec/openapi3": "^1.3.0", + "@typespec/rest": "^0.73.0", + "@typespec/sse": "^0.73.0", + "@typespec/streams": "^0.73.0", "build": "^0.1.4" }, "scripts": { "build": "tsp compile ." }, - "private": true + "private": true, + "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531" } diff --git a/contract-typespec/api/tspconfig.yaml b/contract-typespec/api/tspconfig.yaml index e84c6b273..f0b1f2718 100644 --- a/contract-typespec/api/tspconfig.yaml +++ b/contract-typespec/api/tspconfig.yaml @@ -1,3 +1,5 @@ +output-dir: "{project-root}/../build/tsp/api" + emit: - "@typespec/openapi3" options: From ea75152389d1752f6d9d13c3847d65a97dd5da39 Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 16:06:59 +0300 Subject: [PATCH 13/36] fixed frontend build --- contract-typespec/api/kafka-connect.tsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-typespec/api/kafka-connect.tsp b/contract-typespec/api/kafka-connect.tsp index ad0dc9e49..194f5c45b 100644 --- a/contract-typespec/api/kafka-connect.tsp +++ b/contract-typespec/api/kafka-connect.tsp @@ -11,7 +11,7 @@ using OpenAPI; interface ConnectInstancesApi { @get @operationId("getConnects") - getConnects(@path clusterName: string, @query withStats: boolean): Connect[]; + getConnects(@path clusterName: string, @query withStats?: boolean): Connect[]; @get @route("/{connectName}/plugins") From 5341c20ddb5b3c1b59c1f049c98502a6dc38e3d7 Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 16:08:55 +0300 Subject: [PATCH 14/36] fixed frontend build --- contract-typespec/api/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contract-typespec/api/package.json b/contract-typespec/api/package.json index f865cd1e9..ba38d898e 100644 --- a/contract-typespec/api/package.json +++ b/contract-typespec/api/package.json @@ -3,13 +3,13 @@ "version": "0.1.0", "type": "module", "dependencies": { - "@typespec/compiler": "^1.3.0", - "@typespec/http": "^1.3.1", - "@typespec/openapi": "^1.3.0", - "@typespec/openapi3": "^1.3.0", - "@typespec/rest": "^0.73.0", - "@typespec/sse": "^0.73.0", - "@typespec/streams": "^0.73.0", + "@typespec/compiler": "1.3.0", + "@typespec/http": "1.3.0", + "@typespec/openapi": "1.3.0", + "@typespec/openapi3": "1.3.0", + "@typespec/rest": "0.73.0", + "@typespec/sse": "0.73.0", + "@typespec/streams": "0.73.0", "build": "^0.1.4" }, "scripts": { From c61da13ca8c873f89da47e4d1f8b0ce71c27f7ca Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 16:13:58 +0300 Subject: [PATCH 15/36] fixed frontend build --- frontend/src/lib/fixtures/topics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/fixtures/topics.ts b/frontend/src/lib/fixtures/topics.ts index f4b50ec39..b1f056054 100644 --- a/frontend/src/lib/fixtures/topics.ts +++ b/frontend/src/lib/fixtures/topics.ts @@ -64,7 +64,7 @@ export const topicConsumerGroups: ConsumerGroup[] = [ state: ConsumerGroupState.UNKNOWN, coordinator: { id: 1 }, consumerLag: 9, - inherit: "" + inherit: '' }, { groupId: 'amazon.msk.canary.group.broker-4', @@ -75,7 +75,7 @@ export const topicConsumerGroups: ConsumerGroup[] = [ state: ConsumerGroupState.COMPLETING_REBALANCE, coordinator: { id: 1 }, consumerLag: 9, - inherit: "" + inherit: '' }, ]; From 2c670f6567164f1270db3afa37c5099648eafd0d Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 16:17:28 +0300 Subject: [PATCH 16/36] fixed frontend build --- frontend/src/lib/fixtures/topics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/lib/fixtures/topics.ts b/frontend/src/lib/fixtures/topics.ts index b1f056054..ec8803c4b 100644 --- a/frontend/src/lib/fixtures/topics.ts +++ b/frontend/src/lib/fixtures/topics.ts @@ -64,7 +64,7 @@ export const topicConsumerGroups: ConsumerGroup[] = [ state: ConsumerGroupState.UNKNOWN, coordinator: { id: 1 }, consumerLag: 9, - inherit: '' + inherit: '', }, { groupId: 'amazon.msk.canary.group.broker-4', @@ -75,7 +75,7 @@ export const topicConsumerGroups: ConsumerGroup[] = [ state: ConsumerGroupState.COMPLETING_REBALANCE, coordinator: { id: 1 }, consumerLag: 9, - inherit: '' + inherit: '', }, ]; From 4bf37794a822e646dcd54852143d8cb68e8cf12a Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 16:50:42 +0300 Subject: [PATCH 17/36] fixed frontend build --- contract-typespec/api/pnpm-lock.yaml | 1388 +++++++++++++++++ frontend/package.json | 3 +- .../hooks/__tests__/dateTimeHelpers.spec.ts | 5 +- .../hooks/api/__tests__/topicMessages.spec.ts | 2 +- frontend/test-report.xml | 958 ++++++++++++ 5 files changed, 2352 insertions(+), 4 deletions(-) create mode 100644 contract-typespec/api/pnpm-lock.yaml create mode 100644 frontend/test-report.xml diff --git a/contract-typespec/api/pnpm-lock.yaml b/contract-typespec/api/pnpm-lock.yaml new file mode 100644 index 000000000..5386cf161 --- /dev/null +++ b/contract-typespec/api/pnpm-lock.yaml @@ -0,0 +1,1388 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@typespec/compiler': + specifier: 1.3.0 + version: 1.3.0 + '@typespec/http': + specifier: 1.3.0 + version: 1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)) + '@typespec/openapi': + specifier: 1.3.0 + version: 1.3.0(@typespec/compiler@1.3.0)(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0))) + '@typespec/openapi3': + specifier: 1.3.0 + version: 1.3.0(@typespec/compiler@1.3.0)(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)))(@typespec/openapi@1.3.0(@typespec/compiler@1.3.0)(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)))) + '@typespec/rest': + specifier: 0.73.0 + version: 0.73.0(@typespec/compiler@1.3.0)(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0))) + '@typespec/sse': + specifier: 0.73.0 + version: 0.73.0(@typespec/compiler@1.3.0)(@typespec/events@0.70.0(@typespec/compiler@1.3.0))(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)))(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)) + '@typespec/streams': + specifier: 0.73.0 + version: 0.73.0(@typespec/compiler@1.3.0) + build: + specifier: ^0.1.4 + version: 0.1.4 + +packages: + + '@apidevtools/json-schema-ref-parser@14.0.1': + resolution: {integrity: sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==} + engines: {node: '>= 16'} + + '@apidevtools/openapi-schemas@2.1.0': + resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} + engines: {node: '>=10'} + + '@apidevtools/swagger-methods@3.0.2': + resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} + + '@apidevtools/swagger-parser@12.0.0': + resolution: {integrity: sha512-WLJIWcfOXrSKlZEM+yhA2Xzatgl488qr1FoOxixYmtWapBzwSC0gVGq4WObr4hHClMIiFFdOBdixNkvWqkWIWA==} + peerDependencies: + openapi-types: '>=7' + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@inquirer/checkbox@4.1.8': + resolution: {integrity: sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.12': + resolution: {integrity: sha512-dpq+ielV9/bqgXRUbNH//KsY6WEw9DrGPmipkpmgC1Y46cwuBTNx7PXFWTjc3MQ+urcc0QxoVHcMI0FW4Ok0hg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.1.13': + resolution: {integrity: sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.13': + resolution: {integrity: sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.15': + resolution: {integrity: sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.12': + resolution: {integrity: sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==} + engines: {node: '>=18'} + + '@inquirer/input@4.1.12': + resolution: {integrity: sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.15': + resolution: {integrity: sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.15': + resolution: {integrity: sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.5.3': + resolution: {integrity: sha512-8YL0WiV7J86hVAxrh3fE5mDCzcTDe1670unmJRz6ArDgN+DBK1a0+rbnNWp4DUB5rPMwqD5ZP6YHl9KK1mbZRg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.3': + resolution: {integrity: sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.0.15': + resolution: {integrity: sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.2.3': + resolution: {integrity: sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.7': + resolution: {integrity: sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@sindresorhus/merge-streams@2.3.0': + resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} + engines: {node: '>=18'} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@typespec/asset-emitter@0.73.0': + resolution: {integrity: sha512-SigCa9k8gS+AiHE7Ky/kcwyqFM5kuJ0wXT+Dy89Jbd+wwrYu+mKXyXbScrTdc+MBzut+rFltFENgEYXsSvA/mA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@typespec/compiler': ^1.3.0 + + '@typespec/compiler@1.3.0': + resolution: {integrity: sha512-OqpoNP3C2y8riA6C5RofPMvmj9jNiGyyhde0tM2ZE7IBOv7BBaTDqw4CJD22YnC8JEilRfPmvdVCViNrPHEjrA==} + engines: {node: '>=20.0.0'} + hasBin: true + + '@typespec/events@0.70.0': + resolution: {integrity: sha512-qHW1N05n8PkNf2YQGNMdl/sAYqrJv+zQ1kny+3vg/20nzVj7sZpNFIKqUIc11z0GkT7k3Q9SPTymvq+K00sAUg==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@typespec/compiler': ^1.0.0 + + '@typespec/http@1.3.0': + resolution: {integrity: sha512-4W3KsmBHZGgECVbvyh7S7KQG06948XyVVzae+UbVDDxoUj/x4Ry0AXw3q4HmzB2BVhxw6JBrwBuVa5mxjVMzdw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@typespec/compiler': ^1.3.0 + '@typespec/streams': ^0.73.0 + peerDependenciesMeta: + '@typespec/streams': + optional: true + + '@typespec/openapi3@1.3.0': + resolution: {integrity: sha512-ZG+swQYtdBgyTUbwPI03YQJpPUYhORtbcx6mIFNsKhsTRRC2UDq63jUNCIFCTYI6DJPzkVpra56YPNCXmQLZMg==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@typespec/compiler': ^1.3.0 + '@typespec/http': ^1.3.0 + '@typespec/json-schema': ^1.3.0 + '@typespec/openapi': ^1.3.0 + '@typespec/versioning': ^0.73.0 + '@typespec/xml': '*' + peerDependenciesMeta: + '@typespec/json-schema': + optional: true + '@typespec/versioning': + optional: true + '@typespec/xml': + optional: true + + '@typespec/openapi@1.3.0': + resolution: {integrity: sha512-BSeshjCZQodVGyVHn7ytcUeIcUGjqbG2Ac0NLOQaaKnISVrhTWNcgo5aFTqxAa24ZL/EuhqlSauLyYce2EV9fw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@typespec/compiler': ^1.3.0 + '@typespec/http': ^1.3.0 + + '@typespec/rest@0.73.0': + resolution: {integrity: sha512-28hgFGvreBg34Xuguw+E++pQC/kbRxy1Bpx/9nU7x87Ly6ykns3lpx74gjY9ByE8VYKVbXtC7lzdnp19DRSjIQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@typespec/compiler': ^1.3.0 + '@typespec/http': ^1.3.0 + + '@typespec/sse@0.73.0': + resolution: {integrity: sha512-WTnRJ1b1M3RPzlHxhnK9sh6+AGKPKWpuA0TSAqzyxb/uRHFYLNeoDKPOnlQ749SJ8lJz71Oh0nUsP3vB0EzO6Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@typespec/compiler': ^1.3.0 + '@typespec/events': ^0.73.0 + '@typespec/http': ^1.3.0 + '@typespec/streams': ^0.73.0 + + '@typespec/streams@0.73.0': + resolution: {integrity: sha512-pL4xffHXEIhBQKPlB9L4AKuM0bn44WsGKjnz91wa6wBtP/CbsPrGQicof0Z7GPGdddtDi4G8PWGmJtVFw53V9g==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@typespec/compiler': ^1.3.0 + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + build@0.1.4: + resolution: {integrity: sha512-KwbDJ/zrsU8KZRRMfoURG14cKIAStUlS8D5jBDvtrZbwO5FEkYqc3oB8HIhRiyD64A48w1lc+sOmQ+mmBw5U/Q==} + engines: {node: '>v0.4.12'} + + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + + cssmin@0.3.2: + resolution: {integrity: sha512-bynxGIAJ8ybrnFobjsQotIjA8HFDDgPwbeUWNXXXfR+B4f9kkxdcUyagJoQCSUOfMV+ZZ6bMn8bvbozlCzUGwQ==} + hasBin: true + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@14.1.0: + resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} + engines: {node: '>=18'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@0.3.7: + resolution: {integrity: sha512-/7PsVDNP2tVe2Z1cF9kTEkjamIwz4aooDpRKmN1+g/9eePCgcxsv4QDvEbxO0EH+gdDD7MLyDoR6BASo3hH51g==} + engines: {node: '> 0.4.11'} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsmin@1.0.1: + resolution: {integrity: sha512-OPuL5X/bFKgVdMvEIX3hnpx3jbVpFCrEM8pKPXjFkZUqg521r41ijdyTz7vACOhW6o1neVlcLyd+wkbK5fNHRg==} + engines: {node: '>=0.1.93'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + jxLoader@0.1.1: + resolution: {integrity: sha512-ClEvAj3K68y8uKhub3RgTmcRPo5DfIWvtxqrKQdDPyZ1UVHIIKvVvjrAsJFSVL5wjv0rt5iH9SMCZ0XRKNzeUA==} + engines: {node: '>v0.4.10'} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + moo-server@1.3.0: + resolution: {integrity: sha512-9A8/eor2DXwpv1+a4pZAAydqLFVrWoKoO1fzdzqLUhYVXAO1Kgd1FR2gFZi7YdHzF0s4W8cDNwCfKJQrvLqxDw==} + engines: {node: '>v0.4.10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + path-type@6.0.0: + resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} + engines: {node: '>=18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + promised-io@0.3.6: + resolution: {integrity: sha512-bNwZusuNIW4m0SPR8jooSyndD35ggirHlxVl/UhIaZD/F0OBv9ebfc6tNmbpZts3QXHggkjIBH8lvtnzhtcz0A==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + temporal-polyfill@0.3.0: + resolution: {integrity: sha512-qNsTkX9K8hi+FHDfHmf22e/OGuXmfBm9RqNismxBrnSmZVJKegQ+HYYXT+R7Ha8F/YSm2Y34vmzD4cxMu2u95g==} + + temporal-spec@0.3.0: + resolution: {integrity: sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ==} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + timespan@2.3.0: + resolution: {integrity: sha512-0Jq9+58T2wbOyLth0EU+AUb6JMGCLaTWIykJFa7hyAybjVH9gpVMTfUAwo5fWAvtFt2Tjh/Elg8JtgNpnMnM8g==} + engines: {node: '>= 0.2.0'} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + uglify-js@1.3.5: + resolution: {integrity: sha512-YPX1DjKtom8l9XslmPFQnqWzTBkvI4N0pbkzLuPZZ4QTyig0uQqvZz9NgUdfEV+qccJzi7fVcGWdESvRIjWptQ==} + hasBin: true + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + engines: {node: '>= 12.0.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + wrench@1.3.9: + resolution: {integrity: sha512-srTJQmLTP5YtW+F5zDuqjMEZqLLr/eJOZfDI5ibfPfRMeDh3oBUefAscuH0q5wBKE339ptH/S/0D18ZkfOfmKQ==} + engines: {node: '>=0.1.97'} + deprecated: wrench.js is deprecated! You should check out fs-extra (https://github.com/jprichardson/node-fs-extra) for any operations you were using wrench for. Thanks for all the usage over the years. + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yoctocolors-cjs@2.1.2: + resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} + engines: {node: '>=18'} + +snapshots: + + '@apidevtools/json-schema-ref-parser@14.0.1': + dependencies: + '@types/json-schema': 7.0.15 + js-yaml: 4.1.0 + + '@apidevtools/openapi-schemas@2.1.0': {} + + '@apidevtools/swagger-methods@3.0.2': {} + + '@apidevtools/swagger-parser@12.0.0(openapi-types@12.1.3)': + dependencies: + '@apidevtools/json-schema-ref-parser': 14.0.1 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + ajv: 8.17.1 + ajv-draft-04: 1.0.0(ajv@8.17.1) + call-me-maybe: 1.0.2 + openapi-types: 12.1.3 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.27.1': {} + + '@colors/colors@1.6.0': {} + + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@inquirer/checkbox@4.1.8': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/figures': 1.0.12 + '@inquirer/type': 3.0.7 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + + '@inquirer/confirm@5.1.12': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/type': 3.0.7 + + '@inquirer/core@10.1.13': + dependencies: + '@inquirer/figures': 1.0.12 + '@inquirer/type': 3.0.7 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.2 + + '@inquirer/editor@4.2.13': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/type': 3.0.7 + external-editor: 3.1.0 + + '@inquirer/expand@4.0.15': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/type': 3.0.7 + yoctocolors-cjs: 2.1.2 + + '@inquirer/figures@1.0.12': {} + + '@inquirer/input@4.1.12': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/type': 3.0.7 + + '@inquirer/number@3.0.15': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/type': 3.0.7 + + '@inquirer/password@4.0.15': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/type': 3.0.7 + ansi-escapes: 4.3.2 + + '@inquirer/prompts@7.5.3': + dependencies: + '@inquirer/checkbox': 4.1.8 + '@inquirer/confirm': 5.1.12 + '@inquirer/editor': 4.2.13 + '@inquirer/expand': 4.0.15 + '@inquirer/input': 4.1.12 + '@inquirer/number': 3.0.15 + '@inquirer/password': 4.0.15 + '@inquirer/rawlist': 4.1.3 + '@inquirer/search': 3.0.15 + '@inquirer/select': 4.2.3 + + '@inquirer/rawlist@4.1.3': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/type': 3.0.7 + yoctocolors-cjs: 2.1.2 + + '@inquirer/search@3.0.15': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/figures': 1.0.12 + '@inquirer/type': 3.0.7 + yoctocolors-cjs: 2.1.2 + + '@inquirer/select@4.2.3': + dependencies: + '@inquirer/core': 10.1.13 + '@inquirer/figures': 1.0.12 + '@inquirer/type': 3.0.7 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.2 + + '@inquirer/type@3.0.7': {} + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@sindresorhus/merge-streams@2.3.0': {} + + '@types/json-schema@7.0.15': {} + + '@types/triple-beam@1.3.5': {} + + '@typespec/asset-emitter@0.73.0(@typespec/compiler@1.3.0)': + dependencies: + '@typespec/compiler': 1.3.0 + + '@typespec/compiler@1.3.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@inquirer/prompts': 7.5.3 + ajv: 8.17.1 + change-case: 5.4.4 + env-paths: 3.0.0 + globby: 14.1.0 + is-unicode-supported: 2.1.0 + mustache: 4.2.0 + picocolors: 1.1.1 + prettier: 3.6.2 + semver: 7.7.2 + tar: 7.4.3 + temporal-polyfill: 0.3.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + yaml: 2.8.1 + yargs: 18.0.0 + transitivePeerDependencies: + - '@types/node' + + '@typespec/events@0.70.0(@typespec/compiler@1.3.0)': + dependencies: + '@typespec/compiler': 1.3.0 + + '@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0))': + dependencies: + '@typespec/compiler': 1.3.0 + optionalDependencies: + '@typespec/streams': 0.73.0(@typespec/compiler@1.3.0) + + '@typespec/openapi3@1.3.0(@typespec/compiler@1.3.0)(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)))(@typespec/openapi@1.3.0(@typespec/compiler@1.3.0)(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0))))': + dependencies: + '@apidevtools/swagger-parser': 12.0.0(openapi-types@12.1.3) + '@typespec/asset-emitter': 0.73.0(@typespec/compiler@1.3.0) + '@typespec/compiler': 1.3.0 + '@typespec/http': 1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)) + '@typespec/openapi': 1.3.0(@typespec/compiler@1.3.0)(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0))) + openapi-types: 12.1.3 + yaml: 2.8.1 + + '@typespec/openapi@1.3.0(@typespec/compiler@1.3.0)(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)))': + dependencies: + '@typespec/compiler': 1.3.0 + '@typespec/http': 1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)) + + '@typespec/rest@0.73.0(@typespec/compiler@1.3.0)(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)))': + dependencies: + '@typespec/compiler': 1.3.0 + '@typespec/http': 1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)) + + '@typespec/sse@0.73.0(@typespec/compiler@1.3.0)(@typespec/events@0.70.0(@typespec/compiler@1.3.0))(@typespec/http@1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)))(@typespec/streams@0.73.0(@typespec/compiler@1.3.0))': + dependencies: + '@typespec/compiler': 1.3.0 + '@typespec/events': 0.70.0(@typespec/compiler@1.3.0) + '@typespec/http': 1.3.0(@typespec/compiler@1.3.0)(@typespec/streams@0.73.0(@typespec/compiler@1.3.0)) + '@typespec/streams': 0.73.0(@typespec/compiler@1.3.0) + + '@typespec/streams@0.73.0(@typespec/compiler@1.3.0)': + dependencies: + '@typespec/compiler': 1.3.0 + + ajv-draft-04@1.0.0(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + argparse@2.0.1: {} + + async@3.2.6: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + build@0.1.4: + dependencies: + cssmin: 0.3.2 + jsmin: 1.0.1 + jxLoader: 0.1.1 + moo-server: 1.3.0 + promised-io: 0.3.6 + timespan: 2.3.0 + uglify-js: 1.3.5 + walker: 1.0.8 + winston: 3.17.0 + wrench: 1.3.9 + + call-me-maybe@1.0.2: {} + + change-case@5.4.4: {} + + chardet@0.7.0: {} + + chownr@3.0.0: {} + + cli-width@4.1.0: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + + cssmin@0.3.2: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + enabled@2.0.0: {} + + env-paths@3.0.0: {} + + escalade@3.2.0: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.0.6: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fecha@4.2.3: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fn.name@1.1.0: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@14.1.0: + dependencies: + '@sindresorhus/merge-streams': 2.3.0 + fast-glob: 3.3.3 + ignore: 7.0.5 + path-type: 6.0.0 + slash: 5.1.0 + unicorn-magic: 0.3.0 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + ignore@7.0.5: {} + + inherits@2.0.4: {} + + is-arrayish@0.3.2: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@2.1.0: {} + + js-tokens@4.0.0: {} + + js-yaml@0.3.7: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsmin@1.0.1: {} + + json-schema-traverse@1.0.0: {} + + jxLoader@0.1.1: + dependencies: + js-yaml: 0.3.7 + moo-server: 1.3.0 + promised-io: 0.3.6 + walker: 1.0.8 + + kuler@2.0.0: {} + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + moo-server@1.3.0: {} + + ms@2.1.3: {} + + mustache@4.2.0: {} + + mute-stream@2.0.0: {} + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + openapi-types@12.1.3: {} + + os-tmpdir@1.0.2: {} + + path-type@6.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + prettier@3.6.2: {} + + promised-io@0.3.6: {} + + queue-microtask@1.2.3: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + require-from-string@2.0.2: {} + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + semver@7.7.2: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + slash@5.1.0: {} + + stack-trace@0.0.10: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + temporal-polyfill@0.3.0: + dependencies: + temporal-spec: 0.3.0 + + temporal-spec@0.3.0: {} + + text-hex@1.0.0: {} + + timespan@2.3.0: {} + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + triple-beam@1.4.1: {} + + type-fest@0.21.3: {} + + uglify-js@1.3.5: {} + + unicorn-magic@0.3.0: {} + + util-deprecate@1.0.2: {} + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.17.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + wrench@1.3.9: {} + + y18n@5.0.8: {} + + yallist@5.0.0: {} + + yaml@2.8.1: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yoctocolors-cjs@2.1.2: {} diff --git a/frontend/package.json b/frontend/package.json index 0a0d722a6..41d33a722 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -121,5 +121,6 @@ "axios@>=1.3.2 <=1.7.3": ">=1.7.4", "braces": "3.0.3" } - } + }, + "packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531" } diff --git a/frontend/src/lib/hooks/__tests__/dateTimeHelpers.spec.ts b/frontend/src/lib/hooks/__tests__/dateTimeHelpers.spec.ts index 61e188f29..d3558c94d 100644 --- a/frontend/src/lib/hooks/__tests__/dateTimeHelpers.spec.ts +++ b/frontend/src/lib/hooks/__tests__/dateTimeHelpers.spec.ts @@ -11,12 +11,13 @@ describe('dateTimeHelpers', () => { }); it('should output the correct date', () => { + const language = navigator.language || navigator.languages[0]; const date = new Date(); expect(formatTimestamp({ timestamp: date })).toBe( - date.toLocaleString([], { hourCycle: 'h23' }) + date.toLocaleString(language || [], { hourCycle: 'h23' }) ); expect(formatTimestamp({ timestamp: date.getTime() })).toBe( - date.toLocaleString([], { hourCycle: 'h23' }) + date.toLocaleString(language || [], { hourCycle: 'h23' }) ); }); }); diff --git a/frontend/src/lib/hooks/api/__tests__/topicMessages.spec.ts b/frontend/src/lib/hooks/api/__tests__/topicMessages.spec.ts index 49a143f22..134ff9e8e 100644 --- a/frontend/src/lib/hooks/api/__tests__/topicMessages.spec.ts +++ b/frontend/src/lib/hooks/api/__tests__/topicMessages.spec.ts @@ -25,7 +25,7 @@ jest.mock('lib/errorHandling', () => ({ describe('Topic Messages hooks', () => { beforeEach(() => fetchMock.restore()); it('handles useSerdes', async () => { - const path = `/api/clusters/${clusterName}/topic/${topicName}/serdes?use=SERIALIZE`; + const path = `/api/clusters/${clusterName}/topics/${topicName}/serdes?use=SERIALIZE`; const mock = fetchMock.getOnce(path, {}); const { result } = renderQueryHook(() => diff --git a/frontend/test-report.xml b/frontend/test-report.xml new file mode 100644 index 000000000..16769f955 --- /dev/null +++ b/frontend/test-report.xml @@ -0,0 +1,958 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From fa8b03b1948258c8bf12165eb7d1c4d07ce01e50 Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 16:56:52 +0300 Subject: [PATCH 18/36] fixed frontend build --- contract-typespec/api/acls.tsp | 2 +- contract/build.gradle | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contract-typespec/api/acls.tsp b/contract-typespec/api/acls.tsp index aaeccf4a4..717fffaf3 100644 --- a/contract-typespec/api/acls.tsp +++ b/contract-typespec/api/acls.tsp @@ -62,7 +62,7 @@ interface AclApi { @body payload: CreateProducerAcl, ): void | ApiBadRequestResponse; - @route("/streamApp") + @route("/streamapp") @doc("createStreamAppAcl") @post @operationId("createStreamAppAcl") diff --git a/contract/build.gradle b/contract/build.gradle index 0663d1d42..6e90848be 100644 --- a/contract/build.gradle +++ b/contract/build.gradle @@ -72,7 +72,11 @@ tasks.register('generateBackendApi', GenerateTask) { } openAPIStyleValidator { - inputFile = "${project.projectDir}/src/main/resources/swagger/kafbat-ui-api.yaml" + if (useTypeSpec) { + inputFile = project(":contract-typespec").layout.buildDirectory.dir("tsp/api/openapi.yaml").get().asFile.absolutePath + } else { + inputFile = specDir.file("kafbat-ui-api.yaml").asFile.absolutePath + } validateModelPropertiesDescription = false validateModelPropertiesExample = false parameterNamingConvention = "UnderscoreCase" From bcae096d23e07061f9504826ee45fdb92ce1e9ae Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 17:17:01 +0300 Subject: [PATCH 19/36] Enabled contract validation --- contract-typespec/api/acls.tsp | 17 +- contract-typespec/api/auth.tsp | 4 +- contract-typespec/api/brokers.tsp | 12 +- contract-typespec/api/clusters.tsp | 8 +- contract-typespec/api/config.tsp | 12 +- contract-typespec/api/consumer-groups.tsp | 6 + contract-typespec/api/kafka-connect.tsp | 16 +- contract-typespec/api/ksql.tsp | 4 + contract-typespec/api/messages.tsp | 7 + contract-typespec/api/package-lock.json | 344 ++++++++++++++-------- contract-typespec/api/quotas.tsp | 4 +- contract-typespec/api/schemas.tsp | 12 + contract-typespec/api/topics.tsp | 15 +- 13 files changed, 311 insertions(+), 150 deletions(-) diff --git a/contract-typespec/api/acls.tsp b/contract-typespec/api/acls.tsp index 717fffaf3..a56ddea5c 100644 --- a/contract-typespec/api/acls.tsp +++ b/contract-typespec/api/acls.tsp @@ -8,7 +8,7 @@ using OpenAPI; @route("/api/clusters/{clusterName}/acls") @tag("Acls") interface AclApi { - @doc("listKafkaAcls") + @summary("listKafkaAcls") @get @operationId("listAcls") listAcls( @@ -20,41 +20,42 @@ interface AclApi { ): KafkaAcl[]; @route("/csv") - @doc("getAclAsCsv") + @summary("getAclAsCsv") @get @operationId("getAclAsCsv") getAclAsCsv(@path clusterName: string): string; @route("/csv") - @doc("syncAclsCsv") + @summary("syncAclsCsv") @post @operationId("syncAclsCsv") syncAclsCsv(@path clusterName: string, @body content: string): void | ApiBadRequestResponse; - @doc("createAcl") @post @operationId("createAcl") + @summary("createAcl") createAcl(@path clusterName: string, @body acl: KafkaAcl): void | ApiBadRequestResponse; - @doc("deleteAcl") + @summary("deleteAcl") @delete @operationId("deleteAcl") + @summary("deleteAcl") deleteAcl( @path clusterName: string, @body acl: KafkaAcl, ): void | ApiNotFoundResponse; @route("/consumer") - @doc("createConsumerAcl") @post @operationId("createConsumerAcl") + @summary("createConsumerAcl") createConsumerAcl( @path clusterName: string, @body payload: CreateConsumerAcl, ): void | ApiBadRequestResponse; @route("/producer") - @doc("createProducerAcl") + @summary("createProducerAcl") @operationId("createProducerAcl") @post createProducerAcl( @@ -63,7 +64,7 @@ interface AclApi { ): void | ApiBadRequestResponse; @route("/streamapp") - @doc("createStreamAppAcl") + @summary("createStreamAppAcl") @post @operationId("createStreamAppAcl") createStreamAppAcl( diff --git a/contract-typespec/api/auth.tsp b/contract-typespec/api/auth.tsp index 599403839..a690456b1 100644 --- a/contract-typespec/api/auth.tsp +++ b/contract-typespec/api/auth.tsp @@ -8,14 +8,14 @@ using OpenAPI; @tag("Authorization") interface AuthorizationApi { @route("/api/authorization") - @doc("Get user authorization related info") + @summary("Get user authorization related info") @operationId("getUserAuthInfo") @get getUserAuthInfo(): AuthenticationInfo; } @route("/login") -@doc("Authenticate") +@summary("Authenticate") @operationId("authenticate") @post @tag("Unmapped") diff --git a/contract-typespec/api/brokers.tsp b/contract-typespec/api/brokers.tsp index 0f7442c1e..29a2ceca5 100644 --- a/contract-typespec/api/brokers.tsp +++ b/contract-typespec/api/brokers.tsp @@ -10,19 +10,19 @@ using OpenAPI; interface BrokersApi { @get @operationId("getBrokers") - @doc("getBrokers") + @summary("getBrokers") getBrokers(@path clusterName: string): Broker[]; @get @route("/{id}/configs") @operationId("getBrokerConfig") - @doc("getBrokerConfig") + @summary("getBrokerConfig") getBrokerConfig(@path clusterName: string, @path id: int32): BrokerConfig[]; @put @route("/{id}/configs/{name}") @operationId("updateBrokerConfigByName") - @doc("updateBrokerConfigByName") + @summary("updateBrokerConfigByName") updateBrokerConfigByName( @path clusterName: string, @path id: int32, @@ -33,13 +33,13 @@ interface BrokersApi { @get @route("/{id}/metrics") @operationId("getBrokersMetrics") - @doc("getBrokersMetrics") + @summary("getBrokersMetrics") getBrokersMetrics(@path clusterName: string, @path id: int32): BrokerMetrics; @get @route("/logdirs") @operationId("getAllBrokersLogdirs") - @doc("getAllBrokersLogdirs") + @summary("getAllBrokersLogdirs") getAllBrokersLogdirs( @path clusterName: string, @query broker?: int32[], @@ -48,7 +48,7 @@ interface BrokersApi { @patch(#{implicitOptionality: true}) @route("/{id}/logdirs") @operationId("updateBrokerTopicPartitionLogDir") - @doc("updateBrokerTopicPartitionLogDir") + @summary("updateBrokerTopicPartitionLogDir") updateBrokerTopicPartitionLogDir( @path clusterName: string, @path id: int32, diff --git a/contract-typespec/api/clusters.tsp b/contract-typespec/api/clusters.tsp index de7b95880..c14a521fd 100644 --- a/contract-typespec/api/clusters.tsp +++ b/contract-typespec/api/clusters.tsp @@ -11,25 +11,25 @@ using OpenAPI; interface ClustersApi { @get @operationId("getClusters") - @doc("getClusters") + @summary("getClusters") getClusters(): Cluster[]; @post @route("/{clusterName}/cache") @operationId("updateClusterInfo") - @doc("updateClusterInfo") + @summary("updateClusterInfo") updateClusterInfo(@path clusterName: string): Cluster; @get @route("/{clusterName}/metrics") @operationId("getClusterMetrics") - @doc("getClusterMetrics") + @summary("getClusterMetrics") getClusterMetrics(@path clusterName: string): ClusterMetrics; @get @route("/{clusterName}/stats") @operationId("getClusterStats") - @doc("getClusterStats") + @summary("getClusterStats") getClusterStats(@path clusterName: string): ClusterStats; } diff --git a/contract-typespec/api/config.tsp b/contract-typespec/api/config.tsp index a228a810c..7f639e995 100644 --- a/contract-typespec/api/config.tsp +++ b/contract-typespec/api/config.tsp @@ -7,7 +7,7 @@ using TypeSpec.Http; using OpenAPI; @route("/api/info") -@doc("Gets application info") +@summary("Gets application info") @get @tag("ApplicationConfig") op getApplicationInfo(): ApplicationInfo; @@ -15,25 +15,25 @@ op getApplicationInfo(): ApplicationInfo; @route("/api/config") @tag("ApplicationConfig") interface ApplicationConfigApi { - @doc("Gets current application configuration") + @summary("Gets current application configuration") @get @operationId("getCurrentConfig") getCurrentConfig(): ApplicationConfig; @put - @doc("Restarts application with specified configuration") + @summary("Restarts application with specified configuration") @operationId("restartWithConfig") restartWithConfig(@body config: RestartRequest): void | ApiBadRequestResponse; @put @route("/validated") - @doc("Restarts application with specified configuration") + @summary("Restarts application with specified configuration") @operationId("validateConfig") validateConfig(@body config: ApplicationConfig): ApplicationConfigValidation | ApiBadRequestResponse; @post @route("/relatedfiles") - @doc("Upload config related file") + @summary("Upload config related file") @operationId("uploadConfigRelatedFile") uploadConfigRelatedFile( @multipartBody body: { @@ -43,7 +43,7 @@ interface ApplicationConfigApi { @get @route("/authentication") - @doc("Get authentication methods enabled for the app and other related settings") + @summary("Get authentication methods enabled for the app and other related settings") @operationId("getAuthenticationSettings") getAuthenticationSettings(): AppAuthenticationSettings; } diff --git a/contract-typespec/api/consumer-groups.tsp b/contract-typespec/api/consumer-groups.tsp index 49cb13284..d901fc001 100644 --- a/contract-typespec/api/consumer-groups.tsp +++ b/contract-typespec/api/consumer-groups.tsp @@ -12,6 +12,7 @@ interface ConsumerGroupsApi { @get @route("/paged") @operationId("getConsumerGroupsPage") + @summary("getConsumerGroupsPage") getConsumerGroupsPage( @path clusterName: string, @query page?: int32, @@ -24,6 +25,7 @@ interface ConsumerGroupsApi { @get @route("/{id}") @operationId("getConsumerGroup") + @summary("getConsumerGroup") getConsumerGroup( @path clusterName: string, @path id: string, @@ -32,11 +34,13 @@ interface ConsumerGroupsApi { @delete @route("/{id}") @operationId("deleteConsumerGroup") + @summary("deleteConsumerGroup") deleteConsumerGroup(@path clusterName: string, @path id: string): void; @post @route("/{id}/offsets") @operationId("resetConsumerGroupOffsets") + @summary("resetConsumerGroupOffsets") resetConsumerGroupOffsets( @path clusterName: string, @path id: string, @@ -46,6 +50,7 @@ interface ConsumerGroupsApi { @delete @route("/{id}/topics/{topicName}") @operationId("deleteConsumerGroupOffsets") + @summary("deleteConsumerGroupOffsets") deleteConsumerGroupOffsets( @path clusterName: string, @path id: string, @@ -58,6 +63,7 @@ interface ConsumerGroupsApi { interface TopicConsumerGroupsApi { @get @operationId("getTopicConsumerGroups") + @summary("getTopicConsumerGroups") getTopicConsumerGroups( @path clusterName: string, @path topicName: string, diff --git a/contract-typespec/api/kafka-connect.tsp b/contract-typespec/api/kafka-connect.tsp index 194f5c45b..585e0ac79 100644 --- a/contract-typespec/api/kafka-connect.tsp +++ b/contract-typespec/api/kafka-connect.tsp @@ -11,11 +11,12 @@ using OpenAPI; interface ConnectInstancesApi { @get @operationId("getConnects") + @summary("getConnects") getConnects(@path clusterName: string, @query withStats?: boolean): Connect[]; @get @route("/{connectName}/plugins") - @doc("get connector plugins") + @summary("get connector plugins") @operationId("getConnectorPlugins") getConnectorPlugins( @path clusterName: string, @@ -24,7 +25,7 @@ interface ConnectInstancesApi { @put @route("/{connectName}/plugins/{pluginName}/config/validate") - @doc("validate connector plugin configuration") + @summary("validate connector plugin configuration") @operationId("validateConnectorPluginConfig") validateConnectorPluginConfig( @path clusterName: string, @@ -40,6 +41,7 @@ interface ConnectInstancesApi { interface ConnectorsApi { @get @operationId("getAllConnectors") + @summary("getAllConnectors") getAllConnectors( @path clusterName: string, @query search?: string, @@ -54,10 +56,12 @@ interface ConnectorsApi { interface KafkaConnectConnectorsApi { @get @operationId("getConnectors") + @summary("getConnectors") getConnectors(@path clusterName: string, @path connectName: string): string[]; @post @operationId("createConnector") + @summary("createConnector") createConnector( @path clusterName: string, @path connectName: string, @@ -67,6 +71,7 @@ interface KafkaConnectConnectorsApi { @get @route("/{connectorName}") @operationId("getConnector") + @summary("getConnector") getConnector( @path clusterName: string, @path connectName: string, @@ -76,6 +81,7 @@ interface KafkaConnectConnectorsApi { @delete @route("/{connectorName}") @operationId("deleteConnector") + @summary("deleteConnector") deleteConnector( @path clusterName: string, @path connectName: string, @@ -85,6 +91,7 @@ interface KafkaConnectConnectorsApi { @post @route("/{connectorName}/action/{action}") @operationId("updateConnectorState") + @summary("updateConnectorState") updateConnectorState( @path clusterName: string, @path connectName: string, @@ -95,6 +102,7 @@ interface KafkaConnectConnectorsApi { @get @route("/{connectorName}/config") @operationId("getConnectorConfig") + @summary("getConnectorConfig") getConnectorConfig( @path clusterName: string, @path connectName: string, @@ -104,6 +112,7 @@ interface KafkaConnectConnectorsApi { @put @route("/{connectorName}/config") @operationId("setConnectorConfig") + @summary("setConnectorConfig") setConnectorConfig( @path clusterName: string, @path connectName: string, @@ -114,6 +123,7 @@ interface KafkaConnectConnectorsApi { @get @route("/{connectorName}/tasks") @operationId("getConnectorTasks") + @summary("getConnectorTasks") getConnectorTasks( @path clusterName: string, @path connectName: string, @@ -123,6 +133,7 @@ interface KafkaConnectConnectorsApi { @post @route("/{connectorName}/tasks/{taskId}/action/restart") @operationId("restartConnectorTask") + @summary("restartConnectorTask") restartConnectorTask( @path clusterName: string, @path connectName: string, @@ -133,6 +144,7 @@ interface KafkaConnectConnectorsApi { @delete @route("/{connectorName}/offsets") @operationId("resetConnectorOffsets") + @summary("resetConnectorOffsets") resetConnectorOffsets( @path clusterName: string, @path connectName: string, diff --git a/contract-typespec/api/ksql.tsp b/contract-typespec/api/ksql.tsp index 09375e881..b6a84836d 100644 --- a/contract-typespec/api/ksql.tsp +++ b/contract-typespec/api/ksql.tsp @@ -12,6 +12,7 @@ interface KsqlApi { @post @route("/v2") @operationId("executeKsql") + @summary("executeKsql") executeKsql( @path clusterName: string, @body command: KsqlCommandV2, @@ -20,16 +21,19 @@ interface KsqlApi { @get @route("/tables") @operationId("listTables") + @summary("listTables") listTables(@path clusterName: string): KsqlTableDescription[]; @get @route("/streams") @operationId("listStreams") + @summary("listStreams") listStreams(@path clusterName: string): KsqlStreamDescription[]; @get @route("/response") @operationId("openKsqlResponsePipe") + @summary("openKsqlResponsePipe") openKsqlResponsePipe( @path clusterName: string, @query pipeId: string, diff --git a/contract-typespec/api/messages.tsp b/contract-typespec/api/messages.tsp index 59dc23a84..75f728f86 100644 --- a/contract-typespec/api/messages.tsp +++ b/contract-typespec/api/messages.tsp @@ -13,6 +13,7 @@ interface MessagesApi { @get @route("/serdes") @operationId("getSerdes") + @summary("getSerdes") getSerdes( @path clusterName: string, @path topicName: string, @@ -22,6 +23,7 @@ interface MessagesApi { @get @route("/messages") @operationId("getTopicMessages") + @summary("getTopicMessages") getTopicMessages( @path clusterName: string, @path topicName: string, @@ -38,6 +40,7 @@ interface MessagesApi { @delete @route("/messages") @operationId("deleteTopicMessages") + @summary("deleteTopicMessages") deleteTopicMessages( @path clusterName: string, @path topicName: string, @@ -47,6 +50,7 @@ interface MessagesApi { @post @route("/messages") @operationId("sendTopicMessages") + @summary("sendTopicMessages") sendTopicMessages( @path clusterName: string, @path topicName: string, @@ -56,6 +60,7 @@ interface MessagesApi { @post @route("/smartfilters") @operationId("registerFilter") + @summary("registerFilter") registerFilter( @path clusterName: string, @path topicName: string, @@ -65,6 +70,7 @@ interface MessagesApi { @get @route("/messages/v2") @operationId("getTopicMessagesV2") + @summary("getTopicMessagesV2") getTopicMessagesV2( @path clusterName: string, @path topicName: string, @@ -86,6 +92,7 @@ interface MessagesApi { interface SmartFiltersTestExecutionsApi { @put @operationId("executeSmartFilterTest") + @summary("executeSmartFilterTest") executeSmartFilterTest( @body input: SmartFilterTestExecution, ): SmartFilterTestExecutionResult | ApiBadRequestResponse; diff --git a/contract-typespec/api/package-lock.json b/contract-typespec/api/package-lock.json index 0cc016a26..edd3d6ef4 100644 --- a/contract-typespec/api/package-lock.json +++ b/contract-typespec/api/package-lock.json @@ -8,22 +8,22 @@ "name": "tsp", "version": "0.1.0", "dependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.1", - "@typespec/openapi": "^1.0.0", - "@typespec/openapi3": "^1.0.0", - "@typespec/rest": "^0.70.0", - "@typespec/sse": "^0.70.0", + "@typespec/compiler": "1.3.0", + "@typespec/http": "1.3.0", + "@typespec/openapi": "1.3.0", + "@typespec/openapi3": "1.3.0", + "@typespec/rest": "0.73.0", + "@typespec/sse": "0.73.0", + "@typespec/streams": "0.73.0", "build": "^0.1.4" } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.7.2", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz", - "integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz", + "integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==", "license": "MIT", "dependencies": { - "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" }, @@ -50,15 +50,14 @@ "license": "MIT" }, "node_modules/@apidevtools/swagger-parser": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz", - "integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==", + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.0.0.tgz", + "integrity": "sha512-WLJIWcfOXrSKlZEM+yhA2Xzatgl488qr1FoOxixYmtWapBzwSC0gVGq4WObr4hHClMIiFFdOBdixNkvWqkWIWA==", "license": "MIT", "dependencies": { - "@apidevtools/json-schema-ref-parser": "11.7.2", + "@apidevtools/json-schema-ref-parser": "14.0.1", "@apidevtools/openapi-schemas": "^2.1.0", "@apidevtools/swagger-methods": "^3.0.2", - "@jsdevtools/ono": "^7.1.3", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "call-me-maybe": "^1.0.2" @@ -68,14 +67,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -426,12 +425,6 @@ "node": ">=18.0.0" } }, - "node_modules/@jsdevtools/ono": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", - "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", - "license": "MIT" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -492,24 +485,24 @@ "license": "MIT" }, "node_modules/@typespec/asset-emitter": { - "version": "0.70.1", - "resolved": "https://registry.npmjs.org/@typespec/asset-emitter/-/asset-emitter-0.70.1.tgz", - "integrity": "sha512-X8hRA7LLWkNIWqAkWaWoa84PzDMUvjj3qCLQKT29k5twS419nN1GGT7BQaDQnYfPTsSssEFxgRgugr0AAErEsA==", + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@typespec/asset-emitter/-/asset-emitter-0.73.0.tgz", + "integrity": "sha512-SigCa9k8gS+AiHE7Ky/kcwyqFM5kuJ0wXT+Dy89Jbd+wwrYu+mKXyXbScrTdc+MBzut+rFltFENgEYXsSvA/mA==", "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0" + "@typespec/compiler": "^1.3.0" } }, "node_modules/@typespec/compiler": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@typespec/compiler/-/compiler-1.0.0.tgz", - "integrity": "sha512-QFy0otaB4xkN4kQmYyT17yu3OVhN0gti9+EKnZqs5JFylw2Xecx22BPwUE1Byj42pZYg5d9WlO+WwmY5ALtRDg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@typespec/compiler/-/compiler-1.3.0.tgz", + "integrity": "sha512-OqpoNP3C2y8riA6C5RofPMvmj9jNiGyyhde0tM2ZE7IBOv7BBaTDqw4CJD22YnC8JEilRfPmvdVCViNrPHEjrA==", "license": "MIT", "dependencies": { - "@babel/code-frame": "~7.26.2", + "@babel/code-frame": "~7.27.1", "@inquirer/prompts": "^7.4.0", "ajv": "~8.17.1", "change-case": "~5.4.4", @@ -518,14 +511,14 @@ "is-unicode-supported": "^2.1.0", "mustache": "~4.2.0", "picocolors": "~1.1.1", - "prettier": "~3.5.3", + "prettier": "~3.6.2", "semver": "^7.7.1", "tar": "^7.4.3", "temporal-polyfill": "^0.3.0", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.12", - "yaml": "~2.7.0", - "yargs": "~17.7.2" + "yaml": "~2.8.0", + "yargs": "~18.0.0" }, "bin": { "tsp": "cmd/tsp.js", @@ -536,29 +529,29 @@ } }, "node_modules/@typespec/events": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/events/-/events-0.70.0.tgz", - "integrity": "sha512-qHW1N05n8PkNf2YQGNMdl/sAYqrJv+zQ1kny+3vg/20nzVj7sZpNFIKqUIc11z0GkT7k3Q9SPTymvq+K00sAUg==", + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@typespec/events/-/events-0.73.0.tgz", + "integrity": "sha512-etlhp86amDaElD/UX27u9I4O58zREov73HkkV3xbdTWpv2RqOKyD3mkyGAWsW3hKaGVIxwHOvKcOZ2j+b07Gpw==", "license": "MIT", "peer": true, "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0" + "@typespec/compiler": "^1.3.0" } }, "node_modules/@typespec/http": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@typespec/http/-/http-1.0.1.tgz", - "integrity": "sha512-J5tqBWlmkvI/W+kJn4EFuN0laGxbY8qT68jzEQEiYeAXSfNyFGRSoCwn8Ex6dJphq4IozOMdVTNtOZWIJlwmfw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@typespec/http/-/http-1.3.0.tgz", + "integrity": "sha512-4W3KsmBHZGgECVbvyh7S7KQG06948XyVVzae+UbVDDxoUj/x4Ry0AXw3q4HmzB2BVhxw6JBrwBuVa5mxjVMzdw==", "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/streams": "^0.70.0" + "@typespec/compiler": "^1.3.0", + "@typespec/streams": "^0.73.0" }, "peerDependenciesMeta": { "@typespec/streams": { @@ -567,28 +560,28 @@ } }, "node_modules/@typespec/openapi": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@typespec/openapi/-/openapi-1.0.0.tgz", - "integrity": "sha512-pONzKIdK4wHgD1vBfD9opUk66zDG55DlHbueKOldH2p1LVf5FnMiuKE4kW0pl1dokT/HBNR5OJciCzzVf44AgQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@typespec/openapi/-/openapi-1.3.0.tgz", + "integrity": "sha512-BSeshjCZQodVGyVHn7ytcUeIcUGjqbG2Ac0NLOQaaKnISVrhTWNcgo5aFTqxAa24ZL/EuhqlSauLyYce2EV9fw==", "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.0" + "@typespec/compiler": "^1.3.0", + "@typespec/http": "^1.3.0" } }, "node_modules/@typespec/openapi3": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@typespec/openapi3/-/openapi3-1.0.0.tgz", - "integrity": "sha512-cDsnNtJkQCx0R/+9AqXzqAKH6CgtwmnQGQMQHbkw0/Sxs5uk6hoiexx7vz0DUR7H4492MqPT2kE4351KZbDYMw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@typespec/openapi3/-/openapi3-1.3.0.tgz", + "integrity": "sha512-ZG+swQYtdBgyTUbwPI03YQJpPUYhORtbcx6mIFNsKhsTRRC2UDq63jUNCIFCTYI6DJPzkVpra56YPNCXmQLZMg==", "license": "MIT", "dependencies": { - "@apidevtools/swagger-parser": "~10.1.1", - "@typespec/asset-emitter": "^0.70.0", + "@apidevtools/swagger-parser": "~12.0.0", + "@typespec/asset-emitter": "^0.73.0", "openapi-types": "~12.1.3", - "yaml": "~2.7.0" + "yaml": "~2.8.0" }, "bin": { "tsp-openapi3": "cmd/tsp-openapi3.js" @@ -597,11 +590,11 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.0", - "@typespec/json-schema": "^1.0.0", - "@typespec/openapi": "^1.0.0", - "@typespec/versioning": "^0.70.0" + "@typespec/compiler": "^1.3.0", + "@typespec/http": "^1.3.0", + "@typespec/json-schema": "^1.3.0", + "@typespec/openapi": "^1.3.0", + "@typespec/versioning": "^0.73.0" }, "peerDependenciesMeta": { "@typespec/json-schema": { @@ -616,44 +609,43 @@ } }, "node_modules/@typespec/rest": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/rest/-/rest-0.70.0.tgz", - "integrity": "sha512-pn3roMQV6jBNT4bVA/hnrBAAHleXSyfWQqNO+DhI3+tLU4jCrJHmUZDi82nI9xBl+jkmy2WZFZOelZA9PSABeg==", + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@typespec/rest/-/rest-0.73.0.tgz", + "integrity": "sha512-28hgFGvreBg34Xuguw+E++pQC/kbRxy1Bpx/9nU7x87Ly6ykns3lpx74gjY9ByE8VYKVbXtC7lzdnp19DRSjIQ==", "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/http": "^1.0.0" + "@typespec/compiler": "^1.3.0", + "@typespec/http": "^1.3.0" } }, "node_modules/@typespec/sse": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/sse/-/sse-0.70.0.tgz", - "integrity": "sha512-11VsIRqPuK+bIq7gHVghM5CAqvcfe9TmL9mZkxlPKuV6RRWju831k18KqlwXTOgeEMwVGA1Xbg1TTi1F4S1B+w==", + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@typespec/sse/-/sse-0.73.0.tgz", + "integrity": "sha512-WTnRJ1b1M3RPzlHxhnK9sh6+AGKPKWpuA0TSAqzyxb/uRHFYLNeoDKPOnlQ749SJ8lJz71Oh0nUsP3vB0EzO6Q==", "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0", - "@typespec/events": "^0.70.0", - "@typespec/http": "^1.0.0", - "@typespec/streams": "^0.70.0" + "@typespec/compiler": "^1.3.0", + "@typespec/events": "^0.73.0", + "@typespec/http": "^1.3.0", + "@typespec/streams": "^0.73.0" } }, "node_modules/@typespec/streams": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@typespec/streams/-/streams-0.70.0.tgz", - "integrity": "sha512-WIixoZ7CCLq2INX4UkN+aXlj07Je+ntW0xbeFGmpfq6Z2xifKnL6/sPiztURMXd4Z1I+XXFCn2pw1r9q5i4Cmw==", + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@typespec/streams/-/streams-0.73.0.tgz", + "integrity": "sha512-pL4xffHXEIhBQKPlB9L4AKuM0bn44WsGKjnz91wa6wBtP/CbsPrGQicof0Z7GPGdddtDi4G8PWGmJtVFw53V9g==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@typespec/compiler": "^1.0.0" + "@typespec/compiler": "^1.3.0" } }, "node_modules/ajv": { @@ -824,31 +816,93 @@ } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -1034,6 +1088,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1398,9 +1464,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -1451,15 +1517,6 @@ "node": ">= 6" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1879,42 +1936,91 @@ } }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^7.2.0", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/yoctocolors-cjs": { diff --git a/contract-typespec/api/quotas.tsp b/contract-typespec/api/quotas.tsp index ba94c4e5b..b0b0a0998 100644 --- a/contract-typespec/api/quotas.tsp +++ b/contract-typespec/api/quotas.tsp @@ -11,12 +11,12 @@ using OpenAPI; @tag("ClientQuotas") interface QuoatsApi { @get - @doc("listQuotas") + @summary("listQuotas") @operationId("listQuotas") listQuotas(@path clusterName: string): ClientQuotas[]; @post - @doc("upsertClientQuotas") + @summary("upsertClientQuotas") @operationId("upsertClientQuotas") upsertClientQuotas( @path clusterName: string, diff --git a/contract-typespec/api/schemas.tsp b/contract-typespec/api/schemas.tsp index a935edddc..2d4d7e997 100644 --- a/contract-typespec/api/schemas.tsp +++ b/contract-typespec/api/schemas.tsp @@ -11,6 +11,7 @@ using OpenAPI; interface SchemasApi { @post @operationId("createNewSchema") + @summary("createNewSchema") createNewSchema(@path clusterName: string, @body input: NewSchemaSubject): | SchemaSubject | ApiBadRequestResponse @@ -19,6 +20,7 @@ interface SchemasApi { @get @operationId("getSchemas") + @summary("getSchemas") getSchemas( @path clusterName: string, @query page?: int32, @@ -29,6 +31,7 @@ interface SchemasApi { @delete @route("/{subject}") @operationId("deleteSchema") + @summary("deleteSchema") deleteSchema( @path clusterName: string, @path subject: string, @@ -37,6 +40,7 @@ interface SchemasApi { @get @route("/{subject}/versions") @operationId("getAllVersionsBySubject") + @summary("getAllVersionsBySubject") getAllVersionsBySubject( @path clusterName: string, @path subject: string, @@ -45,6 +49,7 @@ interface SchemasApi { @get @route("/{subject}/latest") @operationId("getLatestSchema") + @summary("getLatestSchema") getLatestSchema( @path clusterName: string, @path subject: string, @@ -53,6 +58,7 @@ interface SchemasApi { @delete @route("/{subject}/latest") @operationId("deleteLatestSchema") + @summary("deleteLatestSchema") deleteLatestSchema( @path clusterName: string, @path subject: string, @@ -61,6 +67,7 @@ interface SchemasApi { @get @route("/{subject}/versions/{version}") @operationId("getSchemaByVersion") + @summary("getSchemaByVersion") getSchemaByVersion( @path clusterName: string, @path subject: string, @@ -70,6 +77,7 @@ interface SchemasApi { @delete @route("/{subject}/versions/{version}") @operationId("deleteSchemaByVersion") + @summary("deleteSchemaByVersion") deleteSchemaByVersion( @path clusterName: string, @path subject: string, @@ -79,6 +87,7 @@ interface SchemasApi { @get @route("/compatibility") @operationId("getGlobalSchemaCompatibilityLevel") + @summary("getGlobalSchemaCompatibilityLevel") getGlobalSchemaCompatibilityLevel( @path clusterName: string, ): CompatibilityLevel; @@ -86,6 +95,7 @@ interface SchemasApi { @put @route("/compatibility") @operationId("updateGlobalSchemaCompatibilityLevel") + @summary("updateGlobalSchemaCompatibilityLevel") updateGlobalSchemaCompatibilityLevel( @path clusterName: string, @body level: CompatibilityLevel, @@ -94,6 +104,7 @@ interface SchemasApi { @put @route("/{subject}/compatibility") @operationId("updateSchemaCompatibilityLevel") + @summary("updateSchemaCompatibilityLevel") updateSchemaCompatibilityLevel( @path clusterName: string, @path subject: string, @@ -103,6 +114,7 @@ interface SchemasApi { @post @route("/{subject}/check") @operationId("checkSchemaCompatibility") + @summary("checkSchemaCompatibility") checkSchemaCompatibility( @path clusterName: string, @path subject: string, diff --git a/contract-typespec/api/topics.tsp b/contract-typespec/api/topics.tsp index 8a50e46f6..c41696764 100644 --- a/contract-typespec/api/topics.tsp +++ b/contract-typespec/api/topics.tsp @@ -11,6 +11,7 @@ using OpenAPI; interface TopicsApi { @get @operationId("getTopics") + @summary("getTopics") getTopics( @path clusterName: string, @query page?: int32, @@ -23,6 +24,7 @@ interface TopicsApi { @post @operationId("createTopic") + @summary("createTopic") createTopic( @path clusterName: string, @body topic: TopicCreation, @@ -31,6 +33,7 @@ interface TopicsApi { @post @route("/{topicName}/clone") @operationId("cloneTopic") + @summary("cloneTopic") cloneTopic( @path clusterName: string, @path topicName: string, @@ -40,6 +43,7 @@ interface TopicsApi { @get @route("/{topicName}/analysis") @operationId("getTopicAnalysis") + @summary("getTopicAnalysis") getTopicAnalysis( @path clusterName: string, @path topicName: string, @@ -48,6 +52,7 @@ interface TopicsApi { @post @route("/{topicName}/analysis") @operationId("analyzeTopic") + @summary("analyzeTopic") analyzeTopic( @path clusterName: string, @path topicName: string, @@ -56,6 +61,7 @@ interface TopicsApi { @delete @route("/{topicName}/analysis") @operationId("cancelTopicAnalysis") + @summary("cancelTopicAnalysis") cancelTopicAnalysis( @path clusterName: string, @path topicName: string, @@ -64,6 +70,7 @@ interface TopicsApi { @get @route("/{topicName}") @operationId("getTopicDetails") + @summary("getTopicDetails") getTopicDetails( @path clusterName: string, @path topicName: string, @@ -72,6 +79,7 @@ interface TopicsApi { @post @route("/{topicName}") @operationId("recreateTopic") + @summary("recreateTopic") recreateTopic(@path clusterName: string, @path topicName: string): | ApiCreatedResponse | ApiTimeoutResponse @@ -81,6 +89,7 @@ interface TopicsApi { @patch(#{implicitOptionality: true}) @route("/{topicName}") @operationId("updateTopic") + @summary("updateTopic") updateTopic( @path clusterName: string, @path topicName: string, @@ -90,6 +99,7 @@ interface TopicsApi { @delete @route("/{topicName}") @operationId("deleteTopic") + @summary("deleteTopic") deleteTopic( @path clusterName: string, @path topicName: string, @@ -98,6 +108,7 @@ interface TopicsApi { @get @route("/{topicName}/config") @operationId("getTopicConfigs") + @summary("getTopicConfigs") getTopicConfigs( @path clusterName: string, @path topicName: string, @@ -106,6 +117,7 @@ interface TopicsApi { @patch(#{ implicitOptionality: true }) @route("/{topicName}/replications") @operationId("changeReplicationFactor") + @summary("changeReplicationFactor") changeReplicationFactor( @path clusterName: string, @path topicName: string, @@ -115,6 +127,7 @@ interface TopicsApi { @get @route("/{topicName}/activeproducers") @operationId("getActiveProducerStates") + @summary("getActiveProducerStates") getActiveProducerStates( @path clusterName: string, @path topicName: string, @@ -122,7 +135,7 @@ interface TopicsApi { @patch(#{ implicitOptionality: true }) @route("/{topicName}/partitions") - @doc("increaseTopicPartitions") + @summary("increaseTopicPartitions") @operationId("increaseTopicPartitions") increaseTopicPartitions( @path clusterName: string, From db746fa649da83f6f9b0e1081da9e1cc117d45c8 Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 7 Aug 2025 17:36:24 +0300 Subject: [PATCH 20/36] Removed frontend test report --- .gitignore | 1 + frontend/test-report.xml | 958 --------------------------------------- 2 files changed, 1 insertion(+), 958 deletions(-) delete mode 100644 frontend/test-report.xml diff --git a/.gitignore b/.gitignore index f62411788..2b6bc78b6 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,4 @@ node_modules/ .gradle @rerun.txt test-results +frontend/test-report.xml diff --git a/frontend/test-report.xml b/frontend/test-report.xml deleted file mode 100644 index 16769f955..000000000 --- a/frontend/test-report.xml +++ /dev/null @@ -1,958 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 38ee6c994060dbb0f84b971b3598f83212d2b9f4 Mon Sep 17 00:00:00 2001 From: German Osin Date: Tue, 12 Aug 2025 16:36:40 +0300 Subject: [PATCH 21/36] Synced with main --- .../kafbat/ui/mapper/DynamicConfigMapper.java | 4 + contract-typespec/api/clusters.tsp | 3 +- contract-typespec/api/config.tsp | 39 +++++++- contract-typespec/api/graphs.tsp | 91 +++++++++++++++++++ contract-typespec/api/main.tsp | 2 + contract-typespec/api/prometheus.tsp | 23 +++++ 6 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 contract-typespec/api/graphs.tsp create mode 100644 contract-typespec/api/prometheus.tsp diff --git a/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java b/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java index 8012c42a4..7fed78928 100644 --- a/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java +++ b/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java @@ -9,6 +9,7 @@ import io.kafbat.ui.model.ApplicationConfigPropertiesDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesKafkaClustersInnerDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesRbacRolesInnerPermissionsInnerDTO; +import io.kafbat.ui.model.RbacPermissionDTO; import io.kafbat.ui.model.rbac.Permission; import io.kafbat.ui.util.DynamicConfigOperations; import java.util.Optional; @@ -26,6 +27,9 @@ public interface DynamicConfigMapper { @Mapping(target = "kafka.clusters[].metrics.store", ignore = true) ApplicationConfigPropertiesDTO toDto(DynamicConfigOperations.PropertiesStructure propertiesStructure); + @Mapping(target = "parsedActions", ignore = true) + Permission toPermission(RbacPermissionDTO dto); + default String map(Resource resource) { return resource.getFilename(); } diff --git a/contract-typespec/api/clusters.tsp b/contract-typespec/api/clusters.tsp index c14a521fd..388aeae8c 100644 --- a/contract-typespec/api/clusters.tsp +++ b/contract-typespec/api/clusters.tsp @@ -55,7 +55,8 @@ alias ClusterFeature = | "TOPIC_DELETION" | "KAFKA_ACL_VIEW" | "KAFKA_ACL_EDIT" - | "CLIENT_QUOTA_MANAGEMENT"; + | "CLIENT_QUOTA_MANAGEMENT" + | "GRAPHS_ENABLED"; enum ServerStatus { ONLINE, diff --git a/contract-typespec/api/config.tsp b/contract-typespec/api/config.tsp index 7f639e995..eea72c412 100644 --- a/contract-typespec/api/config.tsp +++ b/contract-typespec/api/config.tsp @@ -115,12 +115,11 @@ model ApplicationConfig { value?: string; regex?: boolean = false; }[]; - permissions?: { - resource?: ResourceType; - value?: string; - actions?: Action[]; - }[]; + permissions?: RbacPermission[]; }[]; + defaultRole?: { + permissions?: RbacPermission[]; + } }; webclient?: { maxInMemoryBufferSize?: string; @@ -135,6 +134,14 @@ model ApplicationConfig { }; adminClientTimeout?: int32; internalTopicPrefix?: string; + defaultMetricsStorage?: ClusterMetricsStoreConfig; + cache?: { + enabled?: boolean; + @format("duration") + connectCacheExpiry?: string; + @format("duration") + connectClusterCacheExpiry?: string; + }; clusters?: { name?: string; bootstrapServers?: string; @@ -177,6 +184,8 @@ model ApplicationConfig { password?: string; keystoreLocation?: string; keystorePassword?: string; + prometheusExpose?: boolean; + store?: ClusterMetricsStoreConfig; }; properties?: Record; consumerProperties?: Record; @@ -231,6 +240,7 @@ model ClusterConfigValidation { schemaRegistry?: ApplicationPropertyValidation; kafkaConnects?: Record; ksqldb?: ApplicationPropertyValidation; + prometheusStorage?: ApplicationPropertyValidation; } alias ApplicationFeature = @@ -291,3 +301,22 @@ enum ResourceType { AUDIT, CLIENT_QUOTAS, } + +model ClusterMetricsStoreConfig { + prometheus?: PrometheusStorage; +} + +model PrometheusStorage { + url?: string; + remoteWrite?: boolean; + pushGatewayUrl?: string; + pushGatewayUsername?: string; + pushGatewayPassword?: string; + pushGatewayJobName?: string; +} + +model RbacPermission { + resource?: ResourceType; + value?: string; + actions?: Action[]; +} diff --git a/contract-typespec/api/graphs.tsp b/contract-typespec/api/graphs.tsp new file mode 100644 index 000000000..a50ca14e8 --- /dev/null +++ b/contract-typespec/api/graphs.tsp @@ -0,0 +1,91 @@ +import "@typespec/openapi"; +import "./responses.tsp"; + +namespace Api; + +using TypeSpec.Http; +using OpenAPI; + +@route("/api/clusters/{clusterName}/graphs") +@tag("Graphs") +interface GraphsApi { + @get + @operationId("getGraphsList") + @summary("getGraphsList") + getGraphsList( + @path clusterName: string + ): GraphDescriptions; + + @post + @operationId("getGraphData") + @summary("getGraphData") + getGraphData( + @path clusterName: string, + @body request: GraphDataRequest + ) : PrometheusApiQueryResponse +} + +model GraphDescriptions { + graphs?: Array; +} + +model GraphDescription { + @doc("Id that should be used to query data on API level") + id: string; + type?: "range" | "instant"; + @doc(""" + ISO_8601 duration string (for "range" graphs only) + """) + defaultPeriod?: string; + parameters?: Array; +} + +model GraphParameter { + name: string; +} + +model GraphDataRequest { + id?: string; + parameters?: Record; + from?: offsetDateTime; + to?: offsetDateTime; +} + +model PrometheusApiBaseResponse { + status: "success" | "error"; + error?: string; + errorType?: string; + warnings?: Array; +} + +model PrometheusApiQueryResponse extends PrometheusApiBaseResponse { + data?: PrometheusApiQueryResponseData; +} + +model PrometheusApiQueryResponseData { + resultType: "matrix" | "vector" | "scalar" | "string"; + @doc(""" + Depending on resultType format can vary: + "vector": + [ + { + "metric": { "": "", ... }, + "value": [ , "" ], + "histogram": [ , ] + }, ... + ] + "matrix": + [ + { + "metric": { "": "", ... }, + "values": [ [ , "" ], ... ], + "histograms": [ [ , ], ... ] + }, ... + ] + "scalar": + [ , "" ] + "string": + [ , "" ] + """) + result?: Array; +} diff --git a/contract-typespec/api/main.tsp b/contract-typespec/api/main.tsp index c0e566153..a810a5b93 100644 --- a/contract-typespec/api/main.tsp +++ b/contract-typespec/api/main.tsp @@ -11,6 +11,8 @@ import "./acls.tsp"; import "./quotas.tsp"; import "./auth.tsp"; import "./config.tsp"; +import "./graphs.tsp"; +import "./prometheus.tsp"; import "@typespec/http"; import "@typespec/rest"; diff --git a/contract-typespec/api/prometheus.tsp b/contract-typespec/api/prometheus.tsp new file mode 100644 index 000000000..3a0c349d0 --- /dev/null +++ b/contract-typespec/api/prometheus.tsp @@ -0,0 +1,23 @@ +import "@typespec/openapi"; +import "./models.tsp"; +import "./responses.tsp"; + +namespace Api; + +using TypeSpec.Http; +using OpenAPI; + +@route("/metrics") +@tag("PrometheusExpose") +interface PrometheusExposeApi { + @get + @summary("exposeAllMetrics") + @operationId("exposeAllMetrics") + exposeAllMetrics(): string; + + @get + @route("/{clusterName}") + @summary("exposeClusterMetrics") + @operationId("exposeClusterMetrics") + exposeClusterMetrics(@path clusterName: string): string; +} From 0474fd781570436c7a4c417686f6442a4b2b270a Mon Sep 17 00:00:00 2001 From: German Osin Date: Tue, 12 Aug 2025 16:48:31 +0300 Subject: [PATCH 22/36] Synced with main --- api/src/main/java/io/kafbat/ui/mapper/ClusterMapper.java | 7 +++++++ .../main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java | 7 +------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/mapper/ClusterMapper.java b/api/src/main/java/io/kafbat/ui/mapper/ClusterMapper.java index 28793be33..df3051af3 100644 --- a/api/src/main/java/io/kafbat/ui/mapper/ClusterMapper.java +++ b/api/src/main/java/io/kafbat/ui/mapper/ClusterMapper.java @@ -118,6 +118,13 @@ default ConfigSynonymDTO toConfigSynonym(ConfigEntry.ConfigSynonym config) { ReplicaDTO toReplica(InternalReplica replica); + @Mapping(target = "connectorsCount", ignore = true) + @Mapping(target = "failedConnectorsCount", ignore = true) + @Mapping(target = "tasksCount", ignore = true) + @Mapping(target = "failedTasksCount", ignore = true) + @Mapping(target = "version", ignore = true) + @Mapping(target = "commit", ignore = true) + @Mapping(target = "clusterId", ignore = true) ConnectDTO toKafkaConnect(ClustersProperties.ConnectCluster connect); List toFeaturesEnum(List features); diff --git a/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java b/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java index 7fed78928..e913f206e 100644 --- a/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java +++ b/api/src/main/java/io/kafbat/ui/mapper/DynamicConfigMapper.java @@ -2,13 +2,11 @@ import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.ActionDTO; -import io.kafbat.ui.model.ApplicationConfigPropertiesAuthOauth2ClientValueDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesAuthOauth2ResourceServerDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesAuthOauth2ResourceServerJwtDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesAuthOauth2ResourceServerOpaquetokenDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesDTO; import io.kafbat.ui.model.ApplicationConfigPropertiesKafkaClustersInnerDTO; -import io.kafbat.ui.model.ApplicationConfigPropertiesRbacRolesInnerPermissionsInnerDTO; import io.kafbat.ui.model.RbacPermissionDTO; import io.kafbat.ui.model.rbac.Permission; import io.kafbat.ui.util.DynamicConfigOperations; @@ -27,9 +25,6 @@ public interface DynamicConfigMapper { @Mapping(target = "kafka.clusters[].metrics.store", ignore = true) ApplicationConfigPropertiesDTO toDto(DynamicConfigOperations.PropertiesStructure propertiesStructure); - @Mapping(target = "parsedActions", ignore = true) - Permission toPermission(RbacPermissionDTO dto); - default String map(Resource resource) { return resource.getFilename(); } @@ -37,7 +32,7 @@ default String map(Resource resource) { @Mapping(source = "metrics.store", target = "metrics.store", ignore = true) ApplicationConfigPropertiesKafkaClustersInnerDTO map(ClustersProperties.Cluster cluster); - default Permission map(ApplicationConfigPropertiesRbacRolesInnerPermissionsInnerDTO perm) { + default Permission map(RbacPermissionDTO perm) { Permission permission = new Permission(); permission.setResource(perm.getResource().getValue()); permission.setActions(perm.getActions().stream().map(ActionDTO::getValue).toList()); From c27b32dc346bf41f2663c142bf6114ca749108cb Mon Sep 17 00:00:00 2001 From: German Osin Date: Wed, 13 Aug 2025 10:19:52 +0300 Subject: [PATCH 23/36] Added lucene --- api/build.gradle | 4 + .../kafbat/ui/config/ClustersProperties.java | 12 ++ .../java/io/kafbat/ui/model/Statistics.java | 9 +- .../ui/service/index/ConsumerGroupFilter.java | 19 +++ .../kafbat/ui/service/index/NgramFilter.java | 93 ++++++++++++ .../service/index/ShortWordNGramAnalyzer.java | 48 ++++++ .../kafbat/ui/service/index/TopicsIndex.java | 137 ++++++++++++++++++ .../metrics/scrape/ScrapedClusterState.java | 12 +- .../index/ConsumerGroupsFilterTest.java | 45 ++++++ .../ui/service/index/TopicsIndexTest.java | 74 ++++++++++ gradle/libs.versions.toml | 5 + 11 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java create mode 100644 api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java create mode 100644 api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java create mode 100644 api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java create mode 100644 api/src/test/java/io/kafbat/ui/service/index/ConsumerGroupsFilterTest.java create mode 100644 api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java diff --git a/api/build.gradle b/api/build.gradle index 582ed0ed4..7aed11c84 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -54,6 +54,10 @@ dependencies { antlr libs.antlr implementation libs.antlr.runtime + implementation libs.lucene + implementation libs.lucene.queryparser + implementation libs.lucene.analysis.common + implementation libs.opendatadiscovery.oddrn implementation(libs.opendatadiscovery.client) { exclude group: 'org.springframework.boot', module: 'spring-boot-starter-webflux' diff --git a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java index 8d5be375a..6c8bc3b37 100644 --- a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java @@ -41,6 +41,7 @@ public class ClustersProperties { MetricsStorage defaultMetricsStorage = new MetricsStorage(); CacheProperties cache = new CacheProperties(); + FtsProperties fts = new FtsProperties(); @Data public static class Cluster { @@ -217,6 +218,17 @@ public static class CacheProperties { Duration connectClusterCacheExpiry = Duration.ofHours(24); } + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class FtsProperties { + boolean enabled = true; + int topicsMinNGram = 3; + int topicsMaxNGram = 5; + int filterMinNGram = 1; + int filterMaxNGram = 4; + } + @PostConstruct public void validateAndSetDefaults() { if (clusters != null) { diff --git a/api/src/main/java/io/kafbat/ui/model/Statistics.java b/api/src/main/java/io/kafbat/ui/model/Statistics.java index 6caba634a..d5b6ebd1b 100644 --- a/api/src/main/java/io/kafbat/ui/model/Statistics.java +++ b/api/src/main/java/io/kafbat/ui/model/Statistics.java @@ -11,7 +11,7 @@ @Value @Builder(toBuilder = true) -public class Statistics { +public class Statistics implements AutoCloseable { ServerStatusDTO status; Throwable lastKafkaException; String version; @@ -46,4 +46,11 @@ public Stream topicDescriptions() { public Statistics withClusterState(UnaryOperator stateUpdate) { return toBuilder().clusterState(stateUpdate.apply(clusterState)).build(); } + + @Override + public void close() throws Exception { + if (clusterState != null) { + clusterState.close(); + } + } } diff --git a/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java b/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java new file mode 100644 index 000000000..01ead6217 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java @@ -0,0 +1,19 @@ +package io.kafbat.ui.service.index; + +import io.kafbat.ui.model.InternalConsumerGroup; +import java.util.List; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +public class ConsumerGroupFilter extends NgramFilter { + private final List> groups; + + public ConsumerGroupFilter(List groups) { + this.groups = groups.stream().map(g -> Tuples.of(g.getGroupId(), g)).toList(); + } + + @Override + protected List> getItems() { + return this.groups; + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java new file mode 100644 index 000000000..609cea5b0 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java @@ -0,0 +1,93 @@ +package io.kafbat.ui.service.index; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; +import reactor.util.function.Tuple2; + +@Slf4j +public abstract class NgramFilter { + private final Analyzer analyzer = new ShortWordNGramAnalyzer(1, 4, false); + + protected abstract List> getItems(); + private static Map> cache = new ConcurrentHashMap<>(); + + public List find(String search) { + try { + List> result = new ArrayList<>(); + List queryTokens = tokenizeString(analyzer, search); + Map queryFreq = termFreq(queryTokens); + + for (Tuple2 item : getItems()) { + List itemTokens = tokenizeString(analyzer, item.getT1()); + HashSet itemTokensSet = new HashSet<>(itemTokens); + if (itemTokensSet.containsAll(queryTokens)) { + double score = cosineSimilarity(queryFreq, itemTokens); + result.add(new SearchResult(item.getT2(), score)); +// result.add(new SearchResult(item.getT2(), 1)); + } + } + result.sort((o1, o2) -> Double.compare(o2.score, o1.score)); + return result.stream().map(r -> r.item).toList(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private record SearchResult(T item, double score) { } + + + public static List tokenizeString(Analyzer analyzer, String text) throws IOException { + return cache.computeIfAbsent(text, (t) -> tokenizeStringSimple(analyzer, text)); + } + + @SneakyThrows + public static List tokenizeStringSimple(Analyzer analyzer, String text) { + List tokens = new ArrayList<>(); + try (TokenStream tokenStream = analyzer.tokenStream(null, text)) { + CharTermAttribute attr = tokenStream.addAttribute(CharTermAttribute.class); + tokenStream.reset(); + while (tokenStream.incrementToken()) { + tokens.add(attr.toString()); + } + tokenStream.end(); + } + return tokens; + } + + private static double cosineSimilarity(Map queryFreq, List itemTokens) { + // Build frequency maps + Map terms = termFreq(itemTokens); + + double dot = 0.0; + double mag1 = 0.0; + double mag2 = 0.0; + + for (String term : terms.keySet()) { + int f1 = queryFreq.getOrDefault(term, 0); + int f2 = terms.getOrDefault(term, 0); + dot += f1 * f2; + mag1 += f1 * f1; + mag2 += f2 * f2; + } + + return (mag1 == 0 || mag2 == 0) ? 0.0 : dot / (Math.sqrt(mag1) * Math.sqrt(mag2)); + } + + private static Map termFreq(List tokens) { + Map freq = new HashMap<>(); + for (String token : tokens) { + freq.put(token, freq.getOrDefault(token, 0) + 1); + } + return freq; + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java b/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java new file mode 100644 index 000000000..568df366f --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java @@ -0,0 +1,48 @@ +package io.kafbat.ui.service.index; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LowerCaseFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilter; +import org.apache.lucene.analysis.ngram.NGramTokenFilter; +import org.apache.lucene.analysis.standard.StandardTokenizer; + +public class ShortWordNGramAnalyzer extends Analyzer { + private final int minGram; + private final int maxGram; + private final boolean preserveOriginal; + + public ShortWordNGramAnalyzer(int minGram, int maxGram) { + this(minGram, maxGram, true); + } + + public ShortWordNGramAnalyzer(int minGram, int maxGram, boolean preserveOriginal) { + this.minGram = minGram; + this.maxGram = maxGram; + this.preserveOriginal = preserveOriginal; + } + + + + @Override + protected TokenStreamComponents createComponents(String fieldName) { + Tokenizer tokenizer = new StandardTokenizer(); + + TokenStream tokenStream = new WordDelimiterGraphFilter( + tokenizer, + WordDelimiterGraphFilter.GENERATE_WORD_PARTS | + WordDelimiterGraphFilter.SPLIT_ON_CASE_CHANGE | + //WordDelimiterGraphFilter.SPLIT_ON_NUMERICS | + WordDelimiterGraphFilter.STEM_ENGLISH_POSSESSIVE, + null + ); + + tokenStream = new LowerCaseFilter(tokenStream); + + // Add n-gram generation from characters (min=2, max=4) + tokenStream = new NGramTokenFilter(tokenStream, minGram, maxGram, this.preserveOriginal); + + return new TokenStreamComponents(tokenizer, tokenStream); + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java new file mode 100644 index 000000000..9337a65e1 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java @@ -0,0 +1,137 @@ +package io.kafbat.ui.service.index; + +import io.kafbat.ui.model.InternalTopic; +import io.kafbat.ui.model.InternalTopicConfig; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryparser.classic.ParseException; +import org.apache.lucene.queryparser.classic.QueryParser; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.store.Directory; + +public class TopicsIndex implements AutoCloseable { + public static final String FIELD_NAME_RAW = "name_raw"; + public static final String FIELD_NAME = "name"; + public static final String FIELD_INTERNAL = "internal"; + public static final String FIELD_PARTITIONS = "partitions"; + public static final String FIELD_REPLICATION = "replication"; + public static final String FIELD_SIZE = "size"; + public static final String FIELD_CONFIG_PREFIX = "config"; + + private final Directory directory; + private final DirectoryReader indexReader; + private final IndexSearcher indexSearcher; + private final Analyzer analyzer; + + public TopicsIndex(List topics) throws IOException { + this(topics, 3,5); + } + + public TopicsIndex(List topics, int minNgram, int maxNgram) throws IOException { + this.analyzer = new ShortWordNGramAnalyzer(minNgram, maxNgram); + this.directory = build(topics); + this.indexReader = DirectoryReader.open(directory); + this.indexSearcher = new IndexSearcher(indexReader); + } + + private Directory build(List topics) { + Directory directory = new ByteBuffersDirectory(); + try(IndexWriter directoryWriter = new IndexWriter(directory, new IndexWriterConfig(this.analyzer))) { + for (InternalTopic topic : topics) { + Document doc = new Document(); + doc.add(new StringField(FIELD_NAME_RAW, topic.getName(), Field.Store.YES)); + doc.add(new TextField(FIELD_NAME, topic.getName(), Field.Store.NO)); + doc.add(new IntPoint(FIELD_PARTITIONS, topic.getPartitionCount())); + doc.add(new IntPoint(FIELD_REPLICATION, topic.getReplicationFactor())); + doc.add(new LongPoint(FIELD_SIZE, topic.getSegmentSize())); + if (topic.getTopicConfigs() != null && !topic.getTopicConfigs().isEmpty()) { + for (InternalTopicConfig topicConfig : topic.getTopicConfigs()) { + doc.add(new StringField(FIELD_CONFIG_PREFIX+"_"+topicConfig.getName(), topicConfig.getValue(), Field.Store.NO)); + } + } + doc.add(new StringField(FIELD_INTERNAL, String.valueOf(topic.isInternal()), Field.Store.NO)); + directoryWriter.addDocument(doc); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return directory; + } + + @Override + public void close() throws Exception { + if (indexReader != null) { + this.indexReader.close(); + } + if (this.directory != null) { + this.directory.close(); + } + } + + public List find(String search, Boolean showInternal, int count) throws IOException { + return find(search, showInternal, FIELD_NAME, count, 0.0f, 2); + } + + public List find(String search, Boolean showInternal, String sort, int count) throws IOException { + return find(search, showInternal, sort, count, 0.0f, 2); + } + + public List find(String search, Boolean showInternal, String sortField, int count, float minScore, int maxEdits) throws IOException { + QueryParser queryParser = new QueryParser(FIELD_NAME, this.analyzer); + queryParser.setDefaultOperator(QueryParser.Operator.AND); + Query nameQuery = null; + try { + nameQuery = queryParser.parse(search); + } catch (ParseException e) { + throw new RuntimeException(e); + } + + Query internalFilter = new TermQuery(new Term(FIELD_INTERNAL, "true")); + + BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); + queryBuilder.add(nameQuery, BooleanClause.Occur.MUST); + if (showInternal == null || !showInternal) { + queryBuilder.add(internalFilter, BooleanClause.Occur.MUST_NOT); + } + + List sortFields = new ArrayList<>(); + sortFields.add(SortField.FIELD_SCORE); + if (!sortField.equals(FIELD_NAME)) { + sortFields.add(new SortField(sortField, SortField.Type.INT, true)); + } + + Sort sort = new Sort(sortFields.toArray(new SortField[0])); + + TopDocs result = this.indexSearcher.search(queryBuilder.build(), count); + + List topics = new ArrayList<>(); + for (ScoreDoc scoreDoc : result.scoreDocs) { + if (scoreDoc.score > minScore) { + Document document = this.indexSearcher.storedFields().document(scoreDoc.doc); + topics.add(document.get(FIELD_NAME_RAW)); + } + } + return topics; + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java index e5d8c059c..17d64faa8 100644 --- a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java +++ b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java @@ -8,7 +8,9 @@ import io.kafbat.ui.model.InternalLogDirStats; import io.kafbat.ui.model.InternalPartitionsOffsets; import io.kafbat.ui.service.ReactiveAdminClient; +import io.kafbat.ui.service.index.TopicsIndex; import jakarta.annotation.Nullable; +import java.io.Closeable; import java.time.Instant; import java.util.HashMap; import java.util.List; @@ -31,12 +33,20 @@ @Builder(toBuilder = true) @RequiredArgsConstructor @Value -public class ScrapedClusterState { +public class ScrapedClusterState implements AutoCloseable { Instant scrapeFinishedAt; Map nodesStates; Map topicStates; Map consumerGroupsStates; + TopicsIndex topicsIndex; + + @Override + public void close() throws Exception { + if (this.topicsIndex != null) { + this.topicsIndex.close(); + } + } public record NodeState(int id, Node node, diff --git a/api/src/test/java/io/kafbat/ui/service/index/ConsumerGroupsFilterTest.java b/api/src/test/java/io/kafbat/ui/service/index/ConsumerGroupsFilterTest.java new file mode 100644 index 000000000..948e65b65 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/index/ConsumerGroupsFilterTest.java @@ -0,0 +1,45 @@ +package io.kafbat.ui.service.index; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.kafbat.ui.model.InternalConsumerGroup; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ConsumerGroupsFilterTest { + private final List names = List.of( + "connect-test-10", + "connect-test-9", + "connect-test-90", + "connect-local-file-sink", + "martech-mt0-data-integration-platform-rapi-orders", + "pmp-recon-connect.payments.crypto.ctt.agent.created.v1", + "rmd-flink.volatility.trigger.v26", + "iat-ntapi-account-dd-consumer-prod" + ); + + @Test + void testFindTopicsByName() throws Exception { + List groups = + names.stream().map(n -> InternalConsumerGroup.builder().groupId(n).build()).toList(); + + ConsumerGroupFilter filter = new ConsumerGroupFilter(groups); + + Map tests = Map.of( + "test 10", 1, + "test", 3, + "payment created", 1, + "test 9", 2, + "volatility 26", 1 + ); + + for (Map.Entry entry : tests.entrySet()) { + List result = filter.find(entry.getKey()); + assertThat(result).size() + .withFailMessage("Expected %d results for '%s', but got %s", entry.getValue(), entry.getKey(), result) + .isEqualTo(entry.getValue()); + } + } + +} diff --git a/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java b/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java new file mode 100644 index 000000000..7ba484a4b --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java @@ -0,0 +1,74 @@ +package io.kafbat.ui.service.index; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.kafbat.ui.model.InternalTopic; +import io.kafbat.ui.model.InternalTopicConfig; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +class TopicsIndexTest { + @Test + void testFindTopicsByName() throws Exception { + List topics = new ArrayList<>( + Stream.of("topic", "topic-1", "topic-2", "topic-3", + "topic-4", "topic-5", "topic-6", "topic-7", + "topic-8", "red-dog", + "sk.payment.events", + "sk.payment.events.dlq", + "sk.payment.commands", + "sk.payment.changes", + "sk.payment.stats", + "sk.currency.rates", + "audit.payment.events", + "audit.clients.state", + "audit.clients.repartitioned.status", + "reporting.payments.by.clientId", + "reporting.payments.by.currencyid" + ) + .map(s -> InternalTopic.builder().name(s).partitions(Map.of()).build()).toList()); + + topics.addAll( + List.of( + InternalTopic.builder().name("configurable").partitions(Map.of()).topicConfigs( + List.of(InternalTopicConfig.builder().name("retention").value("compact").build()) + ).build() + ) + ); + + int testTopicsCount = (int) topics.stream().filter(s -> s.getName().contains("topic")).count(); + + Map examples = Map.ofEntries( + Map.entry("topic", testTopicsCount), + Map.entry("8", 1), + Map.entry("9", 0), + Map.entry("tpic", testTopicsCount), + Map.entry("dogs red", 1), + Map.entry("tpic-1", 1), + Map.entry("payments dlq", 1), + Map.entry("paymnts dlq", 1), + Map.entry("stats dlq", 0), + Map.entry("stat", 3), + Map.entry("chnges", 1), + Map.entry("comands", 1), + Map.entry("id", 1), + Map.entry("config_retention:compact", 1) + ); + + SoftAssertions softly = new SoftAssertions(); + try(TopicsIndex index = new TopicsIndex(topics)) { + for (Map.Entry entry : examples.entrySet()) { + List resultAll = index.find(entry.getKey(), null, topics.size()); + softly.assertThat(resultAll.size()) + .withFailMessage("Expected %d results for '%s', but got %s", entry.getValue(), entry.getKey(), resultAll) + .isEqualTo(entry.getValue()); + } + } + softly.assertAll(); + } + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7385b6ed8..ad203be36 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,7 @@ testng = '7.10.0' bonigarcia-webdrivermanager = '6.1.1' aspectj = '1.9.21' prometheus = '1.3.6' +lucene = '10.2.2' [plugins] spring-boot = { id = 'org.springframework.boot', version.ref = 'spring-boot' } @@ -152,3 +153,7 @@ snappy = {module = 'org.xerial.snappy:snappy-java', version = '1.1.10.7'} # CVE fixes reactor-netty-http = {module = 'io.projectreactor.netty:reactor-netty-http', version = '1.2.8'} + +lucene = {module = 'org.apache.lucene:lucene-core', version.ref = 'lucene'} +lucene-queryparser = {module = 'org.apache.lucene:lucene-queryparser', version.ref = 'lucene'} +lucene-analysis-common = {module = 'org.apache.lucene:lucene-analysis-common', version.ref = 'lucene'} From 153c44d3244c12d6f41d79a870048dd20d1662b5 Mon Sep 17 00:00:00 2001 From: German Osin Date: Wed, 13 Aug 2025 10:21:47 +0300 Subject: [PATCH 24/36] Close stats --- .../java/io/kafbat/ui/service/StatisticsCache.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/src/main/java/io/kafbat/ui/service/StatisticsCache.java b/api/src/main/java/io/kafbat/ui/service/StatisticsCache.java index 04306e9e8..f71d4a094 100644 --- a/api/src/main/java/io/kafbat/ui/service/StatisticsCache.java +++ b/api/src/main/java/io/kafbat/ui/service/StatisticsCache.java @@ -2,15 +2,18 @@ import io.kafbat.ui.model.InternalPartitionsOffsets; import io.kafbat.ui.model.KafkaCluster; +import io.kafbat.ui.model.ServerStatusDTO; import io.kafbat.ui.model.Statistics; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.ConfigEntry; import org.apache.kafka.clients.admin.TopicDescription; import org.springframework.stereotype.Component; +@Slf4j @Component public class StatisticsCache { @@ -34,6 +37,13 @@ public synchronized void update(KafkaCluster c, c, stats.withClusterState(s -> s.updateTopics(descriptions, configs, partitionsOffsets)) ); + try { + if (!stats.getStatus().equals(ServerStatusDTO.INITIALIZING)) { + stats.close(); + } + } catch (Exception e) { + log.error("Error closing cluster {} stats", c.getName(), e); + } } public synchronized void onTopicDelete(KafkaCluster c, String topic) { From c9a6d94c5692b712c1c32c609f8478aa344dd429 Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 14 Aug 2025 11:15:21 +0300 Subject: [PATCH 25/36] fts --- .../kafbat/ui/config/ClustersProperties.java | 2 +- .../ui/controller/SchemasController.java | 18 ++-- .../ui/controller/TopicsController.java | 24 +++-- .../io/kafbat/ui/model/InternalTopic.java | 6 +- .../ui/service/ConsumerGroupService.java | 22 ++++- .../ui/service/KafkaConnectService.java | 23 ++++- .../io/kafbat/ui/service/StatisticsCache.java | 8 +- .../kafbat/ui/service/StatisticsService.java | 4 +- .../io/kafbat/ui/service/TopicsService.java | 50 ++++++---- .../io/kafbat/ui/service/acl/AclsService.java | 30 +++++- .../service/index/AclBindingNgramFilter.java | 25 +++++ .../ui/service/index/ConsumerGroupFilter.java | 18 ++-- .../index/KafkaConnectNgramFilter.java | 36 +++++++ .../kafbat/ui/service/index/NgramFilter.java | 31 +++++-- .../ui/service/index/SchemasFilter.java | 41 ++++++++ .../service/index/ShortWordNGramAnalyzer.java | 8 +- .../kafbat/ui/service/index/TopicsIndex.java | 93 +++++++++++-------- .../metrics/scrape/ScrapedClusterState.java | 58 ++++++++++-- .../main/resources/application-localtest.yaml | 2 + .../service/SchemaRegistryPaginationTest.java | 3 +- .../service/TopicsServicePaginationTest.java | 11 ++- .../ui/service/acl/AclsServiceTest.java | 3 +- .../index/ConsumerGroupsFilterTest.java | 7 +- .../ui/service/index/TopicsIndexTest.java | 2 +- .../mcp/McpSpecificationGeneratorTest.java | 4 +- 25 files changed, 402 insertions(+), 127 deletions(-) create mode 100644 api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java create mode 100644 api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java create mode 100644 api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java diff --git a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java index 6c8bc3b37..ed6ee387d 100644 --- a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java @@ -222,7 +222,7 @@ public static class CacheProperties { @NoArgsConstructor @AllArgsConstructor public static class FtsProperties { - boolean enabled = true; + boolean enabled = false; int topicsMinNGram = 3; int topicsMaxNGram = 5; int filterMinNGram = 1; diff --git a/api/src/main/java/io/kafbat/ui/controller/SchemasController.java b/api/src/main/java/io/kafbat/ui/controller/SchemasController.java index a34110031..b6bd2bc93 100644 --- a/api/src/main/java/io/kafbat/ui/controller/SchemasController.java +++ b/api/src/main/java/io/kafbat/ui/controller/SchemasController.java @@ -1,8 +1,7 @@ package io.kafbat.ui.controller; -import static org.apache.commons.lang3.Strings.CI; - import io.kafbat.ui.api.SchemasApi; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.exception.ValidationException; import io.kafbat.ui.mapper.KafkaSrMapper; import io.kafbat.ui.mapper.KafkaSrMapperImpl; @@ -15,13 +14,13 @@ import io.kafbat.ui.model.rbac.AccessContext; import io.kafbat.ui.model.rbac.permission.SchemaAction; import io.kafbat.ui.service.SchemaRegistryService; +import io.kafbat.ui.service.index.SchemasFilter; import io.kafbat.ui.service.mcp.McpTool; import java.util.List; import java.util.Map; import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ServerWebExchange; @@ -38,6 +37,7 @@ public class SchemasController extends AbstractController implements SchemasApi, private final KafkaSrMapper kafkaSrMapper = new KafkaSrMapperImpl(); private final SchemaRegistryService schemaRegistryService; + private final ClustersProperties clustersProperties; @Override protected KafkaCluster getCluster(String clusterName) { @@ -214,6 +214,8 @@ public Mono> getSchemas(String cluster .operationName("getSchemas") .build(); + ClustersProperties.FtsProperties fts = clustersProperties.getFts(); + return schemaRegistryService .getAllSubjectNames(getCluster(clusterName)) .flatMapIterable(l -> l) @@ -222,10 +224,12 @@ public Mono> getSchemas(String cluster .flatMap(subjects -> { int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE; int subjectToSkip = ((pageNum != null && pageNum > 0 ? pageNum : 1) - 1) * pageSize; - List filteredSubjects = subjects - .stream() - .filter(subj -> search == null || CI.contains(subj, search)) - .sorted().toList(); + + SchemasFilter filter = + new SchemasFilter(subjects, fts.getFilterMinNGram(), fts.getFilterMaxNGram(), fts.isEnabled()); + + List filteredSubjects = filter.find(search); + var totalPages = (filteredSubjects.size() / pageSize) + (filteredSubjects.size() % pageSize == 0 ? 0 : 1); List subjectsToRender = filteredSubjects.stream() 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 b72a4e395..178ef3862 100644 --- a/api/src/main/java/io/kafbat/ui/controller/TopicsController.java +++ b/api/src/main/java/io/kafbat/ui/controller/TopicsController.java @@ -10,6 +10,7 @@ import static org.apache.commons.lang3.Strings.CI; import io.kafbat.ui.api.TopicsApi; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.mapper.ClusterMapper; import io.kafbat.ui.model.InternalTopic; import io.kafbat.ui.model.InternalTopicConfig; @@ -55,6 +56,7 @@ public class TopicsController extends AbstractController implements TopicsApi, M private final TopicsService topicsService; private final TopicAnalysisService topicAnalysisService; private final ClusterMapper clusterMapper; + private final ClustersProperties clustersProperties; @Override public Mono> createTopic( @@ -181,23 +183,30 @@ public Mono> getTopics(String clusterName, .operationName("getTopics") .build(); - return topicsService.getTopicsForPagination(getCluster(clusterName)) + return topicsService.getTopicsForPagination(getCluster(clusterName), search, showInternal) .flatMap(topics -> accessControlService.filterViewableTopics(topics, clusterName)) .flatMap(topics -> { int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE; var topicsToSkip = ((page != null && page > 0 ? page : 1) - 1) * pageSize; + ClustersProperties.FtsProperties fts = clustersProperties.getFts(); + Comparator comparatorForTopic = getComparatorForTopic(orderBy, fts.isEnabled()); var comparator = sortOrder == null || !sortOrder.equals(SortOrderDTO.DESC) - ? getComparatorForTopic(orderBy) : getComparatorForTopic(orderBy).reversed(); - List filtered = topics.stream() + ? comparatorForTopic : comparatorForTopic.reversed(); + + List filtered = fts.isEnabled() ? topics : topics.stream() .filter(topic -> !topic.isInternal() || showInternal != null && showInternal) - .filter(topic -> search == null || CI.contains(topic.getName(), search)) + .filter( + topic -> search == null || CI.contains(topic.getName(), search) + ) .sorted(comparator) .toList(); + var totalPages = (filtered.size() / pageSize) + (filtered.size() % pageSize == 0 ? 0 : 1); List topicsPage = filtered.stream() + .filter(t -> !t.isInternal() || showInternal != null && showInternal) .skip(topicsToSkip) .limit(pageSize) .map(InternalTopic::getName) @@ -348,9 +357,12 @@ public Mono>> getActiveProducerStates } private Comparator getComparatorForTopic( - TopicColumnsToSortDTO orderBy) { + TopicColumnsToSortDTO orderBy, + boolean ftsEnabled) { var defaultComparator = Comparator.comparing(InternalTopic::getName); - if (orderBy == null) { + if (orderBy == null && ftsEnabled) { + return (o1, o2) -> 0; + } else if (orderBy == null) { return defaultComparator; } return switch (orderBy) { diff --git a/api/src/main/java/io/kafbat/ui/model/InternalTopic.java b/api/src/main/java/io/kafbat/ui/model/InternalTopic.java index 1e8c31b5b..51a79e906 100644 --- a/api/src/main/java/io/kafbat/ui/model/InternalTopic.java +++ b/api/src/main/java/io/kafbat/ui/model/InternalTopic.java @@ -113,8 +113,10 @@ public static InternalTopic from(TopicDescription topicDescription, topic.segmentSize(stats.getSegmentSize()); }); - topic.bytesInPerSec(metrics.getIoRates().topicBytesInPerSec().get(topicDescription.name())); - topic.bytesOutPerSec(metrics.getIoRates().topicBytesOutPerSec().get(topicDescription.name())); + if (metrics != null) { + topic.bytesInPerSec(metrics.getIoRates().topicBytesInPerSec().get(topicDescription.name())); + topic.bytesOutPerSec(metrics.getIoRates().topicBytesOutPerSec().get(topicDescription.name())); + } topic.topicConfigs( configs.stream().map(InternalTopicConfig::from).collect(Collectors.toList())); 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 5fda6d4ce..0ddb06aff 100644 --- a/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java +++ b/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java @@ -4,12 +4,14 @@ import com.google.common.collect.Streams; import com.google.common.collect.Table; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.emitter.EnhancedConsumer; import io.kafbat.ui.model.ConsumerGroupOrderingDTO; import io.kafbat.ui.model.InternalConsumerGroup; import io.kafbat.ui.model.InternalTopicConsumerGroup; import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.SortOrderDTO; +import io.kafbat.ui.service.index.ConsumerGroupFilter; import io.kafbat.ui.service.rbac.AccessControlService; import io.kafbat.ui.util.ApplicationMetrics; import io.kafbat.ui.util.KafkaClientSslPropertiesUtil; @@ -41,6 +43,7 @@ public class ConsumerGroupService { private final AdminClientService adminClientService; private final AccessControlService accessControlService; + private final ClustersProperties clustersProperties; private Mono> getConsumerGroups( ReactiveAdminClient ac, @@ -114,11 +117,7 @@ public Mono getConsumerGroupsPage( SortOrderDTO sortOrderDto) { return adminClientService.get(cluster).flatMap(ac -> ac.listConsumerGroups() - .map(listing -> search == null - ? listing - : listing.stream() - .filter(g -> CI.contains(g.groupId(), search)) - .toList() + .map(listing -> filterGroups(listing, search) ) .flatMapIterable(lst -> lst) .filterWhen(cg -> accessControlService.isConsumerGroupAccessible(cg.groupId(), cluster.getName())) @@ -131,6 +130,19 @@ public Mono getConsumerGroupsPage( (allGroups.size() / perPage) + (allGroups.size() % perPage == 0 ? 0 : 1)))))); } + private Collection filterGroups(Collection groups, String search) { + if (search == null || search.isBlank()) { + return groups; + } + ClustersProperties.FtsProperties fts = clustersProperties.getFts(); + if (fts.isEnabled()) { + ConsumerGroupFilter filter = new ConsumerGroupFilter(groups, fts.getFilterMinNGram(), fts.getFilterMaxNGram()); + return filter.find(search); + } else { + return groups.stream().filter(g -> CI.contains(g.groupId(), search)).toList(); + } + } + private Mono> loadSortedDescriptions(ReactiveAdminClient ac, List groups, int pageNum, diff --git a/api/src/main/java/io/kafbat/ui/service/KafkaConnectService.java b/api/src/main/java/io/kafbat/ui/service/KafkaConnectService.java index b3e5aa058..a77ed99c1 100644 --- a/api/src/main/java/io/kafbat/ui/service/KafkaConnectService.java +++ b/api/src/main/java/io/kafbat/ui/service/KafkaConnectService.java @@ -27,6 +27,7 @@ import io.kafbat.ui.model.NewConnectorDTO; import io.kafbat.ui.model.TaskDTO; import io.kafbat.ui.model.connect.InternalConnectorInfo; +import io.kafbat.ui.service.index.KafkaConnectNgramFilter; import io.kafbat.ui.util.ReactiveFailover; import jakarta.validation.Valid; import java.util.List; @@ -151,15 +152,27 @@ public Flux getAllConnectors(final KafkaCluster cluster, .topics(tuple.getT4().getTopics()) .build()))) .map(kafkaConnectMapper::fullConnectorInfo) - .filter(matchesSearchTerm(search)); + .collectList() + .map(lst -> filterConnectors(lst, search)) + .flatMapMany(Flux::fromIterable); } - private Predicate matchesSearchTerm(@Nullable final String search) { + private List filterConnectors(List connectors, String search) { if (search == null) { - return c -> true; + return connectors; + } + ClustersProperties.FtsProperties fts = clustersProperties.getFts(); + if (fts.isEnabled()) { + KafkaConnectNgramFilter filter = + new KafkaConnectNgramFilter(connectors, fts.getFilterMinNGram(), fts.getFilterMaxNGram()); + return filter.find(search); + } else { + return connectors.stream() + .filter(connector -> getStringsForSearch(connector) + .anyMatch(string -> CI.contains(string, search))) + .toList(); } - return connector -> getStringsForSearch(connector) - .anyMatch(string -> CI.contains(string, search)); + } private Stream getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) { diff --git a/api/src/main/java/io/kafbat/ui/service/StatisticsCache.java b/api/src/main/java/io/kafbat/ui/service/StatisticsCache.java index f71d4a094..0b8274959 100644 --- a/api/src/main/java/io/kafbat/ui/service/StatisticsCache.java +++ b/api/src/main/java/io/kafbat/ui/service/StatisticsCache.java @@ -1,5 +1,6 @@ package io.kafbat.ui.service; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.InternalPartitionsOffsets; import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.ServerStatusDTO; @@ -31,11 +32,14 @@ public synchronized void replace(KafkaCluster c, Statistics stats) { public synchronized void update(KafkaCluster c, Map descriptions, Map> configs, - InternalPartitionsOffsets partitionsOffsets) { + InternalPartitionsOffsets partitionsOffsets, + ClustersProperties clustersProperties) { var stats = get(c); replace( c, - stats.withClusterState(s -> s.updateTopics(descriptions, configs, partitionsOffsets)) + stats.withClusterState(s -> + s.updateTopics(descriptions, configs, partitionsOffsets, clustersProperties) + ) ); try { if (!stats.getStatus().equals(ServerStatusDTO.INITIALIZING)) { diff --git a/api/src/main/java/io/kafbat/ui/service/StatisticsService.java b/api/src/main/java/io/kafbat/ui/service/StatisticsService.java index 9e302596e..9185838dd 100644 --- a/api/src/main/java/io/kafbat/ui/service/StatisticsService.java +++ b/api/src/main/java/io/kafbat/ui/service/StatisticsService.java @@ -2,6 +2,7 @@ import static io.kafbat.ui.service.ReactiveAdminClient.ClusterDescription; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.ClusterFeature; import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.Metrics; @@ -22,6 +23,7 @@ public class StatisticsService { private final AdminClientService adminClientService; private final FeatureService featureService; private final StatisticsCache cache; + private final ClustersProperties clustersProperties; public Mono updateCache(KafkaCluster c) { return getStatistics(c).doOnSuccess(m -> cache.replace(c, m)); @@ -62,7 +64,7 @@ private Statistics createStats(ClusterDescription description, private Mono loadClusterState(ClusterDescription clusterDescription, ReactiveAdminClient ac) { - return ScrapedClusterState.scrape(clusterDescription, ac); + return ScrapedClusterState.scrape(clusterDescription, ac, clustersProperties); } private Mono scrapeMetrics(KafkaCluster cluster, diff --git a/api/src/main/java/io/kafbat/ui/service/TopicsService.java b/api/src/main/java/io/kafbat/ui/service/TopicsService.java index becfd55ad..b60c85d80 100644 --- a/api/src/main/java/io/kafbat/ui/service/TopicsService.java +++ b/api/src/main/java/io/kafbat/ui/service/TopicsService.java @@ -2,6 +2,7 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; +import static org.apache.commons.lang3.Strings.CI; import com.google.common.collect.Sets; import io.kafbat.ui.config.ClustersProperties; @@ -26,6 +27,7 @@ import io.kafbat.ui.model.TopicUpdateDTO; import io.kafbat.ui.service.metrics.scrape.ScrapedClusterState; import io.kafbat.ui.service.metrics.scrape.ScrapedClusterState.TopicState; +import java.io.IOException; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; @@ -48,6 +50,7 @@ import org.apache.kafka.common.errors.TopicExistsException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.retry.Retry; @@ -76,7 +79,7 @@ public Mono> loadTopics(KafkaCluster c, List topics) ac.describeTopics(topics).zipWith(ac.getTopicsConfig(topics, false), (descriptions, configs) -> getPartitionOffsets(descriptions, ac).map(offsets -> { - statisticsCache.update(c, descriptions, configs, offsets); + statisticsCache.update(c, descriptions, configs, offsets, clustersProperties); var stats = statisticsCache.get(c); return createList( topics, @@ -465,23 +468,36 @@ public Mono cloneTopic( ); } - public Mono> getTopicsForPagination(KafkaCluster cluster) { + public Mono> getTopicsForPagination(KafkaCluster cluster, String search, Boolean showInternal) { Statistics stats = statisticsCache.get(cluster); - Map topicStates = stats.getClusterState().getTopicStates(); - return filterExisting(cluster, topicStates.keySet()) - .map(lst -> lst.stream() - .map(topicName -> - InternalTopic.from( - topicStates.get(topicName).description(), - topicStates.get(topicName).configs(), - InternalPartitionsOffsets.empty(), - stats.getMetrics(), - Optional.ofNullable(topicStates.get(topicName)) - .map(TopicState::segmentStats).orElse(null), - Optional.ofNullable(topicStates.get(topicName)) - .map(TopicState::partitionsSegmentStats).orElse(null), - clustersProperties.getInternalTopicPrefix() - )).collect(toList())); + ScrapedClusterState clusterState = stats.getClusterState(); + ClustersProperties.FtsProperties fts = clustersProperties.getFts(); + Mono> topics; + + Map topicStates = clusterState.getTopicStates(); + if (fts.isEnabled() && clusterState.getTopicIndex() != null && search != null && !search.isBlank()) { + try { + topics = Mono.just(clusterState.getTopicIndex().find(search, showInternal, null)); + } catch (IOException e) { + topics = Mono.error(e); + } + } else { + topics = Mono.just(new ArrayList<>(topicStates.keySet())); + } + + return topics.flatMap(lst -> filterExisting(cluster, lst)) + .flatMapMany(Flux::fromIterable) + .map(topicName -> InternalTopic.from( + topicStates.get(topicName).description(), + topicStates.get(topicName).configs(), + InternalPartitionsOffsets.empty(), + stats.getMetrics(), + Optional.ofNullable(topicStates.get(topicName)) + .map(TopicState::segmentStats).orElse(null), + Optional.ofNullable(topicStates.get(topicName)) + .map(TopicState::partitionsSegmentStats).orElse(null), + clustersProperties.getInternalTopicPrefix() + )).collectList(); } public Mono>> getActiveProducersState(KafkaCluster cluster, String topic) { 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 c0ee2469a..5b76108ee 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 @@ -15,12 +15,14 @@ import static org.apache.kafka.common.resource.ResourceType.TRANSACTIONAL_ID; import com.google.common.collect.Sets; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.CreateConsumerAclDTO; import io.kafbat.ui.model.CreateProducerAclDTO; import io.kafbat.ui.model.CreateStreamAppAclDTO; import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.service.AdminClientService; import io.kafbat.ui.service.ReactiveAdminClient; +import io.kafbat.ui.service.index.AclBindingNgramFilter; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -32,6 +34,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.acl.AccessControlEntry; import org.apache.kafka.common.acl.AclBinding; +import org.apache.kafka.common.acl.AclBindingFilter; import org.apache.kafka.common.acl.AclOperation; import org.apache.kafka.common.resource.Resource; import org.apache.kafka.common.resource.ResourcePattern; @@ -48,6 +51,7 @@ public class AclsService { private final AdminClientService adminClientService; + private final ClustersProperties clustersProperties; public Mono createAcl(KafkaCluster cluster, AclBinding aclBinding) { return adminClientService.get(cluster) @@ -70,10 +74,28 @@ public Mono deleteAcl(KafkaCluster cluster, AclBinding aclBinding) { public Flux listAcls(KafkaCluster cluster, ResourcePatternFilter filter, String principalSearch) { return adminClientService.get(cluster) - .flatMap(c -> c.listAcls(filter)) - .flatMapIterable(acls -> acls) - .filter(acl -> principalSearch == null || acl.entry().principal().contains(principalSearch)) - .sort(Comparator.comparing(AclBinding::toString)); //sorting to keep stable order on different calls + .flatMap(c -> c.listAcls(filter)) + .flatMapIterable(acls -> acls) + .filter(acl -> principalSearch == null || acl.entry().principal().contains(principalSearch)) + .collectList() + .map(lst -> filter(lst, principalSearch)) + .flatMapMany(Flux::fromIterable) + .sort(Comparator.comparing(AclBinding::toString)); //sorting to keep stable order on different calls + } + + private List filter(List acls, String principalSearch) { + if (principalSearch == null) { + return acls; + } + ClustersProperties.FtsProperties fts = clustersProperties.getFts(); + if (fts.isEnabled()) { + AclBindingNgramFilter filter = new AclBindingNgramFilter(acls, fts.getFilterMinNGram(), fts.getFilterMaxNGram()); + return filter.find(principalSearch); + } else { + return acls.stream() + .filter(acl -> acl.entry().principal().contains(principalSearch)) + .toList(); + } } public Mono getAclAsCsvString(KafkaCluster cluster) { diff --git a/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java new file mode 100644 index 000000000..6d932cc45 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java @@ -0,0 +1,25 @@ +package io.kafbat.ui.service.index; + +import java.util.Collection; +import java.util.List; +import org.apache.kafka.common.acl.AclBinding; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +public class AclBindingNgramFilter extends NgramFilter { + private final List, AclBinding>> bindings; + + public AclBindingNgramFilter(Collection bindings) { + this(bindings, 1, 4); + } + + public AclBindingNgramFilter(Collection bindings, int minNGram, int maxNGram) { + super(minNGram, maxNGram); + this.bindings = bindings.stream().map(g -> Tuples.of(List.of(g.entry().principal()), g)).toList(); + } + + @Override + protected List, AclBinding>> getItems() { + return this.bindings; + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java b/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java index 01ead6217..f9bc6720a 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java @@ -1,19 +1,25 @@ package io.kafbat.ui.service.index; -import io.kafbat.ui.model.InternalConsumerGroup; +import java.util.Collection; import java.util.List; +import org.apache.kafka.clients.admin.ConsumerGroupListing; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; -public class ConsumerGroupFilter extends NgramFilter { - private final List> groups; +public class ConsumerGroupFilter extends NgramFilter { + private final List, ConsumerGroupListing>> groups; - public ConsumerGroupFilter(List groups) { - this.groups = groups.stream().map(g -> Tuples.of(g.getGroupId(), g)).toList(); + public ConsumerGroupFilter(Collection groups) { + this(groups, 1, 4); + } + + public ConsumerGroupFilter(Collection groups, int minNGram, int maxNGram) { + super(minNGram, maxNGram); + this.groups = groups.stream().map(g -> Tuples.of(List.of(g.groupId()), g)).toList(); } @Override - protected List> getItems() { + protected List, ConsumerGroupListing>> getItems() { return this.groups; } } diff --git a/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java new file mode 100644 index 000000000..5c9e4295c --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java @@ -0,0 +1,36 @@ +package io.kafbat.ui.service.index; + +import io.kafbat.ui.model.FullConnectorInfoDTO; +import java.util.Collection; +import java.util.List; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +public class KafkaConnectNgramFilter extends NgramFilter { + private final List, FullConnectorInfoDTO>> connectors; + + public KafkaConnectNgramFilter(Collection connectors) { + this(connectors, 1, 4); + } + + public KafkaConnectNgramFilter(Collection connectors, int minNGram, int maxNGram) { + super(minNGram, maxNGram); + this.connectors = connectors.stream().map(this::getItem).toList(); + } + + private Tuple2, FullConnectorInfoDTO> getItem(FullConnectorInfoDTO connector) { + return Tuples.of( + List.of( + connector.getName(), + connector.getConnect(), + connector.getStatus().getState().getValue(), + connector.getType().getValue() + ), connector + ); + } + + @Override + protected List, FullConnectorInfoDTO>> getItems() { + return this.connectors; + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java index 609cea5b0..1caaae227 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java @@ -13,27 +13,37 @@ import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; @Slf4j public abstract class NgramFilter { - private final Analyzer analyzer = new ShortWordNGramAnalyzer(1, 4, false); + private final Analyzer analyzer; + + public NgramFilter(int minGram, int maxGram) { + this.analyzer = new ShortWordNGramAnalyzer(minGram, maxGram, false); + } + + protected abstract List, T>> getItems(); - protected abstract List> getItems(); private static Map> cache = new ConcurrentHashMap<>(); public List find(String search) { try { + if (search == null || search.isBlank()) { + return getItems().stream().map(Tuple2::getT2).toList(); + } List> result = new ArrayList<>(); List queryTokens = tokenizeString(analyzer, search); Map queryFreq = termFreq(queryTokens); - for (Tuple2 item : getItems()) { - List itemTokens = tokenizeString(analyzer, item.getT1()); - HashSet itemTokensSet = new HashSet<>(itemTokens); - if (itemTokensSet.containsAll(queryTokens)) { - double score = cosineSimilarity(queryFreq, itemTokens); - result.add(new SearchResult(item.getT2(), score)); -// result.add(new SearchResult(item.getT2(), 1)); + for (Tuple2, T> item : getItems()) { + for (String field : item.getT1()) { + List itemTokens = tokenizeString(analyzer, field); + HashSet itemTokensSet = new HashSet<>(itemTokens); + if (itemTokensSet.containsAll(queryTokens)) { + double score = cosineSimilarity(queryFreq, itemTokens); + result.add(new SearchResult(item.getT2(), score)); + } } } result.sort((o1, o2) -> Double.compare(o2.score, o1.score)); @@ -43,7 +53,8 @@ public List find(String search) { } } - private record SearchResult(T item, double score) { } + private record SearchResult(T item, double score) { + } public static List tokenizeString(Analyzer analyzer, String text) throws IOException { diff --git a/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java b/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java new file mode 100644 index 000000000..3c9890b49 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java @@ -0,0 +1,41 @@ +package io.kafbat.ui.service.index; + +import static org.apache.commons.lang3.Strings.CI; + +import java.util.Collection; +import java.util.List; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +public class SchemasFilter extends NgramFilter { + private final List, String>> subjects; + private final boolean fts; + + public SchemasFilter(Collection subjects) { + this(subjects, 1, 4, true); + } + + public SchemasFilter(Collection subjects, int minNGram, int maxNGram, boolean fts) { + super(minNGram, maxNGram); + this.subjects = subjects.stream().map(g -> Tuples.of(List.of(g), g)).toList(); + this.fts = fts; + } + + @Override + protected List, String>> getItems() { + return this.subjects; + } + + @Override + public List find(String search) { + if (fts) { + return super.find(search); + } else { + return this.subjects + .stream() + .map(Tuple2::getT2) + .filter(subj -> search == null || CI.contains(subj, search)) + .sorted().toList(); + } + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java b/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java index 568df366f..5013edee6 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java +++ b/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java @@ -24,17 +24,15 @@ public ShortWordNGramAnalyzer(int minGram, int maxGram, boolean preserveOriginal } - @Override protected TokenStreamComponents createComponents(String fieldName) { Tokenizer tokenizer = new StandardTokenizer(); TokenStream tokenStream = new WordDelimiterGraphFilter( tokenizer, - WordDelimiterGraphFilter.GENERATE_WORD_PARTS | - WordDelimiterGraphFilter.SPLIT_ON_CASE_CHANGE | - //WordDelimiterGraphFilter.SPLIT_ON_NUMERICS | - WordDelimiterGraphFilter.STEM_ENGLISH_POSSESSIVE, + WordDelimiterGraphFilter.GENERATE_WORD_PARTS + | WordDelimiterGraphFilter.SPLIT_ON_CASE_CHANGE + | WordDelimiterGraphFilter.STEM_ENGLISH_POSSESSIVE, null ); diff --git a/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java index 9337a65e1..91166eaa6 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java +++ b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java @@ -5,6 +5,9 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; @@ -43,9 +46,11 @@ public class TopicsIndex implements AutoCloseable { private final DirectoryReader indexReader; private final IndexSearcher indexSearcher; private final Analyzer analyzer; + private final int maxSize; + private final ReadWriteLock closeLock = new ReentrantReadWriteLock(); public TopicsIndex(List topics) throws IOException { - this(topics, 3,5); + this(topics, 3, 5); } public TopicsIndex(List topics, int minNgram, int maxNgram) throws IOException { @@ -53,11 +58,12 @@ public TopicsIndex(List topics, int minNgram, int maxNgram) throw this.directory = build(topics); this.indexReader = DirectoryReader.open(directory); this.indexSearcher = new IndexSearcher(indexReader); + this.maxSize = topics.size(); } private Directory build(List topics) { Directory directory = new ByteBuffersDirectory(); - try(IndexWriter directoryWriter = new IndexWriter(directory, new IndexWriterConfig(this.analyzer))) { + try (IndexWriter directoryWriter = new IndexWriter(directory, new IndexWriterConfig(this.analyzer))) { for (InternalTopic topic : topics) { Document doc = new Document(); doc.add(new StringField(FIELD_NAME_RAW, topic.getName(), Field.Store.YES)); @@ -67,7 +73,8 @@ private Directory build(List topics) { doc.add(new LongPoint(FIELD_SIZE, topic.getSegmentSize())); if (topic.getTopicConfigs() != null && !topic.getTopicConfigs().isEmpty()) { for (InternalTopicConfig topicConfig : topic.getTopicConfigs()) { - doc.add(new StringField(FIELD_CONFIG_PREFIX+"_"+topicConfig.getName(), topicConfig.getValue(), Field.Store.NO)); + doc.add(new StringField(FIELD_CONFIG_PREFIX + "_" + topicConfig.getName(), topicConfig.getValue(), + Field.Store.NO)); } } doc.add(new StringField(FIELD_INTERNAL, String.valueOf(topic.isInternal()), Field.Store.NO)); @@ -81,57 +88,69 @@ private Directory build(List topics) { @Override public void close() throws Exception { - if (indexReader != null) { - this.indexReader.close(); - } - if (this.directory != null) { - this.directory.close(); + this.closeLock.writeLock().lock(); + try { + if (indexReader != null) { + this.indexReader.close(); + } + if (this.directory != null) { + this.directory.close(); + } + } finally { + this.closeLock.writeLock().unlock(); } } - public List find(String search, Boolean showInternal, int count) throws IOException { - return find(search, showInternal, FIELD_NAME, count, 0.0f, 2); + public List find(String search, Boolean showInternal, Integer count) throws IOException { + return find(search, showInternal, FIELD_NAME, count, 0.0f); } - public List find(String search, Boolean showInternal, String sort, int count) throws IOException { - return find(search, showInternal, sort, count, 0.0f, 2); + public List find(String search, Boolean showInternal, String sort, Integer count) throws IOException { + return find(search, showInternal, sort, count, 0.0f); } - public List find(String search, Boolean showInternal, String sortField, int count, float minScore, int maxEdits) throws IOException { - QueryParser queryParser = new QueryParser(FIELD_NAME, this.analyzer); - queryParser.setDefaultOperator(QueryParser.Operator.AND); - Query nameQuery = null; + public List find(String search, Boolean showInternal, + String sortField, Integer count, float minScore) throws IOException { + closeLock.readLock().lock(); try { - nameQuery = queryParser.parse(search); - } catch (ParseException e) { - throw new RuntimeException(e); - } + QueryParser queryParser = new QueryParser(FIELD_NAME, this.analyzer); + queryParser.setDefaultOperator(QueryParser.Operator.AND); + Query nameQuery; + try { + nameQuery = queryParser.parse(search); + } catch (ParseException e) { + throw new RuntimeException(e); + } - Query internalFilter = new TermQuery(new Term(FIELD_INTERNAL, "true")); + Query internalFilter = new TermQuery(new Term(FIELD_INTERNAL, "true")); - BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); - queryBuilder.add(nameQuery, BooleanClause.Occur.MUST); - if (showInternal == null || !showInternal) { - queryBuilder.add(internalFilter, BooleanClause.Occur.MUST_NOT); - } + BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); + queryBuilder.add(nameQuery, BooleanClause.Occur.MUST); + if (showInternal == null || !showInternal) { + queryBuilder.add(internalFilter, BooleanClause.Occur.MUST_NOT); + } - List sortFields = new ArrayList<>(); - sortFields.add(SortField.FIELD_SCORE); - if (!sortField.equals(FIELD_NAME)) { - sortFields.add(new SortField(sortField, SortField.Type.INT, true)); - } + List sortFields = new ArrayList<>(); + sortFields.add(SortField.FIELD_SCORE); + if (!sortField.equals(FIELD_NAME)) { + sortFields.add(new SortField(sortField, SortField.Type.INT, true)); + } - Sort sort = new Sort(sortFields.toArray(new SortField[0])); + Sort sort = new Sort(sortFields.toArray(new SortField[0])); - TopDocs result = this.indexSearcher.search(queryBuilder.build(), count); + TopDocs result = this.indexSearcher.search(queryBuilder.build(), count != null ? count : this.maxSize, sort); - List topics = new ArrayList<>(); - for (ScoreDoc scoreDoc : result.scoreDocs) { - if (scoreDoc.score > minScore) { + List topics = new ArrayList<>(); + for (ScoreDoc scoreDoc : result.scoreDocs) { + if (minScore > 0.00001f && scoreDoc.score < minScore) { + continue; + } Document document = this.indexSearcher.storedFields().document(scoreDoc.doc); topics.add(document.get(FIELD_NAME_RAW)); } + return topics; + } finally { + this.closeLock.readLock().unlock(); } - return topics; } } diff --git a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java index 17d64faa8..8bcce0b81 100644 --- a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java +++ b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java @@ -5,12 +5,13 @@ import static io.kafbat.ui.service.ReactiveAdminClient.ClusterDescription; import com.google.common.collect.Table; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.InternalLogDirStats; import io.kafbat.ui.model.InternalPartitionsOffsets; +import io.kafbat.ui.model.InternalTopic; import io.kafbat.ui.service.ReactiveAdminClient; import io.kafbat.ui.service.index.TopicsIndex; import jakarta.annotation.Nullable; -import java.io.Closeable; import java.time.Instant; import java.util.HashMap; import java.util.List; @@ -21,6 +22,7 @@ import lombok.Builder; import lombok.RequiredArgsConstructor; import lombok.Value; +import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.ConfigEntry; import org.apache.kafka.clients.admin.ConsumerGroupDescription; import org.apache.kafka.clients.admin.ConsumerGroupListing; @@ -33,18 +35,19 @@ @Builder(toBuilder = true) @RequiredArgsConstructor @Value +@Slf4j public class ScrapedClusterState implements AutoCloseable { Instant scrapeFinishedAt; Map nodesStates; Map topicStates; Map consumerGroupsStates; - TopicsIndex topicsIndex; + TopicsIndex topicIndex; @Override public void close() throws Exception { - if (this.topicsIndex != null) { - this.topicsIndex.close(); + if (this.topicIndex != null) { + this.topicIndex.close(); } } @@ -81,7 +84,8 @@ public static ScrapedClusterState empty() { public ScrapedClusterState updateTopics(Map descriptions, Map> configs, - InternalPartitionsOffsets partitionsOffsets) { + InternalPartitionsOffsets partitionsOffsets, + ClustersProperties clustersProperties) { var updatedTopicStates = new HashMap<>(topicStates); descriptions.forEach((topic, description) -> { SegmentStats segmentStats = null; @@ -103,9 +107,12 @@ public ScrapedClusterState updateTopics(Map descriptio ) ); }); - return toBuilder() + + ScrapedClusterState state = toBuilder() .topicStates(updatedTopicStates) + .topicIndex(buildTopicIndex(clustersProperties, updatedTopicStates)) .build(); + return state; } public ScrapedClusterState topicDeleted(String topic) { @@ -117,7 +124,7 @@ public ScrapedClusterState topicDeleted(String topic) { } public static Mono scrape(ClusterDescription clusterDescription, - ReactiveAdminClient ac) { + ReactiveAdminClient ac, ClustersProperties clustersProperties) { return Mono.zip( ac.describeLogDirs(clusterDescription.getNodes().stream().map(Node::id).toList()) .map(InternalLogDirStats::new), @@ -136,7 +143,8 @@ public static Mono scrape(ClusterDescription clusterDescrip phase1.getT1(), topicStateMap(phase1.getT1(), phase1.getT3(), phase1.getT4(), phase2.getT1(), phase2.getT2()), phase2.getT3(), - phase2.getT4() + phase2.getT4(), + clustersProperties ))); } @@ -167,7 +175,8 @@ private static ScrapedClusterState create(ClusterDescription clusterDescription, InternalLogDirStats segmentStats, Map topicStates, Map consumerDescriptions, - Table consumerOffsets) { + Table consumerOffsets, + ClustersProperties clustersProperties) { Map consumerGroupsStates = new HashMap<>(); consumerDescriptions.forEach((name, desc) -> @@ -194,10 +203,27 @@ private static ScrapedClusterState create(ClusterDescription clusterDescription, Instant.now(), nodesStates, topicStates, - consumerGroupsStates + consumerGroupsStates, + buildTopicIndex(clustersProperties, topicStates) ); } + private static TopicsIndex buildTopicIndex(ClustersProperties clustersProperties, + Map topicStates) { + ClustersProperties.FtsProperties fts = clustersProperties.getFts(); + TopicsIndex topicsIndex = null; + if (fts.isEnabled()) { + try { + return new TopicsIndex(topicStates.values().stream().map( + topicState -> buildInternalTopic(topicState, clustersProperties) + ).toList(), fts.getTopicsMinNGram(), fts.getTopicsMaxNGram()); + } catch (Exception e) { + log.error("Error creating topics index", e); + } + } + return null; + } + private static Map filterTopic(String topicForFilter, Map tpMap) { return tpMap.entrySet() .stream() @@ -205,5 +231,17 @@ private static Map filterTopic(String topicForFilter, Map e.getKey().partition(), Map.Entry::getValue)); } + private static InternalTopic buildInternalTopic(TopicState state, ClustersProperties clustersProperties) { + return InternalTopic.from( + state.description(), + state.configs(), + InternalPartitionsOffsets.empty(), + null, + state.segmentStats(), + state.partitionsSegmentStats(), + clustersProperties.getInternalTopicPrefix() + ); + } + } diff --git a/api/src/main/resources/application-localtest.yaml b/api/src/main/resources/application-localtest.yaml index 91b266390..89550e4e6 100644 --- a/api/src/main/resources/application-localtest.yaml +++ b/api/src/main/resources/application-localtest.yaml @@ -10,6 +10,8 @@ kafka: - name: local bootstrapServers: localhost:9092 schemaRegistry: http://localhost:8085 + fts: + enabled: true dynamic.config.enabled: true diff --git a/api/src/test/java/io/kafbat/ui/service/SchemaRegistryPaginationTest.java b/api/src/test/java/io/kafbat/ui/service/SchemaRegistryPaginationTest.java index 43cb29382..8b77e7cdb 100644 --- a/api/src/test/java/io/kafbat/ui/service/SchemaRegistryPaginationTest.java +++ b/api/src/test/java/io/kafbat/ui/service/SchemaRegistryPaginationTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.controller.SchemasController; import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.SchemaSubjectDTO; @@ -43,7 +44,7 @@ private void init(List subjects) { new SchemaRegistryService.SubjectWithCompatibilityLevel( new SchemaSubject().subject(a.getArgument(1)), Compatibility.FULL))); - this.controller = new SchemasController(schemaRegistryService); + this.controller = new SchemasController(schemaRegistryService, new ClustersProperties()); this.controller.setAccessControlService(new AccessControlServiceMock().getMock()); this.controller.setAuditService(mock(AuditService.class)); this.controller.setClustersStorage(clustersStorage); diff --git a/api/src/test/java/io/kafbat/ui/service/TopicsServicePaginationTest.java b/api/src/test/java/io/kafbat/ui/service/TopicsServicePaginationTest.java index 3fb1a804f..44b2fad38 100644 --- a/api/src/test/java/io/kafbat/ui/service/TopicsServicePaginationTest.java +++ b/api/src/test/java/io/kafbat/ui/service/TopicsServicePaginationTest.java @@ -1,11 +1,14 @@ package io.kafbat.ui.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.controller.TopicsController; import io.kafbat.ui.mapper.ClusterMapper; import io.kafbat.ui.mapper.ClusterMapperImpl; @@ -34,6 +37,7 @@ import org.apache.kafka.clients.admin.TopicDescription; import org.apache.kafka.common.TopicPartitionInfo; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import reactor.core.publisher.Mono; @@ -44,17 +48,20 @@ class TopicsServicePaginationTest { private final TopicsService topicsService = Mockito.mock(TopicsService.class); private final ClustersStorage clustersStorage = Mockito.mock(ClustersStorage.class); private final ClusterMapper clusterMapper = new ClusterMapperImpl(); + private final ClustersProperties clustersProperties = new ClustersProperties(); private final AccessControlService accessControlService = new AccessControlServiceMock().getMock(); private final TopicsController topicsController = - new TopicsController(topicsService, mock(TopicAnalysisService.class), clusterMapper); + new TopicsController(topicsService, mock(TopicAnalysisService.class), clusterMapper, clustersProperties); private void init(Map topicsInCache) { when(clustersStorage.getClusterByName(isA(String.class))) .thenReturn(Optional.of(buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME))); - when(topicsService.getTopicsForPagination(isA(KafkaCluster.class))) + when(topicsService.getTopicsForPagination(isA(KafkaCluster.class), any(), any())) .thenReturn(Mono.just(new ArrayList<>(topicsInCache.values()))); + + when(topicsService.loadTopics(isA(KafkaCluster.class), anyList())) .thenAnswer(a -> { List lst = a.getArgument(1); 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 109683014..517f3ce7d 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 @@ -4,6 +4,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.CreateConsumerAclDTO; import io.kafbat.ui.model.CreateProducerAclDTO; import io.kafbat.ui.model.CreateStreamAppAclDTO; @@ -34,7 +35,7 @@ class AclsServiceTest { private final ReactiveAdminClient adminClientMock = mock(ReactiveAdminClient.class); private final AdminClientService adminClientService = mock(AdminClientService.class); - private final AclsService aclsService = new AclsService(adminClientService); + private final AclsService aclsService = new AclsService(adminClientService, new ClustersProperties()); @BeforeEach void initMocks() { diff --git a/api/src/test/java/io/kafbat/ui/service/index/ConsumerGroupsFilterTest.java b/api/src/test/java/io/kafbat/ui/service/index/ConsumerGroupsFilterTest.java index 948e65b65..6410007c0 100644 --- a/api/src/test/java/io/kafbat/ui/service/index/ConsumerGroupsFilterTest.java +++ b/api/src/test/java/io/kafbat/ui/service/index/ConsumerGroupsFilterTest.java @@ -5,6 +5,7 @@ import io.kafbat.ui.model.InternalConsumerGroup; import java.util.List; import java.util.Map; +import org.apache.kafka.clients.admin.ConsumerGroupListing; import org.junit.jupiter.api.Test; class ConsumerGroupsFilterTest { @@ -21,8 +22,8 @@ class ConsumerGroupsFilterTest { @Test void testFindTopicsByName() throws Exception { - List groups = - names.stream().map(n -> InternalConsumerGroup.builder().groupId(n).build()).toList(); + List groups = + names.stream().map(n -> new ConsumerGroupListing(n, true)).toList(); ConsumerGroupFilter filter = new ConsumerGroupFilter(groups); @@ -35,7 +36,7 @@ void testFindTopicsByName() throws Exception { ); for (Map.Entry entry : tests.entrySet()) { - List result = filter.find(entry.getKey()); + List result = filter.find(entry.getKey()); assertThat(result).size() .withFailMessage("Expected %d results for '%s', but got %s", entry.getValue(), entry.getKey(), result) .isEqualTo(entry.getValue()); diff --git a/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java b/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java index 7ba484a4b..cadea9edf 100644 --- a/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java +++ b/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java @@ -60,7 +60,7 @@ void testFindTopicsByName() throws Exception { ); SoftAssertions softly = new SoftAssertions(); - try(TopicsIndex index = new TopicsIndex(topics)) { + try (TopicsIndex index = new TopicsIndex(topics)) { for (Map.Entry entry : examples.entrySet()) { List resultAll = index.find(entry.getKey(), null, topics.size()); softly.assertThat(resultAll.size()) diff --git a/api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java b/api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java index 6cbcf96c5..eaa1d9cbc 100644 --- a/api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java +++ b/api/src/test/java/io/kafbat/ui/service/mcp/McpSpecificationGeneratorTest.java @@ -8,6 +8,7 @@ import com.github.victools.jsonschema.generator.SchemaGenerator; import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; import com.github.victools.jsonschema.generator.SchemaVersion; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.controller.TopicsController; import io.kafbat.ui.mapper.ClusterMapper; import io.kafbat.ui.model.SortOrderDTO; @@ -35,7 +36,8 @@ private static SchemaGenerator schemaGenerator() { @Test void testConvertController() { TopicsController topicsController = new TopicsController( - mock(TopicsService.class), mock(TopicAnalysisService.class), mock(ClusterMapper.class) + mock(TopicsService.class), mock(TopicAnalysisService.class), mock(ClusterMapper.class), + mock(ClustersProperties.class) ); List specifications = MCP_SPECIFICATION_GENERATOR.convertTool(topicsController); From 6da326960f8b0cc80523009b6e41936c05ae3984 Mon Sep 17 00:00:00 2001 From: German Osin Date: Thu, 21 Aug 2025 19:20:54 +0200 Subject: [PATCH 26/36] Ngram config and toics optimizations --- .../kafbat/ui/config/ClustersProperties.java | 1 + .../ui/service/index/ShortWordAnalyzer.java | 33 ++++++++++++ .../kafbat/ui/service/index/TopicsIndex.java | 53 +++++++++++++++---- .../metrics/scrape/ScrapedClusterState.java | 2 +- .../ui/service/index/TopicsIndexTest.java | 32 ++++++----- 5 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 api/src/main/java/io/kafbat/ui/service/index/ShortWordAnalyzer.java diff --git a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java index ed6ee387d..ff7c827b8 100644 --- a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java @@ -223,6 +223,7 @@ public static class CacheProperties { @AllArgsConstructor public static class FtsProperties { boolean enabled = false; + boolean topicsNgramEnabled = false; int topicsMinNGram = 3; int topicsMaxNGram = 5; int filterMinNGram = 1; diff --git a/api/src/main/java/io/kafbat/ui/service/index/ShortWordAnalyzer.java b/api/src/main/java/io/kafbat/ui/service/index/ShortWordAnalyzer.java new file mode 100644 index 000000000..795187a46 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/ShortWordAnalyzer.java @@ -0,0 +1,33 @@ +package io.kafbat.ui.service.index; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LowerCaseFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilter; +import org.apache.lucene.analysis.standard.StandardTokenizer; + +public class ShortWordAnalyzer extends Analyzer { + + public ShortWordAnalyzer() {} + + @Override + protected TokenStreamComponents createComponents(String fieldName) { + Tokenizer tokenizer = new StandardTokenizer(); + + TokenStream tokenStream = new WordDelimiterGraphFilter( + tokenizer, + WordDelimiterGraphFilter.GENERATE_WORD_PARTS + | WordDelimiterGraphFilter.SPLIT_ON_CASE_CHANGE + | WordDelimiterGraphFilter.PRESERVE_ORIGINAL + | WordDelimiterGraphFilter.GENERATE_NUMBER_PARTS + | WordDelimiterGraphFilter.STEM_ENGLISH_POSSESSIVE, + + null + ); + + tokenStream = new LowerCaseFilter(tokenStream); + + return new TokenStreamComponents(tokenizer, tokenStream); + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java index 91166eaa6..fbc8d1293 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java +++ b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java @@ -9,6 +9,7 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.IntPoint; @@ -28,10 +29,12 @@ import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.Sort; import org.apache.lucene.search.SortField; +import org.apache.lucene.search.SynonymQuery; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.ByteBuffersDirectory; import org.apache.lucene.store.Directory; +import org.apache.lucene.util.QueryBuilder; public class TopicsIndex implements AutoCloseable { public static final String FIELD_NAME_RAW = "name_raw"; @@ -48,13 +51,24 @@ public class TopicsIndex implements AutoCloseable { private final Analyzer analyzer; private final int maxSize; private final ReadWriteLock closeLock = new ReentrantReadWriteLock(); + private final boolean searchNgram; public TopicsIndex(List topics) throws IOException { - this(topics, 3, 5); + this(topics, true); } - public TopicsIndex(List topics, int minNgram, int maxNgram) throws IOException { - this.analyzer = new ShortWordNGramAnalyzer(minNgram, maxNgram); + public TopicsIndex(List topics, boolean ngram) throws IOException { + this(topics, ngram, 1, 5); + } + + public TopicsIndex(List topics, boolean ngram, int minNgram, int maxNgram) throws IOException { + if (ngram) { + this.analyzer = new ShortWordNGramAnalyzer(minNgram, maxNgram, false); + } else { + this.analyzer = new ShortWordAnalyzer(); + } + + this.searchNgram = ngram; this.directory = build(topics); this.indexReader = DirectoryReader.open(directory); this.indexSearcher = new IndexSearcher(indexReader); @@ -113,13 +127,34 @@ public List find(String search, Boolean showInternal, String sortField, Integer count, float minScore) throws IOException { closeLock.readLock().lock(); try { - QueryParser queryParser = new QueryParser(FIELD_NAME, this.analyzer); - queryParser.setDefaultOperator(QueryParser.Operator.AND); Query nameQuery; - try { - nameQuery = queryParser.parse(search); - } catch (ParseException e) { - throw new RuntimeException(e); + if (this.searchNgram) { + List ngrams = NgramFilter.tokenizeStringSimple(this.analyzer, search); + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (String ng : ngrams) { + builder.add(new TermQuery(new Term(FIELD_NAME, ng)), BooleanClause.Occur.MUST); + } + nameQuery = builder.build(); + } else { + QueryParser queryParser = new QueryParser(FIELD_NAME, this.analyzer); + queryParser.setDefaultOperator(QueryParser.Operator.AND); + + try { + nameQuery = queryParser.parse(search); + + if (!search.contains(" ") && !search.contains("*")) { + String wildcardSearch = search + "*"; + Query wildCardNameQuery = queryParser.parse(wildcardSearch); + BooleanQuery.Builder withWildcard = new BooleanQuery.Builder(); + withWildcard.add(nameQuery, BooleanClause.Occur.SHOULD); + withWildcard.add(wildCardNameQuery, BooleanClause.Occur.SHOULD); + nameQuery = withWildcard.build(); + } + + + } catch (Exception e) { + throw new RuntimeException(e); + } } Query internalFilter = new TermQuery(new Term(FIELD_INTERNAL, "true")); diff --git a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java index 8bcce0b81..62c94971f 100644 --- a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java +++ b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java @@ -216,7 +216,7 @@ private static TopicsIndex buildTopicIndex(ClustersProperties clustersProperties try { return new TopicsIndex(topicStates.values().stream().map( topicState -> buildInternalTopic(topicState, clustersProperties) - ).toList(), fts.getTopicsMinNGram(), fts.getTopicsMaxNGram()); + ).toList(), fts.isTopicsNgramEnabled(), fts.getTopicsMinNGram(), fts.getTopicsMaxNGram()); } catch (Exception e) { log.error("Error creating topics index", e); } diff --git a/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java b/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java index cadea9edf..2dc701ff1 100644 --- a/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java +++ b/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java @@ -1,10 +1,9 @@ package io.kafbat.ui.service.index; -import static org.assertj.core.api.Assertions.assertThat; - import io.kafbat.ui.model.InternalTopic; import io.kafbat.ui.model.InternalTopicConfig; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -28,7 +27,7 @@ void testFindTopicsByName() throws Exception { "audit.clients.state", "audit.clients.repartitioned.status", "reporting.payments.by.clientId", - "reporting.payments.by.currencyid" + "reporting.payments.by.currencyId" ) .map(s -> InternalTopic.builder().name(s).partitions(Map.of()).build()).toList()); @@ -46,17 +45,14 @@ void testFindTopicsByName() throws Exception { Map.entry("topic", testTopicsCount), Map.entry("8", 1), Map.entry("9", 0), - Map.entry("tpic", testTopicsCount), - Map.entry("dogs red", 1), - Map.entry("tpic-1", 1), - Map.entry("payments dlq", 1), - Map.entry("paymnts dlq", 1), + Map.entry("dog red", 1), + Map.entry("topic-1", 1), + Map.entry("payment dlq", 1), Map.entry("stats dlq", 0), Map.entry("stat", 3), - Map.entry("chnges", 1), - Map.entry("comands", 1), - Map.entry("id", 1), - Map.entry("config_retention:compact", 1) + Map.entry("changes", 1), + Map.entry("commands", 1), + Map.entry("id", 2) ); SoftAssertions softly = new SoftAssertions(); @@ -68,6 +64,18 @@ void testFindTopicsByName() throws Exception { .isEqualTo(entry.getValue()); } } + + HashMap indexExamples = new HashMap<>(examples); + indexExamples.put("config_retention:compact", 1); + + try (TopicsIndex index = new TopicsIndex(topics, false)) { + for (Map.Entry entry : indexExamples.entrySet()) { + List resultAll = index.find(entry.getKey(), null, topics.size()); + softly.assertThat(resultAll.size()) + .withFailMessage("Expected %d results for '%s', but got %s", entry.getValue(), entry.getKey(), resultAll) + .isEqualTo(entry.getValue()); + } + } softly.assertAll(); } From df6a36485c7ba011367ec39dd80d55e93caf3870 Mon Sep 17 00:00:00 2001 From: German Osin Date: Fri, 29 Aug 2025 15:12:00 +0200 Subject: [PATCH 27/36] Refactoring --- api/build.gradle | 3 - .../kafbat/ui/config/ClustersProperties.java | 21 +- .../ui/controller/SchemasController.java | 6 +- .../ui/controller/TopicsController.java | 12 +- .../io/kafbat/ui/model/InternalTopic.java | 10 + .../ui/service/ConsumerGroupService.java | 16 +- .../ui/service/KafkaConnectService.java | 19 +- .../io/kafbat/ui/service/TopicsService.java | 42 ++-- .../io/kafbat/ui/service/acl/AclsService.java | 16 +- .../service/index/AclBindingNgramFilter.java | 10 +- .../ui/service/index/ConsumerGroupFilter.java | 10 +- .../ui/service/index/FilterTopicIndex.java | 31 +++ .../index/KafkaConnectNgramFilter.java | 10 +- .../ui/service/index/LuceneTopicsIndex.java | 187 +++++++++++++++++ .../kafbat/ui/service/index/NgramFilter.java | 50 +++-- .../ui/service/index/SchemasFilter.java | 24 +-- .../ui/service/index/ShortWordAnalyzer.java | 2 +- .../service/index/ShortWordNGramAnalyzer.java | 2 +- .../kafbat/ui/service/index/TopicsIndex.java | 192 +----------------- .../metrics/scrape/ScrapedClusterState.java | 17 +- .../service/TopicsServicePaginationTest.java | 70 ++++++- .../ui/service/index/TopicsIndexTest.java | 10 +- 22 files changed, 419 insertions(+), 341 deletions(-) create mode 100644 api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java create mode 100644 api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java diff --git a/api/build.gradle b/api/build.gradle index 48a920447..dfbbf8201 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -72,9 +72,6 @@ dependencies { // CVE Fixes implementation libs.apache.commons.compress implementation libs.okhttp3.logging.intercepter - implementation libs.reactor.netty.http - implementation libs.netty.codec.http2 - // CVE Fixes End implementation libs.modelcontextprotocol.spring.webflux implementation libs.victools.jsonschema.generator diff --git a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java index ff7c827b8..664ea46c5 100644 --- a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java @@ -41,7 +41,7 @@ public class ClustersProperties { MetricsStorage defaultMetricsStorage = new MetricsStorage(); CacheProperties cache = new CacheProperties(); - FtsProperties fts = new FtsProperties(); + ClusterFtsProperties fts = new ClusterFtsProperties(); @Data public static class Cluster { @@ -222,12 +222,21 @@ public static class CacheProperties { @NoArgsConstructor @AllArgsConstructor public static class FtsProperties { + boolean ngram = true; + int ngramMin = 1; + int ngramMax = 4; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ClusterFtsProperties { boolean enabled = false; - boolean topicsNgramEnabled = false; - int topicsMinNGram = 3; - int topicsMaxNGram = 5; - int filterMinNGram = 1; - int filterMaxNGram = 4; + FtsProperties topics = new FtsProperties(true, 3, 5); + FtsProperties schemas = new FtsProperties(true, 1, 4); + FtsProperties consumers = new FtsProperties(true, 1, 4); + FtsProperties connect = new FtsProperties(true, 1, 4); + FtsProperties acl = new FtsProperties(true, 1, 4); } @PostConstruct diff --git a/api/src/main/java/io/kafbat/ui/controller/SchemasController.java b/api/src/main/java/io/kafbat/ui/controller/SchemasController.java index b6bd2bc93..4df01cacb 100644 --- a/api/src/main/java/io/kafbat/ui/controller/SchemasController.java +++ b/api/src/main/java/io/kafbat/ui/controller/SchemasController.java @@ -214,7 +214,7 @@ public Mono> getSchemas(String cluster .operationName("getSchemas") .build(); - ClustersProperties.FtsProperties fts = clustersProperties.getFts(); + ClustersProperties.ClusterFtsProperties fts = clustersProperties.getFts(); return schemaRegistryService .getAllSubjectNames(getCluster(clusterName)) @@ -225,9 +225,7 @@ public Mono> getSchemas(String cluster int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE; int subjectToSkip = ((pageNum != null && pageNum > 0 ? pageNum : 1) - 1) * pageSize; - SchemasFilter filter = - new SchemasFilter(subjects, fts.getFilterMinNGram(), fts.getFilterMaxNGram(), fts.isEnabled()); - + SchemasFilter filter = new SchemasFilter(subjects, fts.isEnabled(), fts.getSchemas()); List filteredSubjects = filter.find(search); var totalPages = (filteredSubjects.size() / pageSize) 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 178ef3862..4ad3f11e9 100644 --- a/api/src/main/java/io/kafbat/ui/controller/TopicsController.java +++ b/api/src/main/java/io/kafbat/ui/controller/TopicsController.java @@ -38,7 +38,6 @@ import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; @@ -188,19 +187,12 @@ public Mono> getTopics(String clusterName, .flatMap(topics -> { int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE; var topicsToSkip = ((page != null && page > 0 ? page : 1) - 1) * pageSize; - ClustersProperties.FtsProperties fts = clustersProperties.getFts(); + ClustersProperties.ClusterFtsProperties fts = clustersProperties.getFts(); Comparator comparatorForTopic = getComparatorForTopic(orderBy, fts.isEnabled()); var comparator = sortOrder == null || !sortOrder.equals(SortOrderDTO.DESC) ? comparatorForTopic : comparatorForTopic.reversed(); - List filtered = fts.isEnabled() ? topics : topics.stream() - .filter(topic -> !topic.isInternal() - || showInternal != null && showInternal) - .filter( - topic -> search == null || CI.contains(topic.getName(), search) - ) - .sorted(comparator) - .toList(); + List filtered = topics.stream().sorted(comparator).toList(); var totalPages = (filtered.size() / pageSize) + (filtered.size() % pageSize == 0 ? 0 : 1); diff --git a/api/src/main/java/io/kafbat/ui/model/InternalTopic.java b/api/src/main/java/io/kafbat/ui/model/InternalTopic.java index 51a79e906..5dfab7c42 100644 --- a/api/src/main/java/io/kafbat/ui/model/InternalTopic.java +++ b/api/src/main/java/io/kafbat/ui/model/InternalTopic.java @@ -38,6 +38,16 @@ public class InternalTopic { private final long segmentSize; private final long segmentCount; + + public InternalTopic withMetrics(Metrics metrics) { + var builder = toBuilder(); + if (metrics != null) { + builder.bytesInPerSec(metrics.getIoRates().topicBytesInPerSec().get(this.name)); + builder.bytesOutPerSec(metrics.getIoRates().topicBytesOutPerSec().get(this.name)); + } + return builder.build(); + } + public static InternalTopic from(TopicDescription topicDescription, List configs, InternalPartitionsOffsets partitionsOffsets, 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 0ddb06aff..1f610ff2c 100644 --- a/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java +++ b/api/src/main/java/io/kafbat/ui/service/ConsumerGroupService.java @@ -1,7 +1,5 @@ package io.kafbat.ui.service; -import static org.apache.commons.lang3.Strings.CI; - import com.google.common.collect.Streams; import com.google.common.collect.Table; import io.kafbat.ui.config.ClustersProperties; @@ -27,7 +25,6 @@ import java.util.stream.Stream; import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.apache.kafka.clients.admin.ConsumerGroupDescription; import org.apache.kafka.clients.admin.ConsumerGroupListing; import org.apache.kafka.clients.admin.OffsetSpec; @@ -131,16 +128,9 @@ public Mono getConsumerGroupsPage( } private Collection filterGroups(Collection groups, String search) { - if (search == null || search.isBlank()) { - return groups; - } - ClustersProperties.FtsProperties fts = clustersProperties.getFts(); - if (fts.isEnabled()) { - ConsumerGroupFilter filter = new ConsumerGroupFilter(groups, fts.getFilterMinNGram(), fts.getFilterMaxNGram()); - return filter.find(search); - } else { - return groups.stream().filter(g -> CI.contains(g.groupId(), search)).toList(); - } + ClustersProperties.ClusterFtsProperties fts = clustersProperties.getFts(); + ConsumerGroupFilter filter = new ConsumerGroupFilter(groups, fts.isEnabled(), fts.getConsumers()); + return filter.find(search, false); } private Mono> loadSortedDescriptions(ReactiveAdminClient ac, diff --git a/api/src/main/java/io/kafbat/ui/service/KafkaConnectService.java b/api/src/main/java/io/kafbat/ui/service/KafkaConnectService.java index a77ed99c1..2d6dfae15 100644 --- a/api/src/main/java/io/kafbat/ui/service/KafkaConnectService.java +++ b/api/src/main/java/io/kafbat/ui/service/KafkaConnectService.java @@ -158,21 +158,10 @@ public Flux getAllConnectors(final KafkaCluster cluster, } private List filterConnectors(List connectors, String search) { - if (search == null) { - return connectors; - } - ClustersProperties.FtsProperties fts = clustersProperties.getFts(); - if (fts.isEnabled()) { - KafkaConnectNgramFilter filter = - new KafkaConnectNgramFilter(connectors, fts.getFilterMinNGram(), fts.getFilterMaxNGram()); - return filter.find(search); - } else { - return connectors.stream() - .filter(connector -> getStringsForSearch(connector) - .anyMatch(string -> CI.contains(string, search))) - .toList(); - } - + ClustersProperties.ClusterFtsProperties fts = clustersProperties.getFts(); + KafkaConnectNgramFilter filter = + new KafkaConnectNgramFilter(connectors, fts.isEnabled(), fts.getConnect()); + return filter.find(search); } private Stream getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) { diff --git a/api/src/main/java/io/kafbat/ui/service/TopicsService.java b/api/src/main/java/io/kafbat/ui/service/TopicsService.java index b60c85d80..79da561dd 100644 --- a/api/src/main/java/io/kafbat/ui/service/TopicsService.java +++ b/api/src/main/java/io/kafbat/ui/service/TopicsService.java @@ -2,7 +2,6 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; -import static org.apache.commons.lang3.Strings.CI; import com.google.common.collect.Sets; import io.kafbat.ui.config.ClustersProperties; @@ -471,33 +470,16 @@ public Mono cloneTopic( public Mono> getTopicsForPagination(KafkaCluster cluster, String search, Boolean showInternal) { Statistics stats = statisticsCache.get(cluster); ScrapedClusterState clusterState = stats.getClusterState(); - ClustersProperties.FtsProperties fts = clustersProperties.getFts(); - Mono> topics; - - Map topicStates = clusterState.getTopicStates(); - if (fts.isEnabled() && clusterState.getTopicIndex() != null && search != null && !search.isBlank()) { - try { - topics = Mono.just(clusterState.getTopicIndex().find(search, showInternal, null)); - } catch (IOException e) { - topics = Mono.error(e); - } - } else { - topics = Mono.just(new ArrayList<>(topicStates.keySet())); - } - return topics.flatMap(lst -> filterExisting(cluster, lst)) - .flatMapMany(Flux::fromIterable) - .map(topicName -> InternalTopic.from( - topicStates.get(topicName).description(), - topicStates.get(topicName).configs(), - InternalPartitionsOffsets.empty(), - stats.getMetrics(), - Optional.ofNullable(topicStates.get(topicName)) - .map(TopicState::segmentStats).orElse(null), - Optional.ofNullable(topicStates.get(topicName)) - .map(TopicState::partitionsSegmentStats).orElse(null), - clustersProperties.getInternalTopicPrefix() - )).collectList(); + try { + return Mono.just( + clusterState.getTopicIndex().find(search, showInternal, null) + ).flatMap(lst -> filterExisting(cluster, lst)).map(lst -> + lst.stream().map(t -> t.withMetrics(stats.getMetrics())).toList() + ); + } catch (Exception e) { + return Mono.error(e); + } } public Mono>> getActiveProducersState(KafkaCluster cluster, String topic) { @@ -505,12 +487,12 @@ public Mono>> getActiveProducersState(Ka .flatMap(ac -> ac.getActiveProducersState(topic)); } - private Mono> filterExisting(KafkaCluster cluster, Collection topics) { + private Mono> filterExisting(KafkaCluster cluster, Collection topics) { return adminClientService.get(cluster) .flatMap(ac -> ac.listTopics(true)) - .map(existing -> existing + .map(existing -> topics .stream() - .filter(topics::contains) + .filter(s -> existing.contains(s.getName())) .collect(toList())); } 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 5b76108ee..9e9943946 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 @@ -34,7 +34,6 @@ import lombok.extern.slf4j.Slf4j; import org.apache.kafka.common.acl.AccessControlEntry; import org.apache.kafka.common.acl.AclBinding; -import org.apache.kafka.common.acl.AclBindingFilter; import org.apache.kafka.common.acl.AclOperation; import org.apache.kafka.common.resource.Resource; import org.apache.kafka.common.resource.ResourcePattern; @@ -84,18 +83,9 @@ public Flux listAcls(KafkaCluster cluster, ResourcePatternFilter fil } private List filter(List acls, String principalSearch) { - if (principalSearch == null) { - return acls; - } - ClustersProperties.FtsProperties fts = clustersProperties.getFts(); - if (fts.isEnabled()) { - AclBindingNgramFilter filter = new AclBindingNgramFilter(acls, fts.getFilterMinNGram(), fts.getFilterMaxNGram()); - return filter.find(principalSearch); - } else { - return acls.stream() - .filter(acl -> acl.entry().principal().contains(principalSearch)) - .toList(); - } + ClustersProperties.ClusterFtsProperties fts = clustersProperties.getFts(); + AclBindingNgramFilter filter = new AclBindingNgramFilter(acls, fts.isEnabled(), fts.getAcl()); + return filter.find(principalSearch); } public Mono getAclAsCsvString(KafkaCluster cluster) { diff --git a/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java index 6d932cc45..430b35576 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java @@ -1,5 +1,6 @@ package io.kafbat.ui.service.index; +import io.kafbat.ui.config.ClustersProperties; import java.util.Collection; import java.util.List; import org.apache.kafka.common.acl.AclBinding; @@ -10,11 +11,14 @@ public class AclBindingNgramFilter extends NgramFilter { private final List, AclBinding>> bindings; public AclBindingNgramFilter(Collection bindings) { - this(bindings, 1, 4); + this(bindings, true, new ClustersProperties.FtsProperties(true, 1, 4)); } - public AclBindingNgramFilter(Collection bindings, int minNGram, int maxNGram) { - super(minNGram, maxNGram); + public AclBindingNgramFilter( + Collection bindings, + boolean enabled, + ClustersProperties.FtsProperties properties) { + super(properties, enabled); this.bindings = bindings.stream().map(g -> Tuples.of(List.of(g.entry().principal()), g)).toList(); } diff --git a/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java b/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java index f9bc6720a..7ff1a83f7 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java @@ -1,5 +1,6 @@ package io.kafbat.ui.service.index; +import io.kafbat.ui.config.ClustersProperties; import java.util.Collection; import java.util.List; import org.apache.kafka.clients.admin.ConsumerGroupListing; @@ -10,11 +11,14 @@ public class ConsumerGroupFilter extends NgramFilter { private final List, ConsumerGroupListing>> groups; public ConsumerGroupFilter(Collection groups) { - this(groups, 1, 4); + this(groups, true, new ClustersProperties.FtsProperties(true, 1, 4)); } - public ConsumerGroupFilter(Collection groups, int minNGram, int maxNGram) { - super(minNGram, maxNGram); + public ConsumerGroupFilter( + Collection groups, + boolean enabled, + ClustersProperties.FtsProperties properties) { + super(properties, enabled); this.groups = groups.stream().map(g -> Tuples.of(List.of(g.groupId()), g)).toList(); } diff --git a/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java b/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java new file mode 100644 index 000000000..1067bc8fb --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java @@ -0,0 +1,31 @@ +package io.kafbat.ui.service.index; + +import static org.apache.commons.lang3.Strings.CI; + +import io.kafbat.ui.model.InternalTopic; +import java.util.List; +import java.util.stream.Stream; + +public class FilterTopicIndex implements TopicsIndex { + private List topics; + + public FilterTopicIndex(List topics) { + this.topics = topics; + } + + @Override + public List find(String search, Boolean showInternal, String sort, Integer count) { + Stream stream = topics.stream().filter(topic -> !topic.isInternal() + || showInternal != null && showInternal) + .filter( + topic -> search == null || CI.contains(topic.getName(), search) + ); + + return stream.toList(); + } + + @Override + public void close() throws Exception { + + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java index 5c9e4295c..dd60c8b85 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java @@ -1,5 +1,6 @@ package io.kafbat.ui.service.index; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.FullConnectorInfoDTO; import java.util.Collection; import java.util.List; @@ -10,11 +11,14 @@ public class KafkaConnectNgramFilter extends NgramFilter { private final List, FullConnectorInfoDTO>> connectors; public KafkaConnectNgramFilter(Collection connectors) { - this(connectors, 1, 4); + this(connectors, true, new ClustersProperties.FtsProperties(true, 1, 4)); } - public KafkaConnectNgramFilter(Collection connectors, int minNGram, int maxNGram) { - super(minNGram, maxNGram); + public KafkaConnectNgramFilter( + Collection connectors, + boolean enabled, + ClustersProperties.FtsProperties properties) { + super(properties, enabled); this.connectors = connectors.stream().map(this::getItem).toList(); } diff --git a/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java b/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java new file mode 100644 index 000000000..44c928dc9 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java @@ -0,0 +1,187 @@ +package io.kafbat.ui.service.index; + +import io.kafbat.ui.config.ClustersProperties; +import io.kafbat.ui.model.InternalTopic; +import io.kafbat.ui.model.InternalTopicConfig; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.document.StringField; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryparser.classic.QueryParser; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Sort; +import org.apache.lucene.search.SortField; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.store.Directory; + +public class LuceneTopicsIndex implements TopicsIndex { + public static final String FIELD_NAME_RAW = "name_raw"; + + private final Directory directory; + private final DirectoryReader indexReader; + private final IndexSearcher indexSearcher; + private final Analyzer analyzer; + private final int maxSize; + private final ReadWriteLock closeLock = new ReentrantReadWriteLock(); + private final boolean searchNgram; + private final Map topicMap; + + public LuceneTopicsIndex(List topics) throws IOException { + this(topics, new ClustersProperties.FtsProperties(true, 1, 4)); + } + + public LuceneTopicsIndex(List topics, ClustersProperties.FtsProperties properties) throws IOException { + boolean ngram = properties.isNgram(); + if (ngram) { + this.analyzer = new ShortWordNGramAnalyzer( + properties.getNgramMin(), + properties.getNgramMax(), + false + ); + } else { + this.analyzer = new ShortWordAnalyzer(); + } + + this.searchNgram = ngram; + this.topicMap = topics.stream().collect(Collectors.toMap(InternalTopic::getName, Function.identity())); + this.directory = build(topics); + this.indexReader = DirectoryReader.open(directory); + this.indexSearcher = new IndexSearcher(indexReader); + this.maxSize = topics.size(); + } + + private Directory build(List topics) { + Directory directory = new ByteBuffersDirectory(); + try (IndexWriter directoryWriter = new IndexWriter(directory, new IndexWriterConfig(this.analyzer))) { + for (InternalTopic topic : topics) { + Document doc = new Document(); + doc.add(new StringField(FIELD_NAME_RAW, topic.getName(), Field.Store.YES)); + doc.add(new TextField(FIELD_NAME, topic.getName(), Field.Store.NO)); + doc.add(new IntPoint(FIELD_PARTITIONS, topic.getPartitionCount())); + doc.add(new IntPoint(FIELD_REPLICATION, topic.getReplicationFactor())); + doc.add(new LongPoint(FIELD_SIZE, topic.getSegmentSize())); + if (topic.getTopicConfigs() != null && !topic.getTopicConfigs().isEmpty()) { + for (InternalTopicConfig topicConfig : topic.getTopicConfigs()) { + doc.add(new StringField(FIELD_CONFIG_PREFIX + "_" + topicConfig.getName(), topicConfig.getValue(), + Field.Store.NO)); + } + } + doc.add(new StringField(FIELD_INTERNAL, String.valueOf(topic.isInternal()), Field.Store.NO)); + directoryWriter.addDocument(doc); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return directory; + } + + @Override + public void close() throws Exception { + this.closeLock.writeLock().lock(); + try { + if (indexReader != null) { + this.indexReader.close(); + } + if (this.directory != null) { + this.directory.close(); + } + } finally { + this.closeLock.writeLock().unlock(); + } + } + + public List find(String search, Boolean showInternal, String sort, Integer count) { + return find(search, showInternal, sort, count, 0.0f); + } + + public List find(String search, Boolean showInternal, + String sortField, Integer count, float minScore) { + closeLock.readLock().lock(); + try { + Query nameQuery; + if (this.searchNgram) { + List ngrams = NgramFilter.tokenizeStringSimple(this.analyzer, search); + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (String ng : ngrams) { + builder.add(new TermQuery(new Term(FIELD_NAME, ng)), BooleanClause.Occur.MUST); + } + nameQuery = builder.build(); + } else { + QueryParser queryParser = new QueryParser(FIELD_NAME, this.analyzer); + queryParser.setDefaultOperator(QueryParser.Operator.AND); + + try { + nameQuery = queryParser.parse(search); + + if (!search.contains(" ") && !search.contains("*")) { + String wildcardSearch = search + "*"; + Query wildCardNameQuery = queryParser.parse(wildcardSearch); + BooleanQuery.Builder withWildcard = new BooleanQuery.Builder(); + withWildcard.add(nameQuery, BooleanClause.Occur.SHOULD); + withWildcard.add(wildCardNameQuery, BooleanClause.Occur.SHOULD); + nameQuery = withWildcard.build(); + } + + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + Query internalFilter = new TermQuery(new Term(FIELD_INTERNAL, "true")); + + BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); + queryBuilder.add(nameQuery, BooleanClause.Occur.MUST); + if (showInternal == null || !showInternal) { + queryBuilder.add(internalFilter, BooleanClause.Occur.MUST_NOT); + } + + List sortFields = new ArrayList<>(); + sortFields.add(SortField.FIELD_SCORE); + if (!sortField.equals(FIELD_NAME)) { + sortFields.add(new SortField(sortField, SortField.Type.INT, true)); + } + + Sort sort = new Sort(sortFields.toArray(new SortField[0])); + + TopDocs result = this.indexSearcher.search(queryBuilder.build(), count != null ? count : this.maxSize, sort); + + List topics = new ArrayList<>(); + for (ScoreDoc scoreDoc : result.scoreDocs) { + if (minScore > 0.00001f && scoreDoc.score < minScore) { + continue; + } + Document document = this.indexSearcher.storedFields().document(scoreDoc.doc); + topics.add(document.get(FIELD_NAME_RAW)); + } + return topics.stream().map(topicMap::get).filter(Objects::nonNull).toList(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } finally { + this.closeLock.readLock().unlock(); + } + } +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java index 1caaae227..3f68a0436 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java @@ -1,37 +1,59 @@ package io.kafbat.ui.service.index; -import java.io.IOException; +import static org.apache.commons.lang3.Strings.CI; + +import com.google.common.cache.CacheBuilder; +import io.kafbat.ui.config.ClustersProperties; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.tokenattributes.CharTermAttribute; import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; @Slf4j public abstract class NgramFilter { private final Analyzer analyzer; + private final boolean enabled; - public NgramFilter(int minGram, int maxGram) { - this.analyzer = new ShortWordNGramAnalyzer(minGram, maxGram, false); + public NgramFilter(ClustersProperties.FtsProperties properties, boolean enabled) { + this.enabled = enabled; + this.analyzer = new ShortWordNGramAnalyzer(properties.getNgramMin(), properties.getNgramMax(), false); } protected abstract List, T>> getItems(); - private static Map> cache = new ConcurrentHashMap<>(); + private static final Map> cache = CacheBuilder.newBuilder() + .maximumSize(1_000) + .>build() + .asMap(); public List find(String search) { - try { - if (search == null || search.isBlank()) { - return getItems().stream().map(Tuple2::getT2).toList(); + return find(search, true); + } + + public List find(String search, boolean sort) { + if (search == null || search.isBlank()) { + return this.getItems().stream().map(Tuple2::getT2).toList(); + } + if (!enabled) { + Stream stream = this.getItems() + .stream() + .filter(t -> t.getT1().stream().anyMatch(s -> CI.contains(s, search))) + .map(Tuple2::getT2); + if (sort) { + return stream.sorted().toList(); + } else { + return stream.toList(); } + } + try { List> result = new ArrayList<>(); List queryTokens = tokenizeString(analyzer, search); Map queryFreq = termFreq(queryTokens); @@ -42,11 +64,13 @@ public List find(String search) { HashSet itemTokensSet = new HashSet<>(itemTokens); if (itemTokensSet.containsAll(queryTokens)) { double score = cosineSimilarity(queryFreq, itemTokens); - result.add(new SearchResult(item.getT2(), score)); + result.add(new SearchResult<>(item.getT2(), score)); } } } - result.sort((o1, o2) -> Double.compare(o2.score, o1.score)); + if (sort) { + result.sort((o1, o2) -> Double.compare(o2.score, o1.score)); + } return result.stream().map(r -> r.item).toList(); } catch (Exception e) { throw new RuntimeException(e); @@ -57,12 +81,12 @@ private record SearchResult(T item, double score) { } - public static List tokenizeString(Analyzer analyzer, String text) throws IOException { + static List tokenizeString(Analyzer analyzer, String text) { return cache.computeIfAbsent(text, (t) -> tokenizeStringSimple(analyzer, text)); } @SneakyThrows - public static List tokenizeStringSimple(Analyzer analyzer, String text) { + static List tokenizeStringSimple(Analyzer analyzer, String text) { List tokens = new ArrayList<>(); try (TokenStream tokenStream = analyzer.tokenStream(null, text)) { CharTermAttribute attr = tokenStream.addAttribute(CharTermAttribute.class); diff --git a/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java b/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java index 3c9890b49..8a04f5b9d 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java @@ -2,6 +2,7 @@ import static org.apache.commons.lang3.Strings.CI; +import io.kafbat.ui.config.ClustersProperties; import java.util.Collection; import java.util.List; import reactor.util.function.Tuple2; @@ -9,33 +10,14 @@ public class SchemasFilter extends NgramFilter { private final List, String>> subjects; - private final boolean fts; - public SchemasFilter(Collection subjects) { - this(subjects, 1, 4, true); - } - - public SchemasFilter(Collection subjects, int minNGram, int maxNGram, boolean fts) { - super(minNGram, maxNGram); + public SchemasFilter(Collection subjects, boolean enabled, ClustersProperties.FtsProperties properties) { + super(properties, enabled); this.subjects = subjects.stream().map(g -> Tuples.of(List.of(g), g)).toList(); - this.fts = fts; } @Override protected List, String>> getItems() { return this.subjects; } - - @Override - public List find(String search) { - if (fts) { - return super.find(search); - } else { - return this.subjects - .stream() - .map(Tuple2::getT2) - .filter(subj -> search == null || CI.contains(subj, search)) - .sorted().toList(); - } - } } diff --git a/api/src/main/java/io/kafbat/ui/service/index/ShortWordAnalyzer.java b/api/src/main/java/io/kafbat/ui/service/index/ShortWordAnalyzer.java index 795187a46..ee278fb09 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/ShortWordAnalyzer.java +++ b/api/src/main/java/io/kafbat/ui/service/index/ShortWordAnalyzer.java @@ -7,7 +7,7 @@ import org.apache.lucene.analysis.miscellaneous.WordDelimiterGraphFilter; import org.apache.lucene.analysis.standard.StandardTokenizer; -public class ShortWordAnalyzer extends Analyzer { +class ShortWordAnalyzer extends Analyzer { public ShortWordAnalyzer() {} diff --git a/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java b/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java index 5013edee6..2bb0bcaaa 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java +++ b/api/src/main/java/io/kafbat/ui/service/index/ShortWordNGramAnalyzer.java @@ -8,7 +8,7 @@ import org.apache.lucene.analysis.ngram.NGramTokenFilter; import org.apache.lucene.analysis.standard.StandardTokenizer; -public class ShortWordNGramAnalyzer extends Analyzer { +class ShortWordNGramAnalyzer extends Analyzer { private final int minGram; private final int maxGram; private final boolean preserveOriginal; diff --git a/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java index fbc8d1293..6f5925e33 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java +++ b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java @@ -1,191 +1,19 @@ package io.kafbat.ui.service.index; import io.kafbat.ui.model.InternalTopic; -import io.kafbat.ui.model.InternalTopicConfig; -import java.io.IOException; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import org.apache.lucene.analysis.Analyzer; -import org.apache.lucene.analysis.standard.StandardAnalyzer; -import org.apache.lucene.document.Document; -import org.apache.lucene.document.Field; -import org.apache.lucene.document.IntPoint; -import org.apache.lucene.document.LongPoint; -import org.apache.lucene.document.StringField; -import org.apache.lucene.document.TextField; -import org.apache.lucene.index.DirectoryReader; -import org.apache.lucene.index.IndexWriter; -import org.apache.lucene.index.IndexWriterConfig; -import org.apache.lucene.index.Term; -import org.apache.lucene.queryparser.classic.ParseException; -import org.apache.lucene.queryparser.classic.QueryParser; -import org.apache.lucene.search.BooleanClause; -import org.apache.lucene.search.BooleanQuery; -import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.search.Query; -import org.apache.lucene.search.ScoreDoc; -import org.apache.lucene.search.Sort; -import org.apache.lucene.search.SortField; -import org.apache.lucene.search.SynonymQuery; -import org.apache.lucene.search.TermQuery; -import org.apache.lucene.search.TopDocs; -import org.apache.lucene.store.ByteBuffersDirectory; -import org.apache.lucene.store.Directory; -import org.apache.lucene.util.QueryBuilder; -public class TopicsIndex implements AutoCloseable { - public static final String FIELD_NAME_RAW = "name_raw"; - public static final String FIELD_NAME = "name"; - public static final String FIELD_INTERNAL = "internal"; - public static final String FIELD_PARTITIONS = "partitions"; - public static final String FIELD_REPLICATION = "replication"; - public static final String FIELD_SIZE = "size"; - public static final String FIELD_CONFIG_PREFIX = "config"; +public interface TopicsIndex extends AutoCloseable { + String FIELD_NAME = "name"; + String FIELD_INTERNAL = "internal"; + String FIELD_PARTITIONS = "partitions"; + String FIELD_REPLICATION = "replication"; + String FIELD_SIZE = "size"; + String FIELD_CONFIG_PREFIX = "config"; - private final Directory directory; - private final DirectoryReader indexReader; - private final IndexSearcher indexSearcher; - private final Analyzer analyzer; - private final int maxSize; - private final ReadWriteLock closeLock = new ReentrantReadWriteLock(); - private final boolean searchNgram; - - public TopicsIndex(List topics) throws IOException { - this(topics, true); - } - - public TopicsIndex(List topics, boolean ngram) throws IOException { - this(topics, ngram, 1, 5); + default List find(String search, Boolean showInternal, Integer count) { + return this.find(search, showInternal, FIELD_NAME, count); } - public TopicsIndex(List topics, boolean ngram, int minNgram, int maxNgram) throws IOException { - if (ngram) { - this.analyzer = new ShortWordNGramAnalyzer(minNgram, maxNgram, false); - } else { - this.analyzer = new ShortWordAnalyzer(); - } - - this.searchNgram = ngram; - this.directory = build(topics); - this.indexReader = DirectoryReader.open(directory); - this.indexSearcher = new IndexSearcher(indexReader); - this.maxSize = topics.size(); - } - - private Directory build(List topics) { - Directory directory = new ByteBuffersDirectory(); - try (IndexWriter directoryWriter = new IndexWriter(directory, new IndexWriterConfig(this.analyzer))) { - for (InternalTopic topic : topics) { - Document doc = new Document(); - doc.add(new StringField(FIELD_NAME_RAW, topic.getName(), Field.Store.YES)); - doc.add(new TextField(FIELD_NAME, topic.getName(), Field.Store.NO)); - doc.add(new IntPoint(FIELD_PARTITIONS, topic.getPartitionCount())); - doc.add(new IntPoint(FIELD_REPLICATION, topic.getReplicationFactor())); - doc.add(new LongPoint(FIELD_SIZE, topic.getSegmentSize())); - if (topic.getTopicConfigs() != null && !topic.getTopicConfigs().isEmpty()) { - for (InternalTopicConfig topicConfig : topic.getTopicConfigs()) { - doc.add(new StringField(FIELD_CONFIG_PREFIX + "_" + topicConfig.getName(), topicConfig.getValue(), - Field.Store.NO)); - } - } - doc.add(new StringField(FIELD_INTERNAL, String.valueOf(topic.isInternal()), Field.Store.NO)); - directoryWriter.addDocument(doc); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - return directory; - } - - @Override - public void close() throws Exception { - this.closeLock.writeLock().lock(); - try { - if (indexReader != null) { - this.indexReader.close(); - } - if (this.directory != null) { - this.directory.close(); - } - } finally { - this.closeLock.writeLock().unlock(); - } - } - - public List find(String search, Boolean showInternal, Integer count) throws IOException { - return find(search, showInternal, FIELD_NAME, count, 0.0f); - } - - public List find(String search, Boolean showInternal, String sort, Integer count) throws IOException { - return find(search, showInternal, sort, count, 0.0f); - } - - public List find(String search, Boolean showInternal, - String sortField, Integer count, float minScore) throws IOException { - closeLock.readLock().lock(); - try { - Query nameQuery; - if (this.searchNgram) { - List ngrams = NgramFilter.tokenizeStringSimple(this.analyzer, search); - BooleanQuery.Builder builder = new BooleanQuery.Builder(); - for (String ng : ngrams) { - builder.add(new TermQuery(new Term(FIELD_NAME, ng)), BooleanClause.Occur.MUST); - } - nameQuery = builder.build(); - } else { - QueryParser queryParser = new QueryParser(FIELD_NAME, this.analyzer); - queryParser.setDefaultOperator(QueryParser.Operator.AND); - - try { - nameQuery = queryParser.parse(search); - - if (!search.contains(" ") && !search.contains("*")) { - String wildcardSearch = search + "*"; - Query wildCardNameQuery = queryParser.parse(wildcardSearch); - BooleanQuery.Builder withWildcard = new BooleanQuery.Builder(); - withWildcard.add(nameQuery, BooleanClause.Occur.SHOULD); - withWildcard.add(wildCardNameQuery, BooleanClause.Occur.SHOULD); - nameQuery = withWildcard.build(); - } - - - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - Query internalFilter = new TermQuery(new Term(FIELD_INTERNAL, "true")); - - BooleanQuery.Builder queryBuilder = new BooleanQuery.Builder(); - queryBuilder.add(nameQuery, BooleanClause.Occur.MUST); - if (showInternal == null || !showInternal) { - queryBuilder.add(internalFilter, BooleanClause.Occur.MUST_NOT); - } - - List sortFields = new ArrayList<>(); - sortFields.add(SortField.FIELD_SCORE); - if (!sortField.equals(FIELD_NAME)) { - sortFields.add(new SortField(sortField, SortField.Type.INT, true)); - } - - Sort sort = new Sort(sortFields.toArray(new SortField[0])); - - TopDocs result = this.indexSearcher.search(queryBuilder.build(), count != null ? count : this.maxSize, sort); - - List topics = new ArrayList<>(); - for (ScoreDoc scoreDoc : result.scoreDocs) { - if (minScore > 0.00001f && scoreDoc.score < minScore) { - continue; - } - Document document = this.indexSearcher.storedFields().document(scoreDoc.doc); - topics.add(document.get(FIELD_NAME_RAW)); - } - return topics; - } finally { - this.closeLock.readLock().unlock(); - } - } + List find(String search, Boolean showInternal, String sort, Integer count); } diff --git a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java index 62c94971f..d0cb4c214 100644 --- a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java +++ b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java @@ -10,6 +10,8 @@ import io.kafbat.ui.model.InternalPartitionsOffsets; import io.kafbat.ui.model.InternalTopic; import io.kafbat.ui.service.ReactiveAdminClient; +import io.kafbat.ui.service.index.FilterTopicIndex; +import io.kafbat.ui.service.index.LuceneTopicsIndex; import io.kafbat.ui.service.index.TopicsIndex; import jakarta.annotation.Nullable; import java.time.Instant; @@ -209,17 +211,20 @@ private static ScrapedClusterState create(ClusterDescription clusterDescription, } private static TopicsIndex buildTopicIndex(ClustersProperties clustersProperties, - Map topicStates) { - ClustersProperties.FtsProperties fts = clustersProperties.getFts(); - TopicsIndex topicsIndex = null; + Map topicStates) { + ClustersProperties.ClusterFtsProperties fts = clustersProperties.getFts(); + List topics = topicStates.values().stream().map( + topicState -> buildInternalTopic(topicState, clustersProperties) + ).toList(); + if (fts.isEnabled()) { try { - return new TopicsIndex(topicStates.values().stream().map( - topicState -> buildInternalTopic(topicState, clustersProperties) - ).toList(), fts.isTopicsNgramEnabled(), fts.getTopicsMinNGram(), fts.getTopicsMaxNGram()); + return new LuceneTopicsIndex(topics, fts.getTopics()); } catch (Exception e) { log.error("Error creating topics index", e); } + } else { + return new FilterTopicIndex(topics); } return null; } diff --git a/api/src/test/java/io/kafbat/ui/service/TopicsServicePaginationTest.java b/api/src/test/java/io/kafbat/ui/service/TopicsServicePaginationTest.java index 44b2fad38..9ad9c651f 100644 --- a/api/src/test/java/io/kafbat/ui/service/TopicsServicePaginationTest.java +++ b/api/src/test/java/io/kafbat/ui/service/TopicsServicePaginationTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.isNull; @@ -13,11 +14,13 @@ import io.kafbat.ui.mapper.ClusterMapper; import io.kafbat.ui.mapper.ClusterMapperImpl; import io.kafbat.ui.model.InternalLogDirStats; +import io.kafbat.ui.model.InternalPartition; import io.kafbat.ui.model.InternalPartitionsOffsets; import io.kafbat.ui.model.InternalTopic; import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.Metrics; import io.kafbat.ui.model.SortOrderDTO; +import io.kafbat.ui.model.Statistics; import io.kafbat.ui.model.TopicColumnsToSortDTO; import io.kafbat.ui.model.TopicDTO; import io.kafbat.ui.service.analyze.TopicAnalysisService; @@ -45,33 +48,80 @@ class TopicsServicePaginationTest { private static final String LOCAL_KAFKA_CLUSTER_NAME = "local"; - private final TopicsService topicsService = Mockito.mock(TopicsService.class); + private final AdminClientService adminClientService = Mockito.mock(AdminClientService.class); + private final ReactiveAdminClient reactiveAdminClient = Mockito.mock(ReactiveAdminClient.class); private final ClustersStorage clustersStorage = Mockito.mock(ClustersStorage.class); - private final ClusterMapper clusterMapper = new ClusterMapperImpl(); + private final StatisticsCache statisticsCache = new StatisticsCache(clustersStorage); private final ClustersProperties clustersProperties = new ClustersProperties(); + private final TopicsService topicsService = new TopicsService( + adminClientService, + statisticsCache, + clustersProperties + ); + + private final TopicsService mockTopicsService = Mockito.mock(TopicsService.class); + private final ClusterMapper clusterMapper = new ClusterMapperImpl(); + private final AccessControlService accessControlService = new AccessControlServiceMock().getMock(); private final TopicsController topicsController = - new TopicsController(topicsService, mock(TopicAnalysisService.class), clusterMapper, clustersProperties); + new TopicsController(mockTopicsService, mock(TopicAnalysisService.class), clusterMapper, clustersProperties); private void init(Map topicsInCache) { - + KafkaCluster kafkaCluster = buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME); + statisticsCache.replace(kafkaCluster, Statistics.empty()); + statisticsCache.update( + kafkaCluster, + topicsInCache.entrySet().stream().collect( + Collectors.toMap( + Map.Entry::getKey, + v -> toTopicDescription(v.getValue()) + ) + ), + Map.of(), + new InternalPartitionsOffsets(Map.of()), + clustersProperties + ); + when(adminClientService.get(isA(KafkaCluster.class))).thenReturn(Mono.just(reactiveAdminClient)); + when(reactiveAdminClient.listTopics(anyBoolean())).thenReturn(Mono.just(topicsInCache.keySet())); when(clustersStorage.getClusterByName(isA(String.class))) - .thenReturn(Optional.of(buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME))); - when(topicsService.getTopicsForPagination(isA(KafkaCluster.class), any(), any())) - .thenReturn(Mono.just(new ArrayList<>(topicsInCache.values()))); - - - when(topicsService.loadTopics(isA(KafkaCluster.class), anyList())) + .thenReturn(Optional.of(kafkaCluster)); + when(mockTopicsService.getTopicsForPagination(isA(KafkaCluster.class), any(), any())) + .thenAnswer(a -> + topicsService.getTopicsForPagination( + a.getArgument(0), + a.getArgument(1), + a.getArgument(2) + ) + ); + + + when(mockTopicsService.loadTopics(isA(KafkaCluster.class), anyList())) .thenAnswer(a -> { List lst = a.getArgument(1); return Mono.just(lst.stream().map(topicsInCache::get).collect(Collectors.toList())); }); + topicsController.setAccessControlService(accessControlService); topicsController.setAuditService(mock(AuditService.class)); topicsController.setClustersStorage(clustersStorage); } + private TopicDescription toTopicDescription(InternalTopic t) { + return new TopicDescription( + t.getName(), t.isInternal(), + t.getPartitions().values().stream().map(p -> toTopicPartitionInfo(p)).toList() + ); + } + + private TopicPartitionInfo toTopicPartitionInfo(InternalPartition p) { + return new TopicPartitionInfo( + p.getPartition(), + null, List.of(), List.of() + ); + } + + @Test void shouldListFirst25Topics() { init( diff --git a/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java b/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java index 2dc701ff1..3a9136185 100644 --- a/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java +++ b/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java @@ -1,5 +1,6 @@ package io.kafbat.ui.service.index; +import io.kafbat.ui.config.ClustersProperties; import io.kafbat.ui.model.InternalTopic; import io.kafbat.ui.model.InternalTopicConfig; import java.util.ArrayList; @@ -56,9 +57,9 @@ void testFindTopicsByName() throws Exception { ); SoftAssertions softly = new SoftAssertions(); - try (TopicsIndex index = new TopicsIndex(topics)) { + try (LuceneTopicsIndex index = new LuceneTopicsIndex(topics)) { for (Map.Entry entry : examples.entrySet()) { - List resultAll = index.find(entry.getKey(), null, topics.size()); + List resultAll = index.find(entry.getKey(), null, topics.size()); softly.assertThat(resultAll.size()) .withFailMessage("Expected %d results for '%s', but got %s", entry.getValue(), entry.getKey(), resultAll) .isEqualTo(entry.getValue()); @@ -68,9 +69,10 @@ void testFindTopicsByName() throws Exception { HashMap indexExamples = new HashMap<>(examples); indexExamples.put("config_retention:compact", 1); - try (TopicsIndex index = new TopicsIndex(topics, false)) { + try (LuceneTopicsIndex index = new LuceneTopicsIndex(topics, + new ClustersProperties.FtsProperties(false, 1, 4))) { for (Map.Entry entry : indexExamples.entrySet()) { - List resultAll = index.find(entry.getKey(), null, topics.size()); + List resultAll = index.find(entry.getKey(), null, topics.size()); softly.assertThat(resultAll.size()) .withFailMessage("Expected %d results for '%s', but got %s", entry.getValue(), entry.getKey(), resultAll) .isEqualTo(entry.getValue()); From 5f73e9ce713f34838d49246f94d629a96f513d72 Mon Sep 17 00:00:00 2001 From: German Osin Date: Fri, 29 Aug 2025 16:05:15 +0200 Subject: [PATCH 28/36] Fixes --- .../kafbat/ui/config/ClustersProperties.java | 2 +- .../ui/service/index/LuceneTopicsIndex.java | 13 +----- .../ui/service/index/PrefixQueryParser.java | 42 +++++++++++++++++++ .../kafbat/ui/service/index/TopicsIndex.java | 17 ++++++++ ...exTest.java => LuceneTopicsIndexTest.java} | 15 ++++++- 5 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 api/src/main/java/io/kafbat/ui/service/index/PrefixQueryParser.java rename api/src/test/java/io/kafbat/ui/service/index/{TopicsIndexTest.java => LuceneTopicsIndexTest.java} (83%) diff --git a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java index 664ea46c5..fec45a6fc 100644 --- a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java @@ -232,7 +232,7 @@ public static class FtsProperties { @AllArgsConstructor public static class ClusterFtsProperties { boolean enabled = false; - FtsProperties topics = new FtsProperties(true, 3, 5); + FtsProperties topics = new FtsProperties(false, 3, 5); FtsProperties schemas = new FtsProperties(true, 1, 4); FtsProperties consumers = new FtsProperties(true, 1, 4); FtsProperties connect = new FtsProperties(true, 1, 4); diff --git a/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java b/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java index 44c928dc9..be85b06cd 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java +++ b/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java @@ -130,22 +130,11 @@ public List find(String search, Boolean showInternal, } nameQuery = builder.build(); } else { - QueryParser queryParser = new QueryParser(FIELD_NAME, this.analyzer); + QueryParser queryParser = new PrefixQueryParser(FIELD_NAME, this.analyzer); queryParser.setDefaultOperator(QueryParser.Operator.AND); try { nameQuery = queryParser.parse(search); - - if (!search.contains(" ") && !search.contains("*")) { - String wildcardSearch = search + "*"; - Query wildCardNameQuery = queryParser.parse(wildcardSearch); - BooleanQuery.Builder withWildcard = new BooleanQuery.Builder(); - withWildcard.add(nameQuery, BooleanClause.Occur.SHOULD); - withWildcard.add(wildCardNameQuery, BooleanClause.Occur.SHOULD); - nameQuery = withWildcard.build(); - } - - } catch (Exception e) { throw new RuntimeException(e); } diff --git a/api/src/main/java/io/kafbat/ui/service/index/PrefixQueryParser.java b/api/src/main/java/io/kafbat/ui/service/index/PrefixQueryParser.java new file mode 100644 index 000000000..e8dc1a9c8 --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/service/index/PrefixQueryParser.java @@ -0,0 +1,42 @@ +package io.kafbat.ui.service.index; + +import static org.apache.lucene.search.BoostAttribute.DEFAULT_BOOST; + +import java.util.Optional; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryparser.classic.QueryParser; +import org.apache.lucene.search.BoostQuery; +import org.apache.lucene.search.PrefixQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; + +public class PrefixQueryParser extends QueryParser { + + public PrefixQueryParser(String field, Analyzer analyzer) { + super(field, analyzer); + } + + @Override + protected Query newTermQuery(Term term, float boost) { + + TopicsIndex.FieldType fieldType = Optional.ofNullable(term.field()) + .map(TopicsIndex.FIELD_TYPES::get) + .orElse(TopicsIndex.FieldType.STRING); + + Query query = switch (fieldType) { + case STRING -> new PrefixQuery(term); + case INT -> IntPoint.newExactQuery(term.field(), Integer.parseInt(term.text())); + case LONG -> LongPoint.newExactQuery(term.field(), Long.parseLong(term.text())); + case BOOLEAN -> new TermQuery(term); + }; + + if (boost == DEFAULT_BOOST) { + return query; + } + return new BoostQuery(query, boost); + } + +} diff --git a/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java index 6f5925e33..14fc36501 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java +++ b/api/src/main/java/io/kafbat/ui/service/index/TopicsIndex.java @@ -2,6 +2,7 @@ import io.kafbat.ui.model.InternalTopic; import java.util.List; +import java.util.Map; public interface TopicsIndex extends AutoCloseable { String FIELD_NAME = "name"; @@ -11,6 +12,22 @@ public interface TopicsIndex extends AutoCloseable { String FIELD_SIZE = "size"; String FIELD_CONFIG_PREFIX = "config"; + enum FieldType { + STRING, + INT, + LONG, + BOOLEAN + } + + Map FIELD_TYPES = Map.of( + FIELD_NAME, FieldType.STRING, + FIELD_INTERNAL, FieldType.BOOLEAN, + FIELD_PARTITIONS, FieldType.INT, + FIELD_REPLICATION, FieldType.INT, + FIELD_SIZE, FieldType.LONG, + FIELD_CONFIG_PREFIX, FieldType.STRING + ); + default List find(String search, Boolean showInternal, Integer count) { return this.find(search, showInternal, FIELD_NAME, count); } diff --git a/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java b/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java similarity index 83% rename from api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java rename to api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java index 3a9136185..449cf5af3 100644 --- a/api/src/test/java/io/kafbat/ui/service/index/TopicsIndexTest.java +++ b/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java @@ -1,17 +1,21 @@ package io.kafbat.ui.service.index; import io.kafbat.ui.config.ClustersProperties; +import io.kafbat.ui.model.InternalPartition; import io.kafbat.ui.model.InternalTopic; import io.kafbat.ui.model.InternalTopicConfig; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; -class TopicsIndexTest { +class LuceneTopicsIndexTest { @Test void testFindTopicsByName() throws Exception { List topics = new ArrayList<>( @@ -36,6 +40,14 @@ void testFindTopicsByName() throws Exception { List.of( InternalTopic.builder().name("configurable").partitions(Map.of()).topicConfigs( List.of(InternalTopicConfig.builder().name("retention").value("compact").build()) + ).build(), + InternalTopic.builder().name("multiple_parts").partitionCount(10).partitions( + IntStream.range(0, 10).mapToObj(i -> + InternalPartition.builder().partition(i).build() + ).collect(Collectors.toMap( + InternalPartition::getPartition, + Function.identity() + )) ).build() ) ); @@ -68,6 +80,7 @@ void testFindTopicsByName() throws Exception { HashMap indexExamples = new HashMap<>(examples); indexExamples.put("config_retention:compact", 1); + indexExamples.put("partitions:10", 1); try (LuceneTopicsIndex index = new LuceneTopicsIndex(topics, new ClustersProperties.FtsProperties(false, 1, 4))) { From 82e6847b8d13708ca7cf8f0dce080c3cc701ebf4 Mon Sep 17 00:00:00 2001 From: German Osin Date: Fri, 29 Aug 2025 16:08:12 +0200 Subject: [PATCH 29/36] Fixes --- .github/dependabot.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c11f1d976..014736a71 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,6 +21,9 @@ updates: - "patch" - "minor" other-dependencies: + exclude-patterns: + - "org.springframework.boot:*" + - "io.spring.dependency-management" patterns: - "*" update-types: From 0111aa513d56e5069271ac9f027fadab59719e27 Mon Sep 17 00:00:00 2001 From: German Osin Date: Fri, 29 Aug 2025 16:33:20 +0200 Subject: [PATCH 30/36] Fixed schemas sorting --- .../kafbat/ui/service/index/NgramFilter.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java index 3f68a0436..d4cf326e3 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java @@ -38,20 +38,23 @@ public List find(String search) { return find(search, true); } + private List list(Stream stream, boolean sort) { + if (sort) { + return stream.sorted().toList(); + } else { + return stream.toList(); + } + } + public List find(String search, boolean sort) { if (search == null || search.isBlank()) { - return this.getItems().stream().map(Tuple2::getT2).toList(); + return list(this.getItems().stream().map(Tuple2::getT2), sort); } if (!enabled) { - Stream stream = this.getItems() + return list(this.getItems() .stream() .filter(t -> t.getT1().stream().anyMatch(s -> CI.contains(s, search))) - .map(Tuple2::getT2); - if (sort) { - return stream.sorted().toList(); - } else { - return stream.toList(); - } + .map(Tuple2::getT2), sort); } try { List> result = new ArrayList<>(); From c283f040f0dd338a61cdf1fc9343769d27fd6fa3 Mon Sep 17 00:00:00 2001 From: German Osin Date: Fri, 29 Aug 2025 16:41:22 +0200 Subject: [PATCH 31/36] Fixed checkstyle --- .../io/kafbat/ui/service/index/NgramFilter.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java index d4cf326e3..f499dcef7 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java @@ -38,14 +38,6 @@ public List find(String search) { return find(search, true); } - private List list(Stream stream, boolean sort) { - if (sort) { - return stream.sorted().toList(); - } else { - return stream.toList(); - } - } - public List find(String search, boolean sort) { if (search == null || search.isBlank()) { return list(this.getItems().stream().map(Tuple2::getT2), sort); @@ -80,6 +72,14 @@ public List find(String search, boolean sort) { } } + private List list(Stream stream, boolean sort) { + if (sort) { + return stream.sorted().toList(); + } else { + return stream.toList(); + } + } + private record SearchResult(T item, double score) { } From 6df04611ce048f398350aa5391d4f0a3bf994fe3 Mon Sep 17 00:00:00 2001 From: German Osin Date: Fri, 29 Aug 2025 17:19:48 +0200 Subject: [PATCH 32/36] Added range query support --- .../ui/service/index/PrefixQueryParser.java | 39 +++++++++++++++++-- .../service/index/LuceneTopicsIndexTest.java | 2 + 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/service/index/PrefixQueryParser.java b/api/src/main/java/io/kafbat/ui/service/index/PrefixQueryParser.java index e8dc1a9c8..2db1f7133 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/PrefixQueryParser.java +++ b/api/src/main/java/io/kafbat/ui/service/index/PrefixQueryParser.java @@ -2,6 +2,8 @@ import static org.apache.lucene.search.BoostAttribute.DEFAULT_BOOST; +import io.kafbat.ui.service.index.TopicsIndex.FieldType; +import java.util.List; import java.util.Optional; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.IntPoint; @@ -12,19 +14,50 @@ import org.apache.lucene.search.PrefixQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TermRangeQuery; public class PrefixQueryParser extends QueryParser { - + public PrefixQueryParser(String field, Analyzer analyzer) { super(field, analyzer); } + @Override + protected Query newRangeQuery(String field, String part1, String part2, boolean startInclusive, + boolean endInclusive) { + FieldType fieldType = Optional.ofNullable(field) + .map(TopicsIndex.FIELD_TYPES::get) + .orElse(FieldType.STRING); + + return switch (fieldType) { + case STRING, BOOLEAN -> super.newRangeQuery(field, part1, part2, startInclusive, endInclusive); + case INT -> IntPoint.newRangeQuery(field, parseInt(part1, true), parseInt(part2, false)); + case LONG -> LongPoint.newRangeQuery(field, parseLong(part1, true), parseLong(part2, false)); + }; + } + + private Integer parseInt(String value, boolean min) { + if ("*".equals(value) || value == null) { + return min ? Integer.MIN_VALUE : Integer.MAX_VALUE; + } else { + return Integer.parseInt(value); + } + } + + private Long parseLong(String value, boolean min) { + if ("*".equals(value) || value == null) { + return min ? Long.MIN_VALUE : Long.MAX_VALUE; + } else { + return Long.parseLong(value); + } + } + @Override protected Query newTermQuery(Term term, float boost) { - TopicsIndex.FieldType fieldType = Optional.ofNullable(term.field()) + FieldType fieldType = Optional.ofNullable(term.field()) .map(TopicsIndex.FIELD_TYPES::get) - .orElse(TopicsIndex.FieldType.STRING); + .orElse(FieldType.STRING); Query query = switch (fieldType) { case STRING -> new PrefixQuery(term); diff --git a/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java b/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java index 449cf5af3..ce488aa66 100644 --- a/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java +++ b/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java @@ -81,6 +81,8 @@ void testFindTopicsByName() throws Exception { HashMap indexExamples = new HashMap<>(examples); indexExamples.put("config_retention:compact", 1); indexExamples.put("partitions:10", 1); + indexExamples.put("partitions:{1 TO *]", 1); + indexExamples.put("partitions:{* TO 9]", topics.size() - 1); try (LuceneTopicsIndex index = new LuceneTopicsIndex(topics, new ClustersProperties.FtsProperties(false, 1, 4))) { From 8f365e94e3f92c7eaa04f4e4f42c8ce29036b19e Mon Sep 17 00:00:00 2001 From: German Osin Date: Mon, 1 Sep 2025 12:31:56 +0200 Subject: [PATCH 33/36] Fixed empty query --- .../java/io/kafbat/ui/service/index/FilterTopicIndex.java | 4 ++++ .../java/io/kafbat/ui/service/index/LuceneTopicsIndex.java | 3 +++ 2 files changed, 7 insertions(+) diff --git a/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java b/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java index 1067bc8fb..33b65cae6 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java +++ b/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java @@ -3,6 +3,7 @@ import static org.apache.commons.lang3.Strings.CI; import io.kafbat.ui.model.InternalTopic; +import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -15,6 +16,9 @@ public FilterTopicIndex(List topics) { @Override public List find(String search, Boolean showInternal, String sort, Integer count) { + if (sort == null || sort.isBlank()) { + return new ArrayList<>(this.topics); + } Stream stream = topics.stream().filter(topic -> !topic.isInternal() || showInternal != null && showInternal) .filter( diff --git a/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java b/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java index be85b06cd..252ec778b 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java +++ b/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java @@ -119,6 +119,9 @@ public List find(String search, Boolean showInternal, String sort public List find(String search, Boolean showInternal, String sortField, Integer count, float minScore) { + if (search == null || search.isBlank()) { + return new ArrayList<>(this.topicMap.values()); + } closeLock.readLock().lock(); try { Query nameQuery; From 0f6863ce8c88f007fbd2a2dfbf2c55cf06ea3233 Mon Sep 17 00:00:00 2001 From: German Osin Date: Mon, 1 Sep 2025 12:33:39 +0200 Subject: [PATCH 34/36] Fixed empty query --- .../main/java/io/kafbat/ui/service/index/FilterTopicIndex.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java b/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java index 33b65cae6..0b5a30900 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java +++ b/api/src/main/java/io/kafbat/ui/service/index/FilterTopicIndex.java @@ -16,7 +16,7 @@ public FilterTopicIndex(List topics) { @Override public List find(String search, Boolean showInternal, String sort, Integer count) { - if (sort == null || sort.isBlank()) { + if (search == null || search.isBlank()) { return new ArrayList<>(this.topics); } Stream stream = topics.stream().filter(topic -> !topic.isInternal() From 10d30f362a03c3f05cbf4b18e38171327633dbcf Mon Sep 17 00:00:00 2001 From: German Osin Date: Wed, 3 Sep 2025 17:01:44 +0200 Subject: [PATCH 35/36] Switched topic index to lucene prefixed by default --- .../kafbat/ui/config/ClustersProperties.java | 12 +++--- .../service/index/AclBindingNgramFilter.java | 4 +- .../ui/service/index/ConsumerGroupFilter.java | 4 +- .../index/KafkaConnectNgramFilter.java | 4 +- .../ui/service/index/LuceneTopicsIndex.java | 41 ++++--------------- .../kafbat/ui/service/index/NgramFilter.java | 2 +- .../ui/service/index/SchemasFilter.java | 4 +- .../metrics/scrape/ScrapedClusterState.java | 11 ++--- .../service/index/LuceneTopicsIndexTest.java | 22 +++------- 9 files changed, 29 insertions(+), 75 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java index fec45a6fc..495229ca0 100644 --- a/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/ClustersProperties.java @@ -221,8 +221,7 @@ public static class CacheProperties { @Data @NoArgsConstructor @AllArgsConstructor - public static class FtsProperties { - boolean ngram = true; + public static class NgramProperties { int ngramMin = 1; int ngramMax = 4; } @@ -232,11 +231,10 @@ public static class FtsProperties { @AllArgsConstructor public static class ClusterFtsProperties { boolean enabled = false; - FtsProperties topics = new FtsProperties(false, 3, 5); - FtsProperties schemas = new FtsProperties(true, 1, 4); - FtsProperties consumers = new FtsProperties(true, 1, 4); - FtsProperties connect = new FtsProperties(true, 1, 4); - FtsProperties acl = new FtsProperties(true, 1, 4); + NgramProperties schemas = new NgramProperties(1, 4); + NgramProperties consumers = new NgramProperties(1, 4); + NgramProperties connect = new NgramProperties(1, 4); + NgramProperties acl = new NgramProperties(1, 4); } @PostConstruct diff --git a/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java index 430b35576..1622e20f6 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/AclBindingNgramFilter.java @@ -11,13 +11,13 @@ public class AclBindingNgramFilter extends NgramFilter { private final List, AclBinding>> bindings; public AclBindingNgramFilter(Collection bindings) { - this(bindings, true, new ClustersProperties.FtsProperties(true, 1, 4)); + this(bindings, true, new ClustersProperties.NgramProperties(1, 4)); } public AclBindingNgramFilter( Collection bindings, boolean enabled, - ClustersProperties.FtsProperties properties) { + ClustersProperties.NgramProperties properties) { super(properties, enabled); this.bindings = bindings.stream().map(g -> Tuples.of(List.of(g.entry().principal()), g)).toList(); } diff --git a/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java b/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java index 7ff1a83f7..cc0ab8e03 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/ConsumerGroupFilter.java @@ -11,13 +11,13 @@ public class ConsumerGroupFilter extends NgramFilter { private final List, ConsumerGroupListing>> groups; public ConsumerGroupFilter(Collection groups) { - this(groups, true, new ClustersProperties.FtsProperties(true, 1, 4)); + this(groups, true, new ClustersProperties.NgramProperties(1, 4)); } public ConsumerGroupFilter( Collection groups, boolean enabled, - ClustersProperties.FtsProperties properties) { + ClustersProperties.NgramProperties properties) { super(properties, enabled); this.groups = groups.stream().map(g -> Tuples.of(List.of(g.groupId()), g)).toList(); } diff --git a/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java index dd60c8b85..99ffd5275 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/KafkaConnectNgramFilter.java @@ -11,13 +11,13 @@ public class KafkaConnectNgramFilter extends NgramFilter { private final List, FullConnectorInfoDTO>> connectors; public KafkaConnectNgramFilter(Collection connectors) { - this(connectors, true, new ClustersProperties.FtsProperties(true, 1, 4)); + this(connectors, true, new ClustersProperties.NgramProperties(1, 4)); } public KafkaConnectNgramFilter( Collection connectors, boolean enabled, - ClustersProperties.FtsProperties properties) { + ClustersProperties.NgramProperties properties) { super(properties, enabled); this.connectors = connectors.stream().map(this::getItem).toList(); } diff --git a/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java b/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java index 252ec778b..6953add26 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java +++ b/api/src/main/java/io/kafbat/ui/service/index/LuceneTopicsIndex.java @@ -24,6 +24,7 @@ import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.Term; +import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; @@ -46,26 +47,10 @@ public class LuceneTopicsIndex implements TopicsIndex { private final Analyzer analyzer; private final int maxSize; private final ReadWriteLock closeLock = new ReentrantReadWriteLock(); - private final boolean searchNgram; private final Map topicMap; public LuceneTopicsIndex(List topics) throws IOException { - this(topics, new ClustersProperties.FtsProperties(true, 1, 4)); - } - - public LuceneTopicsIndex(List topics, ClustersProperties.FtsProperties properties) throws IOException { - boolean ngram = properties.isNgram(); - if (ngram) { - this.analyzer = new ShortWordNGramAnalyzer( - properties.getNgramMin(), - properties.getNgramMax(), - false - ); - } else { - this.analyzer = new ShortWordAnalyzer(); - } - - this.searchNgram = ngram; + this.analyzer = new ShortWordAnalyzer(); this.topicMap = topics.stream().collect(Collectors.toMap(InternalTopic::getName, Function.identity())); this.directory = build(topics); this.indexReader = DirectoryReader.open(directory); @@ -124,24 +109,10 @@ public List find(String search, Boolean showInternal, } closeLock.readLock().lock(); try { - Query nameQuery; - if (this.searchNgram) { - List ngrams = NgramFilter.tokenizeStringSimple(this.analyzer, search); - BooleanQuery.Builder builder = new BooleanQuery.Builder(); - for (String ng : ngrams) { - builder.add(new TermQuery(new Term(FIELD_NAME, ng)), BooleanClause.Occur.MUST); - } - nameQuery = builder.build(); - } else { - QueryParser queryParser = new PrefixQueryParser(FIELD_NAME, this.analyzer); - queryParser.setDefaultOperator(QueryParser.Operator.AND); - try { - nameQuery = queryParser.parse(search); - } catch (Exception e) { - throw new RuntimeException(e); - } - } + QueryParser queryParser = new PrefixQueryParser(FIELD_NAME, this.analyzer); + queryParser.setDefaultOperator(QueryParser.Operator.AND); + Query nameQuery = queryParser.parse(search);; Query internalFilter = new TermQuery(new Term(FIELD_INTERNAL, "true")); @@ -172,6 +143,8 @@ public List find(String search, Boolean showInternal, return topics.stream().map(topicMap::get).filter(Objects::nonNull).toList(); } catch (IOException e) { throw new UncheckedIOException(e); + } catch (ParseException e) { + throw new RuntimeException(e); } finally { this.closeLock.readLock().unlock(); } diff --git a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java index f499dcef7..58ab5d582 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/NgramFilter.java @@ -22,7 +22,7 @@ public abstract class NgramFilter { private final Analyzer analyzer; private final boolean enabled; - public NgramFilter(ClustersProperties.FtsProperties properties, boolean enabled) { + public NgramFilter(ClustersProperties.NgramProperties properties, boolean enabled) { this.enabled = enabled; this.analyzer = new ShortWordNGramAnalyzer(properties.getNgramMin(), properties.getNgramMax(), false); } diff --git a/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java b/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java index 8a04f5b9d..bfe1516ca 100644 --- a/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java +++ b/api/src/main/java/io/kafbat/ui/service/index/SchemasFilter.java @@ -1,7 +1,5 @@ package io.kafbat.ui.service.index; -import static org.apache.commons.lang3.Strings.CI; - import io.kafbat.ui.config.ClustersProperties; import java.util.Collection; import java.util.List; @@ -11,7 +9,7 @@ public class SchemasFilter extends NgramFilter { private final List, String>> subjects; - public SchemasFilter(Collection subjects, boolean enabled, ClustersProperties.FtsProperties properties) { + public SchemasFilter(Collection subjects, boolean enabled, ClustersProperties.NgramProperties properties) { super(properties, enabled); this.subjects = subjects.stream().map(g -> Tuples.of(List.of(g), g)).toList(); } diff --git a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java index d0cb4c214..fe8c2e6fc 100644 --- a/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java +++ b/api/src/main/java/io/kafbat/ui/service/metrics/scrape/ScrapedClusterState.java @@ -110,11 +110,10 @@ public ScrapedClusterState updateTopics(Map descriptio ); }); - ScrapedClusterState state = toBuilder() + return toBuilder() .topicStates(updatedTopicStates) .topicIndex(buildTopicIndex(clustersProperties, updatedTopicStates)) .build(); - return state; } public ScrapedClusterState topicDeleted(String topic) { @@ -219,14 +218,12 @@ private static TopicsIndex buildTopicIndex(ClustersProperties clustersProperties if (fts.isEnabled()) { try { - return new LuceneTopicsIndex(topics, fts.getTopics()); + return new LuceneTopicsIndex(topics); } catch (Exception e) { - log.error("Error creating topics index", e); + log.error("Error creating lucene topics index", e); } - } else { - return new FilterTopicIndex(topics); } - return null; + return new FilterTopicIndex(topics); } private static Map filterTopic(String topicForFilter, Map tpMap) { diff --git a/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java b/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java index ce488aa66..557c2a785 100644 --- a/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java +++ b/api/src/test/java/io/kafbat/ui/service/index/LuceneTopicsIndexTest.java @@ -65,7 +65,11 @@ void testFindTopicsByName() throws Exception { Map.entry("stat", 3), Map.entry("changes", 1), Map.entry("commands", 1), - Map.entry("id", 2) + Map.entry("id", 2), + Map.entry("config_retention:compact", 1), + Map.entry("partitions:10", 1), + Map.entry("partitions:{1 TO *]", 1), + Map.entry("partitions:{* TO 9]", topics.size() - 1) ); SoftAssertions softly = new SoftAssertions(); @@ -77,22 +81,6 @@ void testFindTopicsByName() throws Exception { .isEqualTo(entry.getValue()); } } - - HashMap indexExamples = new HashMap<>(examples); - indexExamples.put("config_retention:compact", 1); - indexExamples.put("partitions:10", 1); - indexExamples.put("partitions:{1 TO *]", 1); - indexExamples.put("partitions:{* TO 9]", topics.size() - 1); - - try (LuceneTopicsIndex index = new LuceneTopicsIndex(topics, - new ClustersProperties.FtsProperties(false, 1, 4))) { - for (Map.Entry entry : indexExamples.entrySet()) { - List resultAll = index.find(entry.getKey(), null, topics.size()); - softly.assertThat(resultAll.size()) - .withFailMessage("Expected %d results for '%s', but got %s", entry.getValue(), entry.getKey(), resultAll) - .isEqualTo(entry.getValue()); - } - } softly.assertAll(); } From 08af06642185e79c7e378865586b53041cfc20e0 Mon Sep 17 00:00:00 2001 From: German Osin Date: Sat, 6 Sep 2025 13:59:28 +0200 Subject: [PATCH 36/36] Added console logs to allure --- .../main/java/io/kafbat/ui/settings/drivers/WebDriver.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/e2e-tests/src/main/java/io/kafbat/ui/settings/drivers/WebDriver.java b/e2e-tests/src/main/java/io/kafbat/ui/settings/drivers/WebDriver.java index c884e259e..50efa30fa 100644 --- a/e2e-tests/src/main/java/io/kafbat/ui/settings/drivers/WebDriver.java +++ b/e2e-tests/src/main/java/io/kafbat/ui/settings/drivers/WebDriver.java @@ -11,8 +11,10 @@ import io.kafbat.ui.settings.BaseSource; import io.qameta.allure.Step; import io.qameta.allure.selenide.AllureSelenide; +import io.qameta.allure.selenide.LogType; import java.util.HashMap; import java.util.Map; +import java.util.logging.Level; import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.chrome.ChromeOptions; @@ -97,6 +99,8 @@ public static void browserQuit() { public static void selenideLoggerSetup() { SelenideLogger.addListener("AllureSelenide", new AllureSelenide() .savePageSource(true) - .screenshots(true)); + .screenshots(true) + .enableLogs(LogType.BROWSER, Level.ALL) + ); } }