Browse Source

Merge branch 'thingsboard:master' into feat/chart-dynamic-y-axis-configuration

pull/14488/head
Maksym Tsymbarov 10 months ago
committed by GitHub
parent
commit
f5b17e1498
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      application/pom.xml
  2. 116
      application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java
  3. 86
      application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java
  4. 91
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  5. 27
      application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java
  6. 8
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java
  7. 21
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java
  8. 129
      application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java
  9. 9
      application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java
  10. 7
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  11. 37
      application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java
  12. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  13. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  14. 2
      common/data/pom.xml
  15. 67
      common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java
  16. 5
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
  17. 2
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java
  18. 7
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
  19. 6
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  20. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  21. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java
  22. 12
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  23. 9
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  24. 8
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  25. 6
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
  26. 14
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  27. 2
      dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java
  28. 8
      pom.xml
  29. 20
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  30. 2
      rule-engine/rule-engine-api/pom.xml
  31. 2
      rule-engine/rule-engine-components/pom.xml
  32. 5
      ui-ngx/package.json
  33. 4
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts
  34. 16
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts
  35. 11
      ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss
  36. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts
  37. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html
  38. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts
  39. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html
  40. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts
  41. 2
      ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts
  42. 30
      ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts
  43. 2
      ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html
  44. 31
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  45. 9
      ui-ngx/src/app/shared/models/ai-model.models.ts
  46. 8
      ui-ngx/src/app/shared/models/time/time.models.ts
  47. 451
      ui-ngx/src/assets/dashboard/api_usage.json
  48. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  49. 78
      ui-ngx/yarn.lock

18
application/pom.xml

@ -382,35 +382,35 @@
<artifactId>rocksdbjni</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-azure-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-google-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-vertex-ai-gemini</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-mistral-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-anthropic</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-bedrock</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-github-models</artifactId>
<exclusions>
<exclusion>
@ -420,7 +420,7 @@
</exclusions>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
</dependencies>

116
application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java

@ -86,6 +86,9 @@ public class SwaggerConfiguration {
public static final String LOGIN_ENDPOINT = "/api/auth/login";
public static final String REFRESH_TOKEN_ENDPOINT = "/api/auth/token";
private static final String LOGIN_PASSWORD_SCHEME = "HTTP login form";
private static final String API_KEY_SCHEME = "API key form";
private static final ApiResponses loginResponses = loginResponses();
private static final ApiResponses defaultErrorResponses = defaultErrorResponses(false);
private static final ApiResponses defaultPostErrorResponses = defaultErrorResponses(true);
@ -142,14 +145,28 @@ public class SwaggerConfiguration {
.license(license)
.version(apiVersion);
SecurityScheme securityScheme = new SecurityScheme()
SecurityScheme loginPasswordScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.description("Enter Username / Password")
.scheme("loginPassword")
.bearerFormat("/api/auth/login|X-Authorization");
SecurityScheme apiKeyScheme = new SecurityScheme()
.type(SecurityScheme.Type.APIKEY)
.name("X-Authorization")
.in(SecurityScheme.In.HEADER)
.description("""
Enter the API key value with 'ApiKey' prefix in format: **ApiKey <your_api_key_value>**
Example: **ApiKey tb_5te51SkLRYpjGrujUGwqkjFvooWBlQpVe2An2Dr3w13wjfxDW**
<br>**NOTE**: Use only ONE authentication method at a time. If both are authorized, JWT auth takes the priority.<br>
""");
var openApi = new OpenAPI()
.components(new Components().addSecuritySchemes("HTTP login form", securityScheme))
.components(new Components()
.addSecuritySchemes(LOGIN_PASSWORD_SCHEME, loginPasswordScheme)
.addSecuritySchemes(API_KEY_SCHEME, apiKeyScheme))
.info(info);
addDefaultSchemas(openApi);
addLoginOperation(openApi);
@ -198,13 +215,14 @@ public class SwaggerConfiguration {
operation.summary("Login method to get user JWT token data");
operation.description("""
Login method used to authenticate user and get JWT token data.
Value of the response **token** field can be used as **X-Authorization** header value:
`X-Authorization: Bearer $JWT_TOKEN_VALUE`.""");
var requestBody = new RequestBody().description("Login request")
.content(new Content().addMediaType(APPLICATION_JSON_VALUE,
new MediaType().schema(new Schema<LoginRequest>().$ref("#/components/schemas/LoginRequest"))));
new MediaType().schema(new Schema<LoginRequest>().$ref("#/components/schemas/LoginRequest"))));
operation.requestBody(requestBody);
operation.responses(loginResponses);
@ -218,11 +236,11 @@ public class SwaggerConfiguration {
var operation = new Operation();
operation.summary("Refresh user JWT token data");
operation.description("""
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token.
The response contains a new token that can be used for authorization.
`X-Authorization: Bearer $JWT_TOKEN_VALUE`""");
Method to refresh JWT token. Provide a valid refresh token to get a new JWT token.
The response contains a new token that can be used for authorization.
`X-Authorization: Bearer $JWT_TOKEN_VALUE`""");
var requestBody = new RequestBody().description("Refresh token request")
.content(new Content().addMediaType(APPLICATION_JSON_VALUE,
@ -291,8 +309,9 @@ public class SwaggerConfiguration {
return (routerOperation, handlerMethod) -> {
String[] pNames = localSpringDocParameterNameDiscoverer.getParameterNames(handlerMethod.getMethod());
String[] reflectionParametersNames = Arrays.stream(handlerMethod.getMethod().getParameters()).map(java.lang.reflect.Parameter::getName).toArray(String[]::new);
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull))
if (pNames == null || Arrays.stream(pNames).anyMatch(Objects::isNull)) {
pNames = reflectionParametersNames;
}
MethodParameter[] parameters = handlerMethod.getMethodParameters();
List<String> requestParams = new ArrayList<>();
for (var i = 0; i < parameters.length; i++) {
@ -324,26 +343,25 @@ public class SwaggerConfiguration {
}
private OpenApiCustomizer customOpenApiCustomizer() {
var loginForm = new SecurityRequirement().addList("HTTP login form", Arrays.asList(
Authority.SYS_ADMIN.name(),
Authority.TENANT_ADMIN.name(),
Authority.CUSTOMER_USER.name()
));
var loginRequirement = createSecurityRequirement(LOGIN_PASSWORD_SCHEME);
var apiKeyRequirement = createSecurityRequirement(API_KEY_SCHEME);
return openAPI -> {
var paths = openAPI.getPaths();
paths.entrySet().stream().peek(entry -> {
securityCustomization(loginForm, entry);
if (!entry.getKey().equals(LOGIN_ENDPOINT)) {
defaultErrorResponsesCustomization(entry.getValue());
}
}).map(this::tagsCustomization).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem);
paths.entrySet().stream()
.peek(entry -> {
securityCustomization(entry, loginRequirement, apiKeyRequirement);
if (!entry.getKey().equals(LOGIN_ENDPOINT)) {
defaultErrorResponsesCustomization(entry.getValue());
}
})
.map(this::extractTagFromPath).filter(Objects::nonNull).distinct().sorted(Comparator.comparing(Tag::getName)).forEach(openAPI::addTagsItem);
var pathItemsByTags = new TreeMap<String, Map<String, PathItem>>();
paths.forEach((k, v) -> {
var tagItem = tagItemFromPathItem(v);
if (tagItem != null) {
var pathItemMap = pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>());
pathItemMap.put(k, v);
pathItemsByTags.computeIfAbsent(tagItem, k1 -> new TreeMap<>()).put(k, v);
}
});
var sortedPaths = new Paths();
@ -357,13 +375,17 @@ public class SwaggerConfiguration {
};
}
private SecurityRequirement createSecurityRequirement(String schemeName) {
return new SecurityRequirement().addList(schemeName, Arrays.asList(
Authority.SYS_ADMIN.name(),
Authority.TENANT_ADMIN.name(),
Authority.CUSTOMER_USER.name()
));
}
private Tag tagsCustomization(Map.Entry<String, PathItem> entry) {
var tagItem = tagItemFromPathItem(entry.getValue());
if (tagItem != null) {
return tagFromTagItem(tagItem);
}
return null;
private Tag extractTagFromPath(Map.Entry<String, PathItem> entry) {
var tagName = tagItemFromPathItem(entry.getValue());
return tagName != null ? tagFromTagItem(tagName) : null;
}
private String tagItemFromPathItem(PathItem item) {
@ -383,17 +405,20 @@ public class SwaggerConfiguration {
StringBuilder sb = new StringBuilder();
for (String word : words) {
sb.append(word.substring(0, 1).toUpperCase());
sb.append(word.substring(1).toLowerCase());
sb.append(" ");
if (!word.isEmpty()) {
sb.append(word.substring(0, 1).toUpperCase());
sb.append(word.substring(1).toLowerCase());
sb.append(" ");
}
}
return new Tag().name(tagItem).description(sb.toString().trim());
}
private void defaultErrorResponsesCustomization(PathItem pathItem) {
pathItem.readOperationsMap().forEach(((httpMethod, operation) -> {
pathItem.readOperationsMap().forEach((httpMethod, operation) -> {
var errorResponses = httpMethod.equals(PathItem.HttpMethod.POST) ? defaultPostErrorResponses : defaultErrorResponses;
var responses = operation.getResponses();
if (responses == null) {
responses = errorResponses;
@ -406,16 +431,19 @@ public class SwaggerConfiguration {
});
}
operation.setResponses(responses);
}));
});
}
private void securityCustomization(SecurityRequirement loginForm, Map.Entry<String, PathItem> entry) {
private void securityCustomization(Map.Entry<String, PathItem> entry, SecurityRequirement jwtBearerRequirement, SecurityRequirement apiKeyRequirement) {
var path = entry.getKey();
if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT)) {
if (path.matches(securityPathRegex) && !path.matches(nonSecurityPathRegex) && !path.equals(LOGIN_ENDPOINT) && !path.equals(REFRESH_TOKEN_ENDPOINT)) {
entry.getValue()
.readOperationsMap()
.values()
.forEach(operation -> operation.addSecurityItem(loginForm));
.forEach(operation -> {
operation.addSecurityItem(jwtBearerRequirement);
operation.addSecurityItem(apiKeyRequirement);
});
}
}
@ -430,6 +458,7 @@ public class SwaggerConfiguration {
private static ApiResponses defaultErrorResponses(boolean isPost) {
ApiResponses apiResponses = new ApiResponses();
apiResponses.addApiResponse("400", errorResponse("400", "Bad Request",
ThingsboardErrorResponse.of(isPost ? "Invalid request body" : "Invalid UUID string: 123", ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST)));
@ -465,8 +494,7 @@ public class SwaggerConfiguration {
ThingsboardErrorResponse.of("Authentication failed", ThingsboardErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED))
)
));
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>();
credentialsExpiredSchema.$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse");
var credentialsExpiredSchema = new Schema<ThingsboardCredentialsExpiredResponse>().$ref("#/components/schemas/ThingsboardCredentialsExpiredResponse");
apiResponses.addApiResponse("401 ", errorResponse("Unauthorized (**Expired credentials**)",
Map.of(
"credentials-expired", errorExample("Expired credentials",
@ -482,15 +510,13 @@ public class SwaggerConfiguration {
}
private static ApiResponse errorResponse(String description, Map<String, Example> examples) {
var schema = new Schema<ThingsboardErrorResponse>();
schema.$ref("#/components/schemas/ThingsboardErrorResponse");
var schema = new Schema<ThingsboardErrorResponse>().$ref("#/components/schemas/ThingsboardErrorResponse");
return errorResponse(description, examples, schema);
}
private static ApiResponse errorResponse(String description, Map<String, Example> examples, Schema<? extends ThingsboardErrorResponse> errorResponseSchema) {
MediaType mediaType = new MediaType().schema(errorResponseSchema);
mediaType.setExamples(examples);
Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType);
MediaType mediaType = new MediaType().schema(errorResponseSchema).examples(examples);
Content content = new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType);
return new ApiResponse().description(description).content(content);
}

86
application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java

@ -17,33 +17,30 @@ package org.thingsboard.server.controller;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.edqs.EdqsState;
import org.thingsboard.server.common.data.edqs.ToCoreEdqsRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
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.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataPageLink;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityFilter;
import org.thingsboard.server.common.msg.edqs.EdqsApiService;
import org.thingsboard.server.common.msg.edqs.EdqsService;
import org.thingsboard.server.config.annotations.ApiOperation;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -51,52 +48,46 @@ import org.thingsboard.server.service.query.EntityQueryService;
import org.thingsboard.server.service.security.permission.Operation;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_DATA_QUERY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
public class EntityQueryController extends BaseController {
@Autowired
private EntityQueryService entityQueryService;
@Autowired
private EdqsService edqsService;
@Autowired
private EdqsApiService edqsApiService;
private final EntityQueryService entityQueryService;
private final EdqsService edqsService;
private static final int MAX_PAGE_SIZE = 100;
@ApiOperation(value = "Count Entities by Query", notes = ENTITY_COUNT_QUERY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entitiesQuery/count", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/entitiesQuery/count")
public long countEntitiesByQuery(
@Parameter(description = "A JSON value representing the entity count query. See API call notes above for more details.")
@RequestBody EntityCountQuery query) throws ThingsboardException {
checkNotNull(query);
resolveQuery(query);
return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query);
return entityQueryService.countEntitiesByQuery(getCurrentUser(), query);
}
@ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entitiesQuery/find", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/entitiesQuery/find")
public PageData<EntityData> findEntityDataByQuery(
@Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.")
@RequestBody EntityDataQuery query) throws ThingsboardException {
checkNotNull(query);
resolveQuery(query);
return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query);
return entityQueryService.findEntityDataByQuery(getCurrentUser(), query);
}
@ApiOperation(value = "Find Alarms by Query", notes = ALARM_DATA_QUERY_DESCRIPTION)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarmsQuery/find", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/alarmsQuery/find")
public PageData<AlarmData> findAlarmDataByQuery(
@Parameter(description = "A JSON value representing the alarm data query. See API call notes above for more details.")
@RequestBody AlarmDataQuery query) throws ThingsboardException {
@ -107,13 +98,12 @@ public class EntityQueryController extends BaseController {
checkUserId(assigneeId, Operation.READ);
}
resolveQuery(query);
return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query);
return entityQueryService.findAlarmDataByQuery(getCurrentUser(), query);
}
@ApiOperation(value = "Count Alarms by Query (countAlarmsByQuery)", notes = "Returns the number of alarms that match the query definition.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarmsQuery/count", method = RequestMethod.POST)
@ResponseBody
@PostMapping("/alarmsQuery/count")
public long countAlarmsByQuery(@Parameter(description = "A JSON value representing the alarm count query.")
@RequestBody AlarmCountQuery query) throws ThingsboardException {
checkNotNull(query);
@ -122,31 +112,47 @@ public class EntityQueryController extends BaseController {
checkUserId(assigneeId, Operation.READ);
}
resolveQuery(query);
return this.entityQueryService.countAlarmsByQuery(getCurrentUser(), query);
return entityQueryService.countAlarmsByQuery(getCurrentUser(), query);
}
@ApiOperation(value = "Find Entity Keys by Query",
notes = "Uses entity data query (see 'Find Entity Data by Query') to find first 100 entities. Then fetch and return all unique time-series and/or attribute keys. Used mostly for UI hints.")
@ApiOperation(
value = "Find Available Entity Keys by Query",
notes = """
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
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entitiesQuery/find/keys", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> findEntityTimeseriesAndAttributesKeysByQuery(
@Parameter(description = "A JSON value representing the entity data query. See API call notes above for more details.")
@PostMapping("/entitiesQuery/find/keys")
public DeferredResult<AvailableEntityKeys> findAvailableEntityKeysByQuery(
@Parameter(description = "Entity data query to find entities. Page size is capped at 100.")
@RequestBody EntityDataQuery query,
@Parameter(description = "Include all unique time-series keys to the result.")
@RequestParam("timeseries") boolean isTimeseries,
@Parameter(description = "Include all unique attribute keys to the result.")
@RequestParam("attributes") boolean isAttributes,
@Parameter(description = ATTRIBUTES_SCOPE_DESCRIPTION, schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}))
@RequestParam(value = "scope", required = false) String scope) throws ThingsboardException {
TenantId tenantId = getTenantId();
checkNotNull(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.""")
@RequestParam("timeseries") boolean includeTimeseries,
@Parameter(description = """
When true, includes unique attribute key names in the response.
When false, the 'attribute' list will be empty. Use 'scope' parameter to filter by attribute scope.""")
@RequestParam("attributes") boolean includeAttributes,
@Parameter(description = """
Filters attribute keys by scope. Only applies when 'attributes' is true.
If not specified, returns attribute keys from all scopes.""",
schema = @Schema(allowableValues = {"SERVER_SCOPE", "SHARED_SCOPE", "CLIENT_SCOPE"}))
@RequestParam(value = "scope", required = false) AttributeScope scope
) throws ThingsboardException {
resolveQuery(query);
EntityDataPageLink pageLink = query.getPageLink();
if (pageLink.getPageSize() > MAX_PAGE_SIZE) {
pageLink.setPageSize(MAX_PAGE_SIZE);
}
return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes, scope);
return wrapFuture(entityQueryService.getKeysByQuery(getCurrentUser(), getTenantId(), query, includeTimeseries, includeAttributes, scope));
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")

91
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -69,6 +69,7 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -82,6 +83,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Function;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.ACTIVITY_STATE;
import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAST_CONNECT_TIME;
@ -94,6 +96,7 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS
public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService {
private final ConcurrentMap<EdgeId, EdgeGrpcSession> sessions = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, EdgeGrpcSession> sessionsById = new ConcurrentHashMap<>();
private final ConcurrentMap<EdgeId, Lock> sessionNewEventsLocks = new ConcurrentHashMap<>();
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
@ -283,6 +286,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
destroySession(session);
session.cleanUp();
sessions.remove(edgeId);
sessionsById.remove(session.getSessionId());
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -332,9 +336,15 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
TenantId tenantId = edge.getTenantId();
log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId);
if (sessions.containsKey(edgeId)) {
destroySession(sessions.get(edgeId));
EdgeGrpcSession existing = sessions.get(edgeId);
if (existing != null) {
log.info("[{}][{}] Replacing existing session [{}] for edge [{}]", tenantId, edgeGrpcSession.getSessionId(), existing.getSessionId(), edgeId);
destroySession(existing);
sessionsById.remove(existing.getSessionId());
}
}
sessions.put(edgeId, edgeGrpcSession);
sessionsById.put(edgeGrpcSession.getSessionId(), edgeGrpcSession);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -492,9 +502,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void onEdgeDisconnect(Edge edge, UUID sessionId) {
EdgeId edgeId = edge.getId();
log.info("[{}][{}] edge disconnected!", edgeId, sessionId);
EdgeGrpcSession toRemove = sessions.get(edgeId);
if (toRemove.getSessionId().equals(sessionId)) {
toRemove = sessions.remove(edgeId);
EdgeGrpcSession current = sessions.get(edgeId);
if (current != null && current.getSessionId().equals(sessionId)) {
EdgeGrpcSession toRemove = sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
@ -503,6 +513,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
newEventLock.unlock();
}
destroySession(toRemove);
sessionsById.remove(sessionId);
TenantId tenantId = toRemove.getEdge().getTenantId();
save(tenantId, edgeId, ACTIVITY_STATE, false);
long lastDisconnectTs = System.currentTimeMillis();
@ -510,7 +521,18 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId);
} else {
log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId);
log.info("[{}] edge session [{}] is not current anymore. Attempting to destroy it by sessionId.", edgeId, sessionId);
EdgeGrpcSession stale = sessionsById.remove(sessionId);
if (stale != null) {
try {
destroySession(stale);
log.info("[{}][{}] Successfully destroyed stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId);
} catch (Exception e) {
log.warn("[{}][{}] Failed to destroy stale session for edge [{}]", stale.getTenantId(), sessionId, edgeId, e);
}
} else {
log.debug("[{}] No session found by sessionId [{}] to destroy", edgeId, sessionId);
}
}
edgeIdServiceIdCache.evict(edgeId);
}
@ -522,6 +544,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId());
zombieSessions.add(session);
}
} catch (Exception e) {
log.warn("[{}][{}] Exception during session destroy for edge [{}] with session id [{}]",
session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId(), e);
}
}
@ -631,25 +656,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private void cleanupZombieSessions() {
try {
List<EdgeId> toRemove = new ArrayList<>();
for (EdgeGrpcSession session : sessions.values()) {
if (session instanceof KafkaEdgeGrpcSession kafkaSession &&
!kafkaSession.isConnected() &&
kafkaSession.getConsumer() != null &&
kafkaSession.getConsumer().getConsumer() != null &&
!kafkaSession.getConsumer().getConsumer().isStopped()) {
toRemove.add(kafkaSession.getEdge().getId());
}
}
for (EdgeId edgeId : toRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", edgeId);
EdgeGrpcSession removed = sessions.get(edgeId);
if (removed instanceof KafkaEdgeGrpcSession kafkaSession) {
if (kafkaSession.destroy()) {
sessions.remove(edgeId);
}
}
}
tryToDestroyZombieSessions(getZombieSessions(sessions.values()), s -> sessions.remove(s.getEdge().getId()));
tryToDestroyZombieSessions(getZombieSessions(sessionsById.values()), s -> sessionsById.remove(s.getSessionId()));
zombieSessions.removeIf(zombie -> {
if (zombie.destroy()) {
log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].",
@ -665,4 +674,38 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.warn("Failed to cleanup kafka sessions", e);
}
}
private List<EdgeGrpcSession> getZombieSessions(Collection<EdgeGrpcSession> sessions) {
List<EdgeGrpcSession> result = new ArrayList<>();
for (EdgeGrpcSession session : sessions) {
if (isKafkaSessionAndZombie(session)) {
result.add(session);
}
}
return result;
}
private void tryToDestroyZombieSessions(List<EdgeGrpcSession> sessionsToRemove, Function<EdgeGrpcSession, EdgeGrpcSession> removeFunc) {
for (EdgeGrpcSession toRemove : sessionsToRemove) {
log.info("[{}] Destroying session for edge because edge is not connected", toRemove.getEdge().getId());
if (toRemove.destroy()) {
removeFunc.apply(toRemove);
}
}
}
private boolean isKafkaSessionAndZombie(EdgeGrpcSession session) {
if (session instanceof KafkaEdgeGrpcSession kafkaSession) {
log.debug("[{}] kafkaSession.isConnected() = {}, kafkaSession.getConsumer().getConsumer().isStopped() = {}",
kafkaSession.getEdge().getId(),
kafkaSession.isConnected(),
kafkaSession.getConsumer() != null ? kafkaSession.getConsumer().getConsumer() != null ? kafkaSession.getConsumer().getConsumer().isStopped() : null : null);
return !kafkaSession.isConnected() &&
kafkaSession.getConsumer() != null &&
kafkaSession.getConsumer().getConsumer() != null &&
!kafkaSession.getConsumer().getConsumer().isStopped();
}
return false;
}
}

27
application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java

@ -101,8 +101,21 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
@Override
public ListenableFuture<Boolean> processEdgeEvents() {
if (!isConnected() || isSyncInProgress() || isHighPriorityProcessing) {
log.warn("[{}][{}] Session is not ready (connected={}, syncInProgress={}, highPriority={}), skip starting edge event consumer",
tenantId, edge != null ? edge.getId() : null, isConnected(), isSyncInProgress(), isHighPriorityProcessing);
return Futures.immediateFuture(Boolean.FALSE);
}
if (consumer == null || (consumer.getConsumer() != null && consumer.getConsumer().isStopped())) {
try {
if (consumerExecutor != null && !consumerExecutor.isShutdown()) {
try {
consumerExecutor.shutdown();
awaitConsumerTermination();
} catch (Exception e) {
log.warn("[{}][{}] Failed to shutdown previous consumer executor", tenantId, edge.getId(), e);
}
}
this.consumerExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edge-event-consumer"));
this.consumer = QueueConsumerManager.<TbProtoQueueMsg<ToEdgeEventNotificationMsg>>builder()
.name("TB Edge events [" + edge.getId() + "]")
@ -133,6 +146,7 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
public boolean destroy() {
try {
if (consumer != null) {
log.info("[{}][{}] Stopping edge event consumer...", tenantId, edge != null ? edge.getId() : null);
consumer.stop();
}
} catch (Exception e) {
@ -141,16 +155,25 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession {
}
consumer = null;
try {
if (consumerExecutor != null) {
if (consumerExecutor != null && !consumerExecutor.isShutdown()) {
consumerExecutor.shutdown();
awaitConsumerTermination();
}
} catch (Exception e) {
log.warn("[{}][{}] Failed to shutdown consumer executor", tenantId, edge.getId(), e);
log.warn("[{}][{}] Failed to shutdown edge event consumer executor", tenantId, edge.getId(), e);
return false;
}
return true;
}
private void awaitConsumerTermination() {
try {
consumerExecutor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS);
} catch (InterruptedException ie) {
log.warn("[{}][{}] Interrupted while awaiting consumer executor termination", tenantId, edge.getId());
}
}
@Override
public void cleanUp() {
String topic = topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edge.getId()).getTopic();

8
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java

@ -65,12 +65,12 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
Dashboard savedDashboard = edgeCtx.getDashboardService().saveDashboard(dashboard, false);
updateDashboardAssignments(tenantId, dashboardById, savedDashboard, newAssignedCustomers);
updateDashboardAssignments(tenantId, customerId, dashboardById, savedDashboard, newAssignedCustomers);
return created;
}
private void updateDashboardAssignments(TenantId tenantId, Dashboard dashboardById, Dashboard savedDashboard, Set<ShortCustomerInfo> newAssignedCustomers) {
private void updateDashboardAssignments(TenantId tenantId, CustomerId edgeCustomerId, Dashboard dashboardById, Dashboard savedDashboard, Set<ShortCustomerInfo> newAssignedCustomers) {
Set<ShortCustomerInfo> currentAssignedCustomers = new HashSet<>();
if (dashboardById != null) {
if (dashboardById.getAssignedCustomers() != null) {
@ -78,7 +78,7 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
}
}
newAssignedCustomers = filterNonExistingCustomers(tenantId, currentAssignedCustomers, newAssignedCustomers);
newAssignedCustomers = filterNonExistingCustomers(tenantId, edgeCustomerId, currentAssignedCustomers, newAssignedCustomers);
Set<CustomerId> addedCustomerIds = new HashSet<>();
Set<CustomerId> removedCustomerIds = new HashSet<>();
@ -114,6 +114,6 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor {
}
}
protected abstract Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers);
protected abstract Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, CustomerId customerId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers);
}

21
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
@ -36,8 +37,10 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
@Component
@ -116,14 +119,24 @@ public class DashboardEdgeProcessor extends BaseDashboardProcessor implements Da
}
@Override
protected Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers) {
newAssignedCustomers.addAll(currentAssignedCustomers);
return newAssignedCustomers;
protected Set<ShortCustomerInfo> filterNonExistingCustomers(TenantId tenantId, CustomerId edgeCustomerId, Set<ShortCustomerInfo> currentAssignedCustomers, Set<ShortCustomerInfo> newAssignedCustomers) {
boolean edgeCustomerPresentInNewAssignments = newAssignedCustomers.stream()
.map(ShortCustomerInfo::getCustomerId)
.anyMatch(edgeCustomerId::equals);
if (edgeCustomerPresentInNewAssignments) {
Set<ShortCustomerInfo> result = new HashSet<>(newAssignedCustomers);
result.addAll(currentAssignedCustomers);
return result;
} else {
return currentAssignedCustomers.stream()
.filter(info -> !edgeCustomerId.equals(info.getCustomerId()))
.collect(Collectors.toSet());
}
}
@Override
public EdgeEventType getEdgeEventType() {
return EdgeEventType.DASHBOARD;
}
}

129
application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java

@ -15,24 +15,18 @@
*/
package org.thingsboard.server.service.query;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.async.DeferredResult;
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;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
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;
@ -40,6 +34,7 @@ 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.ComplexFilterPredicate;
import org.thingsboard.server.common.data.query.DynamicValue;
import org.thingsboard.server.common.data.query.EntityCountQuery;
@ -56,16 +51,13 @@ import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.sql.query.EntityKeyMapping;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.security.AccessValidator;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
@ -73,9 +65,10 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.Futures.immediateFuture;
@Service
@Slf4j
@TbCoreComponent
@ -138,20 +131,12 @@ public class DefaultEntityQueryService implements EntityQueryService {
}
private <T> void resolveDynamicValue(DynamicValue<T> dynamicValue, SecurityUser user, FilterPredicateType predicateType) {
EntityId entityId;
switch (dynamicValue.getSourceType()) {
case CURRENT_TENANT:
entityId = user.getTenantId();
break;
case CURRENT_CUSTOMER:
entityId = user.getCustomerId();
break;
case CURRENT_USER:
entityId = user.getId();
break;
default:
throw new RuntimeException("Not supported operation for source type: {" + dynamicValue.getSourceType() + "}");
}
EntityId entityId = switch (dynamicValue.getSourceType()) {
case CURRENT_TENANT -> user.getTenantId();
case CURRENT_CUSTOMER -> user.getCustomerId();
case CURRENT_USER -> user.getId();
default -> throw new RuntimeException("Not supported operation for source type: {" + dynamicValue.getSourceType() + "}");
};
try {
Optional<AttributeKvEntry> valueOpt = attributesService.find(user.getTenantId(), entityId,
@ -242,101 +227,51 @@ public class DefaultEntityQueryService implements EntityQueryService {
}
@Override
public DeferredResult<ResponseEntity> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, String attributesScope) {
final DeferredResult<ResponseEntity> response = new DeferredResult<>();
public ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, AttributeScope scope) {
if (!isAttributes && !isTimeseries) {
replyWithEmptyResponse(response);
return response;
return immediateFuture(AvailableEntityKeys.none());
}
List<EntityId> ids = this.findEntityDataByQuery(securityUser, query).getData().stream()
List<EntityId> ids = findEntityDataByQuery(securityUser, query).getData().stream()
.map(EntityData::getEntityId)
.collect(Collectors.toList());
.toList();
if (ids.isEmpty()) {
replyWithEmptyResponse(response);
return response;
return immediateFuture(AvailableEntityKeys.none());
}
Set<EntityType> types = ids.stream().map(EntityId::getEntityType).collect(Collectors.toSet());
final ListenableFuture<List<String>> timeseriesKeysFuture;
final ListenableFuture<List<String>> attributesKeysFuture;
ListenableFuture<List<String>> timeseriesKeysFuture;
ListenableFuture<List<String>> attributesKeysFuture;
if (isTimeseries) {
timeseriesKeysFuture = dbCallbackExecutor.submit(() -> timeseriesService.findAllKeysByEntityIds(tenantId, ids));
timeseriesKeysFuture = timeseriesService.findAllKeysByEntityIdsAsync(tenantId, ids);
} else {
timeseriesKeysFuture = null;
timeseriesKeysFuture = immediateFuture(Collections.emptyList());
}
if (isAttributes) {
Map<EntityType, List<EntityId>> typesMap = ids.stream().collect(Collectors.groupingBy(EntityId::getEntityType));
List<ListenableFuture<List<String>>> futures = new ArrayList<>(typesMap.size());
typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, attributesScope))));
typesMap.forEach((type, entityIds) -> futures.add(dbCallbackExecutor.submit(() -> attributesService.findAllKeysByEntityIds(tenantId, entityIds, scope))));
attributesKeysFuture = Futures.transform(Futures.allAsList(futures), lists -> {
if (CollectionUtils.isEmpty(lists)) {
return Collections.emptyList();
}
return lists.stream().flatMap(List::stream).distinct().sorted().collect(Collectors.toList());
}, dbCallbackExecutor);
} else {
attributesKeysFuture = null;
}
if (isTimeseries && isAttributes) {
Futures.whenAllComplete(timeseriesKeysFuture, attributesKeysFuture).run(() -> {
try {
replyWithResponse(response, types, timeseriesKeysFuture.get(), attributesKeysFuture.get());
} catch (Exception e) {
log.error("Failed to fetch timeseries and attributes keys!", e);
AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR);
}
return lists.stream().flatMap(List::stream).distinct().sorted().toList();
}, dbCallbackExecutor);
} else if (isTimeseries) {
addCallback(timeseriesKeysFuture, keys -> replyWithResponse(response, types, keys, null),
error -> {
log.error("Failed to fetch timeseries keys!", error);
AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR);
});
} else {
addCallback(attributesKeysFuture, keys -> replyWithResponse(response, types, null, keys),
error -> {
log.error("Failed to fetch attributes keys!", error);
AccessValidator.handleError(error, response, HttpStatus.INTERNAL_SERVER_ERROR);
});
attributesKeysFuture = immediateFuture(Collections.emptyList());
}
return response;
}
private void replyWithResponse(DeferredResult<ResponseEntity> response, Set<EntityType> types, List<String> timeseriesKeys, List<String> attributesKeys) {
ObjectNode json = JacksonUtil.newObjectNode();
addItemsToArrayNode(json.putArray("entityTypes"), types);
addItemsToArrayNode(json.putArray("timeseries"), timeseriesKeys);
addItemsToArrayNode(json.putArray("attribute"), attributesKeys);
response.setResult(new ResponseEntity<>(json, HttpStatus.OK));
}
private void replyWithEmptyResponse(DeferredResult<ResponseEntity> response) {
replyWithResponse(response, Collections.emptySet(), Collections.emptyList(), Collections.emptyList());
}
private void addItemsToArrayNode(ArrayNode arrayNode, Collection<?> collection) {
if (!CollectionUtils.isEmpty(collection)) {
collection.forEach(item -> arrayNode.add(item.toString()));
}
}
private void addCallback(ListenableFuture<List<String>> future, Consumer<List<String>> success, Consumer<Throwable> error) {
Futures.addCallback(future, new FutureCallback<List<String>>() {
@Override
public void onSuccess(@Nullable List<String> keys) {
success.accept(keys);
}
@Override
public void onFailure(Throwable t) {
error.accept(t);
}
}, dbCallbackExecutor);
return Futures.whenAllComplete(timeseriesKeysFuture, attributesKeysFuture)
.call(() -> {
try {
return new AvailableEntityKeys(types, Futures.getDone(timeseriesKeysFuture), Futures.getDone(attributesKeysFuture));
} catch (ExecutionException e) {
throw new ThingsboardException(e.getCause(), ThingsboardErrorCode.DATABASE);
}
}, dbCallbackExecutor);
}
}

9
application/src/main/java/org/thingsboard/server/service/query/EntityQueryService.java

@ -15,13 +15,14 @@
*/
package org.thingsboard.server.service.query;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.async.DeferredResult;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.id.TenantId;
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.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -37,7 +38,7 @@ public interface EntityQueryService {
long countAlarmsByQuery(SecurityUser securityUser, AlarmCountQuery query);
DeferredResult<ResponseEntity> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, String attributesScope);
ListenableFuture<AvailableEntityKeys> getKeysByQuery(SecurityUser securityUser, TenantId tenantId, EntityDataQuery query,
boolean isTimeseries, boolean isAttributes, AttributeScope scope);
}

7
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java

@ -321,9 +321,10 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
"\n" +
" # Environment variables\n" +
" environment:\n" +
" - host=host.docker.internal\n" +
" - port=1883\n" +
" - accessToken=" + credentials.getCredentialsId() + "\n" +
" - TB_GW_HOST=host.docker.internal\n" +
" - TB_GW_PORT=1883\n" +
" - TB_GW_SECURITY_TYPE=accessToken\n" +
" - TB_GW_ACCESS_TOKEN=" + credentials.getCredentialsId() + "\n" +
"\n" +
" # Volumes bind\n" +
" volumes:\n" +

37
application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java

@ -27,11 +27,15 @@ import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
@ -183,6 +187,22 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
customer.setTitle("Edge Customer");
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
// assign edge to customer
edgeImitator.expectMessageAmount(2);
doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages());
Optional<EdgeConfiguration> edgeConfigurationOpt = edgeImitator.findMessageByType(EdgeConfiguration.class);
Assert.assertTrue(edgeConfigurationOpt.isPresent());
EdgeConfiguration edgeConfiguration = edgeConfigurationOpt.get();
Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), edgeConfiguration.getCustomerIdMSB());
Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), edgeConfiguration.getCustomerIdLSB());
Optional<CustomerUpdateMsg> customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class);
Assert.assertTrue(customerUpdateOpt.isPresent());
CustomerUpdateMsg customerUpdateMsg = customerUpdateOpt.get();
Customer customerMsg = JacksonUtil.fromString(customerUpdateMsg.getEntity(), Customer.class, true);
Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomer, customerMsg);
Dashboard dashboard = buildDashboardForUplinkMsg(savedCustomer);
// create dashboard on edge
@ -225,6 +245,23 @@ public class DashboardEdgeTest extends AbstractEdgeTest {
foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class);
Assert.assertEquals(DASHBOARD_TITLE + " Updated", foundDashboard.getName());
// unassign edge from customer
edgeImitator.expectMessageAmount(2);
doDelete("/api/customer/edge/" + edge.getUuidId(), Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages());
edgeConfigurationOpt = edgeImitator.findMessageByType(EdgeConfiguration.class);
Assert.assertTrue(edgeConfigurationOpt.isPresent());
edgeConfiguration = edgeConfigurationOpt.get();
Assert.assertEquals(
new CustomerId(EntityId.NULL_UUID),
new CustomerId(new UUID(edgeConfiguration.getCustomerIdMSB(), edgeConfiguration.getCustomerIdLSB())));
customerUpdateOpt = edgeImitator.findMessageByType(CustomerUpdateMsg.class);
Assert.assertTrue(customerUpdateOpt.isPresent());
customerUpdateMsg = customerUpdateOpt.get();
Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, customerUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomer.getUuidId().getMostSignificantBits(), customerUpdateMsg.getIdMSB());
Assert.assertEquals(savedCustomer.getUuidId().getLeastSignificantBits(), customerUpdateMsg.getIdLSB());
}
@Test

2
common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java

@ -48,7 +48,7 @@ public interface AttributesService {
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, String scope);
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope);
int removeAllByEntityId(TenantId tenantId, EntityId entityId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

@ -63,5 +63,8 @@ public interface TimeseriesService {
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds);
void cleanup(long systemTtl);
}

2
common/data/pom.xml

@ -113,7 +113,7 @@
<scope>compile</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
</dependency>
</dependencies>

67
common/data/src/main/java/org/thingsboard/server/common/data/query/AvailableEntityKeys.java

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2025 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 io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.util.List;
import java.util.Set;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
import static java.util.Objects.requireNonNullElse;
@Schema(
description = "Contains unique time series and attribute key names discovered from entities matching a query. Used primarily for UI hints such as autocomplete suggestions."
)
public record AvailableEntityKeys(
@Schema(
description = "Set of entity types found among the matched entities.",
example = "[\"DEVICE\", \"ASSET\"]",
requiredMode = Schema.RequiredMode.REQUIRED
)
Set<EntityType> entityTypes,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@ArraySchema(
arraySchema = @Schema(description = "List of unique time series key names available on the matched entities."),
schema = @Schema(implementation = String.class, example = "temperature"),
uniqueItems = true
)
List<String> timeseries,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
@ArraySchema(
arraySchema = @Schema(description = "List of unique attribute key names available on the matched entities."),
schema = @Schema(implementation = String.class, example = "serialNumber"),
uniqueItems = true
)
List<String> attribute
) {
public AvailableEntityKeys {
entityTypes = requireNonNullElse(entityTypes, emptySet());
timeseries = requireNonNullElse(timeseries, emptyList());
attribute = requireNonNullElse(attribute, emptyList());
}
public static AvailableEntityKeys none() {
return new AvailableEntityKeys(emptySet(), emptyList(), emptyList());
}
}

5
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java

@ -105,6 +105,7 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
public static final String CLAIMING = "claiming";
public static final String ATTRIBUTE = "attribute";
public static final String RPC_RESPONSE = "Rpc response";
public static final String CONNECT = "connect";
public static final String ATTRIBUTES_REQUEST = "attributes request";
protected final MqttTransportContext context;
@ -257,12 +258,14 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) {
log.trace("[{}][{}][{}] onDeviceConnect: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
int msgId = getMsgId(msg);
AtomicBoolean ackSent = new AtomicBoolean(false);
process(onDeviceConnect(deviceName, deviceType),
result -> {
ack(msg, MqttReasonCodes.PubAck.SUCCESS);
log.trace("[{}][{}][{}] onDeviceConnectOk: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName);
},
t -> logDeviceCreationError(t, deviceName));
t -> processFailure(msgId, deviceName, CONNECT, ackSent, t));
}
public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional<DeviceProfile> deviceProfileOpt) {

2
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributesDao.java

@ -53,7 +53,7 @@ public interface AttributesDao {
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
List<String> findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List<EntityId> entityIds, String attributeType);
List<String> findAllKeysByEntityIdsAndScope(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope);
List<Pair<AttributeScope, String>> removeAllByEntityId(TenantId tenantId, EntityId entityId);

7
dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java

@ -28,7 +28,6 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ObjectType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.edqs.AttributeKv;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
@ -93,11 +92,11 @@ public class BaseAttributesService implements AttributesService {
}
@Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, String scope) {
if (StringUtils.isEmpty(scope)) {
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope) {
if (scope == null) {
return attributesDao.findAllKeysByEntityIds(tenantId, entityIds);
} else {
return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityIds, scope);
return attributesDao.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope);
}
}

6
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -212,11 +212,11 @@ public class CachedAttributesService implements AttributesService {
}
@Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, String scope) {
if (StringUtils.isEmpty(scope)) {
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope) {
if (scope == null) {
return attributesDao.findAllKeysByEntityIds(tenantId, entityIds);
} else {
return attributesDao.findAllKeysByEntityIdsAndAttributeType(tenantId, entityIds, scope);
return attributesDao.findAllKeysByEntityIdsAndScope(tenantId, entityIds, scope);
}
}

8
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java

@ -177,10 +177,12 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
}
@Override
public List<String> findAllKeysByEntityIdsAndAttributeType(TenantId tenantId, List<EntityId> entityIds, String attributeType) {
public List<String> findAllKeysByEntityIdsAndScope(TenantId tenantId, List<EntityId> entityIds, AttributeScope scope) {
return attributeKvRepository
.findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()), AttributeScope.valueOf(attributeType).getId())
.stream().map(id -> keyDictionaryDao.getKey(id)).collect(Collectors.toList());
.findAllKeysByEntityIdsAndAttributeType(entityIds.stream().map(EntityId::getId).toList(), scope.getId())
.stream()
.map(keyDictionaryDao::getKey)
.toList();
}
@Override

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/CachedRedisSqlTimeseriesLatestDao.java

@ -167,5 +167,9 @@ public class CachedRedisSqlTimeseriesLatestDao extends BaseAbstractSqlTimeseries
return sqlDao.findAllKeysByEntityIds(tenantId, entityIds);
}
@Override
public ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds) {
return sqlDao.findAllKeysByEntityIdsAsync(tenantId, entityIds);
}
}

12
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -24,7 +24,6 @@ import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
@ -38,8 +37,6 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.dictionary.KeyDictionaryDao;
@ -64,7 +61,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@Component
@ -185,9 +181,13 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
@Override
public List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds) {
return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).collect(Collectors.toList()));
return tsKvLatestRepository.findAllKeysByEntityIds(entityIds.stream().map(EntityId::getId).toList());
}
@Override
public ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds) {
return service.submit(() -> findAllKeysByEntityIds(tenantId, entityIds));
}
private ListenableFuture<TsKvLatestRemovingResult> getNewLatestEntryFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query, Long version) {
ListenableFuture<List<TsKvEntry>> future = findNewLatestEntryFuture(tenantId, entityId, query);
@ -211,7 +211,7 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
ReadTsKvQueryResult::getData, MoreExecutors.directExecutor());
}
protected TsKvEntry doFindLatestSync(EntityId entityId, String key) {
protected TsKvEntry doFindLatestSync(EntityId entityId, String key) {
TsKvLatestCompositeKey compositeKey =
new TsKvLatestCompositeKey(
entityId.getId(),

9
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -156,6 +156,11 @@ public class BaseTimeseriesService implements TimeseriesService {
return timeseriesLatestDao.findAllKeysByEntityIds(tenantId, entityIds);
}
@Override
public ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds) {
return timeseriesLatestDao.findAllKeysByEntityIdsAsync(tenantId, entityIds);
}
@Override
public void cleanup(long systemTtl) {
timeseriesDao.cleanup(systemTtl);
@ -300,13 +305,13 @@ public class BaseTimeseriesService implements TimeseriesService {
long interval = query.getInterval();
if (interval < 1) {
throw new IncorrectParameterException("Invalid TsKvQuery: 'interval' must be greater than 0, but got " + interval +
". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'.");
". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'.");
}
long step = Math.max(interval, 1000);
long intervalCounts = (query.getEndTs() - query.getStartTs()) / step;
if (intervalCounts > maxTsIntervals || intervalCounts < 0) {
throw new IncorrectParameterException("Incorrect TsKvQuery. Number of intervals is to high - " + intervalCounts + ". " +
"Please increase 'interval' parameter for your query or reduce the time range of the query.");
"Please increase 'interval' parameter for your query or reduce the time range of the query.");
}
}
}

8
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java

@ -36,17 +36,13 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import org.thingsboard.server.dao.nosql.TbResultSet;
import org.thingsboard.server.dao.sqlts.AggregationTimeseriesDao;
import org.thingsboard.server.dao.util.NoSqlTsLatestDao;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
@ -103,6 +99,10 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
return Collections.emptyList();
}
@Override
public ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds) {
return Futures.immediateFuture(Collections.emptyList());
}
@Override
public ListenableFuture<Long> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry) {

6
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java

@ -22,12 +22,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public interface TimeseriesLatestDao {
@ -54,4 +50,6 @@ public interface TimeseriesLatestDao {
List<String> findAllKeysByEntityIds(TenantId tenantId, List<EntityId> entityIds);
ListenableFuture<List<String>> findAllKeysByEntityIdsAsync(TenantId tenantId, List<EntityId> entityIds);
}

14
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -117,24 +117,26 @@ public class DeviceConnectivityUtil {
dockerComposeBuilder.append("\n");
dockerComposeBuilder.append(" # Environment variables\n");
dockerComposeBuilder.append(" environment:\n");
dockerComposeBuilder.append(" - host=").append(isLocalhost(host) ? HOST_DOCKER_INTERNAL : host).append("\n");
dockerComposeBuilder.append(" - port=1883\n");
dockerComposeBuilder.append(" - TB_GW_HOST=").append(isLocalhost(host) ? HOST_DOCKER_INTERNAL : host).append("\n");
dockerComposeBuilder.append(" - TB_GW_PORT=1883\n");
switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN:
dockerComposeBuilder.append(" - accessToken=").append(deviceCredentials.getCredentialsId()).append("\n");
dockerComposeBuilder.append(" - TB_GW_SECURITY_TYPE=accessToken\n");
dockerComposeBuilder.append(" - TB_GW_ACCESS_TOKEN=").append(deviceCredentials.getCredentialsId()).append("\n");
break;
case MQTT_BASIC:
dockerComposeBuilder.append(" - TB_GW_SECURITY_TYPE=usernamePassword\n");
BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(),
BasicMqttCredentials.class);
if (credentials != null) {
if (StringUtils.isNotEmpty(credentials.getClientId())) {
dockerComposeBuilder.append(" - clientId=").append(credentials.getClientId()).append("\n");
dockerComposeBuilder.append(" - TB_GW_CLIENT_ID=").append(credentials.getClientId()).append("\n");
}
if (StringUtils.isNotEmpty(credentials.getUserName())) {
dockerComposeBuilder.append(" - username=").append(credentials.getUserName()).append("\n");
dockerComposeBuilder.append(" - TB_GW_USERNAME=").append(credentials.getUserName()).append("\n");
}
if (StringUtils.isNotEmpty(credentials.getPassword())) {
dockerComposeBuilder.append(" - password=").append(credentials.getPassword()).append("\n");
dockerComposeBuilder.append(" - TB_GW_PASSWORD=").append(credentials.getPassword()).append("\n");
}
}
break;

2
dao/src/test/java/org/thingsboard/server/dao/service/attributes/BaseAttributesServiceTest.java

@ -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<String> keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE.name());
List<String> keys = attributesService.findAllKeysByEntityIds(tenantId, List.of(deviceId), AttributeScope.SERVER_SCOPE);
assertThat(keys).containsOnly("key1", "key2");
});
}

8
pom.xml

@ -110,7 +110,7 @@
<jakarta.el.version>4.0.2</jakarta.el.version>
<antisamy.version>1.7.5</antisamy.version>
<snmp4j.version>3.8.0</snmp4j.version>
<langchain4j.version>1.1.0</langchain4j.version>
<langchain4j.version>1.8.0-TB</langchain4j.version>
<error_prone_annotations.version>2.38.0</error_prone_annotations.version>
<animal-sniffer-annotations.version>1.24</animal-sniffer-annotations.version>
<auto-value-annotations.version>1.11.0</auto-value-annotations.version>
@ -911,7 +911,7 @@
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
@ -1908,6 +1908,10 @@
<id>central</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
<repository>
<id>thingsboard-repo</id>
<url>https://repo.thingsboard.io/artifactory/libs-release-public</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>

20
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -39,10 +39,12 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.rest.client.utils.RestJsonConverter;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.ClaimRequest;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@ -160,6 +162,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
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.EntityCountQuery;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -592,7 +595,7 @@ public class RestClient implements Closeable {
}
public PageData<AlarmInfo> getAllAlarmsV2(List<AlarmSearchStatus> statusList, List<AlarmSeverity> severityList,
List<String> typeList, String assignedId, TimePageLink pageLink) {
List<String> typeList, String assignedId, TimePageLink pageLink) {
String urlSecondPart = "/api/v2/alarms?";
Map<String, String> params = new HashMap<>();
if (!CollectionUtils.isEmpty(statusList)) {
@ -1824,12 +1827,15 @@ public class RestClient implements Closeable {
}).getBody();
}
public JsonNode findEntityTimeseriesAndAttributesKeysByQuery(EntityDataQuery query) {
return restTemplate.exchange(
baseURL + "/api/entitiesQuery/find/keys",
HttpMethod.POST, new HttpEntity<>(query),
new ParameterizedTypeReference<JsonNode>() {
}).getBody();
public AvailableEntityKeys findAvailableEntityKeysByQuery(EntityDataQuery query, boolean includeTimeseries, boolean includeAttributes, AttributeScope scope) {
var uri = UriComponentsBuilder.fromUriString(baseURL)
.path("/api/entitiesQuery/find/keys")
.queryParam("timeseries", includeTimeseries)
.queryParam("attributes", includeAttributes)
.queryParamIfPresent("scope", Optional.ofNullable(scope))
.build()
.toUri();
return restTemplate.exchange(uri, HttpMethod.POST, new HttpEntity<>(query), new ParameterizedTypeReference<AvailableEntityKeys>() {}).getBody();
}
public PageData<AlarmData> findAlarmDataByQuery(AlarmDataQuery query) {

2
rule-engine/rule-engine-api/pom.xml

@ -99,7 +99,7 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>

2
rule-engine/rule-engine-components/pom.xml

@ -154,7 +154,7 @@
<artifactId>json-path</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<groupId>org.thingsboard.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
</dependencies>

5
ui-ngx/package.json

@ -141,6 +141,9 @@
"rollup": "4.22.4",
"@babel/core": "7.25.2",
"esbuild": "0.23.0",
"jquery.terminal/coveralls-next/form-data": "4.0.4"
"jquery.terminal/coveralls-next/form-data": "4.0.4",
"node-forge": "1.3.3",
"glob": "10.5.0",
"path-to-regexp": "0.1.12"
}
}

4
ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.ts

@ -56,9 +56,7 @@ export class CheckConnectivityDialogComponent extends DialogComponent<CheckConne
},
chatModelConfig: {
modelType: ModelType.CHAT,
provider: this.data.AIModel.configuration.provider,
providerConfig: {...this.data.AIModel.configuration.providerConfig},
modelId: this.data.AIModel.configuration.modelId,
...this.data.AIModel.configuration,
maxRetries: 0,
timeoutSeconds: 20
}

16
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { Component, Inject, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -51,7 +51,7 @@ export interface AlarmDetailsDialogData {
templateUrl: './alarm-details-dialog.component.html',
styleUrls: ['./alarm-details-dialog.component.scss']
})
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> implements OnInit {
export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDialogComponent, boolean> {
alarmId: string;
alarmFormGroup: UntypedFormGroup;
@ -128,13 +128,12 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.alarmFormGroup.get('startTime')
.patchValue(this.datePipe.transform(alarm.startTs, 'yyyy-MM-dd HH:mm:ss'));
}
if (alarm.startTs || alarm.endTs) {
if (alarm.startTs || alarm.clearTs) {
let duration = '';
if (alarm.startTs && (alarm.status === AlarmStatus.ACTIVE_ACK || alarm.status === AlarmStatus.ACTIVE_UNACK)) {
if (alarm.startTs && !alarm.cleared) {
duration = this.millisecondsToTimeStringPipe.transform(Date.now() - alarm.startTs);
}
if (alarm.endTs && (alarm.status === AlarmStatus.CLEARED_ACK || alarm.status === AlarmStatus.CLEARED_UNACK)) {
duration = this.millisecondsToTimeStringPipe.transform(alarm.endTs - alarm.startTs);
} else if (alarm.clearTs && alarm.cleared) {
duration = this.millisecondsToTimeStringPipe.transform(alarm.clearTs - alarm.startTs);
}
this.alarmFormGroup.get('duration').patchValue(duration);
}
@ -144,9 +143,6 @@ export class AlarmDetailsDialogComponent extends DialogComponent<AlarmDetailsDia
this.alarmFormGroup.get('alarmDetails').patchValue(alarm.details);
}
ngOnInit(): void {
}
close(): void {
this.dialogRef.close(this.alarmUpdated);
}

11
ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss

@ -81,6 +81,12 @@ $warning-color: #FAA405;
.mat-divider {
--mat-divider-color: #{$tb-primary-color};
}
.api-item-content {
.api-item-title {
font-weight: 500;
color: $tb-primary-color;
}
}
}
.api-item-content {
@ -94,8 +100,9 @@ $warning-color: #FAA405;
display: flex;
flex: 1;
font-size: 14px;
font-weight: 500;
color: $tb-primary-color;
line-height: 20px;
font-weight: 400;
color: rgba(0, 0, 0, 0.54);
}
.api-item-statistic {
display: flex;

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts

@ -193,6 +193,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent {
hideDataKeyColor: true,
hideDataKeyDecimals: true,
hideDataKeyUnits: true,
hideDataKeyAggregation: true,
widget: this.widget,
dashboard: null,
dataKeySettingsForm: null,

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.html

@ -50,6 +50,7 @@
[hideDataKeyColor]="data.hideDataKeyColor"
[hideDataKeyUnits]="data.hideDataKeyUnits"
[hideDataKeyDecimals]="data.hideDataKeyDecimals"
[hideDataKeyAggregation]="data.hideDataKeyDecimals"
[supportsUnitConversion]="data.supportsUnitConversion"
formControlName="dataKey">
</tb-data-key-config>

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config-dialog.component.ts

@ -56,6 +56,7 @@ export interface DataKeyConfigDialogData {
hideDataKeyColor?: boolean;
hideDataKeyUnits?: boolean;
hideDataKeyDecimals?: boolean;
hideDataKeyAggregation?: boolean;
supportsUnitConversion?: boolean
}

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.html

@ -79,7 +79,7 @@
formControlName="funcBody">
</tb-js-func>
</section>
<ng-container *ngIf="widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries">
<ng-container *ngIf="!hideDataKeyAggregation && widgetType === widgetTypes.latest && modelValue.type === dataKeyTypes.timeseries">
<div class="tb-form-row column">
<div class="tb-form-row space-between no-border no-padding">
<div>{{ 'datakey.aggregation' | translate }}</div>

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/key/data-key-config.component.ts

@ -153,6 +153,10 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
@coerceBoolean()
hideDataKeyDecimals = false;
@Input()
@coerceBoolean()
hideDataKeyAggregation = false;
@Input()
@coerceBoolean()
supportsUnitConversion = false;

2
ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app.component.ts

@ -176,7 +176,7 @@ export class MobileAppComponent extends EntityComponent<MobileApp> {
context: ctx,
showCloseButton: false,
popoverContentStyle: {padding: '16px 24px'},
isModal: false
isModal: true
});
releaseNotesPanelPopover.tbComponentRef.instance.popover = releaseNotesPanelPopover;
releaseNotesPanelPopover.tbComponentRef.instance.editorContentApplied.subscribe((releaseNotes) => {

30
ui-ngx/src/app/modules/home/pages/mobile/common/editor-panel.component.ts

@ -59,9 +59,33 @@ export class EditorPanelComponent implements OnInit {
resize: false,
setup: (editor) => {
editor.on('PostRender', function() {
const container = editor.getContainer().closest('.tb-popover-content');
const uiContainer = document.querySelector('.tox.tox-tinymce-aux');
container.parentNode.appendChild(uiContainer);
const container = document.querySelector('.tox.tox-tinymce-aux');
const styleSheet = document.createElement('style');
styleSheet.innerText = `
.tox-tiered-menu .tox-menu {
width: fit-content;
max-width: min(80%, 440px);
@media screen and (max-width: 510px) {
max-width: calc(100% - 64px);
}
media screen and (min-width: 511px) and (max-width: 548px) {
max-width: calc(100% - 84px);
}
media screen and (min-width: 549px) and (max-width: 599px) {
max-width: calc(100% - 104px);
}
}
.tox-tiered-menu .tox-menu .tox-collection__item-label {
word-break: normal;
}
@media screen and (max-width: 890px) {
.tox-tiered-menu > .tox-collection--list:not(:first-child) {
left: auto !important;
right: 0 !important;
}
}
`;
container.prepend(styleSheet);
});
},
relative_urls: false,

2
ui-ngx/src/app/modules/home/pages/notification/recipient/recipient-notification-dialog.component.html

@ -70,6 +70,7 @@
<ng-container *ngIf="targetNotificationForm.get('configuration.usersFilter.filterByTenants').value; else tenantProfiles">
<tb-entity-list
formControlName="tenantsIds"
syncIdsWithDB
subscriptSizing="dynamic"
labelText="{{ 'tenant.tenants' | translate }}"
placeholderText="{{ 'tenant.tenants' | translate }}"
@ -80,6 +81,7 @@
<ng-template #tenantProfiles>
<tb-entity-list
formControlName="tenantProfilesIds"
syncIdsWithDB
subscriptSizing="dynamic"
labelText="{{ 'tenant-profile.tenant-profiles' | translate }}"
placeholderText="{{ 'tenant-profile.tenant-profiles' | translate }}"

31
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts

@ -95,6 +95,37 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
autofocus: false,
branding: false,
promotion: false,
setup: (editor) => {
editor.on('PostRender', function() {
const container = document.querySelector('.tox.tox-tinymce-aux');
const styleSheet = document.createElement('style');
styleSheet.innerText = `
.tox-tiered-menu .tox-menu {
width: fit-content;
max-width: min(80%, 440px);
@media screen and (max-width: 510px) {
max-width: calc(100% - 64px);
}
media screen and (min-width: 511px) and (max-width: 548px) {
max-width: calc(100% - 84px);
}
media screen and (min-width: 549px) and (max-width: 599px) {
max-width: calc(100% - 104px);
}
}
.tox-tiered-menu .tox-menu .tox-collection__item-label {
word-break: normal;
}
@media screen and (max-width: 890px) {
.tox-tiered-menu > .tox-collection--list:not(:first-child) {
left: auto !important;
right: 0 !important;
}
}
`;
container.prepend(styleSheet);
});
},
relative_urls: false,
urlconverter_callback: (url) => url
};

9
ui-ngx/src/app/shared/models/ai-model.models.ts

@ -110,6 +110,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
'o3',
'o3-mini',
'o1',
'gpt-5.1',
'gpt-5',
'gpt-5-mini',
'gpt-5-nano',
@ -135,6 +136,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.GOOGLE_AI_GEMINI,
{
modelList: [
'gemini-3-pro-preview',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
@ -149,6 +151,7 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.GOOGLE_VERTEX_AI_GEMINI,
{
modelList: [
'gemini-3-pro-preview',
'gemini-2.5-pro',
'gemini-2.5-flash',
'gemini-2.5-flash-lite',
@ -181,12 +184,10 @@ export const AiModelMap = new Map<AiProvider, { modelList: string[], providerFie
AiProvider.ANTHROPIC,
{
modelList: [
'claude-opus-4-5',
'claude-opus-4-1',
'claude-opus-4-0',
'claude-sonnet-4-5',
'claude-sonnet-4-0',
'claude-3-7-sonnet-latest',
'claude-3-5-haiku-latest',
'claude-haiku-4-5'
],
providerFieldsList: ['apiKey'],
modelFieldsList: ['temperature', 'topP', 'topK', 'maxOutputTokens'],

8
ui-ngx/src/app/shared/models/time/time.models.ts

@ -285,14 +285,14 @@ export const defaultTimewindow = (timeService: TimeService, isDashboard = false)
selectedTab: TimewindowType.REALTIME,
realtime: {
realtimeType: RealtimeWindowType.LAST_INTERVAL,
interval: SECOND,
timewindowMs: isDashboard ? HOUR : MINUTE,
interval: MINUTE,
timewindowMs: HOUR,
quickInterval: QuickTimeInterval.CURRENT_DAY,
},
history: {
historyType: HistoryWindowType.LAST_INTERVAL,
interval: SECOND,
timewindowMs: MINUTE,
interval: MINUTE,
timewindowMs: HOUR,
fixedTimewindow: {
startTimeMs: currentTime - DAY,
endTimeMs: currentTime

451
ui-ngx/src/assets/dashboard/api_usage.json

File diff suppressed because it is too large

8
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -6475,13 +6475,13 @@
"timewindow": {
"timewindow": "Time window",
"timewindow-settings": "Time window settings",
"years": "{ years, plural, =1 { year } other {# years } }",
"years": "{ years, plural, =1 {1 year } other {# years } }",
"years-short": "{{ years }}y",
"months": "{ months, plural, =1 { month } other {# months } }",
"months": "{ months, plural, =1 {1 month } other {# months } }",
"months-short": "{{ months }}M",
"weeks": "{ weeks, plural, =1 { week } other {# weeks } }",
"weeks": "{ weeks, plural, =1 {1 week } other {# weeks } }",
"weeks-short": "{{ weeks }}w",
"days": "{ days, plural, =1 { day } other {# days } }",
"days": "{ days, plural, =1 {1 day } other {# days } }",
"days-short": "{{ days }}d",
"hours": "{ hours, plural, =0 { hour } =1 {1 hour } other {# hours } }",
"hr": "{{ hr }} hr",

78
ui-ngx/yarn.lock

@ -5604,11 +5604,6 @@ fs-minipass@^3.0.0:
dependencies:
minipass "^7.0.3"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@ -5738,10 +5733,10 @@ glob-to-regexp@^0.4.1:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7:
version "10.4.5"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956"
integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
glob@10.5.0, glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7, glob@^7.1.3:
version "10.5.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^3.1.2"
@ -5750,18 +5745,6 @@ glob@^10.2.2, glob@^10.3.10, glob@^10.3.3, glob@^10.3.7:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
glob@^7.1.3:
version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
global-prefix@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-4.0.0.tgz#e9cc79aab9be1d03287e156a3f912dd0895463ed"
@ -6104,24 +6087,16 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ini@4.1.3, ini@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795"
@ -7170,7 +7145,7 @@ minimatch@9.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.1.1, minimatch@^3.1.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==
@ -7493,10 +7468,10 @@ node-fetch@^3.3.2:
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-forge@^1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
node-forge@1.3.3, node-forge@^1:
version "1.3.3"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
node-gyp-build-optional-packages@5.2.2:
version "5.2.2"
@ -7711,13 +7686,6 @@ on-headers@~1.0.2:
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
onetime@^5.1.0, onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
@ -7982,11 +7950,6 @@ path-exists@^5.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-5.0.0.tgz#a6aad9489200b21fab31e49cf09277e5116fb9e7"
integrity sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@ -8005,10 +7968,10 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
path-to-regexp@0.1.10:
version "0.1.10"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
path-to-regexp@0.1.10, path-to-regexp@0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
path-type@^5.0.0:
version "5.0.0"
@ -10079,11 +10042,6 @@ wrap-ansi@^9.0.0:
string-width "^7.0.0"
strip-ansi "^7.1.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@^8.16.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"

Loading…
Cancel
Save