diff --git a/.github/release.yml b/.github/release.yml index 70852dcbb2..fe88fab0ef 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -19,6 +19,10 @@ changelog: labels: - Ignore for release categories: + - title: 'Security' + labels: + - 'Security' + - title: 'Major Core & Rule Engine' labels: - 'Major Core' diff --git a/.github/workflows/license-header-format.yml b/.github/workflows/license-header-format.yml index 0ac41c7bc8..6be1171b32 100644 --- a/.github/workflows/license-header-format.yml +++ b/.github/workflows/license-header-format.yml @@ -35,7 +35,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'corretto' # https://github.com/actions/setup-java?tab=readme-ov-file#supported-distributions - java-version: '21' + java-version: '25' cache: 'maven' # https://github.com/actions/setup-java?tab=readme-ov-file#caching-sbt-dependencies - name: License header format diff --git a/application/src/main/data/json/edge/instructions/install/centos/instructions.md b/application/src/main/data/json/edge/instructions/install/centos/instructions.md index 1822add6d8..964cdcb887 100644 --- a/application/src/main/data/json/edge/instructions/install/centos/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/centos/instructions.md @@ -1,22 +1,24 @@ -Here is the list of commands that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 7/8 and connect to the server. +Here is the list of commands that can be used to quickly install ThingsBoard Edge on RHEL/CentOS 9/10 and connect to the server. + +**Note:** OpenJDK 25 requires RHEL/CentOS 9+ or RHEL/CentOS 10+. Earlier versions (RHEL/CentOS 7, 8) are not supported. #### Prerequisites Before continuing to installation, execute the following commands to install the necessary tools: ```bash -sudo yum install -y nano wget && sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm +sudo dnf install -y nano wget {:copy-code} ``` -#### Step 1. Install Java 17 (OpenJDK) -ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions: +#### Step 1. Install Java 25 (OpenJDK) +ThingsBoard service is running on Java 25. To install OpenJDK 25, follow these instructions: ```bash -sudo dnf install java-17-openjdk +sudo dnf install -y java-25-openjdk {:copy-code} ``` -Configure your operating system to use OpenJDK 17 by default. You can configure the default version by running the following command: +Configure your operating system to use OpenJDK 25 by default. You can configure the default version by running the following command: ```bash sudo update-alternatives --config java @@ -33,7 +35,7 @@ java -version The expected result is: ```text -openjdk version "17.x.xx" +openjdk version "25.x.xx" OpenJDK Runtime Environment (...) OpenJDK 64-Bit Server VM (build ...) ``` @@ -41,7 +43,7 @@ OpenJDK 64-Bit Server VM (build ...) #### Step 2. Configure ThingsBoard Edge Database ThingsBoard Edge supports **SQL** and **hybrid** database configurations. -In this guide, we’ll use an **SQL** database. +In this guide, we'll use an **SQL** database. For more details about the hybrid setup, please refer to the official installation instructions on the ThingsBoard documentation site. To install the PostgreSQL database, run these commands: @@ -54,19 +56,19 @@ sudo dnf update Install the repository RPM: -* **For CentOS/RHEL 8:** +* **For RHEL/CentOS 9:** ```bash -# Install the repository RPM (For CentOS/RHEL 8): -sudo sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm +# Install the repository RPM (for RHEL/CentOS 9): +sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm {:copy-code} ``` -* **For CentOS/RHEL 9:** +* **For RHEL/CentOS 10:** ```bash -# Install the repository RPM (for CentOS 9): -sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm +# Install the repository RPM (for RHEL/CentOS 10): +sudo dnf -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-10-x86_64/pgdg-redhat-repo-latest.noarch.rpm {:copy-code} ``` @@ -91,8 +93,8 @@ sudo -u postgres psql -c "\password" Then, enter and confirm the password. -Since ThingsBoard Edge uses the PostgreSQL database for local storage, configuring MD5 authentication ensures that only authenticated users or -applications can access the database, thus protecting your data. After configuring the password, +Since ThingsBoard Edge uses the PostgreSQL database for local storage, configuring MD5 authentication ensures that only authenticated users or +applications can access the database, thus protecting your data. After configuring the password, edit the pg_hba.conf file to use MD5 hashing for authentication instead of the default method (ident) for local IPv4 connections. To replace ident with md5, run the following command: @@ -102,7 +104,7 @@ sudo sed -i 's/^host\s\+all\s\+all\s\+127\.0\.0\.1\/32\s\+ident/host all {:copy-code} ``` -Then run the command that will restart the PostgreSQL service to apply configuration changes, connect to the database as a postgres user, +Then run the command that will restart the PostgreSQL service to apply configuration changes, connect to the database as a postgres user, and create the ThingsBoard Edge database (tb_edge). To connect to the PostgreSQL database, enter the PostgreSQL password. ```bash diff --git a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md index b4782dd46a..6f74c09c08 100644 --- a/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md +++ b/application/src/main/data/json/edge/instructions/install/ubuntu/instructions.md @@ -1,14 +1,16 @@ Here is the list of commands that can be used to quickly install ThingsBoard Edge on Ubuntu Server and connect to the server. -#### Step 1. Install Java 17 (OpenJDK) -ThingsBoard service is running on Java 17. To install OpenJDK 17, follow these instructions: +**Note:** OpenJDK 25 requires Ubuntu 22.04 LTS or newer. Earlier versions (20.04 and below) are not supported. + +#### Step 1. Install Java 25 (OpenJDK) +ThingsBoard service is running on Java 25. To install OpenJDK 25, follow these instructions: ```bash -sudo apt update && sudo apt install openjdk-17-jdk +sudo apt update && sudo apt install openjdk-25-jdk {:copy-code} ``` -Configure your operating system to use OpenJDK 17 by default. You can configure the default version by running the following command: +Configure your operating system to use OpenJDK 25 by default. You can configure the default version by running the following command: ```bash sudo update-alternatives --config java @@ -25,7 +27,7 @@ java -version The expected result is: ```text -openjdk version "17.x.xx" +openjdk version "25.x.xx" OpenJDK Runtime Environment (...) OpenJDK 64-Bit Server VM (build ...) ``` diff --git a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json index 7e0bfe1e66..f23747fb52 100644 --- a/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json +++ b/application/src/main/data/json/system/widget_types/bar_chart_with_labels.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.barChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.barChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'humidity', label: 'Humidity', type: 'timeseries' }];\n }\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "latestDataKeySettingsForm": [], diff --git a/application/src/main/data/json/system/widget_types/range_chart.json b/application/src/main/data/json/system/widget_types/range_chart.json index 5d8293debe..1e033ecb2b 100644 --- a/application/src/main/data/json/system/widget_types/range_chart.json +++ b/application/src/main/data/json/system/widget_types/range_chart.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "\n", "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", - "controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.rangeChartWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.rangeChartWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '80%',\n embedTitlePanel: true,\n embedActionsPanel: true,\n supportsUnitConversion: true,\n hasAdditionalLatestDataKeys: false,\n defaultDataKeysFunction: function() {\n return [{ name: 'temperature', label: 'Temperature', type: 'timeseries' }];\n }\n };\n}\n", "settingsForm": [], "dataKeySettingsForm": [], "latestDataKeySettingsForm": [], diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index bb025a5fcf..cde3129d53 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AvailableEntityKeys; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataPageLink; @@ -47,6 +48,8 @@ import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.query.EntityQueryService; import org.thingsboard.server.service.security.permission.Operation; +import java.util.Set; + import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION; @@ -115,9 +118,11 @@ public class EntityQueryController extends BaseController { return entityQueryService.countAlarmsByQuery(getCurrentUser(), query); } + @Deprecated(forRemoval = true) @ApiOperation( - value = "Find Available Entity Keys by Query", + value = "Find Available Entity Keys by Query (deprecated)", notes = """ + **Deprecated.** Use the V2 endpoint (`POST /api/v2/entitiesQuery/find/keys`) instead.\n Returns unique time series and/or attribute key names from entities matching the query.\n Executes the Entity Data Query to find up to 100 entities, then fetches and aggregates all distinct key names.\n Primarily used for UI features like autocomplete suggestions.""" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH @@ -128,9 +133,6 @@ public class EntityQueryController extends BaseController { @Parameter(description = "Entity data query to find entities. Page size is capped at 100.") @RequestBody EntityDataQuery query, - // fixme: combination of timeseries = false and attributes = false is allowed, but always results in empty response, therefore does not make any sense - // such combinations should NOT be allowed, but changing this will break clients - @Parameter(description = """ When true, includes unique time series key names in the response. When false, the 'timeseries' list will be empty.""") @@ -155,6 +157,54 @@ public class EntityQueryController extends BaseController { return wrapFuture(entityQueryService.getKeysByQuery(getCurrentUser(), getTenantId(), query, includeTimeseries, includeAttributes, scope)); } + @ApiOperation( + value = "Find Available Entity Keys By Query", + notes = """ + Discovers unique time series and/or attribute key names available on entities that match the given query. + Works in two steps: first, the request body (an Entity Data Query) is executed to find matching entities + (page size is capped at 100); then, all distinct key names are collected from those entities.\n + Optionally, each key can include a sample — the most recent value (by timestamp) for that key + across all matched entities.""" + + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH + ) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @PostMapping("/v2/entitiesQuery/find/keys") + public DeferredResult findAvailableEntityKeysByQueryV2( + @Parameter(description = "Entity data query to find entities. Page size is capped at 100.") + @RequestBody EntityDataQuery query, + + @Parameter(description = """ + When true, includes unique time series keys in the response. + When false, the 'timeseries' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""") + @RequestParam(defaultValue = "true") boolean includeTimeseries, + + @Parameter(description = """ + When true, includes unique attribute keys in the response. + When false, the 'attributes' field is omitted. At least one of 'includeTimeseries' or 'includeAttributes' must be true.""") + @RequestParam(defaultValue = "true") boolean includeAttributes, + + @Parameter(description = """ + Filters attribute keys by scope. Only applies when 'includeAttributes' is true. + When not specified, scopes are auto-determined: all three scopes (server, client, shared) for device entities, + server scope only for other entity types.""", + schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"})) + @RequestParam(required = false) Set scopes, + + @Parameter(description = """ + When true, each key entry includes a 'sample' object with the most recent value and timestamp. + When false, only key names are returned (sample is omitted from JSON).""") + @RequestParam(defaultValue = "false") boolean includeSamples + ) throws ThingsboardException { + resolveQuery(query); + EntityDataPageLink pageLink = query.getPageLink(); + if (pageLink.getPageSize() > MAX_PAGE_SIZE) { + pageLink.setPageSize(MAX_PAGE_SIZE); + } + return wrapFuture(entityQueryService.findAvailableEntityKeysByQuery( + getCurrentUser(), query, + includeTimeseries, includeAttributes, scopes, includeSamples)); + } + @PreAuthorize("hasAnyAuthority('SYS_ADMIN')") @PostMapping("/edqs/system/request") public void processSystemEdqsRequest(@RequestBody ToCoreEdqsRequest request) { diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index b0f5f41c9a..856b17fc61 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -284,7 +284,7 @@ public class TelemetryController extends BaseController { + MARKDOWN_CODE_BLOCK_END + "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"keys", "startTs", "endTs"}) + @GetMapping(value = "/{entityType}/{entityId}/values/timeseries", params = {"startTs", "endTs"}) public DeferredResult getTimeseries( @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @PathVariable("entityType") String entityType, @Parameter(description = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 5f4a6d29ee..c3d275883e 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -45,6 +45,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.notification.NotificationRequest; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -360,10 +361,12 @@ public class EntityStateSourcingListener { jobManager.onJobUpdate(job); ComponentLifecycleEvent event; - if (job.getResult().getCancellationTs() > 0) { + if (job.getStatus() == JobStatus.CANCELLED) { event = ComponentLifecycleEvent.STOPPED; - } else if (job.getResult().getGeneralError() != null) { + } else if (job.getStatus() == JobStatus.FAILED) { event = ComponentLifecycleEvent.FAILED; + } else if (job.getStatus() == JobStatus.COMPLETED) { + event = ComponentLifecycleEvent.UPDATED; } else { return; } diff --git a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java index f17f10bfd2..d14ba2067a 100644 --- a/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java +++ b/application/src/main/java/org/thingsboard/server/service/job/DefaultJobManager.java @@ -15,15 +15,20 @@ */ package org.thingsboard.server.service.job; +import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.Nullable; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ObjectUtils; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.rule.engine.api.JobManager; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; @@ -33,7 +38,10 @@ import org.thingsboard.server.common.data.job.JobStatus; import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.Task; import org.thingsboard.server.common.data.job.task.TaskResult; +import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; +import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.gen.transport.TransportProtos.TaskProto; @@ -50,7 +58,10 @@ import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -65,6 +76,8 @@ public class DefaultJobManager implements JobManager { private final Map jobProcessors; private final Map>> taskProducers; private final ExecutorService executor; + private final ConcurrentHashMap finishCallbacks = new ConcurrentHashMap<>(); + private final ScheduledExecutorService cleanupScheduler; public DefaultJobManager(JobService jobService, JobStatsService jobStatsService, PartitionService partitionService, TaskProducerQueueFactory queueFactory, TasksQueueConfig queueConfig, @@ -76,6 +89,8 @@ public class DefaultJobManager implements JobManager { this.jobProcessors = jobProcessors.stream().collect(Collectors.toMap(JobProcessor::getType, Function.identity())); this.taskProducers = Arrays.stream(JobType.values()).collect(Collectors.toMap(Function.identity(), queueFactory::createTaskProducer)); this.executor = ThingsBoardExecutors.newWorkStealingPool(Math.max(4, Runtime.getRuntime().availableProcessors()), getClass()); + this.cleanupScheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("job-callback-cleanup"); + this.cleanupScheduler.scheduleWithFixedDelay(this::cleanupStaleCallbacks, 1, 1, TimeUnit.HOURS); } @Override @@ -84,6 +99,25 @@ public class DefaultJobManager implements JobManager { return Futures.submit(() -> jobService.saveJob(job.getTenantId(), job), executor); } + @Override + public ListenableFuture submitJob(Job job, TbCallback finishCallback) { + ListenableFuture saveFuture = submitJob(job); + if (finishCallback != null) { + Futures.addCallback(saveFuture, new FutureCallback<>() { + @Override + public void onSuccess(Job savedJob) { + finishCallbacks.put(savedJob.getId(), finishCallback); + } + + @Override + public void onFailure(Throwable t) { + finishCallback.onFailure(t); + } + }, MoreExecutors.directExecutor()); + } + return saveFuture; + } + @Override public void onJobUpdate(Job job) { JobStatus status = job.getStatus(); @@ -109,6 +143,35 @@ public class DefaultJobManager implements JobManager { } } + @EventListener + public void onJobUpdateEvent(ComponentLifecycleMsg event) { + EntityId entityId = event.getEntityId(); + if (entityId.getEntityType() != EntityType.JOB) { + return; + } + + ComponentLifecycleEvent lifecycleEvent = event.getEvent(); + if (!lifecycleEvent.equals(ComponentLifecycleEvent.STOPPED) && + !lifecycleEvent.equals(ComponentLifecycleEvent.FAILED) && + !lifecycleEvent.equals(ComponentLifecycleEvent.UPDATED)) { + return; + } + JobId jobId = new JobId(entityId.getId()); + TbCallback callback = finishCallbacks.remove(jobId); + if (callback == null) { + return; + } + executor.execute(() -> { + try { + Job job = jobService.findJobById(event.getTenantId(), jobId); + invokeFinishCallback(job, callback); + } catch (Throwable e) { + log.error("[{}] Failed to invoke finish callback", jobId, e); + callback.onFailure(e); + } + }); + } + private void processJob(Job job) { TenantId tenantId = job.getTenantId(); JobId jobId = job.getId(); @@ -195,12 +258,41 @@ public class DefaultJobManager implements JobManager { }); } + private void invokeFinishCallback(@Nullable Job job, TbCallback callback) { + if (job == null) { + callback.onFailure(new RuntimeException("Job not found")); + } else if (job.getStatus() == JobStatus.COMPLETED) { + callback.onSuccess(); + } else { + callback.onFailure(new RuntimeException(job.getError())); + } + } + + private void cleanupStaleCallbacks() { + finishCallbacks.entrySet().removeIf(entry -> { + JobId jobId = entry.getKey(); + try { + Job job = jobService.findJobById(TenantId.SYS_TENANT_ID, jobId); + if (job == null || job.getStatus().isOneOf(JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED)) { + invokeFinishCallback(job, entry.getValue()); + return true; + } + return false; + } catch (Throwable e) { + log.error("[{}] Failed to cleanup stale callback", jobId, e); + entry.getValue().onFailure(e); + return true; + } + }); + } + private JobProcessor getJobProcessor(JobType jobType) { return jobProcessors.get(jobType); } @PreDestroy private void destroy() { + cleanupScheduler.shutdownNow(); executor.shutdownNow(); } diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 4d76989a92..50f0f0e75a 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -15,13 +15,15 @@ */ package org.thingsboard.server.service.query; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.util.CollectionUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.KvUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; @@ -30,11 +32,17 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AvailableEntityKeys; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeySample; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.EntityCountQuery; @@ -59,11 +67,13 @@ import org.thingsboard.server.service.security.model.SecurityUser; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeMap; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -253,7 +263,7 @@ public class DefaultEntityQueryService implements EntityQueryService { if (isAttributes) { Map> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType)); List>> futures = new ArrayList<>(typesMap.size()); - typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, scope)))); + typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope)))); attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> { if (CollectionUtils.isEmpty(lists)) { return Collections.emptyList(); @@ -274,4 +284,135 @@ public class DefaultEntityQueryService implements EntityQueryService { }, dbCallbackExecutor); } + @Override + public ListenableFuture findAvailableEntityKeysByQuery(SecurityUser securityUser, EntityDataQuery query, + boolean includeTimeseries, boolean includeAttributes, + Set scopes, boolean includeSamples) { + if (!includeTimeseries && !includeAttributes) { + return Futures.immediateFailedFuture( + new IllegalArgumentException("At least one of 'includeTimeseries' or 'includeAttributes' must be true")); + } + + return Futures.transformAsync(findEntityIdsByQueryAsync(securityUser, query), ids -> { + if (ids.isEmpty()) { + return immediateFuture(new AvailableEntityKeysV2( + Collections.emptySet(), + includeTimeseries ? Collections.emptyList() : null, + includeAttributes ? Collections.emptyMap() : null)); + } + + TenantId tenantId = securityUser.getTenantId(); + Set entityTypes = ids.stream().map(EntityId::getEntityType).collect(Collectors.toSet()); + + var tsFuture = includeTimeseries ? fetchTimeseriesKeys(tenantId, ids, includeSamples) : null; + + Set effectiveScopes = includeAttributes + ? resolveAttributeScopes(scopes, entityTypes) : Collections.emptySet(); + var attrFutures = effectiveScopes.stream() + .map(scope -> fetchAttributeKeys(tenantId, ids, scope, includeSamples)) + .toList(); + + return assembleResult(entityTypes, tsFuture, attrFutures); + }, dbCallbackExecutor); + } + + private ListenableFuture> findEntityIdsByQueryAsync(SecurityUser securityUser, EntityDataQuery query) { + return Futures.transform(entityService.findEntityDataByQueryAsync(securityUser.getTenantId(), securityUser.getCustomerId(), query), + page -> page.getData().stream() + .map(EntityData::getEntityId) + .toList(), + dbCallbackExecutor); + } + + private static Set resolveAttributeScopes(Set requestedScopes, Set entityTypes) { + boolean hasDevices = entityTypes.contains(EntityType.DEVICE); + Set scopes; + if (CollectionUtils.isNotEmpty(requestedScopes)) { + scopes = requestedScopes; + } else { // auto-determine scopes + scopes = hasDevices + ? Set.of(AttributeScope.SERVER_SCOPE, AttributeScope.CLIENT_SCOPE, AttributeScope.SHARED_SCOPE) + : Collections.singleton(AttributeScope.SERVER_SCOPE); + } + // Non-device entities only support SERVER_SCOPE + if (!hasDevices) { + return scopes.contains(AttributeScope.SERVER_SCOPE) + ? Collections.singleton(AttributeScope.SERVER_SCOPE) + : Collections.emptySet(); + } + return scopes; + } + + private ListenableFuture> fetchTimeseriesKeys(TenantId tenantId, List entityIds, boolean includeSamples) { + if (includeSamples) { + return Futures.transform( + timeseriesService.findLatestByEntityIdsAsync(tenantId, entityIds), + entries -> toKeyInfos(entries, true), + dbCallbackExecutor); + } + return Futures.transform( + timeseriesService.findAllKeysByEntityIdsAsync(tenantId, entityIds), + keys -> keys.stream().sorted().map(k -> new KeyInfo(k, null)).toList(), + dbCallbackExecutor); + } + + private ListenableFuture>> fetchAttributeKeys( + TenantId tenantId, List entityIds, AttributeScope scope, boolean includeSamples) { + if (includeSamples) { + return Futures.transform( + attributesService.findLatestByEntityIdsAndScopeAsync(tenantId, entityIds, scope), + entries -> Map.entry(scope, toKeyInfos(entries, true)), + dbCallbackExecutor); + } + return Futures.transform( + attributesService.findAllKeysByEntityIdsAndScopeAsync(tenantId, entityIds, scope), + keys -> Map.entry(scope, keys.stream().sorted().map(k -> new KeyInfo(k, null)).toList()), + dbCallbackExecutor); + } + + private ListenableFuture assembleResult( + Set entityTypes, + ListenableFuture> tsFuture, + List>>> attrFutures) { + var allAttrFuture = attrFutures.isEmpty() + ? immediateFuture(List.>>of()) + : Futures.allAsList(attrFutures); + + List> allFutures = new ArrayList<>(); + if (tsFuture != null) { + allFutures.add(tsFuture); + } + allFutures.add(allAttrFuture); + + var finalTsFuture = tsFuture; + return Futures.whenAllComplete(allFutures) + .call(() -> { + List tsKeys = finalTsFuture != null ? Futures.getDone(finalTsFuture) : null; + Map> attrMap = attrFutures.isEmpty() ? null : new TreeMap<>(); + if (attrMap != null) { + for (var entry : Futures.getDone(allAttrFuture)) { + attrMap.put(entry.getKey(), entry.getValue()); + } + } + return new AvailableEntityKeysV2(entityTypes, tsKeys, attrMap); + }, dbCallbackExecutor); + } + + private static List toKeyInfos(List entries, boolean includeSamples) { + return entries.stream() + .map(e -> new KeyInfo(e.getKey(), includeSamples ? toKeySample(e) : null)) + .sorted(Comparator.comparing(KeyInfo::key)) + .toList(); + } + + private static KeySample toKeySample(KvEntry entry) { + long ts = entry instanceof TsKvEntry tsKv ? tsKv.getTs() + : entry instanceof AttributeKvEntry attr ? attr.getLastUpdateTs() + : 0; + JsonNode value = entry.getDataType() == DataType.JSON + ? JacksonUtil.toJsonNode(entry.getJsonValue().get()) + : JacksonUtil.valueToTree(entry.getValue()); + return new KeySample(ts, value); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java index a90cf684a1..354f8e9278 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java @@ -23,11 +23,14 @@ import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AvailableEntityKeys; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.service.security.model.SecurityUser; +import java.util.Set; + public interface EntityQueryService { long countEntitiesByQuery(SecurityUser securityUser, EntityCountQuery query); @@ -41,4 +44,8 @@ public interface EntityQueryService { ListenableFuture getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query, boolean isTimeseries, boolean isAttributes, AttributeScope scope); + ListenableFuture findAvailableEntityKeysByQuery(SecurityUser securityUser, EntityDataQuery query, + boolean includeTimeseries, boolean includeAttributes, + Set scopes, boolean includeSamples); + } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 611eae26fe..0e8213fafd 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -341,6 +341,8 @@ cassandra: set_null_values_enabled: "${CASSANDRA_QUERY_SET_NULL_VALUES_ENABLED:true}" # log one of cassandra queries with specified frequency (0 - logging is disabled) print_queries_freq: "${CASSANDRA_QUERY_PRINT_FREQ:0}" + # Maximum total size in bytes of a Cassandra query result set across all pages. Default is 50MB. 0 means unlimited + max_result_set_size_in_bytes: "${CASSANDRA_QUERY_MAX_RESULT_SET_SIZE_IN_BYTES:52428800}" tenant_rate_limits: # Whether to print rate-limited tenant names when printing Cassandra query queue statistic print_tenant_names: "${CASSANDRA_QUERY_TENANT_RATE_LIMITS_PRINT_TENANT_NAMES:false}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index eb053cff76..e8fcd6a723 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -123,6 +123,8 @@ import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.job.Job; import org.thingsboard.server.common.data.job.JobType; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.notification.Notification; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; import org.thingsboard.server.common.data.notification.NotificationType; @@ -177,6 +179,7 @@ import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -718,6 +721,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return assetProfile; } + protected Device createDevice(String name) throws Exception { + return createDevice(name, "default", null, null); + } + protected Device createDevice(String name, String accessToken) throws Exception { return createDevice(name, "default", null, accessToken); } @@ -731,7 +738,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); deviceData.setConfiguration(new DefaultDeviceConfiguration()); device.setDeviceData(deviceData); - return doPost("/api/device?accessToken=" + accessToken, device, Device.class); + if (accessToken != null) { + return doPost("/api/device?accessToken=" + accessToken, device, Device.class); + } else { + return doPost("/api/device", device, Device.class); + } } protected Device assignDeviceToCustomer(DeviceId deviceId, CustomerId customerId) { @@ -1219,7 +1230,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { CalculatedFieldState calculatedFieldState = statesMap.get(cfId); boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastScheduledRefreshTs() < - System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); + System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); return isReady; }); @@ -1411,7 +1422,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected List findJobs(List types, List entities) throws Exception { return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) + - "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", + "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } @@ -1425,12 +1436,37 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected void postTelemetry(EntityId entityId, String payload) throws Exception { doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + - "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + } + + protected void postTelemetry(EntityId entityId, TsKvEntry entry) throws Exception { + var values = JacksonUtil.newObjectNode(); + JacksonUtil.addKvEntry(values, entry); + + var payload = JacksonUtil.newObjectNode() + .put("ts", entry.getTs()) + .set("values", values); + + var url = "/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/timeseries/any"; + doPostAsync(url, payload, 30_000L).andExpect(status().isOk()); } protected void postAttributes(EntityId entityId, AttributeScope scope, String payload) throws Exception { doPostAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + - "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + "/attributes/" + scope, JacksonUtil.toJsonNode(payload), 30_000L).andExpect(status().isOk()); + } + + protected void postAttributes(EntityId entityId, AttributeScope scope, KvEntry... attributes) throws Exception { + postAttributes(entityId, scope, Arrays.asList(attributes)); + } + + protected void postAttributes(EntityId entityId, AttributeScope scope, Collection attributes) throws Exception { + var url = "/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/attributes/" + scope; + var payload = JacksonUtil.newObjectNode(); + for (KvEntry entry : attributes) { + JacksonUtil.addKvEntry(payload, entry); + } + doPostAsync(url, payload, 30_000L).andExpect(status().isOk()); } protected CalculatedField saveCalculatedField(CalculatedField calculatedField) { @@ -1439,7 +1475,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected PageData getEntityCalculatedFields(EntityId entityId, CalculatedFieldType type, PageLink pageLink) throws Exception { return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields" + - (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); + (type != null ? "?type=" + type.name() + "&" : "?"), new TypeReference<>() {}, pageLink); } protected PageData getCalculatedFieldNames(CalculatedFieldType type, PageLink pageLink) throws Exception { @@ -1452,11 +1488,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { List entities, List names) throws Exception { return doGetTypedWithPageLink("/api/calculatedFields?" + - (type != null ? "types=" + type + "&" : "") + - (entityType != null ? "entityType=" + entityType + "&" : "") + - (entities != null ? "entities=" + String.join(",", - entities.stream().map(UUID::toString).toList()) + "&" : "") + - (names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""), + (type != null ? "types=" + type + "&" : "") + + (entityType != null ? "entityType=" + entityType + "&" : "") + + (entities != null ? "entities=" + String.join(",", + entities.stream().map(UUID::toString).toList()) + "&" : "") + + (names != null ? names.stream().map(name -> "name=" + name + "&").collect(Collectors.joining("")) : ""), new TypeReference>() {}, new PageLink(10)).getData(); } diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index 7332ee26c6..07b68210bb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -1732,11 +1732,4 @@ public class DeviceControllerTest extends AbstractControllerTest { assertThat(fifthDevice.getName()).isEqualTo("My unique device_2"); } - private Device createDevice(String name) { - Device device = new Device(); - device.setName(name); - device.setType("default"); - return doPost("/api/device", device, Device.class); - } - } diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index f15ab09ab2..6c1a63194c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -23,6 +23,7 @@ import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.edqs.EdqsState; import org.thingsboard.server.common.data.edqs.EdqsState.EdqsApiMode; import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest; +import org.awaitility.core.ThrowingRunnable; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; @@ -86,6 +87,11 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { result -> result == expectedResult); } + @Override + protected void verifyAvailableKeysByQueryV2(ThrowingRunnable assertion) { + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(assertion); + } + @Test public void testEdqsState() throws Exception { loginSysAdmin(); diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index e1173ef9a7..835f00985b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -17,7 +17,11 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.awaitility.core.ThrowingRunnable; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -43,12 +47,21 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataPageLink; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AliasEntityId; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2.KeyInfo; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.DynamicValueSourceType; @@ -84,6 +97,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.stream.Collectors; @@ -1329,7 +1343,7 @@ public class EntityQueryControllerTest extends AbstractControllerTest { //assign dashboard doPost("/api/customer/" + savedCustomer.getId().getId().toString() - + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); // check entity data query by customer User customerUser = new User(); @@ -1454,6 +1468,10 @@ public class EntityQueryControllerTest extends AbstractControllerTest { return result; } + protected void verifyAvailableKeysByQueryV2(ThrowingRunnable assertion) throws Throwable { + assertion.run(); + } + private KeyFilter getEntityFieldEqualFilter(String keyName, String value) { return getEntityFieldKeyFilter(keyName, value, StringFilterPredicate.StringOperation.EQUAL); } @@ -1494,4 +1512,221 @@ public class EntityQueryControllerTest extends AbstractControllerTest { return nameFilter; } + // --- findAvailableEntityKeysV2 tests --- + + @Test + public void testFindAvailableKeysByQueryV2() throws Throwable { + // GIVEN — two devices matched by query; a third device should not be matched + var device1 = createDevice("Test device 1"); + var device2 = createDevice("Test device 2"); + var unmatchedDevice = createDevice("Unmatched device"); + + // unmatched device has unique keys that must NOT appear in the result + postTelemetry(unmatchedDevice.getId(), new BasicTsKvEntry(9000, new DoubleDataEntry("unmatchedTs", 999.0))); + postAttributes(unmatchedDevice.getId(), AttributeScope.SHARED_SCOPE, new StringDataEntry("unmatchedAttr", "nope")); + + // device1: timeseries1 (Double) with two data points, and timeseries2 older data point + postTelemetry(device1.getId(), new BasicTsKvEntry(1000, new DoubleDataEntry("timeseries1", 10.0))); + postTelemetry(device1.getId(), new BasicTsKvEntry(2000, new DoubleDataEntry("timeseries1", 20.5))); + postTelemetry(device1.getId(), new BasicTsKvEntry(1000, new LongDataEntry("timeseries2", 100L))); + + // device2: timeseries2 (Long) with a newer data point, and timeseries3 only on this device + postTelemetry(device2.getId(), new BasicTsKvEntry(3000, new LongDataEntry("timeseries2", 300L))); + postTelemetry(device2.getId(), new BasicTsKvEntry(5000, new DoubleDataEntry("timeseries3", 99.9))); + + // device1: SHARED_SCOPE attributes + postAttributes(device1.getId(), AttributeScope.SHARED_SCOPE, + new BooleanDataEntry("sharedAttribute1", true), new DoubleDataEntry("sharedAttribute2", 3.14)); + + // device2: CLIENT_SCOPE attributes (saved via service to bypass API restriction) + attributesService.save(tenantId, device2.getId(), AttributeScope.CLIENT_SCOPE, List.of( + new BaseAttributeKvEntry(new JsonDataEntry("clientAttribute1", "{\"key\":\"val\"}"), System.currentTimeMillis()), + new BaseAttributeKvEntry(new BooleanDataEntry("clientAttribute2", false), System.currentTimeMillis()) + )).get(); + + // device1 also has SERVER_SCOPE attributes (should be omitted by scope filter) + postAttributes(device1.getId(), AttributeScope.SERVER_SCOPE, + new StringDataEntry("serverAttribute1", "sv1"), new LongDataEntry("serverAttribute2", 42L)); + + // WHEN — query matches both devices; request timeseries + only SHARED and CLIENT attribute scopes + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("default")); + filter.setDeviceNameFilter("Test device"); + EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, null); + EntityDataQuery query = new EntityDataQuery(filter, pageLink, List.of(), null, null); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query, + true, true, List.of(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE), true); + + assertThat(result.entityTypes()).containsExactly(EntityType.DEVICE); + + // timeseries: keys collected from both devices, samples contain the freshest data points + assertThat(result.timeseries()).extracting(KeyInfo::key) + .containsExactly("timeseries1", "timeseries2", "timeseries3"); + assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNotNull()); + assertKeySample(result.timeseries(), "timeseries1", new DoubleNode(20.5), 2000); // from device1 + assertKeySample(result.timeseries(), "timeseries2", new IntNode(300), 3000); // from device2 (newer) + assertKeySample(result.timeseries(), "timeseries3", new DoubleNode(99.9), 5000); // only on device2 + + // SERVER_SCOPE must be fully omitted from the response + assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SHARED_SCOPE, AttributeScope.CLIENT_SCOPE); + + // SHARED_SCOPE: from device1 (alphabetical order) + assertThat(result.attributes().get(AttributeScope.SHARED_SCOPE)) + .extracting(KeyInfo::key).containsExactly("sharedAttribute1", "sharedAttribute2"); + assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute1", BooleanNode.TRUE); + assertKeySample(result.attributes().get(AttributeScope.SHARED_SCOPE), "sharedAttribute2", new DoubleNode(3.14)); + + // CLIENT_SCOPE: from device2 (alphabetical order) + assertThat(result.attributes().get(AttributeScope.CLIENT_SCOPE)) + .extracting(KeyInfo::key).containsExactly("clientAttribute1", "clientAttribute2"); + assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute1", JacksonUtil.toJsonNode("{\"key\":\"val\"}")); + assertKeySample(result.attributes().get(AttributeScope.CLIENT_SCOPE), "clientAttribute2", BooleanNode.FALSE); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_withoutSamples() throws Throwable { + // GIVEN + var device = createDevice("Test device"); + postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0))); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0")); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("Test device"), true, true, null, false); + + assertThat(result.timeseries()).allSatisfy(ki -> assertThat(ki.sample()).isNull()); + assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) + .allSatisfy(ki -> assertThat(ki.sample()).isNull()); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_timeseriesOnly() throws Throwable { + // GIVEN + var device = createDevice("Test device"); + postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0))); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0")); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("Test device"), true, false, null, false); + + assertThat(result.timeseries()).extracting(KeyInfo::key).contains("temperature"); + assertThat(result.attributes()).isNull(); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_attributesOnly() throws Throwable { + // GIVEN + var device = createDevice("Test device"); + postTelemetry(device.getId(), new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 10.0))); + postAttributes(device.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("firmware", "v1.0")); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("Test device"), false, true, null, false); + + assertThat(result.timeseries()).isNull(); + assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) + .extracting(KeyInfo::key).contains("firmware"); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_noMatchingEntities() throws Throwable { + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2( + buildDeviceQuery("NonExistentDevice_" + UUID.randomUUID()), true, true, null, true); + + assertThat(result.entityTypes()).isEmpty(); + assertThat(result.timeseries()).isEmpty(); + assertThat(result.attributes()).isEmpty(); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_assetUsesServerScopeOnly() throws Throwable { + // GIVEN + var asset = new Asset(); + asset.setName("Test asset"); + asset.setType("default"); + asset = doPost("/api/asset", asset, Asset.class); + postAttributes(asset.getId(), AttributeScope.SERVER_SCOPE, new StringDataEntry("location", "warehouse")); + + // WHEN + var filter = new SingleEntityFilter(); + filter.setSingleEntity(AliasEntityId.fromEntityId(asset.getId())); + var query = new EntityDataQuery(filter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), null, null); + + // THEN + verifyAvailableKeysByQueryV2(() -> { + AvailableEntityKeysV2 result = findAvailableEntityKeysByQueryV2(query, false, true, null, false); + + assertThat(result.entityTypes()).containsExactly(EntityType.ASSET); + assertThat(result.attributes()).containsOnlyKeys(AttributeScope.SERVER_SCOPE); + assertThat(result.attributes().get(AttributeScope.SERVER_SCOPE)) + .extracting(KeyInfo::key).containsExactly("location"); + }); + } + + @Test + public void testFindAvailableKeysByQueryV2_rejectsWhenNoKeyTypeRequested() throws Exception { + // WHEN / THEN + EntityDataQuery query = buildDeviceQuery("NonExistent"); + + doPostAsync("/api/v2/entitiesQuery/find/keys?includeTimeseries=false&includeAttributes=false", + query, 30_000L).andExpect(status().isBadRequest()); + } + + protected AvailableEntityKeysV2 findAvailableEntityKeysByQueryV2(EntityDataQuery query, + boolean includeTimeseries, boolean includeAttributes, + List scopes, boolean includeSamples) throws Exception { + StringBuilder url = new StringBuilder("/api/v2/entitiesQuery/find/keys?") + .append("includeTimeseries=").append(includeTimeseries) + .append("&includeAttributes=").append(includeAttributes) + .append("&includeSamples=").append(includeSamples); + if (scopes != null) { + for (AttributeScope scope : scopes) { + url.append("&scopes=").append(scope); + } + } + return doPostAsyncWithTypedResponse(url.toString(), query, + new TypeReference<>() {}, status().isOk()); + } + + private static void assertKeySample(List keys, String expectedKey, JsonNode expectedValue, long expectedTs) { + KeyInfo keyInfo = findKeyInfo(keys, expectedKey); + assertThat(keyInfo.sample()).isNotNull(); + assertThat(keyInfo.sample().value()).isEqualTo(expectedValue); + assertThat(keyInfo.sample().ts()).isEqualTo(expectedTs); + } + + private static void assertKeySample(List keys, String expectedKey, JsonNode expectedValue) { + KeyInfo keyInfo = findKeyInfo(keys, expectedKey); + assertThat(keyInfo.sample()).isNotNull(); + assertThat(keyInfo.sample().value()).isEqualTo(expectedValue); + assertThat(keyInfo.sample().ts()).isGreaterThan(0); + } + + private static KeyInfo findKeyInfo(List keys, String key) { + return keys.stream() + .filter(ki -> ki.key().equals(key)).findFirst().orElseThrow(); + } + + private static EntityDataQuery buildDeviceQuery(String deviceName) { + var filter = new DeviceTypeFilter(); + filter.setDeviceTypes(Collections.singletonList("default")); + filter.setDeviceNameFilter(deviceName); + return new EntityDataQuery(filter, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), null, null); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java index b0c76a16a8..b4ce7179d2 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityRelationControllerTest.java @@ -633,13 +633,6 @@ public class EntityRelationControllerTest extends AbstractControllerTest { deleteDifferentTenant(); } - private Device createDevice(String name) { - var device = new Device(); - device.setName(name); - device.setType("default"); - return doPost("/api/device", device, Device.class); - } - private ResultActions getRelation(EntityRelation relation) throws Exception { return doGet("/api/relation?" + "fromId=" + relation.getFrom().getId() + diff --git a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java index dd51cdd440..ce6d291546 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TelemetryControllerTest.java @@ -102,24 +102,29 @@ public class TelemetryControllerTest extends AbstractControllerTest { Assert.assertEquals(11L, thirdIntervalResult.get("value").asLong()); Assert.assertEquals(thirdIntervalTs, thirdIntervalResult.get("ts").asLong()); - result = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + + ObjectNode resultByMonth = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?keys=t&startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}", ObjectNode.class, startTs, endTs, "SUM", "MONTH", "Europe/Kyiv"); - Assert.assertNotNull(result); - Assert.assertNotNull(result.get("t")); - Assert.assertEquals(1, result.get("t").size()); + Assert.assertNotNull(resultByMonth); + Assert.assertNotNull(resultByMonth.get("t")); + Assert.assertEquals(1, resultByMonth.get("t").size()); - var monthResult = result.get("t").get(0); + var monthResult = resultByMonth.get("t").get(0); Assert.assertEquals(22L, monthResult.get("value").asLong()); Assert.assertEquals(middleOfTheInterval, monthResult.get("ts").asLong()); - // get all latest (without keys parameter) - ObjectNode allLatest = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + + // check timeseries history with key parameter (instead of keys) has the same result as with keys parameter + ObjectNode resultByKey = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + + "/values/timeseries?key=t&startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}", + ObjectNode.class, startTs, endTs, "SUM", "WEEK_ISO", "Europe/Kyiv"); + Assert.assertEquals(result, resultByKey); + + // check timeseries history without keys and key results into empty object + ObjectNode resultWithoutKeyAndKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/values/timeseries?startTs={startTs}&endTs={endTs}&agg={agg}&intervalType={intervalType}&timeZone={timeZone}", ObjectNode.class, startTs, endTs, "SUM", "WEEK_ISO", "Europe/Kyiv"); - Assert.assertNotNull(allLatest); - Assert.assertNotNull(allLatest.get("t")); + Assert.assertTrue(resultWithoutKeyAndKeys.isEmpty()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java index c17a9f6bfb..63fa4a7da4 100644 --- a/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.job; +import com.google.common.util.concurrent.SettableFuture; import lombok.SneakyThrows; import org.junit.After; import org.junit.Before; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.job.JobType; import org.thingsboard.server.common.data.job.task.DummyTaskResult; import org.thingsboard.server.common.data.job.task.DummyTaskResult.DummyTaskFailure; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.job.JobDao; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -44,10 +46,12 @@ import org.thingsboard.server.queue.task.JobStatsService; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; @@ -518,19 +522,114 @@ public class JobManagerTest extends AbstractControllerTest { }); } + @Test + public void testSubmitJob_finishCallback_success() { + SettableFuture future = SettableFuture.create(); + + int tasksCount = 3; + submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(tasksCount) + .taskProcessingTimeMs(100) + .build(), "test-job", TbCallback.wrap(future)); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThat(future.get()).isNull(); + }); + } + + @Test + public void testSubmitJob_finishCallback_taskFailure() { + SettableFuture future = SettableFuture.create(); + + submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(1) + .failedTasksCount(2) + .errors(List.of("task error")) + .retries(0) + .taskProcessingTimeMs(100) + .build(), "test-job", TbCallback.wrap(future)); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .cause() + .hasMessage("task error; task error"); + }); + } + + @Test + public void testSubmitJob_finishCallback_generalError() { + SettableFuture future = SettableFuture.create(); + + submitJob(DummyJobConfiguration.builder() + .generalError("Something went wrong") + .submittedTasksBeforeGeneralError(0) + .build(), "test-job", TbCallback.wrap(future)); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .cause() + .hasMessage("Something went wrong"); + }); + } + + @Test + public void testSubmitJob_finishCallback_cancelled() throws Exception { + SettableFuture future = SettableFuture.create(); + + JobId jobId = submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(200) + .taskProcessingTimeMs(50) + .build(), "test-job", TbCallback.wrap(future)).getId(); + + Thread.sleep(500); + cancelJob(jobId); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .cause() + .hasMessage("The task was cancelled"); + }); + } + + @Test + public void testSubmitJob_finishCallback_zeroTasks() { + SettableFuture future = SettableFuture.create(); + + submitJob(DummyJobConfiguration.builder() + .successfulTasksCount(0) + .build(), "test-job", TbCallback.wrap(future)); + + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertThat(future.isDone()).isTrue(); + assertThat(future.get()).isNull(); + }); + } + private Job submitJob(DummyJobConfiguration configuration) { return submitJob(configuration, "test-job"); } @SneakyThrows private Job submitJob(DummyJobConfiguration configuration, String key) { + return submitJob(configuration, key, null); + } + + @SneakyThrows + private Job submitJob(DummyJobConfiguration configuration, String key, TbCallback callback) { return jobManager.submitJob(Job.builder() .tenantId(tenantId) .type(JobType.DUMMY) .key(key) .entityId(jobEntity.getId()) .configuration(configuration) - .build()).get(); + .build(), callback).get(); } private List getFailures(JobResult jobResult) { diff --git a/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java index c227ec76aa..d33f1755d5 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.resource; +import org.awaitility.Awaitility; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; @@ -29,6 +30,8 @@ import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.concurrent.TimeUnit; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.timeout; @@ -61,6 +64,8 @@ public class DefaultResourceDataCacheTest extends AbstractControllerTest { TbResourceInfo savedResource = tbResourceService.save(resource); verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId()); + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get()).isNotNull()); TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); assertThat(cachedData.getData()).isEqualTo(data); assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor); @@ -76,8 +81,8 @@ public class DefaultResourceDataCacheTest extends AbstractControllerTest { TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId()); tbResourceService.delete(resourceById, true, null); verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId()); - TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); - assertThat(cachedDataAfterDeletion).isEqualTo(null); + Awaitility.await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> + assertThat(resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get()).isNull()); } } diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index 67e4ba8f32..76caa4d53c 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -2355,7 +2355,8 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { expected.put("date_1", d1.toString()); TbDate d2 = new TbDate(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); expected.put("dLocal_2", d2.toLocaleString()); - expected.put("dLocal_2_us", "8/5/23, 10:04:05 PM"); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + expected.put("dLocal_2_us", Runtime.version().feature() >= 21 ? "8/5/23, 10:04:05\u202FPM" : "8/5/23, 10:04:05 PM"); expected.put("dIso_2", d2.toISOString()); expected.put("date_2", d2.toString()); Object actual = invokeScript(evalScript(decoderStr), msgStr); @@ -2387,7 +2388,8 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { """, s2); LinkedHashMap expected = new LinkedHashMap<>(); TbDate d1 = new TbDate(2023, 8, 6, 4, 4, 5); - expected.put("dLocal_1_us", "8/6/23, 4:04:05 AM"); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + expected.put("dLocal_1_us", Runtime.version().feature() >= 21 ? "8/6/23, 4:04:05\u202FAM" : "8/6/23, 4:04:05 AM"); expected.put("dIso_1", d1.toISOString()); expected.put("d1", d1.toString()); TbDate d2 = new TbDate(s2); @@ -2419,7 +2421,8 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { TbDate d = new TbDate(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); expected.put("dIso", d.toISOString()); expected.put("dLocal_utc", d.toLocaleString("UTC")); - expected.put("dLocal_us", "8/5/23, 10:04:05 PM"); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + expected.put("dLocal_us", Runtime.version().feature() >= 21 ? "8/5/23, 10:04:05\u202FPM" : "8/5/23, 10:04:05 PM"); expected.put("dLocal_de", "06.08.23, 04:04:05"); Object actual = invokeScript(evalScript(decoderStr), msgStr); assertEquals(expected, actual); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java index 3166c64835..c5e359e2d5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java @@ -27,9 +27,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -/** - * @author Andrew Shvayka - */ public interface AttributesService { ListenableFuture> find(TenantId tenantId, EntityId entityId, AttributeScope scope, String attributeKey); @@ -48,7 +45,13 @@ public interface AttributesService { List findAllKeysByEntityIds(TenantId tenantId, List entityIds); - List findAllKeysByEntityIds(TenantId tenantId, List entityIds, AttributeScope scope); + List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope); + + ListenableFuture> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope); + + List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope); + + ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope); int removeAllByEntityId(TenantId tenantId, EntityId entityId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java index 3a67c31b57..fdd722a266 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.entity; import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -51,4 +52,6 @@ public interface EntityService { PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); + ListenableFuture> findEntityDataByQueryAsync(TenantId tenantId, CustomerId customerId, EntityDataQuery query); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/ResultSetSizeLimitExceededException.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/ResultSetSizeLimitExceededException.java new file mode 100644 index 0000000000..f20a8ebde0 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/ResultSetSizeLimitExceededException.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.nosql; + +import lombok.Getter; + +@Getter +public class ResultSetSizeLimitExceededException extends IllegalArgumentException { + + private final long limitBytes; + private final long actualBytes; + + public ResultSetSizeLimitExceededException(long limitBytes, long actualBytes) { + super("Result set size exceeds the maximum allowed limit. Please narrow your query"); + this.limitBytes = limitBytes; + this.actualBytes = actualBytes; + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSet.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSet.java index 9dd5c6f4b1..a7a4aa5fa8 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSet.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/nosql/TbResultSet.java @@ -34,6 +34,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; public class TbResultSet implements AsyncResultSet { @@ -89,9 +90,14 @@ public class TbResultSet implements AsyncResultSet { } public ListenableFuture> allRows(Executor executor) { + return allRows(executor, 0); + } + + public ListenableFuture> allRows(Executor executor, long maxResultSetSizeBytes) { List allRows = new ArrayList<>(); SettableFuture> resultFuture = SettableFuture.create(); - this.processRows(originalStatement, delegate, allRows, resultFuture, executor); + AtomicLong accumulatedBytes = new AtomicLong(0); + this.processRows(originalStatement, delegate, allRows, resultFuture, executor, maxResultSetSizeBytes, accumulatedBytes); return resultFuture; } @@ -99,7 +105,19 @@ public class TbResultSet implements AsyncResultSet { AsyncResultSet resultSet, List allRows, SettableFuture> resultFuture, - Executor executor) { + Executor executor, + long maxResultSetSizeBytes, + AtomicLong accumulatedBytes) { + if (maxResultSetSizeBytes > 0) { + int pageSizeInBytes = resultSet.getExecutionInfo().getResponseSizeInBytes(); + if (pageSizeInBytes > 0) { + accumulatedBytes.addAndGet(pageSizeInBytes); + } + if (accumulatedBytes.get() > maxResultSetSizeBytes) { + resultFuture.setException(new ResultSetSizeLimitExceededException(maxResultSetSizeBytes, accumulatedBytes.get())); + return; + } + } allRows.addAll(loadRows(resultSet)); if (resultSet.hasMorePages()) { ByteBuffer nextPagingState = resultSet.getExecutionInfo().getPagingState(); @@ -110,7 +128,7 @@ public class TbResultSet implements AsyncResultSet { @Override public void onSuccess(@Nullable TbResultSet result) { processRows(nextStatement, result, - allRows, resultFuture, executor); + allRows, resultFuture, executor, maxResultSetSizeBytes, accumulatedBytes); } @Override diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index 5bf2c63176..6d5727474a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java @@ -30,9 +30,6 @@ import java.util.Collection; import java.util.List; import java.util.Optional; -/** - * @author Andrew Shvayka - */ public interface TimeseriesService { ListenableFuture> findAllByQueries(TenantId tenantId, EntityId entityId, List queries); @@ -65,6 +62,10 @@ public interface TimeseriesService { ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds); + List findLatestByEntityIds(TenantId tenantId, List entityIds); + + ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds); + void cleanup(long systemTtl); } diff --git a/common/dao-api/src/test/java/org/thingsboard/server/dao/nosql/TbResultSetTest.java b/common/dao-api/src/test/java/org/thingsboard/server/dao/nosql/TbResultSetTest.java new file mode 100644 index 0000000000..01c1c2c506 --- /dev/null +++ b/common/dao-api/src/test/java/org/thingsboard/server/dao/nosql/TbResultSetTest.java @@ -0,0 +1,140 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.nosql; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TbResultSetTest { + + @Test + void allRows_withinLimit_returnsAllRows() throws Exception { + Row row = mock(Row.class); + AsyncResultSet asyncResultSet = createMockResultSet(List.of(row), false, 1000); + Statement statement = mock(Statement.class); + + TbResultSet tbResultSet = new TbResultSet(statement, asyncResultSet, s -> null); + ListenableFuture> future = tbResultSet.allRows(MoreExecutors.directExecutor(), 5000); + + List result = future.get(); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isSameAs(row); + } + + @Test + void allRows_exceedsLimitOnFirstPage_failsWithException() { + Row row = mock(Row.class); + AsyncResultSet asyncResultSet = createMockResultSet(List.of(row), false, 6000); + Statement statement = mock(Statement.class); + + TbResultSet tbResultSet = new TbResultSet(statement, asyncResultSet, s -> null); + ListenableFuture> future = tbResultSet.allRows(MoreExecutors.directExecutor(), 5000); + + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(ResultSetSizeLimitExceededException.class); + } + + @Test + void allRows_exceedsLimitOnSecondPage_failsAfterSecondPage() { + Row row1 = mock(Row.class); + Row row2 = mock(Row.class); + Statement statement = mock(Statement.class); + doReturn(statement).when(statement).setPagingState((ByteBuffer) null); + + AsyncResultSet page2 = createMockResultSet(List.of(row2), false, 3000); + TbResultSet tbResultSetPage2 = new TbResultSet(statement, page2, s -> null); + SettableFuture page2Future = SettableFuture.create(); + page2Future.set(tbResultSetPage2); + TbResultSetFuture tbPage2Future = new TbResultSetFuture(page2Future); + + ExecutionInfo page1ExecInfo = mock(ExecutionInfo.class); + when(page1ExecInfo.getResponseSizeInBytes()).thenReturn(3000); + when(page1ExecInfo.getPagingState()).thenReturn(null); + + AsyncResultSet page1 = createMockResultSet(List.of(row1), true, 3000); + when(page1.getExecutionInfo()).thenReturn(page1ExecInfo); + + Function executeAsync = s -> tbPage2Future; + TbResultSet tbResultSet = new TbResultSet(statement, page1, executeAsync); + ListenableFuture> future = tbResultSet.allRows(MoreExecutors.directExecutor(), 5000); + + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(ResultSetSizeLimitExceededException.class); + } + + @Test + void allRows_unlimitedWithZero_returnsAllRowsRegardlessOfSize() throws Exception { + Row row = mock(Row.class); + AsyncResultSet asyncResultSet = createMockResultSet(List.of(row), false, 999999); + Statement statement = mock(Statement.class); + + TbResultSet tbResultSet = new TbResultSet(statement, asyncResultSet, s -> null); + ListenableFuture> future = tbResultSet.allRows(MoreExecutors.directExecutor(), 0); + + List result = future.get(); + assertThat(result).hasSize(1); + } + + @Test + void allRows_noLimitOverload_returnsAllRows() throws Exception { + Row row = mock(Row.class); + AsyncResultSet asyncResultSet = createMockResultSet(List.of(row), false, 999999); + Statement statement = mock(Statement.class); + + TbResultSet tbResultSet = new TbResultSet(statement, asyncResultSet, s -> null); + ListenableFuture> future = tbResultSet.allRows(MoreExecutors.directExecutor()); + + List result = future.get(); + assertThat(result).hasSize(1); + } + + private AsyncResultSet createMockResultSet(List rows, boolean hasMorePages, int responseSizeInBytes) { + AsyncResultSet resultSet = mock(AsyncResultSet.class); + ExecutionInfo executionInfo = mock(ExecutionInfo.class); + ColumnDefinitions columnDefs = mock(ColumnDefinitions.class); + + when(executionInfo.getResponseSizeInBytes()).thenReturn(responseSizeInBytes); + when(executionInfo.getPagingState()).thenReturn(null); + when(resultSet.getExecutionInfo()).thenReturn(executionInfo); + when(resultSet.getColumnDefinitions()).thenReturn(columnDefs); + when(resultSet.currentPage()).thenReturn(rows); + when(resultSet.hasMorePages()).thenReturn(hasMorePages); + when(resultSet.remaining()).thenReturn(rows.size()); + + return resultSet; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java index f68012d793..d99fdf1961 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/Job.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.job; +import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.job.task.TaskResult; import java.util.Set; import java.util.UUID; @@ -82,4 +84,36 @@ public class Job extends BaseData implements HasTenantId { return (C) configuration; } + @JsonIgnore + public String getError() { + if (status == JobStatus.CANCELLED) { + return "The task was cancelled"; + } + if (result.getGeneralError() != null) { + return result.getGeneralError(); + } + if (result.getFailedCount() > 0 && result.getResults() != null) { + StringBuilder errorMessage = new StringBuilder(); + for (TaskResult taskResult : result.getResults()) { + if (taskResult.isSuccess() || taskResult.isDiscarded()) { + continue; + } + String error = taskResult.getError(); + if (error == null) { + continue; + } + if (!errorMessage.isEmpty()) { + if (errorMessage.length() + 2 + error.length() > 256) { + errorMessage.append("..."); + break; + } + errorMessage.append("; "); + } + errorMessage.append(error); + } + return errorMessage.toString(); + } + return "Task failed"; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java index e6a4702b81..37f851f611 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java @@ -64,6 +64,11 @@ public class DummyTaskResult extends TaskResult { return JobType.DUMMY; } + @Override + public String getError() { + return failure != null ? failure.getError() : null; + } + @Data @NoArgsConstructor @EqualsAndHashCode(callSuper = true) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java index 761b518e98..808738a78b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java @@ -46,4 +46,7 @@ public abstract class TaskResult { @JsonIgnore public abstract JobType getJobType(); + @JsonIgnore + public abstract String getError(); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeysV2.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeysV2.java new file mode 100644 index 0000000000..536b70e4ff --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeysV2.java @@ -0,0 +1,99 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.query; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import org.jspecify.annotations.Nullable; +import org.thingsboard.server.common.data.AttributeScope; +import org.thingsboard.server.common.data.EntityType; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Schema( + description = """ + Contains unique time series and attribute key names discovered from entities matching a query, + optionally including a sample value for each key.""" +) +@JsonInclude(JsonInclude.Include.NON_NULL) +public record AvailableEntityKeysV2( + @Schema( + description = "Set of entity types found among the matched entities.", + example = "[\"DEVICE\", \"ASSET\"]", + requiredMode = Schema.RequiredMode.REQUIRED + ) + Set entityTypes, + + @ArraySchema( + arraySchema = @Schema( + description = """ + List of unique time series keys available on the matched entities, sorted alphabetically. + Omitted when timeseries keys were not requested.""", + nullable = true + ), + schema = @Schema(implementation = KeyInfo.class) + ) + @Nullable List timeseries, + + @Schema( + description = """ + Map of attribute scope to the list of unique attribute keys available on the matched entities. + Only scopes supported by the matched entity types are included. + Omitted when attribute keys were not requested or when none of the requested scopes apply to the matched entity types.""", + nullable = true + ) + @Nullable Map> attributes +) { + + @Schema(description = "Key name with an optional sample value.") + @JsonInclude(JsonInclude.Include.NON_NULL) + public record KeyInfo( + @Schema( + description = "Key name.", + example = "temperature", + requiredMode = Schema.RequiredMode.REQUIRED + ) + String key, + + @Schema( + description = "Most recent sample value for this key across the matched entities. Omitted when samples were not requested.", + nullable = true + ) + @Nullable KeySample sample + ) {} + + @Schema(description = "Most recent value and its timestamp.") + public record KeySample( + @Schema( + description = "Timestamp in milliseconds since epoch.", example = "1707000000000", + requiredMode = Schema.RequiredMode.REQUIRED + ) + long ts, + + @Schema( + description = "Sample value.", + example = "23.5", + requiredMode = Schema.RequiredMode.REQUIRED, + implementation = Object.class + ) + JsonNode value + ) {} + +} diff --git a/common/message/src/test/java/org/thingsboard/server/common/msg/tools/TbRateLimitsTest.java b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/TbRateLimitsTest.java index 39840d8f1c..0ec7ce0d76 100644 --- a/common/message/src/test/java/org/thingsboard/server/common/msg/tools/TbRateLimitsTest.java +++ b/common/message/src/test/java/org/thingsboard/server/common/msg/tools/TbRateLimitsTest.java @@ -42,7 +42,7 @@ public class TbRateLimitsTest { assertThat(rateLimits.tryConsume()).as("new token is available").isFalse(); int expectedRefillTime = (int) (((double) period / capacity) * 1000); - int gap = 500; + int gap = 1000; for (int i = 0; i < capacity; i++) { await("token refill for rate limit " + rateLimitConfig) @@ -71,7 +71,7 @@ public class TbRateLimitsTest { assertThat(rateLimits.tryConsume()).as("new token is available").isFalse(); int expectedRefillTime = period * 1000; - int gap = 500; + int gap = 1000; await("tokens refill for rate limit " + rateLimitConfig) .pollInterval(new FixedPollInterval(10, TimeUnit.MILLISECONDS)) diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java index 3529c94ee4..494eb64cde 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java @@ -209,7 +209,9 @@ class TbDateTest { .put("timeZone", "America/New_York") .put("dateStyle", "full") .toString())); - Assertions.assertEquals("середа, 6 вересня 2023 р.", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() + // Java 21+ uses narrow no-break space (U+202F) before "р." per CLDR 42+ + String expectedDate_ukUA = Runtime.version().feature() >= 21 ? "середа, 6 вересня 2023\u202Fр." : "середа, 6 вересня 2023 р."; + Assertions.assertEquals(expectedDate_ukUA, d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("dateStyle", "full") .toString())); @@ -244,12 +246,18 @@ class TbDateTest { Assertions.assertNotNull(d.toLocaleTimeString()); Assertions.assertNotNull(d.toLocaleTimeString("en-US")); - Assertions.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", "America/New_York")); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + String expectedTime_enUS = Runtime.version().feature() >= 21 ? "9:04:05\u202FPM" : "9:04:05 PM"; + Assertions.assertEquals(expectedTime_enUS, d.toLocaleTimeString("en-US", "America/New_York")); Assertions.assertEquals("오후 9:04:05", d.toLocaleTimeString("ko-KR", "America/New_York")); Assertions.assertEquals("04:04:05", d.toLocaleTimeString( "uk-UA", "Europe/Kiev")); Assertions.assertEquals("9:04:05 م", d.toLocaleTimeString( "ar-EG", "America/New_York")); - Assertions.assertEquals("9:04:05 PM Eastern Daylight Time", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode() + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + String expectedFullTime_enUS = Runtime.version().feature() >= 21 + ? "9:04:05\u202FPM Eastern Daylight Time" + : "9:04:05 PM Eastern Daylight Time"; + Assertions.assertEquals(expectedFullTime_enUS, d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("timeStyle", "full") .toString())); @@ -292,14 +300,27 @@ class TbDateTest { Assertions.assertNotNull(d.toLocaleString()); Assertions.assertNotNull(d.toLocaleString("en-US")); - Assertions.assertEquals("9/5/23, 9:04:05 PM", d.toLocaleString("en-US", "America/New_York")); + // Java 21+ uses narrow no-break space (U+202F) before AM/PM per CLDR 42+ + String expectedLocale_enUS = Runtime.version().feature() >= 21 ? "9/5/23, 9:04:05\u202FPM" : "9/5/23, 9:04:05 PM"; + Assertions.assertEquals(expectedLocale_enUS, d.toLocaleString("en-US", "America/New_York")); Assertions.assertEquals("23. 9. 5. 오후 9:04:05", d.toLocaleString("ko-KR", "America/New_York")); Assertions.assertEquals("06.09.23, 04:04:05", d.toLocaleString( "uk-UA", "Europe/Kiev")); - String expected_ver = Runtime.version().feature() == 11 ? "5\u200F/9\u200F/2023 9:04:05 م" : - "5\u200F/9\u200F/2023, 9:04:05 م"; - Assertions.assertEquals(expected_ver, d.toLocaleString( "ar-EG", "America/New_York")); + // Java 11 has no comma, Java 17 uses ASCII comma, Java 21+ uses Arabic comma (U+060C) + String expected_ar; + if (Runtime.version().feature() == 11) { + expected_ar = "5\u200F/9\u200F/2023 9:04:05 م"; + } else if (Runtime.version().feature() >= 21) { + expected_ar = "5\u200F/9\u200F/2023\u060C 9:04:05 م"; + } else { + expected_ar = "5\u200F/9\u200F/2023, 9:04:05 م"; + } + Assertions.assertEquals(expected_ar, d.toLocaleString( "ar-EG", "America/New_York")); - Assertions.assertEquals("Tuesday, September 5, 2023 at 9:04:05 PM Eastern Daylight Time", d.toLocaleString("en-US", JacksonUtil.newObjectNode() + // Java 21+ changed "at" to "," and uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_full = Runtime.version().feature() >= 21 + ? "Tuesday, September 5, 2023, 9:04:05\u202FPM Eastern Daylight Time" + : "Tuesday, September 5, 2023 at 9:04:05 PM Eastern Daylight Time"; + Assertions.assertEquals(expected_enUS_full, d.toLocaleString("en-US", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") .put("timeStyle", "full") @@ -309,15 +330,26 @@ class TbDateTest { .put("dateStyle", "full") .put("timeStyle", "full") .toString())); - Assertions.assertEquals("середа, 6 вересня 2023 р. о 04:04:05 за східноєвропейським літнім часом", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode() + // Java 21+ changed Ukrainian locale: "о" replaced with "," and NNBSP before "р." per CLDR 42+ + String expected_ukUA_full_summer = Runtime.version().feature() >= 21 + ? "середа, 6 вересня 2023\u202Fр., 04:04:05 за східноєвропейським літнім часом" + : "середа, 6 вересня 2023 р. о 04:04:05 за східноєвропейським літнім часом"; + Assertions.assertEquals(expected_ukUA_full_summer, d.toLocaleString("uk-UA", JacksonUtil.newObjectNode() .put("timeZone", "Europe/Kiev") .put("dateStyle", "full") .put("timeStyle", "full") .toString())); - expected_ver = Runtime.version().feature() == 11 ? "الثلاثاء، 5 سبتمبر 2023 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية" : - "الثلاثاء، 5 سبتمبر 2023 في 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية"; - Assertions.assertEquals(expected_ver, d.toLocaleString("ar-EG", JacksonUtil.newObjectNode() + // Java 11 has no connector, Java 17 uses "في", Java 21+ uses Arabic comma "،" + String expected_ar_full; + if (Runtime.version().feature() == 11) { + expected_ar_full = "الثلاثاء، 5 سبتمبر 2023 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية"; + } else if (Runtime.version().feature() >= 21) { + expected_ar_full = "الثلاثاء، 5 سبتمبر 2023\u060C 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية"; + } else { + expected_ar_full = "الثلاثاء، 5 سبتمبر 2023 في 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية"; + } + Assertions.assertEquals(expected_ar_full, d.toLocaleString("ar-EG", JacksonUtil.newObjectNode() .put("timeZone", "America/New_York") .put("dateStyle", "full") .put("timeStyle", "full") @@ -825,16 +857,44 @@ class TbDateTest { @Test public void toStringAsJs() { TbDate d1 = new TbDate(1975, 12, 31, 23,15,30, 567,"-04:00"); - Assertions.assertEquals("четвер, 1 січня 1976 р. о 06:15:30 за східноєвропейським стандартним часом", d1.toString("uk-UA", "Europe/Kyiv")); - Assertions.assertEquals("Thursday, January 1, 1976 at 6:15:30 AM Eastern European Standard Time", d1.toString("en-US", "Europe/Kyiv")); - Assertions.assertEquals("1976 Jan 1, Thu 06:15:30 Eastern European Time", d1.toString("UTC", "Europe/Kyiv")); - Assertions.assertEquals("Wednesday, December 31, 1975 at 10:15:30 PM Eastern Standard Time", d1.toString("en-US", "America/New_York")); - Assertions.assertEquals("1975 Dec 31, Wed 22:15:30 Eastern Standard Time", d1.toString("GMT", "America/New_York")); - Assertions.assertEquals("1975 Dec 31, Wed 22:15:30 Eastern Standard Time", d1.toString("UTC", "America/New_York")); + // Java 21+ changed Ukrainian locale: "о" (at) replaced with "," and NNBSP before "р." per CLDR 42+ + String expected_ukUA_full = Runtime.version().feature() >= 21 + ? "четвер, 1 січня 1976\u202Fр., 06:15:30 за східноєвропейським стандартним часом" + : "четвер, 1 січня 1976 р. о 06:15:30 за східноєвропейським стандартним часом"; + Assertions.assertEquals(expected_ukUA_full, d1.toString("uk-UA", "Europe/Kyiv")); + // Java 21+ changed "at" to "," and uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_Kyiv = Runtime.version().feature() >= 21 + ? "Thursday, January 1, 1976, 6:15:30\u202FAM Eastern European Standard Time" + : "Thursday, January 1, 1976 at 6:15:30 AM Eastern European Standard Time"; + Assertions.assertEquals(expected_enUS_Kyiv, d1.toString("en-US", "Europe/Kyiv")); + // Java 21+ changed timezone display for UTC locale + String expected_UTC_Kyiv = Runtime.version().feature() >= 21 + ? "1976 Jan 1, Thu 06:15:30 Kyiv (+0)" + : "1976 Jan 1, Thu 06:15:30 Eastern European Time"; + Assertions.assertEquals(expected_UTC_Kyiv, d1.toString("UTC", "Europe/Kyiv")); + // Java 21+ changed "at" to "," and uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_NY = Runtime.version().feature() >= 21 + ? "Wednesday, December 31, 1975, 10:15:30\u202FPM Eastern Standard Time" + : "Wednesday, December 31, 1975 at 10:15:30 PM Eastern Standard Time"; + Assertions.assertEquals(expected_enUS_NY, d1.toString("en-US", "America/New_York")); + // Java 21+ changed timezone display for GMT/UTC locales + String expected_GMT_NY = Runtime.version().feature() >= 21 + ? "1975 Dec 31, Wed 22:15:30 New York (+0)" + : "1975 Dec 31, Wed 22:15:30 Eastern Standard Time"; + Assertions.assertEquals(expected_GMT_NY, d1.toString("GMT", "America/New_York")); + Assertions.assertEquals(expected_GMT_NY, d1.toString("UTC", "America/New_York")); Assertions.assertEquals(d1.toUTCString("UTC"), d1.toUTCString()); - Assertions.assertEquals("четвер, 1 січня 1976 р., 03:15:30", d1.toUTCString("uk-UA")); - Assertions.assertEquals("Thursday, January 1, 1976, 3:15:30 AM", d1.toUTCString("en-US")); + // Java 21+ uses NNBSP before "р." per CLDR 42+ + String expected_ukUA_utc = Runtime.version().feature() >= 21 + ? "четвер, 1 січня 1976\u202Fр., 03:15:30" + : "четвер, 1 січня 1976 р., 03:15:30"; + Assertions.assertEquals(expected_ukUA_utc, d1.toUTCString("uk-UA")); + // Java 21+ uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_utc = Runtime.version().feature() >= 21 + ? "Thursday, January 1, 1976, 3:15:30\u202FAM" + : "Thursday, January 1, 1976, 3:15:30 AM"; + Assertions.assertEquals(expected_enUS_utc, d1.toUTCString("en-US")); Assertions.assertEquals("1976-01-01T03:15:30.567Z", d1.toJSON()); Assertions.assertEquals("1976-01-01T03:15:30.567Z", d1.toISOString()); @@ -842,22 +902,44 @@ class TbDateTest { Assertions.assertEquals("1976-01-01 06:15:30", d1.toLocaleString("UTC", "Europe/Kyiv")); Assertions.assertEquals("01.01.76, 06:15:30", d1.toLocaleString("uk-UA", "Europe/Kyiv")); Assertions.assertEquals("1975-12-31 22:15:30", d1.toLocaleString("UTC", "America/New_York")); - Assertions.assertEquals("12/31/75, 10:15:30 PM", d1.toLocaleString("en-US", "America/New_York")); + // Java 21+ uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_locale_NY = Runtime.version().feature() >= 21 + ? "12/31/75, 10:15:30\u202FPM" + : "12/31/75, 10:15:30 PM"; + Assertions.assertEquals(expected_enUS_locale_NY, d1.toLocaleString("en-US", "America/New_York")); Assertions.assertEquals("1976 Jan 1, Thu", d1.toDateString("UTC", "Europe/Kyiv")); - Assertions.assertEquals("четвер, 1 січня 1976 р.", d1.toDateString("uk-UA", "Europe/Kyiv")); + // Java 21+ uses NNBSP before "р." per CLDR 42+ + String expected_ukUA_date = Runtime.version().feature() >= 21 + ? "четвер, 1 січня 1976\u202Fр." + : "четвер, 1 січня 1976 р."; + Assertions.assertEquals(expected_ukUA_date, d1.toDateString("uk-UA", "Europe/Kyiv")); Assertions.assertEquals("1975 Dec 31, Wed", d1.toDateString("UTC", "America/New_York")); Assertions.assertEquals("Wednesday, December 31, 1975", d1.toDateString("en-US", "America/New_York")); Assertions.assertEquals("06:15:30", d1.toLocaleTimeString("uk-UA", "Europe/Kyiv")); Assertions.assertEquals("06:15:30", d1.toLocaleTimeString("UTC", "Europe/Kyiv")); - Assertions.assertEquals("10:15:30 PM", d1.toLocaleTimeString("en-US", "America/New_York")); + // Java 21+ uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_time = Runtime.version().feature() >= 21 ? "10:15:30\u202FPM" : "10:15:30 PM"; + Assertions.assertEquals(expected_enUS_time, d1.toLocaleTimeString("en-US", "America/New_York")); Assertions.assertEquals("22:15:30", d1.toLocaleTimeString("UTC", "America/New_York")); Assertions.assertEquals("06:15:30 за східноєвропейським стандартним часом", d1.toTimeString("uk-UA", "Europe/Kyiv")); - Assertions.assertEquals("06:15:30 Eastern European Time", d1.toTimeString("UTC", "Europe/Kyiv")); - Assertions.assertEquals("10:15:30 PM Eastern Standard Time", d1.toTimeString("en-US", "America/New_York")); - Assertions.assertEquals("22:15:30 Eastern Standard Time", d1.toTimeString("UTC", "America/New_York")); + // Java 21+ changed timezone display for UTC locale + String expected_UTC_time_Kyiv = Runtime.version().feature() >= 21 + ? "06:15:30 Kyiv (+0)" + : "06:15:30 Eastern European Time"; + Assertions.assertEquals(expected_UTC_time_Kyiv, d1.toTimeString("UTC", "Europe/Kyiv")); + // Java 21+ uses NNBSP before AM/PM per CLDR 42+ + String expected_enUS_timeStr = Runtime.version().feature() >= 21 + ? "10:15:30\u202FPM Eastern Standard Time" + : "10:15:30 PM Eastern Standard Time"; + Assertions.assertEquals(expected_enUS_timeStr, d1.toTimeString("en-US", "America/New_York")); + // Java 21+ changed timezone display for UTC locale + String expected_UTC_time_NY = Runtime.version().feature() >= 21 + ? "22:15:30 New York (+0)" + : "22:15:30 Eastern Standard Time"; + Assertions.assertEquals(expected_UTC_time_NY, d1.toTimeString("UTC", "America/New_York")); } @Test diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java index a6218b2eb8..272e44ba7e 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ThingsBoardThreadFactory.java @@ -33,9 +33,7 @@ public class ThingsBoardThreadFactory implements ThreadFactory { } private ThingsBoardThreadFactory(String name) { - SecurityManager s = System.getSecurityManager(); - group = (s != null) ? s.getThreadGroup() : - Thread.currentThread().getThreadGroup(); + group = Thread.currentThread().getThreadGroup(); namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-"; diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java index 2abf025cda..3f405e145b 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/DefaultGitSyncService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.sync; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.eclipse.jgit.revwalk.RevCommit; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; @@ -47,6 +48,8 @@ public class DefaultGitSyncService implements GitSyncService { private final Map repositories = new ConcurrentHashMap<>(); private final Map updateListeners = new ConcurrentHashMap<>(); + private RevCommit lastCommit; + @Override public void registerSync(String key, String repoUri, String branch, long fetchFrequencyMs, Runnable onUpdate) { RepositorySettings settings = new RepositorySettings(); @@ -84,7 +87,7 @@ public class DefaultGitSyncService implements GitSyncService { @Override public List listFiles(String key, String path, int depth, FileType type) { GitRepository repository = getRepository(key); - return repository.listFilesAtCommit(getBranchRef(repository), path, depth).stream() + return repository.listFilesAtCommit(lastCommit, path, depth).stream() .filter(file -> type == null || file.type() == type) .toList(); } @@ -93,7 +96,7 @@ public class DefaultGitSyncService implements GitSyncService { @Override public byte[] getFileContent(String key, String path) { GitRepository repository = getRepository(key); - return repository.getFileContentAtCommit(path, getBranchRef(repository)); + return repository.getFileContentAtCommit(path, lastCommit); } @Override @@ -137,6 +140,15 @@ public class DefaultGitSyncService implements GitSyncService { } private void onUpdate(String key) { + GitRepository repository = getRepository(key); + String branchRef = getBranchRef(repository); + try { + lastCommit = repository.resolveCommit(branchRef); + } catch (Throwable e) { + log.error("[{}] Failed to resolve commit for ref {}", key, branchRef, e); + return; + } + Runnable listener = updateListeners.get(key); if (listener != null) { log.debug("[{}] Handling repository update", key); diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java index 434d52a4ca..3602dd55ca 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java @@ -277,13 +277,17 @@ public class GitRepository { return listFilesAtCommit(commitId, path, -1).stream().map(RepoFile::path).toList(); } - @SneakyThrows public List listFilesAtCommit(String commitId, String path, int depth) { - log.debug("Executing listFilesAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commitId, path); + RevCommit commit = resolveCommit(commitId); + return listFilesAtCommit(commit, path, depth); + } + + @SneakyThrows + public List listFilesAtCommit(RevCommit commit, String path, int depth) { + log.debug("Executing listFilesAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commit, path); List files = new ArrayList<>(); - RevCommit revCommit = resolveCommit(commitId); try (TreeWalk treeWalk = new TreeWalk(git.getRepository())) { - treeWalk.reset(revCommit.getTree().getId()); + treeWalk.reset(commit.getTree().getId()); if (StringUtils.isNotEmpty(path)) { treeWalk.setFilter(PathFilter.create(path)); } @@ -301,11 +305,15 @@ public class GitRepository { return files; } - @SneakyThrows public byte[] getFileContentAtCommit(String file, String commitId) { - log.debug("Executing getFileContentAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commitId, file); - RevCommit revCommit = resolveCommit(commitId); - try (TreeWalk treeWalk = TreeWalk.forPath(git.getRepository(), file, revCommit.getTree())) { + RevCommit commit = resolveCommit(commitId); + return getFileContentAtCommit(file, commit); + } + + @SneakyThrows + public byte[] getFileContentAtCommit(String file, RevCommit commit) { + log.debug("Executing getFileContentAtCommit [{}][{}][{}]", settings.getRepositoryUri(), commit, file); + try (TreeWalk treeWalk = TreeWalk.forPath(git.getRepository(), file, commit.getTree())) { if (treeWalk == null) { throw new IllegalArgumentException("File not found"); } @@ -373,9 +381,9 @@ public class GitRepository { for (RemoteRefUpdate update : pushResult.getRemoteUpdates()) { RemoteRefUpdate.Status status = update.getStatus(); if (status == REJECTED_NONFASTFORWARD || status == REJECTED_NODELETE || - status == REJECTED_REMOTE_CHANGED || status == REJECTED_OTHER_REASON) { + status == REJECTED_REMOTE_CHANGED || status == REJECTED_OTHER_REASON) { throw new RuntimeException("Remote repository answered with error: " + - Optional.ofNullable(update.getMessage()).orElseGet(status::name)); + Optional.ofNullable(update.getMessage()).orElseGet(status::name)); } } }); @@ -450,7 +458,8 @@ public class GitRepository { revCommit.getFullMessage(), revCommit.getAuthorIdent().getName(), revCommit.getAuthorIdent().getEmailAddress()); } - private RevCommit resolveCommit(String id) throws IOException { + @SneakyThrows + public RevCommit resolveCommit(String id) { return git.getRepository().parseCommit(resolve(id)); } @@ -481,8 +490,8 @@ public class GitRepository { private static final Function> revCommitComparatorFunction = pageLink -> { SortOrder sortOrder = pageLink.getSortOrder(); if (sortOrder != null - && sortOrder.getProperty().equals("timestamp") - && SortOrder.Direction.ASC.equals(sortOrder.getDirection())) { + && sortOrder.getProperty().equals("timestamp") + && SortOrder.Direction.ASC.equals(sortOrder.getDirection())) { return Comparator.comparingInt(RevCommit::getCommitTime); } return null; diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java index 2241b20e14..7c563debd6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java @@ -55,6 +55,12 @@ public interface AttributesDao { List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope); + ListenableFuture> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope); + + List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope); + + ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope); + List> removeAllByEntityId(TenantId tenantId, EntityId entityId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 117c47b61c..ca4c937d4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java @@ -45,9 +45,6 @@ import java.util.Optional; import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; -/** - * @author Andrew Shvayka - */ @Service @ConditionalOnProperty(prefix = "cache.attributes", value = "enabled", havingValue = "false", matchIfMissing = true) @Primary @@ -92,7 +89,7 @@ public class BaseAttributesService implements AttributesService { } @Override - public List findAllKeysByEntityIds(TenantId tenantId, List entityIds, AttributeScope scope) { + public List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { if (scope == null) { return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); } else { @@ -100,6 +97,21 @@ public class BaseAttributesService implements AttributesService { } } + @Override + public ListenableFuture> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findAllKeysByEntityIdsAndScopeAsync(tenantId, entityIds, scope); + } + + @Override + public List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findLatestByEntityIdsAndScope(tenantId, entityIds, scope); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findLatestByEntityIdsAndScopeAsync(tenantId, entityIds, scope); + } + @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index c150eabfa2..1c99dfc495 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -66,6 +66,7 @@ import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; @Primary @Slf4j public class CachedAttributesService implements AttributesService { + private static final String STATS_NAME = "attributes.cache"; public static final String LOCAL_CACHE_TYPE = "caffeine"; @@ -212,7 +213,7 @@ public class CachedAttributesService implements AttributesService { } @Override - public List findAllKeysByEntityIds(TenantId tenantId, List entityIds, AttributeScope scope) { + public List findAllKeysByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { if (scope == null) { return attributesDao.findAllKeysByEntityIds(tenantId, entityIds); } else { @@ -220,6 +221,21 @@ public class CachedAttributesService implements AttributesService { } } + @Override + public ListenableFuture> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findAllKeysByEntityIdsAndScopeAsync(tenantId, entityIds, scope); + } + + @Override + public List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findLatestByEntityIdsAndScope(tenantId, entityIds, scope); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return attributesDao.findLatestByEntityIdsAndScopeAsync(tenantId, entityIds, scope); + } + @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope scope, AttributeKvEntry attribute) { validate(entityId, scope); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index dc950cebd4..b20535dbf4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -16,6 +16,9 @@ package org.thingsboard.server.dao.entity; import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -55,6 +58,7 @@ import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.common.stats.EdqsStatsService; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.sql.JpaExecutorService; import java.util.ArrayList; import java.util.Collections; @@ -104,6 +108,9 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Autowired private EdqsStatsService edqsStatsService; + @Autowired + private JpaExecutorService jpaExecutorService; + @Override public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { log.trace("Executing countEntitiesByQuery, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); @@ -142,41 +149,78 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe EdqsResponse response = processEdqsRequest(tenantId, customerId, request); result = response.getEntityDataQueryResult(); } else { - if (!isValidForOptimization(query)) { - result = entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); - } else { - // 1 step - find entity data by filter and sort columns - PageData entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); - if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { - result = entityDataByQuery; - } else { - // 2 step - find entity data by entity ids from the 1st step - List entities = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); - result = new PageData<>(entities, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); - } - } + result = findEntityDataByQueryInternal(tenantId, customerId, query); } edqsStatsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs); return result; } + @Override + public ListenableFuture> findEntityDataByQueryAsync(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { + log.trace("Executing findEntityDataByQueryAsync, tenantId [{}], customerId [{}], query [{}]", tenantId, customerId, query); + + try { + validateId(tenantId, id -> INCORRECT_TENANT_ID + id); + validateId(customerId, id -> INCORRECT_CUSTOMER_ID + id); + validateEntityDataQuery(query); + } catch (Exception e) { + return Futures.immediateFailedFuture(e); + } + + if (edqsService.isApiEnabled() && validForEdqs(query) && !tenantId.isSysTenantId()) { + EdqsRequest request = EdqsRequest.builder() + .entityDataQuery(query) + .build(); + long startNs = System.nanoTime(); + return Futures.transform(processEdqsRequestAsync(tenantId, customerId, request), response -> { + edqsStatsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs); + return response.getEntityDataQueryResult(); + }, MoreExecutors.directExecutor()); + } + + return jpaExecutorService.submit(() -> { + long startNs = System.nanoTime(); + PageData result = findEntityDataByQueryInternal(tenantId, customerId, query); + edqsStatsService.reportEntityDataQuery(tenantId, query, System.nanoTime() - startNs); + return result; + }); + } + + private PageData findEntityDataByQueryInternal(TenantId tenantId, CustomerId customerId, EntityDataQuery query) { + if (!isValidForOptimization(query)) { + return entityQueryDao.findEntityDataByQuery(tenantId, customerId, query); + } + // 1 step - find entity data by filter and sort columns + PageData entityDataByQuery = findEntityIdsByFilterAndSorterColumns(tenantId, customerId, query); + if (entityDataByQuery == null || entityDataByQuery.getData().isEmpty()) { + return entityDataByQuery; + } + // 2 step - find entity data by entity ids from the 1st step + List entities = fetchEntityDataByIdsFromInitialQuery(tenantId, customerId, query, entityDataByQuery.getData()); + return new PageData<>(entities, entityDataByQuery.getTotalPages(), entityDataByQuery.getTotalElements(), entityDataByQuery.hasNext()); + } + private boolean validForEdqs(EntityCountQuery query) { // for compatibility with PE return true; } private EdqsResponse processEdqsRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { - EdqsResponse response; try { - log.debug("[{}] Sending request to EDQS: {}", tenantId, request); - response = edqsApiService.processRequest(tenantId, customerId, request).get(); + return processEdqsRequestAsync(tenantId, customerId, request).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } - log.debug("[{}] Received response from EDQS: {}", tenantId, response); - if (response.getError() != null) { - throw new RuntimeException(response.getError()); - } - return response; + } + + private ListenableFuture processEdqsRequestAsync(TenantId tenantId, CustomerId customerId, EdqsRequest request) { + log.debug("[{}] Sending request to EDQS: {}", tenantId, request); + return Futures.transform(edqsApiService.processRequest(tenantId, customerId, request), response -> { + log.debug("[{}] Received response from EDQS: {}", tenantId, response); + if (response.getError() != null) { + throw new RuntimeException(response.getError()); + } + return response; + }, MoreExecutors.directExecutor()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java index 44a38bf445..971c06c7a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/latest/TsKvLatestEntity.java @@ -65,6 +65,12 @@ import static org.thingsboard.server.dao.model.ModelConstants.VERSION_COLUMN; query = SearchTsKvLatestRepository.FIND_ALL_BY_ENTITY_ID_QUERY, resultSetMapping = "tsKvLatestFindMapping", resultClass = TsKvLatestEntity.class + ), + @NamedNativeQuery( + name = SearchTsKvLatestRepository.FIND_LATEST_BY_ENTITY_IDS, + query = SearchTsKvLatestRepository.FIND_LATEST_BY_ENTITY_IDS_QUERY, + resultSetMapping = "tsKvLatestFindMapping", + resultClass = TsKvLatestEntity.class ) }) public final class TsKvLatestEntity extends AbstractTsKvEntity { diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java index 900958dcba..3d2ebfd1fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractAsyncDao.java @@ -37,6 +37,9 @@ public abstract class CassandraAbstractAsyncDao extends CassandraAbstractDao { @Value("${cassandra.query.result_processing_threads:50}") private int threadPoolSize; + @Value("${cassandra.query.max_result_set_size_in_bytes:52428800}") + protected long maxResultSetSizeBytes; + @PostConstruct public void startExecutor() { readResultsProcessingExecutor = ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "cassandra-callback"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index f812d13f65..67e71ebcdf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -98,10 +98,8 @@ public interface AlarmRepository extends JpaRepository { "AND ea.entityType = :affectedEntityType " + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -119,10 +117,8 @@ public interface AlarmRepository extends JpaRepository { "AND ea.entityType = :affectedEntityType " + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -183,10 +179,8 @@ public interface AlarmRepository extends JpaRepository { "WHERE a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -199,10 +193,8 @@ public interface AlarmRepository extends JpaRepository { "WHERE a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -263,10 +255,8 @@ public interface AlarmRepository extends JpaRepository { "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + @@ -280,10 +270,8 @@ public interface AlarmRepository extends JpaRepository { "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#alarmTypes == null} = true) OR a.type IN (:alarmTypes)) " + //HHH-15968 - "AND ((:#{#alarmSeverities == null} = true) OR a.severity IN (:alarmSeverities)) " + //HHH-15968 -// "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + -// "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + + "AND ((:alarmTypes) IS NULL OR a.type IN (:alarmTypes)) " + + "AND ((:alarmSeverities) IS NULL OR a.severity IN (:alarmSeverities)) " + "AND ((:clearFilterEnabled) = FALSE OR a.cleared = :clearFilter) " + "AND ((:ackFilterEnabled) = FALSE OR a.acknowledged = :ackFilter) " + "AND (:assigneeId IS NULL OR a.assigneeId = :assigneeId) " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java index aa7f0490d7..8a3405549e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java @@ -20,6 +20,14 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; import org.thingsboard.server.dao.model.sql.AttributeKvEntity; @@ -60,6 +68,19 @@ public interface AttributeKvRepository extends JpaRepository findAllKeysByEntityIdsAndAttributeType(@Param("entityIds") List entityIds, @Param("attributeType") int attributeType); + @Query(value = """ + SELECT DISTINCT ON (a.attribute_key) + kd.key AS strKey, + a.bool_v AS boolV, a.str_v AS strV, a.long_v AS longV, + a.dbl_v AS dblV, a.json_v AS jsonV, + a.last_update_ts AS lastUpdateTs, a.version AS version + FROM attribute_kv a + INNER JOIN key_dictionary kd ON a.attribute_key = kd.key_id + WHERE a.entity_id IN :entityIds AND a.attribute_type = :attributeType + ORDER BY a.attribute_key, a.last_update_ts DESC""", nativeQuery = true) + List findLatestByEntityIdsAndAttributeType(@Param("entityIds") List entityIds, + @Param("attributeType") int attributeType); + @Query(value = "SELECT attribute_key, attribute_type, entity_id, bool_v, dbl_v, json_v, last_update_ts, long_v, str_v, version FROM attribute_kv WHERE (entity_id, attribute_type, attribute_key) > " + "(:entityId, :attributeType, :attributeKey) ORDER BY entity_id, attribute_type, attribute_key LIMIT :batchSize", nativeQuery = true) List findNextBatch(@Param("entityId") UUID entityId, @@ -67,4 +88,40 @@ public interface AttributeKvRepository extends JpaRepository> findAllKeysByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return service.submit(() -> findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope)); + } + + @Override + public List findLatestByEntityIdsAndScope(TenantId tenantId, List entityIds, AttributeScope scope) { + if (CollectionUtils.isEmpty(entityIds)) { + return Collections.emptyList(); + } + var uniqueIds = entityIds.stream().map(EntityId::getId).distinct().toList(); + return attributeKvRepository.findLatestByEntityIdsAndAttributeType(uniqueIds, scope.getId()) + .stream() + .map(AttributeKvRepository.AttributeKvProjection::toAttributeKvEntry) + .toList(); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAndScopeAsync(TenantId tenantId, List entityIds, AttributeScope scope) { + return service.submit(() -> findLatestByEntityIdsAndScope(tenantId, entityIds, scope)); + } + @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, AttributeScope attributeScope, AttributeKvEntry attribute) { AttributeKvEntity entity = new AttributeKvEntity(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java index d685d9ee78..4266003706 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java @@ -33,8 +33,7 @@ public interface AuditLogRepository extends JpaRepository "a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#actionTypes == null} = true) OR a.actionType IN (:actionTypes)) " + //HHH-15968 -// "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND ((:actionTypes) IS NULL OR a.actionType IN (:actionTypes)) " + "AND (:textSearch IS NULL OR ilike(a.entityType, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.entityName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.userName, CONCAT('%', :textSearch, '%')) = true " + @@ -54,8 +53,7 @@ public interface AuditLogRepository extends JpaRepository "AND a.entityType = :entityType AND a.entityId = :entityId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#actionTypes == null} = true) OR a.actionType IN (:actionTypes)) " + //HHH-15968 -// "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND ((:actionTypes) IS NULL OR a.actionType IN (:actionTypes)) " + "AND (:textSearch IS NULL OR ilike(a.entityName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.userName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.actionType, CONCAT('%', :textSearch, '%')) = true " + @@ -75,8 +73,7 @@ public interface AuditLogRepository extends JpaRepository "AND a.customerId = :customerId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#actionTypes == null} = true) OR a.actionType IN (:actionTypes)) " + //HHH-15968 -// "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND ((:actionTypes) IS NULL OR a.actionType IN (:actionTypes)) " + "AND (:textSearch IS NULL OR ilike(a.entityType, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.entityName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.userName, CONCAT('%', :textSearch, '%')) = true " + @@ -96,8 +93,7 @@ public interface AuditLogRepository extends JpaRepository "AND a.userId = :userId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:#{#actionTypes == null} = true) OR a.actionType IN (:actionTypes)) " + //HHH-15968 -// "AND ((:actionTypes) IS NULL OR a.actionType in (:actionTypes)) " + + "AND ((:actionTypes) IS NULL OR a.actionType IN (:actionTypes)) " + "AND (:textSearch IS NULL OR ilike(a.entityType, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.entityName, CONCAT('%', :textSearch, '%')) = true " + "OR ilike(a.actionType, CONCAT('%', :textSearch, '%')) = true " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java index eabbdfed80..143ab53e18 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java @@ -172,4 +172,14 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries return sqlDao.findAllKeysByEntityIdsAsync(tenantId, entityIds); } + @Override + public List findLatestByEntityIds(TenantId tenantId, List entityIds) { + return sqlDao.findLatestByEntityIds(tenantId, entityIds); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds) { + return sqlDao.findLatestByEntityIdsAsync(tenantId, entityIds); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index d807794ddc..b33ceca63b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.MoreExecutors; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -54,6 +55,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; import org.thingsboard.server.dao.util.SqlTsLatestAnyDao; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -189,6 +191,21 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme return service.submit(() -> findAllKeysByEntityIds(tenantId, entityIds)); } + @Override + public List findLatestByEntityIds(TenantId tenantId, List entityIds) { + if (CollectionUtils.isEmpty(entityIds)) { + return Collections.emptyList(); + } + return DaoUtil.convertDataList( + searchTsKvLatestRepository.findLatestByEntityIds(entityIds.stream().map(EntityId::getId).toList()) + ); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds) { + return service.submit(() -> findLatestByEntityIds(tenantId, entityIds)); + } + private ListenableFuture getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query, Long version) { ListenableFuture> future = findNewLatestEntryFuture(tenantId, entityId, query); return Futures.transformAsync(future, entryList -> { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java index 0ab1994059..8b3a66612c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/latest/SearchTsKvLatestRepository.java @@ -34,6 +34,20 @@ public class SearchTsKvLatestRepository { " ts_kv_latest.bool_v AS boolValue, ts_kv_latest.long_v AS longValue, ts_kv_latest.dbl_v AS doubleValue, ts_kv_latest.json_v AS jsonValue, ts_kv_latest.ts AS ts, ts_kv_latest.version AS version FROM ts_kv_latest " + "INNER JOIN key_dictionary ON ts_kv_latest.key = key_dictionary.key_id WHERE ts_kv_latest.entity_id = cast(:id AS uuid)"; + public static final String FIND_LATEST_BY_ENTITY_IDS = "findLatestByEntityIds"; + + public static final String FIND_LATEST_BY_ENTITY_IDS_QUERY = """ + SELECT DISTINCT ON (ts_kv_latest.key) + ts_kv_latest.entity_id AS entityId, ts_kv_latest.key AS key, + key_dictionary.key AS strKey, ts_kv_latest.str_v AS strValue, + ts_kv_latest.bool_v AS boolValue, ts_kv_latest.long_v AS longValue, + ts_kv_latest.dbl_v AS doubleValue, ts_kv_latest.json_v AS jsonValue, + ts_kv_latest.ts AS ts, ts_kv_latest.version AS version + FROM ts_kv_latest + INNER JOIN key_dictionary ON ts_kv_latest.key = key_dictionary.key_id + WHERE ts_kv_latest.entity_id IN (:entityIds) + ORDER BY ts_kv_latest.key, ts_kv_latest.ts DESC"""; + @PersistenceContext private EntityManager entityManager; @@ -43,4 +57,10 @@ public class SearchTsKvLatestRepository { .getResultList(); } + public List findLatestByEntityIds(List entityIds) { + return entityManager.createNamedQuery(FIND_LATEST_BY_ENTITY_IDS, TsKvLatestEntity.class) + .setParameter("entityIds", entityIds) + .getResultList(); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index 747197470c..70e04eb5e3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -55,9 +55,6 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.StringUtils.isBlank; -/** - * @author Andrew Shvayka - */ @Service @Slf4j public class BaseTimeseriesService implements TimeseriesService { @@ -161,6 +158,16 @@ public class BaseTimeseriesService implements TimeseriesService { return timeseriesLatestDao.findAllKeysByEntityIdsAsync(tenantId, entityIds); } + @Override + public List findLatestByEntityIds(TenantId tenantId, List entityIds) { + return timeseriesLatestDao.findLatestByEntityIds(tenantId, entityIds); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds) { + return timeseriesLatestDao.findLatestByEntityIdsAsync(tenantId, entityIds); + } + @Override public void cleanup(long systemTtl) { timeseriesDao.cleanup(systemTtl); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index e68e315b4b..a421cd6842 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -52,6 +52,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper; import org.thingsboard.server.common.data.kv.TsKvQuery; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.nosql.ResultSetSizeLimitExceededException; import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.nosql.TbResultSetFuture; import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao; @@ -256,7 +257,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), minPartition, maxPartition, t); + log.error("[{}][{}][{}] Failed to fetch partitions for interval {}-{}", tenantId, entityId.getEntityType(), entityId.getId(), minPartition, maxPartition, t); + resultFuture.setException(t); } }, readResultsProcessingExecutor); return resultFuture; @@ -330,7 +332,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to fetch partitions for interval {}-{}", entityId.getEntityType().name(), entityId.getId(), toPartitionTs(query.getStartTs()), toPartitionTs(query.getEndTs()), t); + log.error("[{}][{}][{}] Failed to fetch partitions for interval {}-{}", tenantId, entityId.getEntityType(), entityId.getId(), toPartitionTs(query.getStartTs()), toPartitionTs(query.getEndTs()), t); + resultFuture.setException(t); } }, readResultsProcessingExecutor); @@ -372,8 +375,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD cursor.addData(convertResultToTsKvEntryList(Collections.emptyList())); findAllAsyncSequentiallyWithLimit(tenantId, cursor, resultFuture); } else { - Futures.addCallback(result.allRows(readResultsProcessingExecutor), new FutureCallback>() { - + Futures.addCallback(result.allRows(readResultsProcessingExecutor, maxResultSetSizeBytes), new FutureCallback>() { @Override public void onSuccess(@Nullable List result) { cursor.addData(convertResultToTsKvEntryList(result == null ? Collections.emptyList() : result)); @@ -382,7 +384,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to fetch data for query {}-{}", stmt, t); + if (t instanceof ResultSetSizeLimitExceededException e) { + log.warn("[{}][{}][{}] Result set size limit exceeded for key [{}], query [{}]: {} bytes, limit {} bytes", + tenantId, cursor.getEntityType(), cursor.getEntityId(), cursor.getKey(), stmt.getPreparedStatement().getQuery(), e.getActualBytes(), e.getLimitBytes()); + } else { + log.error("[{}][{}][{}] Failed to fetch data for key [{}], query [{}]", tenantId, cursor.getEntityType(), cursor.getEntityId(), cursor.getKey(), stmt.getPreparedStatement().getQuery(), t); + } + resultFuture.setException(t); } }, readResultsProcessingExecutor); @@ -392,7 +400,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to fetch data for query {}-{}", stmt, t); + log.error("[{}][{}][{}] Failed to fetch data for key [{}], query [{}]", tenantId, cursor.getEntityType(), cursor.getEntityId(), cursor.getKey(), stmt.getPreparedStatement().getQuery(), t); + resultFuture.setException(t); } }, readResultsProcessingExecutor); } @@ -425,7 +434,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD } if (!isUseTsKeyValuePartitioningOnRead()) { final long estimatedPartitionCount = estimatePartitionCount(minPartition, maxPartition); - if (estimatedPartitionCount <= useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount) { + if (estimatedPartitionCount <= useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount) { return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition, (int) estimatedPartitionCount)); } } @@ -446,7 +455,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD } List calculatePartitions(long minPartition, long maxPartition) { - return calculatePartitions(minPartition, maxPartition, 0); + return calculatePartitions(minPartition, maxPartition, 0); } List calculatePartitions(long minPartition, long maxPartition, int estimatedPartitionCount) { @@ -533,6 +542,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD public void onFailure(Throwable t) { } + } private long computeTtl(long ttl) { @@ -569,7 +579,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @Override public void onFailure(Throwable t) { - log.error("[{}][{}] Failed to delete data for query {}-{}", stmt, t); + log.error("[{}][{}][{}] Failed to delete data for key [{}], query [{}]", tenantId, cursor.getEntityType(), cursor.getEntityId(), cursor.getKey(), stmt.getPreparedStatement().getQuery(), t); + resultFuture.setException(t); } }, readResultsProcessingExecutor); } @@ -581,12 +592,12 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD try { if (deleteStmt == null) { deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF + - " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.TS_COLUMN + " >= ? " - + "AND " + ModelConstants.TS_COLUMN + " < ?"); + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.TS_COLUMN + " >= ? " + + "AND " + ModelConstants.TS_COLUMN + " < ?"); } } finally { stmtCreationLock.unlock(); @@ -661,13 +672,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD private String getPreparedStatementQuery(DataType type) { return INSERT_INTO + ModelConstants.TS_KV_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.KEY_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.TS_COLUMN + - "," + getColumnName(type) + ")" + - " VALUES(?, ?, ?, ?, ?, ?)"; + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.KEY_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.TS_COLUMN + + "," + getColumnName(type) + ")" + + " VALUES(?, ?, ?, ?, ?, ?)"; } private String getPreparedStatementQueryWithTtl(DataType type) { @@ -680,11 +691,11 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD try { if (partitionInsertStmt == null) { partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.KEY_COLUMN + ")" + - " VALUES(?, ?, ?, ?)"); + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.KEY_COLUMN + ")" + + " VALUES(?, ?, ?, ?)"); } } finally { stmtCreationLock.unlock(); @@ -699,11 +710,11 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD try { if (partitionInsertTtlStmt == null) { partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + - "(" + ModelConstants.ENTITY_TYPE_COLUMN + - "," + ModelConstants.ENTITY_ID_COLUMN + - "," + ModelConstants.PARTITION_COLUMN + - "," + ModelConstants.KEY_COLUMN + ")" + - " VALUES(?, ?, ?, ?) USING TTL ?"); + "(" + ModelConstants.ENTITY_TYPE_COLUMN + + "," + ModelConstants.ENTITY_ID_COLUMN + + "," + ModelConstants.PARTITION_COLUMN + + "," + ModelConstants.KEY_COLUMN + ")" + + " VALUES(?, ?, ?, ?) USING TTL ?"); } } finally { stmtCreationLock.unlock(); @@ -809,16 +820,17 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD fetchStmts[type.ordinal()] = fetchStmts[Aggregation.SUM.ordinal()]; } else { fetchStmts[type.ordinal()] = prepare(SELECT_PREFIX + - String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF - + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM - + "AND " + ModelConstants.TS_COLUMN + " >= ? " - + "AND " + ModelConstants.TS_COLUMN + " < ?" - + (type == Aggregation.NONE ? " ORDER BY " + ModelConstants.TS_COLUMN + " " + orderBy + " LIMIT ?" : "")); + String.join(", ", ModelConstants.getFetchColumnNames(type)) + " FROM " + ModelConstants.TS_KV_CF + + " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM + + "AND " + ModelConstants.TS_COLUMN + " >= ? " + + "AND " + ModelConstants.TS_COLUMN + " < ?" + + (type == Aggregation.NONE ? " ORDER BY " + ModelConstants.TS_COLUMN + " " + orderBy + " LIMIT ?" : "")); } } return fetchStmts; } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 6bcc88c2df..e9bf3db134 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -104,6 +104,16 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes return Futures.immediateFuture(Collections.emptyList()); } + @Override + public List findLatestByEntityIds(TenantId tenantId, List entityIds) { + return Collections.emptyList(); + } + + @Override + public ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds) { + return Futures.immediateFuture(Collections.emptyList()); + } + @Override public ListenableFuture saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) { BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getLatestStmt().bind()); @@ -184,7 +194,7 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes } private ListenableFuture> convertAsyncResultSetToTsKvEntryList(TbResultSet rs) { - return Futures.transform(rs.allRows(readResultsProcessingExecutor), + return Futures.transform(rs.allRows(readResultsProcessingExecutor, maxResultSetSizeBytes), rows -> this.convertResultToTsKvEntryList(rows), readResultsProcessingExecutor); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/SimpleListenableFuture.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/SimpleListenableFuture.java index 01581026d6..ded8c74b98 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/SimpleListenableFuture.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/SimpleListenableFuture.java @@ -26,4 +26,8 @@ public class SimpleListenableFuture extends AbstractFuture { return super.set(value); } + public boolean setException(Throwable throwable) { + return super.setException(throwable); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java index 26e784b760..c7582944a9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java @@ -52,4 +52,13 @@ public interface TimeseriesLatestDao { ListenableFuture> findAllKeysByEntityIdsAsync(TenantId tenantId, List entityIds); + /** + * For each unique timeseries key across the given entities, returns the single most recent {@link TsKvEntry} + * (i.e. the entry with the highest timestamp). If the same key exists on multiple entities, + * only the freshest value is kept. Useful for discovering available keys together with a representative sample value. + */ + List findLatestByEntityIds(TenantId tenantId, List entityIds); + + ListenableFuture> findLatestByEntityIdsAsync(TenantId tenantId, List entityIds); + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java index d3aa0fde0f..373112293d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java @@ -23,7 +23,6 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.awaitility.Awaitility; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.AttributeScope; @@ -31,8 +30,12 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.dao.attributes.AttributesDao; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.service.AbstractServiceTest; @@ -40,6 +43,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.concurrent.Executors; @@ -48,9 +52,6 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; -/** - * @author Andrew Shvayka - */ @Slf4j public abstract class BaseAttributesServiceTest extends AbstractServiceTest { @@ -60,9 +61,8 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { @Autowired private AttributesService attributesService; - @Before - public void before() { - } + @Autowired + private AttributesDao attributesDao; @Test public void saveAndFetch() throws Exception { @@ -223,7 +223,7 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { saveAttribute(tenantId, deviceId, AttributeScope.SERVER_SCOPE, "key2", "123"); Awaitility.await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { - List keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE); + List keys = attributesService.findAllKeysByEntityIdsAndScope(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE); assertThat(keys).containsOnly("key1", "key2"); }); } @@ -241,6 +241,84 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { }); } + @Test + public void findLatestByEntityIdsAndScope_returnsOneEntryPerKey() { + var device1 = new DeviceId(UUID.randomUUID()); + var device2 = new DeviceId(UUID.randomUUID()); + + // Both devices have "temperature", device2 has a newer ts + saveAttribute(tenantId, device1, AttributeScope.SERVER_SCOPE, 1000, new DoubleDataEntry("temperature", 20.0)); + saveAttribute(tenantId, device2, AttributeScope.SERVER_SCOPE, 2000, new DoubleDataEntry("temperature", 25.0)); + // Only device1 has "humidity" + saveAttribute(tenantId, device1, AttributeScope.SERVER_SCOPE, 1500, new LongDataEntry("humidity", 60L)); + // Only device2 has "active" + saveAttribute(tenantId, device2, AttributeScope.SERVER_SCOPE, 3000, new BooleanDataEntry("active", true)); + + List results = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(device1, device2), AttributeScope.SERVER_SCOPE); + Map byKey = results.stream().collect(Collectors.toMap(AttributeKvEntry::getKey, e -> e)); + + Assert.assertEquals(3, byKey.size()); + + // "temperature" should pick device2's value (ts=2000 > ts=1000) + AttributeKvEntry temp = byKey.get("temperature"); + Assert.assertNotNull(temp); + Assert.assertEquals(25.0, temp.getDoubleValue().orElseThrow(), 0.0); + Assert.assertEquals(2000, temp.getLastUpdateTs()); + + // "humidity" — only device1 has it + AttributeKvEntry humidity = byKey.get("humidity"); + Assert.assertNotNull(humidity); + Assert.assertEquals(60L, (long) humidity.getLongValue().orElseThrow()); + Assert.assertEquals(1500, humidity.getLastUpdateTs()); + + // "active" — only device2 has it + AttributeKvEntry active = byKey.get("active"); + Assert.assertNotNull(active); + Assert.assertEquals(true, active.getBooleanValue().orElseThrow()); + Assert.assertEquals(3000, active.getLastUpdateTs()); + } + + @Test + public void findLatestByEntityIdsAndScope_emptyList() { + List results = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(), AttributeScope.SERVER_SCOPE); + Assert.assertTrue(results.isEmpty()); + } + + @Test + public void findLatestByEntityIdsAndScope_singleEntity() throws Exception { + var device = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, device, AttributeScope.SERVER_SCOPE, 1000, new StringDataEntry("key1", "value1")); + saveAttribute(tenantId, device, AttributeScope.SERVER_SCOPE, 2000, new StringDataEntry("key2", "value2")); + + // sync + List results = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(device), AttributeScope.SERVER_SCOPE); + Assert.assertEquals(2, results.size()); + Map byKey = results.stream().collect(Collectors.toMap(AttributeKvEntry::getKey, e -> e)); + Assert.assertEquals("value1", byKey.get("key1").getStrValue().orElseThrow()); + Assert.assertEquals(1000, byKey.get("key1").getLastUpdateTs()); + Assert.assertEquals("value2", byKey.get("key2").getStrValue().orElseThrow()); + Assert.assertEquals(2000, byKey.get("key2").getLastUpdateTs()); + + // async — same result + List asyncResults = attributesDao.findLatestByEntityIdsAndScopeAsync(tenantId, List.of(device), AttributeScope.SERVER_SCOPE).get(); + Assert.assertEquals(results, asyncResults); + } + + @Test + public void findLatestByEntityIdsAndScope_filtersScope() { + var device = new DeviceId(UUID.randomUUID()); + saveAttribute(tenantId, device, AttributeScope.SERVER_SCOPE, 1000, new StringDataEntry("serverKey", "sv")); + saveAttribute(tenantId, device, AttributeScope.CLIENT_SCOPE, 1000, new StringDataEntry("clientKey", "cv")); + + List serverResults = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(device), AttributeScope.SERVER_SCOPE); + Assert.assertEquals(1, serverResults.size()); + Assert.assertEquals("serverKey", serverResults.get(0).getKey()); + + List clientResults = attributesDao.findLatestByEntityIdsAndScope(tenantId, List.of(device), AttributeScope.CLIENT_SCOPE); + Assert.assertEquals(1, clientResults.size()); + Assert.assertEquals("clientKey", clientResults.get(0).getKey()); + } + private void testConcurrentFetchAndUpdate(TenantId tenantId, DeviceId deviceId, ListeningExecutorService pool) throws Exception { var scope = AttributeScope.SERVER_SCOPE; var key = "TEST"; @@ -313,6 +391,15 @@ public abstract class BaseAttributesServiceTest extends AbstractServiceTest { } } + private void saveAttribute(TenantId tenantId, DeviceId deviceId, AttributeScope scope, long ts, KvEntry value) { + try { + attributesService.save(tenantId, deviceId, scope, Collections.singletonList(new BaseAttributeKvEntry(ts, value))).get(10, TimeUnit.SECONDS); + } catch (Exception e) { + log.warn("Failed to save attribute", e.getCause()); + throw new RuntimeException(e); + } + } + private void equalsIgnoreVersion(AttributeKvEntry expected, AttributeKvEntry actual) { Assert.assertEquals(expected.getKey(), actual.getKey()); Assert.assertEquals(expected.getValue(), actual.getValue()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java index 495ffc131b..aa43826734 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java @@ -16,7 +16,10 @@ package org.thingsboard.server.dao.service.timeseries.nosql; import com.datastax.oss.driver.api.core.uuid.Uuids; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; @@ -27,9 +30,12 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.nosql.ResultSetSizeLimitExceededException; import org.thingsboard.server.dao.service.DaoNoSqlTest; import org.thingsboard.server.dao.service.timeseries.BaseTimeseriesServiceTest; +import org.thingsboard.server.dao.timeseries.CassandraBaseTimeseriesDao; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; @@ -37,6 +43,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -44,6 +51,9 @@ import static org.junit.Assert.assertTrue; @DaoNoSqlTest public class TimeseriesServiceNoSqlTest extends BaseTimeseriesServiceTest { + @Autowired + private CassandraBaseTimeseriesDao cassandraBaseTimeseriesDao; + @Test public void shouldSaveEntryOfEachTypeWithTtl() throws ExecutionException, InterruptedException, TimeoutException { long ttlInSec = TimeUnit.SECONDS.toSeconds(3); @@ -94,4 +104,36 @@ public class TimeseriesServiceNoSqlTest extends BaseTimeseriesServiceTest { double expectedValue = (doubleValue + longValue)/ 2; assertThat(listWithAgg.get(0).getDoubleValue().get()).isEqualTo(expectedValue); } + + @Test + public void testResultSetSizeLimitExceeded() throws Exception { + DeviceId deviceId = new DeviceId(Uuids.timeBased()); + + String value = "x".repeat(500); + List entries = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + entries.add(new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(i + 1), new StringDataEntry("bigKey", value))); + } + tsService.save(tenantId, deviceId, entries, 0).get(MAX_TIMEOUT, TimeUnit.SECONDS); + + long originalLimit = (long) ReflectionTestUtils.getField(cassandraBaseTimeseriesDao, "maxResultSetSizeBytes"); + try { + // Set a very low limit to trigger the exception + ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "maxResultSetSizeBytes", 1024L); + + assertThatThrownBy(() -> tsService.findAll(tenantId, deviceId, Collections.singletonList( + new BaseReadTsKvQuery("bigKey", 0L, TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE) + )).get(MAX_TIMEOUT, TimeUnit.SECONDS)) + .isInstanceOf(ExecutionException.class) + .satisfies(e -> assertThat(ExceptionUtils.getRootCause(e)).isInstanceOf(ResultSetSizeLimitExceededException.class)); + } finally { + ReflectionTestUtils.setField(cassandraBaseTimeseriesDao, "maxResultSetSizeBytes", originalLimit); + } + + // Verify query succeeds with original limit restored + List result = tsService.findAll(tenantId, deviceId, Collections.singletonList( + new BaseReadTsKvQuery("bigKey", 0L, TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE) + )).get(MAX_TIMEOUT, TimeUnit.SECONDS); + assertThat(result).hasSize(5); + } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java index 96f3390b13..02abf1e5fd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDaoTest.java @@ -22,6 +22,9 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.service.AbstractServiceTest; @@ -30,7 +33,9 @@ import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -102,6 +107,69 @@ public class SqlTimeseriesLatestDaoTest extends AbstractServiceTest { } } + @Test + public void findLatestByEntityIds_returnsOneEntryPerKey() throws Exception { + DeviceId device1 = new DeviceId(UUID.randomUUID()); + DeviceId device2 = new DeviceId(UUID.randomUUID()); + + // Both devices have "temperature" key, device2 has a newer ts + timeseriesLatestDao.saveLatest(tenantId, device1, new BasicTsKvEntry(1000, new DoubleDataEntry("temperature", 20.0))).get(); + timeseriesLatestDao.saveLatest(tenantId, device2, new BasicTsKvEntry(2000, new DoubleDataEntry("temperature", 25.0))).get(); + // Only device1 has "humidity" + timeseriesLatestDao.saveLatest(tenantId, device1, new BasicTsKvEntry(1500, new LongDataEntry("humidity", 60L))).get(); + // Only device2 has "active" + timeseriesLatestDao.saveLatest(tenantId, device2, new BasicTsKvEntry(3000, new BooleanDataEntry("active", true))).get(); + + List results = timeseriesLatestDao.findLatestByEntityIds(tenantId, List.of(device1, device2)); + Map byKey = results.stream().collect(Collectors.toMap(TsKvEntry::getKey, e -> e)); + + assertEquals(3, byKey.size()); + + // "temperature" should pick device2's value (ts=2000 > ts=1000) + TsKvEntry temp = byKey.get("temperature"); + assertNotNull(temp); + assertEquals(25.0, temp.getDoubleValue().orElseThrow()); + assertEquals(2000, temp.getTs()); + + // "humidity" — only device1 has it + TsKvEntry humidity = byKey.get("humidity"); + assertNotNull(humidity); + assertEquals(60L, humidity.getLongValue().orElseThrow()); + assertEquals(1500, humidity.getTs()); + + // "active" — only device2 has it + TsKvEntry active = byKey.get("active"); + assertNotNull(active); + assertEquals(true, active.getBooleanValue().orElseThrow()); + assertEquals(3000, active.getTs()); + } + + @Test + public void findLatestByEntityIds_emptyList() { + List results = timeseriesLatestDao.findLatestByEntityIds(tenantId, List.of()); + assertTrue(results.isEmpty()); + } + + @Test + public void findLatestByEntityIds_singleEntity() throws Exception { + DeviceId device = new DeviceId(UUID.randomUUID()); + timeseriesLatestDao.saveLatest(tenantId, device, new BasicTsKvEntry(1000, new StringDataEntry("key1", "value1"))).get(); + timeseriesLatestDao.saveLatest(tenantId, device, new BasicTsKvEntry(2000, new StringDataEntry("key2", "value2"))).get(); + + // sync + List results = timeseriesLatestDao.findLatestByEntityIds(tenantId, List.of(device)); + assertEquals(2, results.size()); + Map byKey = results.stream().collect(Collectors.toMap(TsKvEntry::getKey, e -> e)); + assertEquals("value1", byKey.get("key1").getStrValue().orElseThrow()); + assertEquals(1000, byKey.get("key1").getTs()); + assertEquals("value2", byKey.get("key2").getStrValue().orElseThrow()); + assertEquals(2000, byKey.get("key2").getTs()); + + // async — same result + List asyncResults = timeseriesLatestDao.findLatestByEntityIdsAsync(tenantId, List.of(device)).get(); + assertEquals(results, asyncResults); + } + private TsKvEntry createEntry(String key, long ts) { return new BasicTsKvEntry(ts, new StringDataEntry(key, RandomStringUtils.random(10))); } diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index cf7310a084..ed4565ec08 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -41,6 +41,9 @@ "ts-node": "^10.9.2", "typescript": "5.9.2" }, + "resolutions": { + "@yao-pkg/pkg/tar": ">=7.5.8" + }, "pkg": { "assets": [ "node_modules/config/**/*.*" diff --git a/msa/js-executor/yarn.lock b/msa/js-executor/yarn.lock index 7a54682fa7..1e4a802d3c 100644 --- a/msa/js-executor/yarn.lock +++ b/msa/js-executor/yarn.lock @@ -1036,9 +1036,9 @@ mimic-response@^3.1.0: integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + version "3.1.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624" + integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA== dependencies: brace-expansion "^1.1.7" @@ -1052,10 +1052,10 @@ minipass@^7.0.4, minipass@^7.1.2: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -minizlib@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.2.tgz#f33d638eb279f664439aa38dc5f91607468cb574" - integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA== +minizlib@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.1.0.tgz#6ad76c3a8f10227c9b51d1c9ac8e30b27f5a251c" + integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw== dependencies: minipass "^7.1.2" @@ -1064,11 +1064,6 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" - integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== - moment@^2.29.1: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" @@ -1553,16 +1548,15 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^7.4.3: - version "7.4.3" - resolved "https://registry.yarnpkg.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571" - integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw== +tar@>=7.5.8, tar@^7.4.3: + version "7.5.9" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8" + integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg== dependencies: "@isaacs/fs-minipass" "^4.0.0" chownr "^3.0.0" minipass "^7.1.2" - minizlib "^3.0.1" - mkdirp "^3.0.1" + minizlib "^3.1.0" yallist "^5.0.0" text-hex@1.0.x: diff --git a/msa/tb/docker-cassandra/Dockerfile b/msa/tb/docker-cassandra/Dockerfile index 34a90e6fb7..358f2907c8 100644 --- a/msa/tb/docker-cassandra/Dockerfile +++ b/msa/tb/docker-cassandra/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM thingsboard/openjdk25:trixie-slim ENV PG_MAJOR=16 @@ -44,15 +44,14 @@ COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb. RUN apt-get update \ && apt-get install -y --no-install-recommends wget nmap procps gnupg2 \ - && echo "deb http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null \ - && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O- | apt-key add - \ - && echo "deb https://debian.cassandra.apache.org 40x main" | tee -a /etc/apt/sources.list.d/cassandra.sources.list > /dev/null \ - && wget -q https://downloads.apache.org/cassandra/KEYS -O- | apt-key add - \ + && mkdir -p /etc/apt/keyrings \ + && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O- | gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list > /dev/null \ + && wget -q https://downloads.apache.org/cassandra/KEYS -O- | gpg --dearmor -o /etc/apt/keyrings/cassandra.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/cassandra.gpg] https://debian.cassandra.apache.org 40x main" | tee /etc/apt/sources.list.d/cassandra.sources.list > /dev/null \ && apt-get update \ && apt-get install -y --no-install-recommends cassandra cassandra-tools postgresql-${PG_MAJOR} \ && rm -rf /var/lib/apt/lists/* \ - && update-rc.d cassandra disable \ - && update-rc.d postgresql disable \ && apt-get purge -y --auto-remove \ && sed -i.old '/ulimit/d' /etc/init.d/cassandra \ && chmod a+x /tmp/*.sh \ diff --git a/msa/tb/docker-postgres/Dockerfile b/msa/tb/docker-postgres/Dockerfile index 0a0f61f904..6ce066d1ed 100644 --- a/msa/tb/docker-postgres/Dockerfile +++ b/msa/tb/docker-postgres/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM thingsboard/openjdk25:trixie-slim ENV PG_MAJOR 12 @@ -36,12 +36,12 @@ COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb. RUN apt-get update \ && apt-get install -y --no-install-recommends wget gnupg2 \ - && echo "deb http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null \ - && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O- | apt-key add - \ + && mkdir -p /etc/apt/keyrings \ + && wget -q https://www.postgresql.org/media/keys/ACCC4CF8.asc -O- | gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(. /etc/os-release && echo -n $VERSION_CODENAME)-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list > /dev/null \ && apt-get update \ && apt-get install -y --no-install-recommends postgresql-${PG_MAJOR} \ && rm -rf /var/lib/apt/lists/* \ - && update-rc.d postgresql disable \ && apt-get purge -y --auto-remove \ && chmod a+x /tmp/*.sh \ && mv /tmp/start-tb.sh /usr/bin \ diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 4544aece37..bd54cc26de 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -44,6 +44,9 @@ "ts-node": "^10.9.2", "typescript": "5.9.2" }, + "resolutions": { + "@yao-pkg/pkg/tar": ">=7.5.8" + }, "pkg": { "assets": [ "node_modules/config/**/*.*" diff --git a/msa/web-ui/yarn.lock b/msa/web-ui/yarn.lock index 383d64d5f9..d11efb01f6 100644 --- a/msa/web-ui/yarn.lock +++ b/msa/web-ui/yarn.lock @@ -1098,9 +1098,9 @@ mimic-response@^3.1.0: integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + version "3.1.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624" + integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA== dependencies: brace-expansion "^1.1.7" @@ -1114,10 +1114,10 @@ minipass@^7.0.4, minipass@^7.1.2: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== -minizlib@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.2.tgz#f33d638eb279f664439aa38dc5f91607468cb574" - integrity sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA== +minizlib@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.1.0.tgz#6ad76c3a8f10227c9b51d1c9ac8e30b27f5a251c" + integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw== dependencies: minipass "^7.1.2" @@ -1126,11 +1126,6 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" - integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== - moment@^2.29.1: version "2.30.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" @@ -1635,16 +1630,15 @@ tar-stream@^2.1.4: inherits "^2.0.3" readable-stream "^3.1.1" -tar@^7.4.3: - version "7.4.3" - resolved "https://registry.yarnpkg.com/tar/-/tar-7.4.3.tgz#88bbe9286a3fcd900e94592cda7a22b192e80571" - integrity sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw== +tar@>=7.5.8, tar@^7.4.3: + version "7.5.9" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8" + integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg== dependencies: "@isaacs/fs-minipass" "^4.0.0" chownr "^3.0.0" minipass "^7.1.2" - minizlib "^3.0.1" - mkdirp "^3.0.1" + minizlib "^3.1.0" yallist "^5.0.0" text-hex@1.0.x: diff --git a/packaging/java/build.gradle b/packaging/java/build.gradle index 5ef4576a91..6bd28968e9 100644 --- a/packaging/java/build.gradle +++ b/packaging/java/build.gradle @@ -16,12 +16,14 @@ import org.apache.tools.ant.filters.ReplaceTokens plugins { - id "nebula.ospackage" version "8.6.3" + id "com.netflix.nebula.ospackage" version "12.1.1" } buildDir = projectBuildDir version = projectVersion -distsDirName = "./" +base { + distsDirectory = layout.buildDirectory.dir("./") +} // OS Package plugin configuration ospackage { @@ -33,8 +35,8 @@ ospackage { into pkgInstallFolder - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the actual .jar file from(mainJar) { @@ -42,19 +44,25 @@ ospackage { rename { String fileName -> "${pkgName}.jar" } - fileMode 0500 + filePermissions { + unix('r-x------') // 0500 + } into "bin" } if("${pkgCopyInstallScripts}".equalsIgnoreCase("true")) { // Copy the install files from("${buildDir}/bin/install/install.sh") { - fileMode 0775 + filePermissions { + unix('rwxrwxr-x') // 0775 + } into "bin/install" } from("${buildDir}/bin/install/upgrade.sh") { - fileMode 0775 + filePermissions { + unix('rwxrwxr-x') // 0775 + } into "bin/install" } @@ -67,14 +75,18 @@ ospackage { from("${buildDir}/conf") { exclude "${pkgName}.conf" fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "conf" } // Copy the data files from("${buildDir}/data") { fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "data" } @@ -92,7 +104,7 @@ buildRpm { archiveVersion = projectVersion.replace('-', '') archiveFileName = "${pkgName}.rpm" - // Support Java 17 (existing), plus Java 21 and Java 25 for RPM-based distros + // Support Java 25 (current), plus Java 21 and Java 17 (backward compatibility) for RPM-based distros // Keep using RPM boolean expression syntax since .or() chaining is for DEB only requires("(java-17 or java-17-headless or jre-17 or jre-17-headless or " + "java-21 or java-21-headless or jre-21 or jre-21-headless or " + @@ -102,7 +114,9 @@ buildRpm { include "${pkgName}.conf" filter(ReplaceTokens, tokens: ['pkg.platform': 'rpm']) fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "${pkgInstallFolder}/conf" } @@ -111,13 +125,15 @@ buildRpm { preUninstall file("${buildDir}/control/rpm/prerm") postUninstall file("${buildDir}/control/rpm/postrm") - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the system unit files from("${buildDir}/control/template.service") { addParentDirs = false - fileMode 0644 + filePermissions { + unix('rw-r--r--') // 0644 + } into "/usr/lib/systemd/system" rename { String filename -> "${pkgName}.service" @@ -143,7 +159,9 @@ buildDeb { include "${pkgName}.conf" filter(ReplaceTokens, tokens: ['pkg.platform': 'deb']) fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "${pkgInstallFolder}/conf" } @@ -157,13 +175,15 @@ buildDeb { preUninstall file("${buildDir}/control/deb/prerm") postUninstall file("${buildDir}/control/deb/postrm") - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the system unit files from("${buildDir}/control/template.service") { addParentDirs = false - fileMode 0644 + filePermissions { + unix('rw-r--r--') // 0644 + } into "/lib/systemd/system" rename { String filename -> "${pkgName}.service" diff --git a/packaging/java/scripts/windows/install.bat b/packaging/java/scripts/windows/install.bat index ca17408dfe..566286d4b5 100644 --- a/packaging/java/scripts/windows/install.bat +++ b/packaging/java/scripts/windows/install.bat @@ -7,11 +7,11 @@ setlocal ENABLEEXTENSIONS for /f tokens^=2-5^ delims^=.-_^" %%j in ('java -fullversion 2^>^&1') do set "jver=%%j%%k" @ECHO CurrentVersion %jver% -if %jver% NEQ 170 GOTO JAVA_NOT_INSTALLED +if %jver% NEQ 250 GOTO JAVA_NOT_INSTALLED :JAVA_INSTALLED -@ECHO Java 17 found! +@ECHO Java 25 found! @ECHO Installing thingsboard ... SET loadDemo=false @@ -50,8 +50,8 @@ POPD GOTO END :JAVA_NOT_INSTALLED -@ECHO Java 17 is not installed. Only Java 17 is supported -@ECHO Please go to https://adoptopenjdk.net/index.html and install Java 17. Then retry installation. +@ECHO Java 25 is not installed. Only Java 25 is supported +@ECHO Please go to https://adoptopenjdk.net/index.html and install Java 25. Then retry installation. PAUSE GOTO END diff --git a/packaging/js/build.gradle b/packaging/js/build.gradle index 15b8e546a1..13a2779f34 100644 --- a/packaging/js/build.gradle +++ b/packaging/js/build.gradle @@ -13,15 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import org.apache.tools.ant.filters.ReplaceTokens plugins { - id "nebula.ospackage" version "8.6.3" + id "com.netflix.nebula.ospackage" version "12.1.1" } buildDir = projectBuildDir version = projectVersion -distsDirName = "./" +base { + distsDirectory = layout.buildDirectory.dir("./") +} // OS Package plugin configuration ospackage { @@ -33,18 +34,22 @@ ospackage { into pkgInstallFolder - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the executable file from("${buildDir}/package/linux/bin/${pkgName}") { - fileMode 0500 + filePermissions { + unix('r-x------') // 0500 + } into "bin" } // Copy the init file from("${buildDir}/package/linux/init/template") { - fileMode 0500 + filePermissions { + unix('r-x------') // 0500 + } into "init" rename { String filename -> "${pkgName}" @@ -54,7 +59,9 @@ ospackage { // Copy the config files from("${buildDir}/package/linux/conf") { fileType CONFIG | NOREPLACE - fileMode 0754 + filePermissions { + unix('rwxr-xr--') // 0754 + } into "conf" } @@ -78,13 +85,15 @@ buildRpm { preUninstall file("${buildDir}/control/rpm/prerm") postUninstall file("${buildDir}/control/rpm/postrm") - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the system unit files from("${buildDir}/control/template.service") { addParentDirs = false - fileMode 0644 + filePermissions { + unix('rw-r--r--') // 0644 + } into "/usr/lib/systemd/system" rename { String filename -> "${pkgName}.service" @@ -111,13 +120,15 @@ buildDeb { preUninstall file("${buildDir}/control/deb/prerm") postUninstall file("${buildDir}/control/deb/postrm") - user pkgUser - permissionGroup pkgUser + user = pkgUser + permissionGroup = pkgUser // Copy the system unit files from("${buildDir}/control/template.service") { addParentDirs = false - fileMode 0644 + filePermissions { + unix('rw-r--r--') // 0644 + } into "/lib/systemd/system" rename { String filename -> "${pkgName}.service" diff --git a/pom.xml b/pom.xml index c4cfec0ad3..adbef4f0b3 100755 --- a/pom.xml +++ b/pom.xml @@ -28,8 +28,8 @@ 2016 - 17 - 17 + 25 + 25 ${basedir} true none @@ -38,7 +38,8 @@ ${project.name} /var/log/${pkg.name} /usr/share/${pkg.name} - 3.4.10 + 3.4.13 + 10.1.52 2.4.0-b180830.0359 5.1.5 0.12.5 @@ -65,7 +66,7 @@ 3.25.5 1.76.0 1.2.8 - 1.18.38 + 1.18.40 1.2.5 1.2.5 1.7.1 @@ -111,6 +112,7 @@ 4.0.2 1.7.5 3.8.0 + 1.18.4 1.8.0-TB 2.38.0 1.24 @@ -147,7 +149,6 @@ 9.2.0 1.1.10.5 9.10.0 - 4.1.128.Final @@ -605,7 +606,7 @@ maven-compiler-plugin 3.11.0 - 17 + 25 -Xlint:deprecation -Xlint:removal @@ -649,7 +650,7 @@ org.thingsboard gradle-maven-plugin - 1.0.12 + 1.0.15 com.github.eirslett @@ -663,6 +664,7 @@ -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=200 + -XX:+EnableDynamicAgentLoading --add-opens=java.base/java.lang.reflect=ALL-UNNAMED -Dqueue.edqs.local.rocksdb_path="target/rocksdb/fork_${surefire.forkNumber}/edqs" -Dqueue.calculated_fields.rocks_db_path="target/rocksdb/fork_${surefire.forkNumber}/cf" @@ -899,13 +901,24 @@ + - io.netty - netty-bom - ${netty.version} - pom - import + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${tomcat.version} + + org.springframework.boot spring-boot-dependencies @@ -913,6 +926,16 @@ pom import + + net.bytebuddy + byte-buddy + ${bytebuddy.version} + + + net.bytebuddy + byte-buddy-agent + ${bytebuddy.version} + org.thingsboard.langchain4j langchain4j-bom @@ -1929,7 +1952,7 @@ https://repo1.maven.org/maven2/ - thingsboard-repo + thingsboard-public-repo https://repo.thingsboard.io/artifactory/libs-release-public @@ -1955,4 +1978,10 @@ + + + thingsboard-repo + https://repo.thingsboard.io/artifactory/libs-release-public + + diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index edef60f30d..f133527ddc 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -22,7 +22,6 @@ import com.google.common.base.Strings; import lombok.Getter; import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; -import org.apache.hc.core5.net.URIBuilder; import org.apache.commons.lang3.concurrent.LazyInitializer; import org.apache.hc.core5.net.URIBuilder; import org.springframework.core.ParameterizedTypeReference; @@ -172,6 +171,7 @@ import org.thingsboard.server.common.data.query.AlarmCountQuery; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.common.data.query.AvailableEntityKeys; +import org.thingsboard.server.common.data.query.AvailableEntityKeysV2; import org.thingsboard.server.common.data.query.EntityCountQuery; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -1898,6 +1898,10 @@ public class RestClient implements Closeable { }).getBody(); } + /** + * @deprecated Use {@link #findAvailableEntityKeysV2(EntityDataQuery, boolean, boolean, Set, boolean)} instead. + */ + @Deprecated(forRemoval = true) public AvailableEntityKeys findAvailableEntityKeysByQuery(EntityDataQuery query, boolean includeTimeseries, boolean includeAttributes, AttributeScope scope) { var uri = UriComponentsBuilder.fromUriString(baseURL) .path("/api/entitiesQuery/find/keys") @@ -1909,6 +1913,22 @@ public class RestClient implements Closeable { return restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference() {}).getBody(); } + @SneakyThrows(URISyntaxException.class) + public AvailableEntityKeysV2 findAvailableEntityKeysV2( + EntityDataQuery query, boolean includeTimeseries, boolean includeAttributes, Set scopes, boolean includeSamples + ) { + var builder = new URIBuilder(baseURL).appendPath("/api/v2/entitiesQuery/find/keys") + .addParameter("includeTimeseries", String.valueOf(includeTimeseries)) + .addParameter("includeAttributes", String.valueOf(includeAttributes)) + .addParameter("includeSamples", String.valueOf(includeSamples)); + if (scopes != null) { + for (AttributeScope scope : scopes) { + builder.addParameter("scopes", scope.name()); + } + } + return restTemplate.exchange(builder.build(), HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference() {}).getBody(); + } + public PageData findAlarmDataByQuery(AlarmDataQuery query) { return restTemplate.exchange( baseURL + "/api/alarmsQuery/find", diff --git a/rest-client/src/main/resources/logback.xml b/rest-client/src/main/resources/logback.xml deleted file mode 100644 index f8c9b51bd0..0000000000 --- a/rest-client/src/main/resources/logback.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - - - diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java index e4cb573cfe..f12d59cd0a 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/JobManager.java @@ -19,11 +19,14 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.JobId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.job.Job; +import org.thingsboard.server.common.msg.queue.TbCallback; public interface JobManager { ListenableFuture submitJob(Job job); // TODO: rate limits + ListenableFuture submitJob(Job job, TbCallback finishCallback); + void cancelJob(TenantId tenantId, JobId jobId); void reprocessJob(TenantId tenantId, JobId jobId); diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 6867a6c952..ab15b2e7fe 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -94,11 +94,11 @@ }, "devDependencies": { "@angular-builders/custom-esbuild": "20.0.0", - "@angular-devkit/build-angular": "20.3.15", - "@angular-devkit/core": "20.3.15", - "@angular-devkit/schematics": "20.3.15", - "@angular/build": "20.3.15", - "@angular/cli": "20.3.15", + "@angular-devkit/build-angular": "20.3.16", + "@angular-devkit/core": "20.3.16", + "@angular-devkit/schematics": "20.3.16", + "@angular/build": "20.3.16", + "@angular/cli": "20.3.16", "@angular/compiler-cli": "20.3.16", "@angular/language-service": "20.3.16", "@types/ace-diff": "^2.1.4", @@ -121,7 +121,7 @@ "angular-eslint": "~20.7.0", "autoprefixer": "^10.4.23", "directory-tree": "^3.5.2", - "eslint": "~9.39.2", + "eslint": "9.39.3", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsdoc": "^62.4.1", "eslint-plugin-prefer-arrow": "^1.2.3", @@ -139,6 +139,9 @@ "ace-builds": "1.43.6", "tinymce": "6.8.6", "@babel/core": "7.28.3", - "esbuild": "0.25.9" + "esbuild": "0.25.9", + "rollup": "4.52.3", + "jquery.terminal/**/form-data": ">=4.0.4", + "js-beautify/**/minimatch": "^9.0.6" } } diff --git a/ui-ngx/patches/@angular+build+20.3.15.patch b/ui-ngx/patches/@angular+build+20.3.16.patch similarity index 99% rename from ui-ngx/patches/@angular+build+20.3.15.patch rename to ui-ngx/patches/@angular+build+20.3.16.patch index 1931d8d2b1..6c88eb6de2 100644 --- a/ui-ngx/patches/@angular+build+20.3.15.patch +++ b/ui-ngx/patches/@angular+build+20.3.16.patch @@ -12,7 +12,7 @@ index 53168ea..f07c80a 100755 // remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */. removeComments: false, diff --git a/node_modules/@angular/build/src/tools/esbuild/angular/compiler-plugin.js b/node_modules/@angular/build/src/tools/esbuild/angular/compiler-plugin.js -index ee68408..ac15cbf 100755 +index ee68408..2667b84 100755 --- a/node_modules/@angular/build/src/tools/esbuild/angular/compiler-plugin.js +++ b/node_modules/@angular/build/src/tools/esbuild/angular/compiler-plugin.js @@ -90,7 +90,7 @@ function createCompilerPlugin(pluginOptions, compilationOrFactory, stylesheetBun diff --git a/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch b/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch index 211a3c8b0b..b0087a67e9 100644 --- a/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch +++ b/ui-ngx/patches/@iplab+ngx-color-picker+20.0.0.patch @@ -1,46 +1,13 @@ diff --git a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs -index a372799..a3d709a 100644 +index a372799..f64a6f8 100644 --- a/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs +++ b/node_modules/@iplab/ngx-color-picker/fesm2022/iplab-ngx-color-picker.mjs -@@ -1129,11 +1129,11 @@ class RgbaComponent { - this.color.set(newColor); +@@ -516,7 +516,7 @@ class Color { + const s = (color.saturation / 100) * (l <= 1 ? l : 2 - l); + const value = (l + s) / 2; + const saturation = (2 * s) / (l + s) || 0; +- return new Hsva(hue, saturation, value, color.alpha); ++ return new Hsva(hue, saturation * 100, value * 100, color.alpha); } - static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RgbaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } -- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } -+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: RgbaComponent, isStandalone: true, selector: "rgba-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } - } - i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: RgbaComponent, decorators: [{ - type: Component, -- args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] -+ args: [{ selector: `rgba-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n R\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n G\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n B\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] - }] }); - - class HslaComponent { -@@ -1155,11 +1155,11 @@ class HslaComponent { - this.color.set(newColor); - } - static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HslaComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } -- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } -+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HslaComponent, isStandalone: true, selector: "hsla-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, isAlphaVisible: { classPropertyName: "isAlphaVisible", publicName: "alpha", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], dependencies: [{ kind: "directive", type: ColorPickerInputDirective, selector: "[inputChange]", inputs: ["min", "max"], outputs: ["inputChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } - } - i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HslaComponent, decorators: [{ - type: Component, -- args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] -+ args: [{ selector: `hsla-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, imports: [ColorPickerInputDirective], template: "
\r\n \r\n @if (labelVisible()) {\r\n H\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n S\r\n }\r\n
\r\n
\r\n \r\n @if (labelVisible()) {\r\n L\r\n }\r\n
\r\n@if (isAlphaVisible()) {\r\n
\r\n \r\n @if (labelVisible()) {\r\n A\r\n }\r\n
\r\n}", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] - }] }); - - class HexComponent { -@@ -1190,11 +1190,11 @@ class HexComponent { - } - } - static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HexComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } -- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } -+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.4", type: HexComponent, isStandalone: true, selector: "hex-input-component", inputs: { color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: true, transformFunction: null }, labelVisible: { classPropertyName: "labelVisible", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, prefixValue: { classPropertyName: "prefixValue", publicName: "prefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { color: "colorChange" }, ngImport: i0, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n", ""], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } - } - i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.4", ngImport: i0, type: HexComponent, decorators: [{ - type: Component, -- args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] -+ args: [{ selector: `hex-input-component`, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "
\r\n \r\n @if (labelVisible()) {\r\n HEX\r\n }\r\n
", styles: [":host,:host ::ng-deep *{padding:0;margin:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}\n", ":host{display:table;width:100%;text-align:center;color:#b4b4b4;font-size:11px}.column{display:table-cell;padding:0 2px}input{width:100%;border:1px solid rgb(218,218,218);color:#272727;text-align:center;font-size:12px;-webkit-appearance:none;border-radius:0;margin:0 0 6px;height:26px;outline:none}\n"] }] - }] }); - - const OpacityAnimation = trigger('opacityAnimation', [ + rgbaToHsva(color) { + const red = color.red / 255; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts index 9204a901b2..d569bcf37b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/bar-chart-with-labels-basic-config.component.ts @@ -45,7 +45,10 @@ import { barChartWithLabelsDefaultSettings, BarChartWithLabelsWidgetSettings } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; -import { TimeSeriesChartType } from '@home/components/widget/lib/chart/time-series-chart.models'; +import { + TimeSeriesChartType, + updateLatestDataKeys +} from '@home/components/widget/lib/chart/time-series-chart.models'; import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; @Component({ @@ -76,7 +79,7 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom tooltipDatePreviewFn = this._tooltipDatePreviewFn.bind(this); predefinedValues = widgetTitleAutocompleteValues; - + constructor(protected store: Store, protected widgetConfigComponent: WidgetConfigComponent, private $injector: Injector, @@ -167,6 +170,11 @@ export class BarChartWithLabelsBasicConfigComponent extends BasicWidgetConfigCom }); } + protected onConfigChanged(widgetConfig: WidgetConfigComponentData) { + updateLatestDataKeys([widgetConfig.config.settings.yAxis], this.datasource, this.callbacks); + super.onConfigChanged(widgetConfig); + } + protected prepareOutputConfig(config: any): WidgetConfigComponentData { setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); this.widgetConfig.config.datasources = config.datasources; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts index 0527045650..95dbabfbfc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/range-chart-basic-config.component.ts @@ -47,7 +47,7 @@ import { } from '@home/components/widget/lib/chart/range-chart-widget.models'; import { lineSeriesStepTypes, - lineSeriesStepTypeTranslations + lineSeriesStepTypeTranslations, updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; import { chartLabelPositions, @@ -289,6 +289,11 @@ export class RangeChartBasicConfigComponent extends BasicWidgetConfigComponent { return this.widgetConfig; } + protected onConfigChanged(widgetConfig: WidgetConfigComponentData) { + updateLatestDataKeys([widgetConfig.config.settings.yAxis], this.datasource, this.callbacks); + super.onConfigChanged(widgetConfig); + } + protected validatorTriggers(): string[] { return ['showTitle', 'showIcon', 'showRangeThresholds', 'fillArea', 'showLine', 'step', 'showPointLabel', 'enablePointLabelBackground', 'showLegend', 'showTooltip', 'tooltipShowDate']; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts index 1f63cd8638..0287b775c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-with-labels-widget.component.ts @@ -134,6 +134,12 @@ export class BarChartWithLabelsWidgetComponent implements OnInit, OnDestroy, Aft } } + public onLatestDataUpdated() { + if (this.timeSeriesChart) { + this.timeSeriesChart.latestUpdated(); + } + } + public onLegendKeyEnter(key: DataKey) { this.timeSeriesChart.keyEnter(key); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts index 2412d51a83..4ad45c85a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts @@ -162,6 +162,12 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn } } + public onLatestDataUpdated() { + if (this.timeSeriesChart) { + this.timeSeriesChart.latestUpdated(); + } + } + public toggleRangeItem(item: RangeItem) { item.enabled = !item.enabled; this.timeSeriesChart.toggleVisualMapRange(item.index); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index 4816852768..e815eefe11 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -99,6 +99,8 @@ import { TimeSeriesChartTooltipWidgetSettings } from '@home/components/widget/lib/chart/time-series-chart-tooltip.models'; import { TbUnit, TbUnitConverter } from '@shared/models/unit.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; type TimeSeriesChartDataEntry = [number, any, number, number]; @@ -1495,3 +1497,101 @@ const createSeriesLabelOption = (item: TimeSeriesChartDataItem, show: boolean, } return labelOption; }; + +export const checkLatestDataKeys = (yAxes: TimeSeriesChartYAxes, datasource: Datasource): TimeSeriesChartYAxes => { + const latestKeys = datasource?.latestDataKeys || []; + const result: TimeSeriesChartYAxes = {}; + + for (const [id, axis] of Object.entries(yAxes)) { + axis.min = normalizeAxisLimit(axis.min); + axis.max = normalizeAxisLimit(axis.max); + const minCfg = axis.min; + const maxCfg = axis.max; + + const minValid = !!minCfg && ( + minCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => isYAxisKey(k, minCfg)) + ); + + const maxValid = !!maxCfg && ( + maxCfg.type !== ValueSourceType.latestKey || + latestKeys.some(k => isYAxisKey(k, maxCfg)) + ); + + if (minValid && maxValid) { + result[id] = axis; + } + } + + return result; +} + +export const updateLatestDataKeys = (yAxes: TimeSeriesChartYAxisSettings[], datasource: Datasource, dataKeyCallbacks: DataKeysCallbacks)=> { + if (datasource) { + let latestKeys = datasource.latestDataKeys; + if (!latestKeys) { + latestKeys = []; + datasource.latestDataKeys = latestKeys; + } + const existingYAxisKeys = latestKeys.filter(k => k.settings?.__yAxisMinKey === true || k.settings?.__yAxisMaxKey === true); + const foundYAxisKeys: DataKey[] = []; + + for(const yAxis of yAxes) { + const min = yAxis.min as ValueSourceConfig; + const max = yAxis.max as ValueSourceConfig; + if (min && min.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => isYAxisKey(k, min)); + if (!found) { + const newKey = dataKeyCallbacks.generateDataKey(min.latestKey, min.latestKeyType, + null, true, null); + newKey.settings.__yAxisMinKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + if (max && max.type === ValueSourceType.latestKey) { + const found = existingYAxisKeys.find(k => isYAxisKey(k, max)); + if (!found) { + const newKey = dataKeyCallbacks.generateDataKey(max.latestKey, max.latestKeyType, + null, true, null); + newKey.settings.__yAxisMaxKey = true; + latestKeys.push(newKey); + } else if (foundYAxisKeys.indexOf(found) === -1) { + foundYAxisKeys.push(found); + } + } + } + const toRemove = existingYAxisKeys.filter(k => foundYAxisKeys.indexOf(k) === -1); + for (const key of toRemove) { + const index = latestKeys.indexOf(key); + if (index > -1) { + latestKeys.splice(index, 1); + } + } + } +} + +export const isYAxisKey = (d: DataKey, limit: ValueSourceConfig): boolean => { + return (d.type === DataKeyType.function && d.label === limit.latestKey) || + (d.type !== DataKeyType.function && d.name === limit.latestKey && + d.type === limit.latestKeyType); +} + +export const normalizeAxisLimit = (limit: string | number | ValueSourceConfig): ValueSourceConfig => { + if (!limit) { + return { + type: ValueSourceType.constant, + value: null, + entityAlias: null + }; + } else if (typeof limit === 'number' || typeof limit === 'string') { + return { + type: ValueSourceType.constant, + value: Number(limit), + entityAlias: null + }; + } + return limit; +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 21f5f2d68c..d1928d75f0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -23,7 +23,7 @@ import { createTimeSeriesYAxis, defaultTimeSeriesChartYAxisSettings, generateChartData, - LineSeriesStepType, + LineSeriesStepType, normalizeAxisLimit, parseThresholdData, TimeSeriesChartAxis, TimeSeriesChartDataItem, @@ -581,8 +581,8 @@ export class TbTimeSeriesChart { yAxisSettingsList.sort((a1, a2) => a1.order - a2.order); const axisLimitDatasources: Datasource[] = []; for (const yAxisSettings of yAxisSettingsList) { - yAxisSettings.min = this.normalizeAxisLimit(yAxisSettings.min); - yAxisSettings.max = this.normalizeAxisLimit(yAxisSettings.max); + yAxisSettings.min = normalizeAxisLimit(yAxisSettings.min); + yAxisSettings.max = normalizeAxisLimit(yAxisSettings.max); const axisSettings = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, yAxisSettings); const units = isNotEmptyTbUnits(axisSettings.units) ? axisSettings.units : this.ctx.units; @@ -1080,21 +1080,4 @@ export class TbTimeSeriesChart { this.timeSeriesChart.setOption(this.timeSeriesChartOptions); } } - - private normalizeAxisLimit(limit: string | number | ValueSourceConfig): string | number | ValueSourceConfig { - if (!limit) { - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } else if (typeof limit === 'number' || typeof limit === 'string') { - return { - type: ValueSourceType.constant, - value: Number(limit), - entityAlias: null - }; - } - return limit; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts index 6290572770..55ed3d5035 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/bar-chart-with-labels-widget-settings.component.ts @@ -31,6 +31,7 @@ import { barChartWithLabelsDefaultSettings } from '@home/components/widget/lib/chart/bar-chart-with-labels-widget.models'; import { getSourceTbUnitSymbol } from '@shared/models/unit.models'; +import { updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; @Component({ selector: 'tb-bar-chart-with-labels-widget-settings', @@ -123,6 +124,11 @@ export class BarChartWithLabelsWidgetSettingsComponent extends WidgetSettingsCom }); } + protected onSettingsChanged(updated: WidgetSettings) { + updateLatestDataKeys([updated.yAxis], this.datasource, this.dataKeyCallbacks); + super.onSettingsChanged(updated); + } + protected validatorTriggers(): string[] { return ['showBarLabel', 'showBarValue', 'showBarBorder', 'showLegend', 'showTooltip', 'tooltipShowDate']; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts index 703c621d62..053b6858bc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/range-chart-widget-settings.component.ts @@ -30,7 +30,7 @@ import { rangeChartDefaultSettings } from '@home/components/widget/lib/chart/ran import { DateFormatProcessor, DateFormatSettings } from '@shared/models/widget-settings.models'; import { lineSeriesStepTypes, - lineSeriesStepTypeTranslations + lineSeriesStepTypeTranslations, updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; import { chartLabelPositions, @@ -269,6 +269,11 @@ export class RangeChartWidgetSettingsComponent extends WidgetSettingsComponent { } } + protected onSettingsChanged(updated: WidgetSettings) { + updateLatestDataKeys([updated.yAxis], this.datasource, this.dataKeyCallbacks); + super.onSettingsChanged(updated); + } + private _pointLabelPreviewFn(): string { const units = getSourceTbUnitSymbol(this.widgetConfig.config.units); const decimals: number = this.widgetConfig.config.decimals; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts index 24037f2170..914eb0abb2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/axis-scale-row.component.ts @@ -97,7 +97,7 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali this.limitForm = this.fb.group({ type: [ValueSourceType.constant], value: [null], - entityAlias: [null] + entityAlias: [null, [Validators.required]] }); this.latestKeyFormControl = this.fb.control(null, [Validators.required]); this.entityKeyFormControl = this.fb.control(null, [Validators.required]); @@ -169,23 +169,24 @@ export class AxisScaleRowComponent implements ControlValueAccessor, OnInit, Vali } private updateValidators() { - const axisTypeControl = this.limitForm.get('type'); - if (axisTypeControl && this.entityKeyFormControl && this.latestKeyFormControl) { - const type = axisTypeControl.value; - if (type === ValueSourceType.latestKey) { - this.latestKeyFormControl.setValidators([Validators.required]); - this.entityKeyFormControl.clearValidators(); - } else if (type === ValueSourceType.entity) { - this.latestKeyFormControl.clearValidators(); - this.limitForm.get('entityAlias').setValidators([Validators.required]); - this.entityKeyFormControl.setValidators([Validators.required]); - } else { - this.latestKeyFormControl.clearValidators(); - this.entityKeyFormControl.clearValidators(); - } - this.latestKeyFormControl.updateValueAndValidity({ emitEvent: false }); - this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); - } + const type = this.limitForm.get('type')?.value; + const entityAliasCtr = this.limitForm.get('entityAlias'); + + const isLatestKey = type === ValueSourceType.latestKey; + const isEntity = type === ValueSourceType.entity; + + isLatestKey ? this.latestKeyFormControl.enable({ emitEvent: false }) + : this.latestKeyFormControl.disable({ emitEvent: false }); + + isEntity ? this.entityKeyFormControl.enable({ emitEvent: false }) + : this.entityKeyFormControl.disable({ emitEvent: false }); + + isEntity ? entityAliasCtr.enable({ emitEvent: false }) + : entityAliasCtr.disable({ emitEvent: false }); + + this.latestKeyFormControl.updateValueAndValidity({ emitEvent: false }); + this.entityKeyFormControl.updateValueAndValidity({ emitEvent: false }); + entityAliasCtr.updateValueAndValidity({ emitEvent: false }); } private updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts index db0f8534a9..2f23666574 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-axis-settings.component.ts @@ -25,7 +25,7 @@ import { Validators } from '@angular/forms'; import { - AxisPosition, defaultXAxisTicksFormat, + AxisPosition, defaultXAxisTicksFormat, normalizeAxisLimit, timeSeriesAxisPositionTranslations, TimeSeriesChartAxisSettings, TimeSeriesChartXAxisSettings, TimeSeriesChartYAxisSettings @@ -138,8 +138,8 @@ export class TimeSeriesChartAxisSettingsComponent implements OnInit, ControlValu this.axisSettingsFormGroup.addControl('ticksGenerator', this.fb.control(null, [])); this.axisSettingsFormGroup.addControl('interval', this.fb.control(null, [Validators.min(0)])); this.axisSettingsFormGroup.addControl('splitNumber', this.fb.control(null, [Validators.min(1)])); - this.axisSettingsFormGroup.addControl('min', this.fb.control(null, [])); - this.axisSettingsFormGroup.addControl('max', this.fb.control(null, [])); + this.axisSettingsFormGroup.addControl('min', this.fb.control(normalizeAxisLimit(null), [])); + this.axisSettingsFormGroup.addControl('max', this.fb.control(normalizeAxisLimit(null), [])); } else if (this.axisType === 'xAxis') { this.axisSettingsFormGroup.addControl('ticksFormat', this.fb.control(null, [])); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts index 694e856bfa..662214a1df 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axes-panel.component.ts @@ -36,13 +36,15 @@ import { Validator } from '@angular/forms'; import { + checkLatestDataKeys, defaultTimeSeriesChartYAxisSettings, getNextTimeSeriesYAxisId, TimeSeriesChartYAxes, TimeSeriesChartYAxisId, TimeSeriesChartYAxisSettings, timeSeriesChartYAxisValid, - timeSeriesChartYAxisValidator + timeSeriesChartYAxisValidator, + updateLatestDataKeys } from '@home/components/widget/lib/chart/time-series-chart.models'; import { mergeDeep } from '@core/utils'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; @@ -50,7 +52,7 @@ import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { IAliasController } from '@app/core/public-api'; import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; -import { DataKey, DataKeyType, Datasource, ValueSourceConfig, ValueSourceType } from '@app/shared/public-api'; +import { Datasource } from '@app/shared/public-api'; @Component({ selector: 'tb-time-series-chart-y-axes-panel', @@ -127,7 +129,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, for (const axis of axes) { yAxes[axis.id] = axis; } - this.updateLatestDataKeys(Object.values(yAxes)); + updateLatestDataKeys(Object.values(yAxes), this.datasource, this.dataKeyCallbacks); this.propagateChange(yAxes); } ); @@ -150,7 +152,7 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, } writeValue(value: TimeSeriesChartYAxes | undefined): void { - const yAxes: TimeSeriesChartYAxes = this.checkLatestDataKeys(value || {}); + const yAxes: TimeSeriesChartYAxes = checkLatestDataKeys(value || {}, this.datasource); if (!yAxes.default) { yAxes.default = mergeDeep({} as TimeSeriesChartYAxisSettings, defaultTimeSeriesChartYAxisSettings, {id: 'default', order: 0} as TimeSeriesChartYAxisSettings); @@ -197,8 +199,6 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, const axes: TimeSeriesChartYAxisSettings[] = this.yAxesFormGroup.get('axes').value; axis.id = getNextTimeSeriesYAxisId(axes); axis.order = axes.length; - axis.min = this.normalizeAxisLimit(axis.min); - axis.max = this.normalizeAxisLimit(axis.max); const axesArray = this.yAxesFormGroup.get('axes') as UntypedFormArray; const axisControl = this.fb.control(axis, [timeSeriesChartYAxisValidator]); axesArray.push(axisControl); @@ -212,100 +212,4 @@ export class TimeSeriesChartYAxesPanelComponent implements ControlValueAccessor, return this.fb.array(axesControls); } - private checkLatestDataKeys(yAxes: TimeSeriesChartYAxes): TimeSeriesChartYAxes { - const latestKeys = this.datasource?.latestDataKeys || []; - const result: TimeSeriesChartYAxes = {}; - - for (const [id, axis] of Object.entries(yAxes)) { - axis.min = this.normalizeAxisLimit(axis.min); - axis.max = this.normalizeAxisLimit(axis.max); - const minCfg = axis.min; - const maxCfg = axis.max; - - const minValid = !!minCfg && ( - minCfg.type !== ValueSourceType.latestKey || - latestKeys.some(k => this.isYAxisKey(k, minCfg)) - ); - - const maxValid = !!maxCfg && ( - maxCfg.type !== ValueSourceType.latestKey || - latestKeys.some(k => this.isYAxisKey(k, maxCfg)) - ); - - if (minValid && maxValid) { - result[id] = axis; - } - } - - return result; - } - - private updateLatestDataKeys(yAxes: TimeSeriesChartYAxisSettings[]) { - if (this.datasource) { - let latestKeys = this.datasource.latestDataKeys; - if (!latestKeys) { - latestKeys = []; - this.datasource.latestDataKeys = latestKeys; - } - const existingYAxisKeys = latestKeys.filter(k => k.settings?.__yAxisMinKey === true || k.settings?.__yAxisMaxKey === true); - const foundYAxisKeys: DataKey[] = []; - - for(const yAxis of yAxes) { - const min = yAxis.min as ValueSourceConfig; - const max = yAxis.max as ValueSourceConfig; - if (min.type === ValueSourceType.latestKey) { - const found = existingYAxisKeys.find(k => this.isYAxisKey(k, min)); - if (!found) { - const newKey = this.dataKeyCallbacks.generateDataKey(min.latestKey, min.latestKeyType, - null, true, null); - newKey.settings.__yAxisMinKey = true; - latestKeys.push(newKey); - } else if (foundYAxisKeys.indexOf(found) === -1) { - foundYAxisKeys.push(found); - } - } - if (max.type === ValueSourceType.latestKey) { - const found = existingYAxisKeys.find(k => this.isYAxisKey(k, max)); - if (!found) { - const newKey = this.dataKeyCallbacks.generateDataKey(max.latestKey, max.latestKeyType, - null, true, null); - newKey.settings.__yAxisMaxKey = true; - latestKeys.push(newKey); - } else if (foundYAxisKeys.indexOf(found) === -1) { - foundYAxisKeys.push(found); - } - } - } - const toRemove = existingYAxisKeys.filter(k => foundYAxisKeys.indexOf(k) === -1); - for (const key of toRemove) { - const index = latestKeys.indexOf(key); - if (index > -1) { - latestKeys.splice(index, 1); - } - } - } - } - - private isYAxisKey(d: DataKey, limit: ValueSourceConfig): boolean { - return (d.type === DataKeyType.function && d.label === limit.latestKey) || - (d.type !== DataKeyType.function && d.name === limit.latestKey && - d.type === limit.latestKeyType); - } - - private normalizeAxisLimit(limit: string | number | ValueSourceConfig): ValueSourceConfig { - if (!limit) { - return { - type: ValueSourceType.constant, - value: null, - entityAlias: null - }; - } else if (typeof limit === 'number' || typeof limit === 'string') { - return { - type: ValueSourceType.constant, - value: Number(limit), - entityAlias: null - }; - } - return limit; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts index 5298b7f45b..93e91124d5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/chart/time-series-chart-y-axis-row.component.ts @@ -29,7 +29,7 @@ import { } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { - AxisPosition, + AxisPosition, normalizeAxisLimit, timeSeriesAxisPositionTranslations, TimeSeriesChartYAxisSettings } from '@home/components/widget/lib/chart/time-series-chart.models'; @@ -136,8 +136,8 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O writeValue(value: TimeSeriesChartYAxisSettings): void { this.modelValue = value; - const min = this.normalizeLimit(value.min); - const max = this.normalizeLimit(value.max); + const min = normalizeAxisLimit(value.min); + const max = normalizeAxisLimit(value.max); this.axisFormGroup.patchValue({ label: value.label, @@ -252,27 +252,4 @@ export class TimeSeriesChartYAxisRowComponent implements ControlValueAccessor, O entityKeyType: [null, []] }); } - - private normalizeLimit(limit: any) { - const base = { - type: ValueSourceType.constant, - value: null, - latestKey: null, - latestKeyType: null, - entityAlias: null, - entityKey: null, - entityKeyType: null - }; - - if (limit == null) return base; - - if (typeof limit === 'number' || typeof limit === 'string') { - return { ...base, type: ValueSourceType.constant, value: Number(limit) }; - } - - return { - ...base, - ...limit, - }; - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts index ecbb03b580..0c0e504d74 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts @@ -34,6 +34,7 @@ import { IAliasController } from '@core/api/widget-api.models'; import { coerceBoolean } from '@shared/decorators/coercion'; import { DataKeysCallbacks } from '@home/components/widget/lib/settings/common/key/data-keys.component.models'; import { Datasource } from '@shared/models/widget.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'tb-color-settings-panel', @@ -107,6 +108,12 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit colorFunction: [this.colorSettings?.colorFunction, []] } ); + this.colorSettingsFormGroup.get('type').valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe(() => { + this.updateValidators(); + setTimeout(() => {this.popover?.updatePosition();}, 0); + }); this.updateValidators(); } diff --git a/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss b/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss new file mode 100644 index 0000000000..bae2e433aa --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/color-input.base.scss @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .color-input-container { + display: flex; + gap: 4px; + align-items: center; + } + .color-input { + max-width: 72px; + margin-bottom: 4px; + } +} + + diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss index f9db566be8..5e867f9e61 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker-panel.component.scss @@ -16,7 +16,7 @@ @import "../scss/constants"; .tb-color-picker-panel { - width: 342px; + width: 370px; display: flex; flex-direction: column; max-height: calc(100vh - 24px); diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html index 676dafbfef..fcb1cb067d 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.html @@ -37,10 +37,8 @@ HSLA
- - + +
diff --git a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss index ed161a5b9f..799bcdf00c 100644 --- a/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/color-picker.component.scss @@ -83,7 +83,7 @@ height: 56px; display: flex; align-items: center; - gap: 20px; + gap: 8px; .presentation-select { font-size: 14px; @@ -104,7 +104,7 @@ display: flex; flex-direction: row; flex-wrap: wrap; - justify-content: space-between; + justify-content: center; gap: 8px; @media #{$mat-xs} { flex-direction: column; diff --git a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss index 2f7a404c0d..73a6aff9f6 100644 --- a/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss +++ b/ui-ngx/src/app/shared/components/color-picker/hex-input.component.scss @@ -19,11 +19,11 @@ gap: 8px; } .hex-input { - max-width: 190px; + max-width: 220px; } .alpha-input { - min-width: 60px; - max-width: 60px; + min-width: 72px; + max-width: 72px; } ::ng-deep { diff --git a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html new file mode 100644 index 0000000000..cf8e25eb51 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.html @@ -0,0 +1,54 @@ + +
+
+ + + + @if (labelVisible) { + H + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + S + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + L + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + A + } +
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts new file mode 100644 index 0000000000..40ec056972 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/hsla-input.component.ts @@ -0,0 +1,72 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Color } from '@iplab/ngx-color-picker'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +type Channel = 'H' | 'S' | 'L'; + +@Component({ + selector: 'tb-hsla-input', + templateUrl: './hsla-input.component.html', + styleUrl: './color-input.base.scss', + standalone: false +}) +export class HslaInputComponent { + + @Input() + public color: Color; + + @Output() + public colorChange = new EventEmitter(false); + + @Input() + @coerceBoolean() + public labelVisible = false; + + @Input() + public suffixValue = '%'; + + public get value() { + return this.color.getHsla(); + } + + public get alphaValue(): number { + return this.color ? Math.round(this.color.getHsla().getAlpha() * 100) : 0; + } + + public onAlphaInputChange(inputValue: number): void { + if (!this.color) return; + const hsla = this.color.getHsla(); + const alpha = +inputValue / 100; + if (hsla.alpha !== alpha) { + const newColor = new Color().setHsla(hsla.getHue(), hsla.getSaturation(), hsla.getLightness(), alpha); + this.colorChange.emit(newColor); + } + } + + public onInputChange(newValue: number, channel: Channel): void { + if (!this.color) return; + const hsla = this.value; + const hue = channel === 'H' ? +newValue : hsla.getHue(); + const saturation = channel === 'S' ? +newValue : hsla.getSaturation(); + const lightness = channel === 'L' ? +newValue : hsla.getLightness(); + if (hue === hsla.getHue() && saturation === hsla.getSaturation() && lightness === hsla.getLightness()) return; + const newColor = new Color().setHsla(hue, saturation, lightness, hsla.getAlpha()); + this.colorChange.emit(newColor); + } +} diff --git a/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts new file mode 100644 index 0000000000..55604df28e --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/input-change.directive.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Directive, EventEmitter, HostBinding, HostListener, Input, numberAttribute, Output } from '@angular/core'; + +@Directive({ + selector: '[inputChange]', + standalone: false +}) +export class InputChangeDirective { + + @Input({transform: numberAttribute}) + @HostBinding('attr.min') + min = 0; + + @Input({transform: numberAttribute}) + @HostBinding('attr.max') + max = 255; + + @Output() + public inputChange = new EventEmitter(); + + @HostListener('input', ['$event']) + public inputChanges(event: any): void { + const element = event.target as HTMLInputElement || event.srcElement as HTMLInputElement; + const value = element.value; + + const numeric = parseFloat(value); + if (!isNaN(numeric) && numeric >= this.min && numeric <= this.max) { + this.inputChange.emit(numeric); + } + } +} diff --git a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html new file mode 100644 index 0000000000..3cb856f147 --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.html @@ -0,0 +1,52 @@ + +
+
+ + + + @if (labelVisible) { + R + } +
+
+ + + + @if (labelVisible) { + G + } +
+
+ + + + @if (labelVisible) { + B + } +
+
+ + + {{suffixValue}} + + @if (labelVisible) { + A + } +
+
diff --git a/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts new file mode 100644 index 0000000000..04f2dbc44b --- /dev/null +++ b/ui-ngx/src/app/shared/components/color-picker/rgba-input.component.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2026 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Color } from '@iplab/ngx-color-picker'; +import { coerceBoolean } from '@shared/decorators/coercion'; + +type Channel = 'R' | 'G' | 'B'; + +@Component({ + selector: 'tb-rgba-input', + templateUrl: './rgba-input.component.html', + styleUrl: './color-input.base.scss', + standalone: false +}) +export class RgbaInputComponent { + + @Input() + public color: Color; + + @Output() + public colorChange = new EventEmitter(false); + + @Input() + @coerceBoolean() + public labelVisible = false; + + @Input() + public suffixValue = '%'; + + public get value() { + return this.color.getRgba(); + } + + public get alphaValue(): string { + return this.color ? Math.round(this.color.getRgba().getAlpha() * 100).toString() : ''; + } + + public onAlphaInputChange(inputValue: number): void { + if (!this.color) return; + const color = this.color.getRgba(); + const alpha = +inputValue / 100; + if (color.getAlpha() !== alpha) { + const newColor = new Color().setRgba(color.getRed(), color.getGreen(), color.getBlue(), alpha).toRgbaString(); + this.colorChange.emit(new Color(newColor)); + } + } + + onInputChange(newValue: number, channel: Channel) { + if (!this.color) return; + const rgba = this.value; + const red = channel === 'R' ? newValue : rgba.getRed(); + const green = channel === 'G' ? newValue : rgba.getGreen(); + const blue = channel === 'B' ? newValue : rgba.getBlue(); + if (red === rgba.getRed() && green === rgba.getGreen() && blue === rgba.getBlue()) return; + this.colorChange.emit(new Color().setRgba(red, green, blue, rgba.alpha)); + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts index 078234dbda..50d847609f 100644 --- a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.module.ts @@ -15,36 +15,47 @@ /// import { Overlay, OverlayContainer, OverlayModule } from '@angular/cdk/overlay'; -import { NgModule } from '@angular/core'; +import { inject, Injector, NgModule } from '@angular/core'; import { DEFAULT_DIALOG_CONFIG, Dialog, DialogConfig, DialogModule } from '@angular/cdk/dialog'; import { MatDialogModule } from '@angular/material/dialog'; import { DynamicDialog, DynamicMatDialog } from './dynamic-dialog'; import { DynamicOverlay } from './dynamic-overlay'; -import { DynamicOverlayContainer } from './dynamic-overlay-container'; +import { DynamicOverlayContainer, PARENT_OVERLAY_CONTAINER } from './dynamic-overlay-container'; export const DYNAMIC_MAT_DIALOG_PROVIDERS = [ - DynamicOverlayContainer, - { provide: OverlayContainer, useExisting: DynamicOverlayContainer }, - DynamicOverlay, - { provide: Overlay, useExisting: DynamicOverlay }, - DynamicDialog, - { provide: Dialog, useExisting: DynamicDialog }, - DynamicMatDialog, { - provide: DEFAULT_DIALOG_CONFIG, - useValue: { - ...new DialogConfig() + provide: DynamicMatDialog, + useFactory: () => { + const parentInjector = inject(Injector); + const parentOverlayContainer = parentInjector.get(OverlayContainer); + + const customInjector = Injector.create({ + providers: [ + { provide: PARENT_OVERLAY_CONTAINER, useValue: parentOverlayContainer }, + DynamicOverlayContainer, + { provide: OverlayContainer, useExisting: DynamicOverlayContainer }, + DynamicOverlay, + { provide: Overlay, useExisting: DynamicOverlay }, + DynamicDialog, + { provide: Dialog, useExisting: DynamicDialog }, + DynamicMatDialog, + { provide: DEFAULT_DIALOG_CONFIG, useValue: new DialogConfig() } + ], + parent: parentInjector + }); + + return customInjector.get(DynamicMatDialog); } } ]; -@NgModule( { +@NgModule({ imports: [ OverlayModule, DialogModule, MatDialogModule ], providers: DYNAMIC_MAT_DIALOG_PROVIDERS -} ) +}) export class DynamicMatDialogModule { } diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts index f717ccb0a7..df707ad49d 100644 --- a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-dialog.ts @@ -34,20 +34,13 @@ export class DynamicMatDialog extends MatDialog { config.containerElement.style.transform = 'translateZ(0)'; this._customOverlay.setContainerElement(config.containerElement); } - const ref = super.open(component, config); - if (config?.containerElement) { - ref.afterClosed().subscribe( - { - next: () => { - this._customOverlay.setContainerElement(null); - }, - error: () => { - this._customOverlay.setContainerElement(null); - } - } - ); + try { + return super.open(component, config); + } finally { + if (config?.containerElement) { + this._customOverlay.setContainerElement(null); + } } - return ref; } } diff --git a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts index 83feda56fc..99a84f5a6a 100644 --- a/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts +++ b/ui-ngx/src/app/shared/components/dialog/dynamic/dynamic-overlay-container.ts @@ -15,13 +15,21 @@ /// import { OverlayContainer } from "@angular/cdk/overlay"; -import { Injectable } from "@angular/core"; +import { inject, Injectable, InjectionToken } from "@angular/core"; + +export const PARENT_OVERLAY_CONTAINER = new InjectionToken('PARENT_OVERLAY_CONTAINER'); @Injectable() export class DynamicOverlayContainer extends OverlayContainer { - public setContainerElement( containerElement:HTMLElement ):void { + private _globalContainer = inject(PARENT_OVERLAY_CONTAINER); + private _customElement: HTMLElement | null = null; + + public override getContainerElement(): HTMLElement { + return this._customElement || this._globalContainer.getContainerElement(); + } - this._containerElement = containerElement; + setContainerElement(element: HTMLElement | null): void { + this._customElement = element; } -} +} \ No newline at end of file diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index dfe86539bc..cefc1c8824 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -239,6 +239,9 @@ import { DateExpirationPipe } from '@shared/pipe/date-expiration.pipe'; import { EntityLimitExceededDialogComponent } from '@shared/components/dialog/entity-limit-exceeded-dialog.component'; import { DynamicMatDialogModule } from '@shared/components/dialog/dynamic/dynamic-dialog.module'; import { MAT_BUTTON_TOGGLE_DEFAULT_OPTIONS } from '@angular/material/button-toggle'; +import { RgbaInputComponent } from '@shared/components/color-picker/rgba-input.component'; +import { HslaInputComponent } from '@shared/components/color-picker/hsla-input.component'; +import { InputChangeDirective } from '@shared/components/color-picker/input-change.directive'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -466,7 +469,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) MqttVersionSelectComponent, PasswordRequirementsTooltipComponent, TimeUnitInputComponent, - StringPatternAutocompleteComponent + StringPatternAutocompleteComponent, + RgbaInputComponent, + HslaInputComponent, + InputChangeDirective ], imports: [ CommonModule, diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index d36c9c8684..c43dc47bd2 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -160,24 +160,24 @@ "@angular-devkit/core" "^20.0.0" "@angular/build" "^20.0.0" -"@angular-devkit/architect@0.2003.15", "@angular-devkit/architect@>= 0.2000.0 < 0.2100.0", "@angular-devkit/architect@>=0.2000.0 < 0.2100.0": - version "0.2003.15" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.2003.15.tgz#09d4e520205dcfe44ae0dd5e4f865596c1fdec98" - integrity sha512-HmGnUTLVwpvOFilc3gTP6CL9o+UbkVyu9S4WENkQbInbW3zp54lkzY71uWJIP7QvuXPa+bS4WHEmoGNQtNvv1A== +"@angular-devkit/architect@0.2003.16", "@angular-devkit/architect@>= 0.2000.0 < 0.2100.0", "@angular-devkit/architect@>=0.2000.0 < 0.2100.0": + version "0.2003.16" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.2003.16.tgz#bc45b8b5eb9ae9b26b2d03dcd70b3fade9f22f79" + integrity sha512-W7FPVhZzIeHVP/duuKepfZU66LpQ0k9YMHFhrGpzaUuHPOwKmza6+pjVvvti3g6jzT8b1uVlb+XlYgNPZ5jrPQ== dependencies: - "@angular-devkit/core" "20.3.15" + "@angular-devkit/core" "20.3.16" rxjs "7.8.2" -"@angular-devkit/build-angular@20.3.15": - version "20.3.15" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-20.3.15.tgz#144b7ca4cec60aebaa09453e731fe58207955c11" - integrity sha512-md5ikmdH0rM4jkVFwNQhN++onUUZPUDUfKijnb1XZArQisT9hPiNqTPbO+OZTBK/hQYYV/GfTOhDH30whG32xQ== +"@angular-devkit/build-angular@20.3.16": + version "20.3.16" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-20.3.16.tgz#7c71ce4d3cb0173cdff6aaf221048b390caf39e4" + integrity sha512-SsJmxRnBTrivG3UiRy0gaU1mGupRCAiEKrKlW30oe6Dm0UoIVXDi4srpSEECcng5Obr3jFPzJE6P16/gfp3ZBw== dependencies: "@ampproject/remapping" "2.3.0" - "@angular-devkit/architect" "0.2003.15" - "@angular-devkit/build-webpack" "0.2003.15" - "@angular-devkit/core" "20.3.15" - "@angular/build" "20.3.15" + "@angular-devkit/architect" "0.2003.16" + "@angular-devkit/build-webpack" "0.2003.16" + "@angular-devkit/core" "20.3.16" + "@angular/build" "20.3.16" "@babel/core" "7.28.3" "@babel/generator" "7.28.3" "@babel/helper-annotate-as-pure" "7.27.3" @@ -188,7 +188,7 @@ "@babel/preset-env" "7.28.3" "@babel/runtime" "7.28.3" "@discoveryjs/json-ext" "0.6.3" - "@ngtools/webpack" "20.3.15" + "@ngtools/webpack" "20.3.16" ansi-colors "4.1.3" autoprefixer "10.4.21" babel-loader "10.0.0" @@ -222,7 +222,7 @@ terser "5.43.1" tree-kill "1.2.2" tslib "2.8.1" - webpack "5.104.1" + webpack "5.105.0" webpack-dev-middleware "7.4.2" webpack-dev-server "5.2.2" webpack-merge "6.0.1" @@ -230,18 +230,18 @@ optionalDependencies: esbuild "0.25.9" -"@angular-devkit/build-webpack@0.2003.15": - version "0.2003.15" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.2003.15.tgz#780e56a2ab48cea75de73386baeda3600c36ca93" - integrity sha512-u1gjLH0T+s4PiwbSUhzYZUKCsIF9CvTGJoN+Ki+CQqZAlTGLOp2y55VXmdrRX4e3tsKkS+chpQ8sN2b5vbzv9w== +"@angular-devkit/build-webpack@0.2003.16": + version "0.2003.16" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.2003.16.tgz#a4e89f97595743597d6d23dae6d3b014931765bb" + integrity sha512-88c6jTNAzqVinwYswsHOJAinIUSq08PQd1PfRwoH7RXiZt8XzkSmeLmXKchtamSflDXdcnjNd+AZY29b279zDA== dependencies: - "@angular-devkit/architect" "0.2003.15" + "@angular-devkit/architect" "0.2003.16" rxjs "7.8.2" -"@angular-devkit/core@20.3.15", "@angular-devkit/core@>= 20.0.0 < 21.0.0", "@angular-devkit/core@^20.0.0": - version "20.3.15" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.15.tgz#d61e2b915b2347da9f33c37f39c6c163cc5d29ea" - integrity sha512-s7sE4S5Hy62dLrtHwizbZaMcupAE8fPhm6rF+jBkhHZ75zXGhGzXP8WKFztYCAuGnis4pPnGSEKP/xVTc2lw6Q== +"@angular-devkit/core@20.3.16", "@angular-devkit/core@>= 20.0.0 < 21.0.0", "@angular-devkit/core@^20.0.0": + version "20.3.16" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-20.3.16.tgz#ad1c2b2e099cf1591cd3146944e95a3af614a82b" + integrity sha512-6L9Lpe3lbkyz32gzqxZGVC8MhXxXht+yV+4LUsb4+6T/mG/V9lW6UTW0dhwVOS3vpWMEwpy75XHT298t7HcKEg== dependencies: ajv "8.17.1" ajv-formats "3.0.1" @@ -250,12 +250,12 @@ rxjs "7.8.2" source-map "0.7.6" -"@angular-devkit/schematics@20.3.15", "@angular-devkit/schematics@>= 20.0.0 < 21.0.0": - version "20.3.15" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-20.3.15.tgz#7dd2f966a530fadb0c425ed01fb3cbb303df868a" - integrity sha512-xMN1fyuhhP8Y5sNlmQvl4nMiOouHTKPkLR0zlhu5z6fHuwxxlverh31Gpq3eFzPHqmOzzb2TkgYCptCFXsXcrg== +"@angular-devkit/schematics@20.3.16", "@angular-devkit/schematics@>= 20.0.0 < 21.0.0": + version "20.3.16" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-20.3.16.tgz#7a79c7b1cf91ff8ebb63ab318460c1f1ac073d51" + integrity sha512-3K8QwTpKjnLo3hIvNzB9sTjrlkeRyMK0TxdwgTbwJseewGhXLl98oBoTCWM2ygtpskiWNpYqXJNIhoslNN65WQ== dependencies: - "@angular-devkit/core" "20.3.15" + "@angular-devkit/core" "20.3.16" jsonc-parser "3.3.1" magic-string "0.30.17" ora "8.2.0" @@ -328,13 +328,13 @@ dependencies: tslib "^2.3.0" -"@angular/build@20.3.15", "@angular/build@^20.0.0": - version "20.3.15" - resolved "https://registry.yarnpkg.com/@angular/build/-/build-20.3.15.tgz#fa1c4661a1ca7991cd8c19957442076a7266bf18" - integrity sha512-DMp/wb3I9/izveXRuOkCTYEQlEzvNlJVnqA215tijOSiJGjYoUsQLazTCxtEx/trftOhVpnMP/2OvvMQVAJJoQ== +"@angular/build@20.3.16", "@angular/build@^20.0.0": + version "20.3.16" + resolved "https://registry.yarnpkg.com/@angular/build/-/build-20.3.16.tgz#ea33cc51483e474236190f7ef3efefca39fd6c4d" + integrity sha512-p1W3wwMG1Bs4tkPW7ceXO4woO1KCP28sjfpBJg32dIMW3dYSC+iWNmUkYS/wb4YEkqCV0wd6Apnd98mZjL6rNg== dependencies: "@ampproject/remapping" "2.3.0" - "@angular-devkit/architect" "0.2003.15" + "@angular-devkit/architect" "0.2003.16" "@babel/core" "7.28.3" "@babel/helper-annotate-as-pure" "7.27.3" "@babel/helper-split-export-declaration" "7.24.7" @@ -370,18 +370,18 @@ parse5 "^8.0.0" tslib "^2.3.0" -"@angular/cli@20.3.15": - version "20.3.15" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-20.3.15.tgz#13eec3cf73beb400348a74decacfe2e8bf908580" - integrity sha512-OgPMhXtNLXds0wIw6YU5/X3dU8TlAZbmPy6LYHs9ifF8K4pXpbm27vWGSZhUevSf66dMvfz8wB/aE2e0s2e5Ng== +"@angular/cli@20.3.16": + version "20.3.16" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-20.3.16.tgz#cdb6c0c9300a15f9bbce5e9ec1cf18f48fb0c765" + integrity sha512-kjGp0ywIWebWrH6U5eCRkS4Tx1D/yMe2iT7DXMfEcLc8iMSrBozEriMJppbot9ou8O2LeEH5d1Nw0efNNo78Kw== dependencies: - "@angular-devkit/architect" "0.2003.15" - "@angular-devkit/core" "20.3.15" - "@angular-devkit/schematics" "20.3.15" + "@angular-devkit/architect" "0.2003.16" + "@angular-devkit/core" "20.3.16" + "@angular-devkit/schematics" "20.3.16" "@inquirer/prompts" "7.8.2" "@listr2/prompt-adapter-inquirer" "3.0.1" - "@modelcontextprotocol/sdk" "1.25.2" - "@schematics/angular" "20.3.15" + "@modelcontextprotocol/sdk" "1.26.0" + "@schematics/angular" "20.3.16" "@yarnpkg/lockfile" "1.1.0" algoliasearch "5.35.0" ini "5.0.0" @@ -489,19 +489,19 @@ dependencies: tslib "^2.0.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7" - integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== dependencies: "@babel/helper-validator-identifier" "^7.28.5" js-tokens "^4.0.0" picocolors "^1.1.1" "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.0", "@babel/compat-data@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.6.tgz#103f466803fa0f059e82ccac271475470570d74c" - integrity sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg== + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== "@babel/core@7.28.3", "@babel/core@^7.23.9": version "7.28.3" @@ -535,13 +535,13 @@ "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" -"@babel/generator@^7.28.3", "@babel/generator@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.6.tgz#48dcc65d98fcc8626a48f72b62e263d25fc3c3f1" - integrity sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw== +"@babel/generator@^7.28.3", "@babel/generator@^7.29.0": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== dependencies: - "@babel/parser" "^7.28.6" - "@babel/types" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" "@jridgewell/gen-mapping" "^0.3.12" "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" @@ -704,12 +704,12 @@ "@babel/template" "^7.28.6" "@babel/types" "^7.28.6" -"@babel/parser@^7.23.9", "@babel/parser@^7.28.3", "@babel/parser@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd" - integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ== +"@babel/parser@^7.23.9", "@babel/parser@^7.28.3", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== dependencies: - "@babel/types" "^7.28.6" + "@babel/types" "^7.29.0" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": version "7.28.5" @@ -1304,22 +1304,22 @@ "@babel/types" "^7.28.6" "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.6.tgz#871ddc79a80599a5030c53b1cc48cbe3a5583c2e" - integrity sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg== + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== dependencies: - "@babel/code-frame" "^7.28.6" - "@babel/generator" "^7.28.6" + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" "@babel/helper-globals" "^7.28.0" - "@babel/parser" "^7.28.6" + "@babel/parser" "^7.29.0" "@babel/template" "^7.28.6" - "@babel/types" "^7.28.6" + "@babel/types" "^7.29.0" debug "^4.3.1" -"@babel/types@^7.24.7", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.4.4": - version "7.28.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df" - integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg== +"@babel/types@^7.24.7", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.4.4": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" @@ -1569,10 +1569,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.39.2": - version "9.39.2" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599" - integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA== +"@eslint/js@9.39.3": + version "9.39.3" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.3.tgz#c6168736c7e0c43ead49654ed06a4bcb3833363d" + integrity sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw== "@eslint/object-schema@^2.1.7": version "2.1.7" @@ -1611,7 +1611,7 @@ lodash "4.17.21" polyclip-ts "^0.16.5" -"@hono/node-server@^1.19.7": +"@hono/node-server@^1.19.9": version "1.19.9" resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.9.tgz#8f37119b1acf283fd3f6035f3d1356fdb97a09ac" integrity sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw== @@ -1822,18 +1822,6 @@ dependencies: tslib "^2.3.0" -"@isaacs/balanced-match@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz#3081dadbc3460661b751e7591d7faea5df39dd29" - integrity sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== - -"@isaacs/brace-expansion@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz#4b3dabab7d8e75a429414a96bd67bf4c1d13e0f3" - integrity sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== - dependencies: - "@isaacs/balanced-match" "^4.0.1" - "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1885,9 +1873,9 @@ "@jridgewell/trace-mapping" "^0.3.25" "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== "@jridgewell/trace-mapping@0.3.9": version "0.3.9" @@ -2220,12 +2208,12 @@ dependencies: make-plural "^7.0.0" -"@modelcontextprotocol/sdk@1.25.2": - version "1.25.2" - resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz#2284560b4e044b4ce5f328ee180931110cb8c5cf" - integrity sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww== +"@modelcontextprotocol/sdk@1.26.0": + version "1.26.0" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz#5b35d73062125f126cc70b0be83cbab53bcdde74" + integrity sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg== dependencies: - "@hono/node-server" "^1.19.7" + "@hono/node-server" "^1.19.9" ajv "^8.17.1" ajv-formats "^3.0.1" content-type "^1.0.5" @@ -2233,14 +2221,15 @@ cross-spawn "^7.0.5" eventsource "^3.0.2" eventsource-parser "^3.0.0" - express "^5.0.1" - express-rate-limit "^7.5.0" - jose "^6.1.1" + express "^5.2.1" + express-rate-limit "^8.2.1" + hono "^4.11.4" + jose "^6.1.3" json-schema-typed "^8.0.2" pkce-challenge "^5.0.0" raw-body "^3.0.0" zod "^3.25 || ^4.0" - zod-to-json-schema "^3.25.0" + zod-to-json-schema "^3.25.1" "@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3": version "3.0.3" @@ -2408,10 +2397,10 @@ dependencies: tslib "^2.0.0" -"@ngtools/webpack@20.3.15": - version "20.3.15" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-20.3.15.tgz#77161b676ff8f98bf8a3549acb3abb710f874f0b" - integrity sha512-7bH91SdVN9xawMg4G+OoQG+nFz9/huUXx/MptR5iOsuRUpNo3PQwMNr07VTRV8o4YAtTTRIf2Yam4fNkb3rRAg== +"@ngtools/webpack@20.3.16": + version "20.3.16" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-20.3.16.tgz#b5163cc57326381409fd535c1f902bd2d2139cc4" + integrity sha512-yXxo0CKkCa9SuP05OskOeFt9l1wirCzh7MhwW1S18Rpi6cyPbV1bc7GEz05+dfi5Ee8Dlbx3sbmdty0xtHvFQw== "@ngx-translate/core@^17.0.0": version "17.0.0" @@ -2627,161 +2616,76 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz#7050c2acdc1214a730058e21f613ab0e1fe1ced9" integrity sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw== -"@rollup/rollup-android-arm-eabi@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz#067cfcd81f1c1bfd92aefe3ad5ef1523549d5052" - integrity sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw== - "@rollup/rollup-android-arm64@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz#3f5b2afbfcbe9021649701cf6ff0d54b1fb7e4a5" integrity sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw== -"@rollup/rollup-android-arm64@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz#85e39a44034d7d4e4fee2a1616f0bddb85a80517" - integrity sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q== - "@rollup/rollup-darwin-arm64@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz#70a1679fb4393ba7bafb730ee56a5278cbcdafb0" integrity sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg== -"@rollup/rollup-darwin-arm64@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz#17d92fe98f2cc277b91101eb1528b7c0b6c00c54" - integrity sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w== - "@rollup/rollup-darwin-x64@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz#ae75aec88fa72069de9bca3a3ec22bf4e6a962bf" integrity sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A== -"@rollup/rollup-darwin-x64@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz#89ae6c66b1451609bd1f297da9384463f628437d" - integrity sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g== - "@rollup/rollup-freebsd-arm64@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz#8a2bda997faa1d7e335ce1961ce71d1a76ac6288" integrity sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ== -"@rollup/rollup-freebsd-arm64@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz#cdbdb9947b26e76c188a31238c10639347413628" - integrity sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ== - "@rollup/rollup-freebsd-x64@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz#fc287bcc39b9a9c0df97336d68fd5f4458f87977" integrity sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A== -"@rollup/rollup-freebsd-x64@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz#9b1458d07b6e040be16ee36d308a2c9520f7f7cc" - integrity sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg== - "@rollup/rollup-linux-arm-gnueabihf@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz#5b5a2a55dffaa64d7c7a231e80e491219e33d4f3" integrity sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA== -"@rollup/rollup-linux-arm-gnueabihf@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz#1d50ded7c965d5f125f5832c971ad5b287befef7" - integrity sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A== - "@rollup/rollup-linux-arm-musleabihf@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz#979eab95003c21837ea0fdd8a721aa3e69fa4aa3" integrity sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA== -"@rollup/rollup-linux-arm-musleabihf@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz#53597e319b7e65990d3bc2a5048097384814c179" - integrity sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw== - "@rollup/rollup-linux-arm64-gnu@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz#53b89f1289cbeca5ed9b6ca1602a6fe1a29dd4e2" integrity sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ== -"@rollup/rollup-linux-arm64-gnu@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz#597002909dec198ca4bdccb25f043d32db3d6283" - integrity sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ== - "@rollup/rollup-linux-arm64-musl@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz#3bbcf5e13c09d0c4c55bd9c75ec6a7aeee56fe28" integrity sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw== -"@rollup/rollup-linux-arm64-musl@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz#286f0e0f799545ce288bdc5a7c777261fcba3d54" - integrity sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA== - "@rollup/rollup-linux-loong64-gnu@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz#1cc71838465a8297f92ccc5cc9c29756b71f6e73" integrity sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg== -"@rollup/rollup-linux-loong64-gnu@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz#1fab07fa1a4f8d3697735b996517f1bae0ba101b" - integrity sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg== - -"@rollup/rollup-linux-loong64-musl@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz#efc2cb143d6c067f95205482afb177f78ed9ea3d" - integrity sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA== - "@rollup/rollup-linux-ppc64-gnu@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz#fe3fdf2ef57dc2d58fedd4f1e0678660772c843a" integrity sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw== -"@rollup/rollup-linux-ppc64-gnu@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz#e8de8bd3463f96b92b7dfb7f151fd80ffe8a937c" - integrity sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw== - -"@rollup/rollup-linux-ppc64-musl@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz#8c508fe28a239da83b3a9da75bcf093186e064b4" - integrity sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg== - "@rollup/rollup-linux-riscv64-gnu@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz#eebc99e75832891d58532501879ca749b1592f93" integrity sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg== -"@rollup/rollup-linux-riscv64-gnu@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz#ff6d51976e0830732880770a9e18553136b8d92b" - integrity sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew== - "@rollup/rollup-linux-riscv64-musl@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz#9a2df234d61763a44601eba17c36844a18f20539" integrity sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg== -"@rollup/rollup-linux-riscv64-musl@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz#325fb35eefc7e81d75478318f0deee1e4a111493" - integrity sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ== - "@rollup/rollup-linux-s390x-gnu@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz#f0e45ea7e41ee473c85458b1ec8fab9572cc1834" integrity sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg== -"@rollup/rollup-linux-s390x-gnu@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz#37410fabb5d3ba4ad34abcfbe9ba9b6288413f30" - integrity sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ== - "@rollup/rollup-linux-x64-gnu@4.46.2": version "4.46.2" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz#1e936446f90b2574ea4a83b4842a762cc0a0aed3" @@ -2792,76 +2696,36 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz#ed63dec576799fa5571eee5b2040f65faa82b49b" integrity sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA== -"@rollup/rollup-linux-x64-gnu@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz#8ef907a53b2042068fc03fcc6a641e2b02276eca" - integrity sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw== - "@rollup/rollup-linux-x64-musl@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz#755c56ac79b17fbdf0359bce7e2293a11de30ad0" integrity sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw== -"@rollup/rollup-linux-x64-musl@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz#61b9ba09ea219e0174b3f35a6ad2afc94bdd5662" - integrity sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA== - -"@rollup/rollup-openbsd-x64@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz#fc4e54133134c1787d0b016ffdd5aeb22a5effd3" - integrity sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA== - "@rollup/rollup-openharmony-arm64@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz#84b4170fe28c2b41e406add6ccf8513bf91195ea" integrity sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA== -"@rollup/rollup-openharmony-arm64@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz#959ae225b1eeea0cc5b7c9f88e4834330fb6cd09" - integrity sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ== - "@rollup/rollup-win32-arm64-msvc@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz#4fb0cd004183da819bec804eba70f1ef6936ccbf" integrity sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA== -"@rollup/rollup-win32-arm64-msvc@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz#842acd38869fa1cbdbc240c76c67a86f93444c27" - integrity sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing== - "@rollup/rollup-win32-ia32-msvc@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz#1788ba80313477a31e6214390906201604ee38eb" integrity sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g== -"@rollup/rollup-win32-ia32-msvc@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz#7ab654def4042df44cb29f8ed9d5044e850c66d5" - integrity sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg== - "@rollup/rollup-win32-x64-gnu@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz#867222f288a9557487900c7836998123ebbadc9d" integrity sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ== -"@rollup/rollup-win32-x64-gnu@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz#7426cdec1b01d2382ffd5cda83cbdd1c8efb3ca6" - integrity sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ== - "@rollup/rollup-win32-x64-msvc@4.52.3": version "4.52.3" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz#3f55b6e8fe809a7d29959d6bc686cce1804581f0" integrity sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA== -"@rollup/rollup-win32-x64-msvc@4.56.0": - version "4.56.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz#9eec0212732a432c71bde0350bc40b673d15b2db" - integrity sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g== - "@rtsao/scc@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" @@ -2872,13 +2736,13 @@ resolved "https://registry.yarnpkg.com/@sanity/diff-match-patch/-/diff-match-patch-3.2.0.tgz#7ce587273f7372a146308cb1075ba26177d42cdb" integrity sha512-4hPADs0qUThFZkBK/crnfKKHg71qkRowfktBljH2UIxGHHTxIzt8g8fBiXItyCjxkuNy+zpYOdRMifQNv8+Yww== -"@schematics/angular@20.3.15": - version "20.3.15" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-20.3.15.tgz#b66d9c3b5550bc660fb80ec8fdeead43df9a776d" - integrity sha512-WkhW1HO8pA8JT8e27tvjQHQg8eO5KaOz+WsGkN00RyL5DwHgPSzu4a3eYug+b3rW7OGFub7jadXBuGSrzqgonA== +"@schematics/angular@20.3.16": + version "20.3.16" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-20.3.16.tgz#f29cb0cf93aec3a13f00ad514d6f9cbe563bdc07" + integrity sha512-KeOcsM5piwv/6tUKBmLD1zXTwtJlZBnR2WM/4T9ImaQbmFGe1MMHUABT5SQ3Bifv1YKCw58ImxiaQUY9sdNqEQ== dependencies: - "@angular-devkit/core" "20.3.15" - "@angular-devkit/schematics" "20.3.15" + "@angular-devkit/core" "20.3.16" + "@angular-devkit/schematics" "20.3.16" jsonc-parser "3.3.1" "@sigstore/bundle@^4.0.0": @@ -3920,9 +3784,9 @@ ansi-colors@4.1.3: integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-escapes@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" - integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + version "7.3.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz#5395bb74b2150a4a1d6e3c2565f4aeca78d28627" + integrity sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg== dependencies: environment "^1.0.0" @@ -3937,9 +3801,9 @@ ansi-regex@^5.0.1: integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" - integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + version "6.2.2" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" + integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== ansi-styles@^3.2.1: version "3.2.1" @@ -3956,9 +3820,9 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: color-convert "^2.0.1" ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== ansidec@^0.3.4: version "0.3.4" @@ -4181,15 +4045,20 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + base64-arraybuffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc" integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ== baseline-browser-mapping@^2.9.0: - version "2.9.17" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz#9d6019766cd7eba738cb5f32c84b9f937cc87780" - integrity sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ== + version "2.10.0" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9" + integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA== batch@0.6.1: version "0.6.1" @@ -4272,19 +4141,19 @@ boolbase@^1.0.0: integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== +brace-expansion@^5.0.2: + version "5.0.3" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.3.tgz#6a9c6c268f85b53959ec527aeafe0f7300258eef" + integrity sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA== dependencies: - balanced-match "^1.0.0" + balanced-match "^4.0.2" braces@^3.0.3, braces@~3.0.2: version "3.0.3" @@ -4385,9 +4254,9 @@ camelcase@^5.0.0: integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001759, caniuse-lite@^1.0.30001760: - version "1.0.30001766" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz#b6f6b55cb25a2d888d9393104d14751c6a7d6f7a" - integrity sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA== + version "1.0.30001774" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz#0e576b6f374063abcd499d202b9ba1301be29b70" + integrity sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA== canvas-gauges@^2.1.7: version "2.1.7" @@ -4456,9 +4325,9 @@ chokidar@^3.6.0: fsevents "~2.3.2" chokidar@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" - integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== dependencies: readdirp "^4.0.1" @@ -5428,14 +5297,14 @@ ee-first@1.1.1: integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== electron-to-chromium@^1.5.263: - version "1.5.278" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.278.tgz#807a5e321f012a41bfd64e653f35993c9af95493" - integrity sha512-dQ0tM1svDRQOwxnXxm+twlGTjr9Upvt8UFWAgmLsxEzFQxhbti4VwxmMjsDxVC51Zo84swW7FVCXEV+VAkhuPw== + version "1.5.302" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz#032a5802b31f7119269959c69fe2015d8dad5edb" + integrity sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg== emoji-regex@^10.3.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" - integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + version "10.6.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.6.0.tgz#bf3d6e8f7f8fd22a65d9703475bc0147357a6b0d" + integrity sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A== emoji-regex@^8.0.0: version "8.0.0" @@ -5469,13 +5338,13 @@ encoding@^0.1.13: dependencies: iconv-lite "^0.6.2" -enhanced-resolve@^5.17.4: - version "5.18.4" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz#c22d33055f3952035ce6a144ce092447c525f828" - integrity sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q== +enhanced-resolve@^5.19.0: + version "5.19.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz#6687446a15e969eaa63c2fa2694510e17ae6d97c" + integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg== dependencies: graceful-fs "^4.2.4" - tapable "^2.2.0" + tapable "^2.3.0" entities@^4.2.0: version "4.5.0" @@ -5801,10 +5670,10 @@ eslint-visitor-keys@^5.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz#b9aa1a74aa48c44b3ae46c1597ce7171246a94a9" integrity sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q== -eslint@~9.39.2: - version "9.39.2" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c" - integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw== +eslint@9.39.3: + version "9.39.3" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.3.tgz#08d63df1533d7743c0907b32a79a7e134e63ee2f" + integrity sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg== dependencies: "@eslint-community/eslint-utils" "^4.8.0" "@eslint-community/regexpp" "^4.12.1" @@ -5812,7 +5681,7 @@ eslint@~9.39.2: "@eslint/config-helpers" "^0.4.2" "@eslint/core" "^0.17.0" "@eslint/eslintrc" "^3.3.1" - "@eslint/js" "9.39.2" + "@eslint/js" "9.39.3" "@eslint/plugin-kit" "^0.4.1" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" @@ -5904,9 +5773,9 @@ eventemitter3@^4.0.0: integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== eventemitter3@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" - integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + version "5.0.4" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.4.tgz#a86d66170433712dde814707ac52b5271ceb1feb" + integrity sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw== events@^3.2.0: version "3.3.0" @@ -5930,10 +5799,12 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== -express-rate-limit@^7.5.0: - version "7.5.1" - resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.5.1.tgz#8c3a42f69209a3a1c969890070ece9e20a879dec" - integrity sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw== +express-rate-limit@^8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-8.2.1.tgz#ec75fdfe280ecddd762b8da8784c61bae47d7f7f" + integrity sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g== + dependencies: + ip-address "10.0.1" express@^4.21.2: version "4.22.1" @@ -5972,7 +5843,7 @@ express@^4.21.2: utils-merge "1.0.1" vary "~1.1.2" -express@^5.0.1: +express@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04" integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw== @@ -6033,9 +5904,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fast-uri@^3.0.1: - version "3.0.3" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.3.tgz#892a1c91802d5d7860de728f18608a0573142241" - integrity sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw== + version "3.1.0" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== fastq@^1.6.0: version "1.17.1" @@ -6184,13 +6055,15 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -form-data@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" - integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== +form-data@4.0.0, form-data@>=4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" mime-types "^2.1.12" formdata-polyfill@^4.0.10: @@ -6299,10 +6172,10 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-east-asian-width@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" - integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== +get-east-asian-width@^1.0.0, get-east-asian-width@^1.3.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz#ce7008fe345edcf5497a6f557cfa54bc318a9ce7" + integrity sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA== get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: version "1.3.0" @@ -6494,6 +6367,11 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +hono@^4.11.4: + version "4.12.2" + resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.2.tgz#05c311c271b06685a0f229c484e3a2637d7d5f2a" + integrity sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg== + hosted-git-info@^9.0.0: version "9.0.2" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-9.0.2.tgz#b38c8a802b274e275eeeccf9f4a1b1a0a8557ada" @@ -6748,6 +6626,11 @@ internmap@^1.0.0: resolved "https://registry.yarnpkg.com/internmap/-/internmap-1.0.1.tgz#0017cc8a3b99605f0302f2b198d272e015e5df95" integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw== +ip-address@10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.0.1.tgz#a8180b783ce7788777d796286d61bce4276818ed" + integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA== + ip-address@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" @@ -6875,11 +6758,11 @@ is-fullwidth-code-point@^4.0.0: integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== is-fullwidth-code-point@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" - integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + version "5.1.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz#046b2a6d4f6b156b2233d3207d4b5a9783999b98" + integrity sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ== dependencies: - get-east-asian-width "^1.0.0" + get-east-asian-width "^1.3.1" is-generator-function@^1.0.10: version "1.1.2" @@ -7120,7 +7003,7 @@ jiti@^1.20.0, jiti@^1.21.7: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.7.tgz#9dd81043424a3d28458b193d965f0d18a2300ba9" integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== -jose@^6.1.1: +jose@^6.1.3: version "6.1.3" resolved "https://registry.yarnpkg.com/jose/-/jose-6.1.3.tgz#8453d7be88af7bb7d64a0481d6a35a0145ba3ea5" integrity sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ== @@ -7849,34 +7732,27 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" - integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== +minimatch@9.0.1, minimatch@^9.0.4, minimatch@^9.0.5, minimatch@~9.0.6: + version "9.0.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.6.tgz#a7e3bccfcb3d78ec1bf8d51c9ba749080237a5c8" + integrity sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ== dependencies: - brace-expansion "^2.0.1" + brace-expansion "^5.0.2" minimatch@^10.0.3, minimatch@^10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55" - integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== + version "10.2.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f" + integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw== dependencies: - "@isaacs/brace-expansion" "^5.0.0" + brace-expansion "^5.0.2" minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + version "3.1.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.3.tgz#6a5cba9b31f503887018f579c89f81f61162e624" + integrity sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA== dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.4, minimatch@^9.0.5: - version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - minimist@1.2.8, minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -9050,9 +8926,9 @@ readable-stream@^3.0.6: util-deprecate "^1.0.1" readdirp@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.0.2.tgz#388fccb8b75665da3abffe2d8f8ed59fe74c230a" - integrity sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA== + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== readdirp@~3.6.0: version "3.6.0" @@ -9237,7 +9113,7 @@ robust-predicates@^3.0.2: resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -rollup@4.52.3: +rollup@4.52.3, rollup@^4.43.0: version "4.52.3" resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.52.3.tgz#cc5c28d772b022ce48b235a97b347ccd9d88c1a3" integrity sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A== @@ -9268,40 +9144,6 @@ rollup@4.52.3: "@rollup/rollup-win32-x64-msvc" "4.52.3" fsevents "~2.3.2" -rollup@^4.43.0: - version "4.56.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.56.0.tgz#65959d13cfbd7e48b8868c05165b1738f0143862" - integrity sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg== - dependencies: - "@types/estree" "1.0.8" - optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.56.0" - "@rollup/rollup-android-arm64" "4.56.0" - "@rollup/rollup-darwin-arm64" "4.56.0" - "@rollup/rollup-darwin-x64" "4.56.0" - "@rollup/rollup-freebsd-arm64" "4.56.0" - "@rollup/rollup-freebsd-x64" "4.56.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.56.0" - "@rollup/rollup-linux-arm-musleabihf" "4.56.0" - "@rollup/rollup-linux-arm64-gnu" "4.56.0" - "@rollup/rollup-linux-arm64-musl" "4.56.0" - "@rollup/rollup-linux-loong64-gnu" "4.56.0" - "@rollup/rollup-linux-loong64-musl" "4.56.0" - "@rollup/rollup-linux-ppc64-gnu" "4.56.0" - "@rollup/rollup-linux-ppc64-musl" "4.56.0" - "@rollup/rollup-linux-riscv64-gnu" "4.56.0" - "@rollup/rollup-linux-riscv64-musl" "4.56.0" - "@rollup/rollup-linux-s390x-gnu" "4.56.0" - "@rollup/rollup-linux-x64-gnu" "4.56.0" - "@rollup/rollup-linux-x64-musl" "4.56.0" - "@rollup/rollup-openbsd-x64" "4.56.0" - "@rollup/rollup-openharmony-arm64" "4.56.0" - "@rollup/rollup-win32-arm64-msvc" "4.56.0" - "@rollup/rollup-win32-ia32-msvc" "4.56.0" - "@rollup/rollup-win32-x64-gnu" "4.56.0" - "@rollup/rollup-win32-x64-msvc" "4.56.0" - fsevents "~2.3.2" - roughjs@^4.6.6: version "4.6.6" resolved "https://registry.yarnpkg.com/roughjs/-/roughjs-4.6.6.tgz#1059f49a5e0c80dee541a005b20cc322b222158b" @@ -9463,7 +9305,7 @@ semver@7.7.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== -semver@7.7.3, semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.7.3: +semver@7.7.3: version "7.7.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== @@ -9478,6 +9320,11 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== +semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.5.3, semver@^7.5.4, semver@^7.7.3: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + send@^1.1.0, send@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed" @@ -9700,9 +9547,9 @@ slice-ansi@^5.0.0: is-fullwidth-code-point "^4.0.0" slice-ansi@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" - integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + version "7.1.2" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.2.tgz#adf7be70aa6d72162d907cd0e6d5c11f507b5403" + integrity sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w== dependencies: ansi-styles "^6.2.1" is-fullwidth-code-point "^5.0.0" @@ -9978,9 +9825,9 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: ansi-regex "^5.0.1" strip-ansi@^7.0.1, strip-ansi@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + version "7.1.2" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" + integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== dependencies: ansi-regex "^6.0.1" @@ -10088,15 +9935,15 @@ tailwindcss@^3.4.19: resolve "^1.22.8" sucrase "^3.35.0" -tapable@^2.2.0, tapable@^2.2.1, tapable@^2.3.0: +tapable@^2.2.1, tapable@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.0.tgz#7e3ea6d5ca31ba8e078b560f0d83ce9a14aa8be6" integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== tar@^7.4.3, tar@^7.5.2: - version "7.5.6" - resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.6.tgz#2db7a210748a82f0a89cc31527b90d3a24984fb7" - integrity sha512-xqUeu2JAIJpXyvskvU3uvQW8PAmHrtXp2KDuMJwQqW8Sqq0CaZBAQ+dKS3RBXVhU4wC5NjAdKrmh84241gO9cA== + version "7.5.9" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.5.9.tgz#817ac12a54bc4362c51340875b8985d7dc9724b8" + integrity sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg== dependencies: "@isaacs/fs-minipass" "^4.0.0" chownr "^3.0.0" @@ -10626,7 +10473,7 @@ watchpack@2.4.4: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" -watchpack@^2.4.4: +watchpack@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.5.1.tgz#dd38b601f669e0cbf567cb802e75cead82cde102" integrity sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg== @@ -10737,10 +10584,10 @@ webpack-subresource-integrity@5.1.0: dependencies: typed-assert "^1.0.8" -webpack@5.104.1: - version "5.104.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.104.1.tgz#94bd41eb5dbf06e93be165ba8be41b8260d4fb1a" - integrity sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA== +webpack@5.105.0: + version "5.105.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.105.0.tgz#38b5e6c5db8cbe81debbd16e089335ada05ea23a" + integrity sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw== dependencies: "@types/eslint-scope" "^3.7.7" "@types/estree" "^1.0.8" @@ -10752,7 +10599,7 @@ webpack@5.104.1: acorn-import-phases "^1.0.3" browserslist "^4.28.1" chrome-trace-event "^1.0.2" - enhanced-resolve "^5.17.4" + enhanced-resolve "^5.19.0" es-module-lexer "^2.0.0" eslint-scope "5.1.1" events "^3.2.0" @@ -10765,7 +10612,7 @@ webpack@5.104.1: schema-utils "^4.3.3" tapable "^2.3.0" terser-webpack-plugin "^5.3.16" - watchpack "^2.4.4" + watchpack "^2.5.1" webpack-sources "^3.3.3" websocket-driver@>=0.5.1, websocket-driver@^0.7.4: @@ -10907,9 +10754,9 @@ wrap-ansi@^8.1.0: strip-ansi "^7.0.1" wrap-ansi@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" - integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + version "9.0.2" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz#956832dea9494306e6d209eb871643bb873d7c98" + integrity sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww== dependencies: ansi-styles "^6.2.1" string-width "^7.0.0" @@ -11019,7 +10866,7 @@ yoctocolors-cjs@^2.1.3: resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz#7e4964ea8ec422b7a40ac917d3a344cfd2304baa" integrity sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw== -zod-to-json-schema@^3.25.0: +zod-to-json-schema@^3.25.1: version "3.25.1" resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz#7f24962101a439ddade2bf1aeab3c3bfec7d84ba" integrity sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==