Browse Source

Merge branch 'rc'

pull/13820/head
Igor Kulikov 1 year ago
parent
commit
04ecc107ba
  1. 16
      application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java
  2. 60
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  3. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java
  4. 43
      application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java
  5. 101
      application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java
  6. 7
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java
  7. 7
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java
  8. 3
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java
  9. 3
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java
  10. 3
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java
  11. 9
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  12. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
  13. 21
      pom.xml
  14. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java
  15. 5
      rule-engine/rule-engine-components/pom.xml
  16. 10
      ui-ngx/src/app/core/auth/auth.actions.ts
  17. 3
      ui-ngx/src/app/core/auth/auth.reducer.ts
  18. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts
  19. 5
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  20. 6
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss
  21. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html
  22. 53
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts
  23. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html
  24. 77
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts
  25. 2
      ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html
  26. 7
      ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts
  27. 2
      ui-ngx/src/app/shared/components/json-object-edit.component.scss

16
application/src/main/java/org/thingsboard/server/controller/AlarmCommentController.java

@ -19,12 +19,13 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; 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.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmComment;
@ -54,6 +55,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("/api") @RequestMapping("/api")
public class AlarmCommentController extends BaseController { public class AlarmCommentController extends BaseController {
public static final String ALARM_ID = "alarmId"; public static final String ALARM_ID = "alarmId";
public static final String ALARM_COMMENT_ID = "commentId"; public static final String ALARM_COMMENT_ID = "commentId";
@ -68,8 +70,7 @@ public class AlarmCommentController extends BaseController {
"\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." + "\n\n If comment type is not specified the default value 'OTHER' will be saved. If 'alarmId' or 'userId' specified in body it will be ignored." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/comment")
@ResponseBody
public AlarmComment saveAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public AlarmComment saveAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException { @PathVariable(ALARM_ID) String strAlarmId, @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the comment.") @RequestBody AlarmComment alarmComment) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -82,8 +83,7 @@ public class AlarmCommentController extends BaseController {
@ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)", @ApiOperation(value = "Delete Alarm comment (deleteAlarmComment)",
notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Deletes the Alarm comment. Referencing non-existing Alarm comment Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/comment/{commentId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/alarm/{alarmId}/comment/{commentId}")
@ResponseBody
public void deleteAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @Parameter(description = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException { public void deleteAlarmComment(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId, @Parameter(description = ALARM_COMMENT_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_COMMENT_ID) String strCommentId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
@ -98,8 +98,7 @@ public class AlarmCommentController extends BaseController {
notes = "Returns a page of alarm comments for specified alarm. " + notes = "Returns a page of alarm comments for specified alarm. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/comment", method = RequestMethod.GET) @GetMapping(value = "/alarm/{alarmId}/comment")
@ResponseBody
public PageData<AlarmCommentInfo> getAlarmComments( public PageData<AlarmCommentInfo> getAlarmComments(
@Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true) @Parameter(description = ALARM_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ALARM_ID) String strAlarmId, @PathVariable(ALARM_ID) String strAlarmId,
@ -118,4 +117,5 @@ public class AlarmCommentController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder); PageLink pageLink = createPageLink(pageSize, page, null, sortProperty, sortOrder);
return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink)); return checkNotNull(alarmCommentService.findAlarmComments(alarm.getTenantId(), alarmId, pageLink));
} }
} }

60
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -21,12 +21,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; 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.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
@ -58,7 +59,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION;
@ -104,8 +104,7 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Get Alarm (getAlarmById)", @ApiOperation(value = "Get Alarm (getAlarmById)",
notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK) notes = "Fetch the Alarm object based on the provided Alarm Id. " + ALARM_SECURITY_CHECK)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/{alarmId}")
@ResponseBody
public Alarm getAlarmById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public Alarm getAlarmById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -117,8 +116,7 @@ public class AlarmController extends BaseController {
notes = "Fetch the Alarm Info object based on the provided Alarm Id. " + notes = "Fetch the Alarm Info object based on the provided Alarm Id. " +
ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) ALARM_SECURITY_CHECK + ALARM_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/info/{alarmId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/info/{alarmId}")
@ResponseBody
public AlarmInfo getAlarmInfoById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public AlarmInfo getAlarmInfoById(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -136,11 +134,9 @@ public class AlarmController extends BaseController {
"If the user tries to create 'HighTemperature' alarm for the same device again, the previous alarm will be updated (the 'end_ts' will be set to current timestamp). " + "If the user tries to create 'HighTemperature' alarm for the same device again, the previous alarm will be updated (the 'end_ts' will be set to current timestamp). " +
"If the user clears the alarm (see 'Clear Alarm(clearAlarm)'), than new alarm with the same type and same device may be created. " + "If the user clears the alarm (see 'Clear Alarm(clearAlarm)'), than new alarm with the same type and same device may be created. " +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Alarm entity. " + "Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Alarm entity. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm", method = RequestMethod.POST) @PostMapping(value = "/alarm")
@ResponseBody
public Alarm saveAlarm(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { public Alarm saveAlarm(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException {
alarm.setTenantId(getTenantId()); alarm.setTenantId(getTenantId());
checkNotNull(alarm.getOriginator()); checkNotNull(alarm.getOriginator());
@ -155,8 +151,7 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Delete Alarm (deleteAlarm)", @ApiOperation(value = "Delete Alarm (deleteAlarm)",
notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) notes = "Deletes the Alarm. Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}", method = RequestMethod.DELETE) @DeleteMapping(value = "/alarm/{alarmId}")
@ResponseBody
public boolean deleteAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { public boolean deleteAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
@ -169,7 +164,7 @@ public class AlarmController extends BaseController {
"Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " + "Once acknowledged, the 'ack_ts' field will be set to current timestamp and special rule chain event 'ALARM_ACK' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/ack")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public AlarmInfo ackAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { public AlarmInfo ackAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -184,7 +179,7 @@ public class AlarmController extends BaseController {
"Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " + "Once cleared, the 'clear_ts' field will be set to current timestamp and special rule chain event 'ALARM_CLEAR' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/clear")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public AlarmInfo clearAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { public AlarmInfo clearAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception {
checkParameter(ALARM_ID, strAlarmId); checkParameter(ALARM_ID, strAlarmId);
@ -200,7 +195,7 @@ public class AlarmController extends BaseController {
"(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " + "(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/assign/{assigneeId}", method = RequestMethod.POST) @PostMapping(value = "/alarm/{alarmId}/assign/{assigneeId}")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public Alarm assignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public Alarm assignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId, @PathVariable(ALARM_ID) String strAlarmId,
@ -221,7 +216,7 @@ public class AlarmController extends BaseController {
"Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " + "Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " +
"Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{alarmId}/assign", method = RequestMethod.DELETE) @DeleteMapping(value = "/alarm/{alarmId}/assign")
@ResponseStatus(value = HttpStatus.OK) @ResponseStatus(value = HttpStatus.OK)
public Alarm unassignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION) public Alarm unassignAlarm(@Parameter(description = ALARM_ID_PARAM_DESCRIPTION)
@PathVariable(ALARM_ID) String strAlarmId @PathVariable(ALARM_ID) String strAlarmId
@ -236,8 +231,7 @@ public class AlarmController extends BaseController {
notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + notes = "Returns a page of alarms for the selected entity. Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/{entityType}/{entityId}")
@ResponseBody
public PageData<AlarmInfo> getAlarms( public PageData<AlarmInfo> getAlarms(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@ -265,7 +259,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long endTime, @RequestParam(required = false) Long endTime,
@Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
@RequestParam(required = false) Boolean fetchOriginator @RequestParam(required = false) Boolean fetchOriginator
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
checkParameter("EntityId", strEntityId); checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType); checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
@ -292,8 +286,7 @@ public class AlarmController extends BaseController {
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " + "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarms", method = RequestMethod.GET) @GetMapping(value = "/alarms")
@ResponseBody
public PageData<AlarmInfo> getAllAlarms( public PageData<AlarmInfo> getAllAlarms(
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, schema = @Schema(allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"})) @Parameter(description = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, schema = @Schema(allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"}))
@RequestParam(required = false) String searchStatus, @RequestParam(required = false) String searchStatus,
@ -317,7 +310,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long endTime, @RequestParam(required = false) Long endTime,
@Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION) @Parameter(description = ALARM_QUERY_FETCH_ORIGINATOR_DESCRIPTION)
@RequestParam(required = false) Boolean fetchOriginator @RequestParam(required = false) Boolean fetchOriginator
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus); AlarmSearchStatus alarmSearchStatus = StringUtils.isEmpty(searchStatus) ? null : AlarmSearchStatus.valueOf(searchStatus);
AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status); AlarmStatus alarmStatus = StringUtils.isEmpty(status) ? null : AlarmStatus.valueOf(status);
if (alarmSearchStatus != null && alarmStatus != null) { if (alarmSearchStatus != null && alarmStatus != null) {
@ -341,8 +334,7 @@ public class AlarmController extends BaseController {
notes = "Returns a page of alarms for the selected entity. " + notes = "Returns a page of alarms for the selected entity. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarm/{entityType}/{entityId}", method = RequestMethod.GET) @GetMapping(value = "/v2/alarm/{entityType}/{entityId}")
@ResponseBody
public PageData<AlarmInfo> getAlarmsV2( public PageData<AlarmInfo> getAlarmsV2(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@ -370,7 +362,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long startTime, @RequestParam(required = false) Long startTime,
@Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION) @Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime @RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
checkParameter("EntityId", strEntityId); checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType); checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId); EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
@ -407,8 +399,7 @@ public class AlarmController extends BaseController {
"If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " + "If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH) PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarms", method = RequestMethod.GET) @GetMapping(value = "/v2/alarms")
@ResponseBody
public PageData<AlarmInfo> getAllAlarmsV2( public PageData<AlarmInfo> getAllAlarmsV2(
@Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"}))) @Parameter(description = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, array = @ArraySchema(schema = @Schema(type = "string", allowableValues = {"ANY", "ACTIVE", "CLEARED", "ACK", "UNACK"})))
@RequestParam(required = false) String[] statusList, @RequestParam(required = false) String[] statusList,
@ -432,7 +423,7 @@ public class AlarmController extends BaseController {
@RequestParam(required = false) Long startTime, @RequestParam(required = false) Long startTime,
@Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION) @Parameter(description = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime @RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException { ) throws ThingsboardException {
List<AlarmSearchStatus> alarmStatusList = new ArrayList<>(); List<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) { if (statusList != null) {
for (String strStatus : statusList) { for (String strStatus : statusList) {
@ -465,11 +456,9 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)", @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)",
notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " + notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " +
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET) @GetMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}")
@ResponseBody
public AlarmSeverity getHighestAlarmSeverity( public AlarmSeverity getHighestAlarmSeverity(
@Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE")) @Parameter(description = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, schema = @Schema(defaultValue = "DEVICE"))
@PathVariable(ENTITY_TYPE) String strEntityType, @PathVariable(ENTITY_TYPE) String strEntityType,
@ -499,8 +488,7 @@ public class AlarmController extends BaseController {
@ApiOperation(value = "Get Alarm Types (getAlarmTypes)", @ApiOperation(value = "Get Alarm Types (getAlarmTypes)",
notes = "Returns a set of unique alarm types based on alarms that are either owned by the tenant or assigned to the customer which user is performing the request.") notes = "Returns a set of unique alarm types based on alarms that are either owned by the tenant or assigned to the customer which user is performing the request.")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/alarm/types", method = RequestMethod.GET) @GetMapping(value = "/alarm/types")
@ResponseBody
public PageData<EntitySubtype> getAlarmTypes(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true) public PageData<EntitySubtype> getAlarmTypes(@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize, @RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@ -508,7 +496,7 @@ public class AlarmController extends BaseController {
@Parameter(description = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION) @Parameter(description = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch, @RequestParam(required = false) String textSearch,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"})) @Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException, ExecutionException, InterruptedException { @RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, "type", sortOrder); PageLink pageLink = createPageLink(pageSize, page, textSearch, "type", sortOrder);
return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId(), pageLink)); return checkNotNull(alarmService.findAlarmTypesByTenantId(getTenantId(), pageLink));
} }

3
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmCommentService.java

@ -32,7 +32,7 @@ import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService{ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implements TbAlarmCommentService {
@Autowired @Autowired
private AlarmCommentService alarmCommentService; private AlarmCommentService alarmCommentService;
@ -68,4 +68,5 @@ public class DefaultTbAlarmCommentService extends AbstractTbEntityService implem
throw new ThingsboardException("System comment could not be deleted", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("System comment could not be deleted", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
} }
} }

43
application/src/main/java/org/thingsboard/server/service/housekeeper/processor/AlarmsDeletionTaskProcessor.java

@ -43,33 +43,30 @@ public class AlarmsDeletionTaskProcessor extends HousekeeperTaskProcessor<Alarms
EntityType entityType = entityId.getEntityType(); EntityType entityType = entityId.getEntityType();
TenantId tenantId = task.getTenantId(); TenantId tenantId = task.getTenantId();
if (entityType == EntityType.DEVICE || entityType == EntityType.ASSET) { if (task.getAlarms() == null) {
if (task.getAlarms() == null) { AlarmId lastId = null;
AlarmId lastId = null; long lastCreatedTime = 0;
long lastCreatedTime = 0; while (true) {
while (true) { List<TbPair<UUID, Long>> alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, lastCreatedTime, lastId, 128);
List<TbPair<UUID, Long>> alarms = alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, lastCreatedTime, lastId, 128); if (alarms.isEmpty()) {
if (alarms.isEmpty()) { break;
break; }
}
housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream().map(TbPair::getFirst).toList())); housekeeperClient.submitTask(new AlarmsDeletionHousekeeperTask(tenantId, entityId, alarms.stream().map(TbPair::getFirst).toList()));
TbPair<UUID, Long> last = alarms.get(alarms.size() - 1); TbPair<UUID, Long> last = alarms.get(alarms.size() - 1);
lastId = new AlarmId(last.getFirst()); lastId = new AlarmId(last.getFirst());
lastCreatedTime = last.getSecond(); lastCreatedTime = last.getSecond();
log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size()); log.debug("[{}][{}][{}] Submitted task for deleting {} alarms", tenantId, entityType, entityId, alarms.size());
}
} else {
for (UUID alarmId : task.getAlarms()) {
alarmService.delAlarm(tenantId, new AlarmId(alarmId));
}
log.debug("[{}][{}][{}] Deleted {} alarms", tenantId, entityType, entityId, task.getAlarms().size());
} }
int count = alarmService.deleteEntityAlarmRecords(tenantId, entityId);
log.debug("[{}][{}][{}] Deleted {} entity alarms", tenantId, entityType, entityId, count);
} else {
for (UUID alarmId : task.getAlarms()) {
alarmService.delAlarm(tenantId, new AlarmId(alarmId));
}
log.debug("[{}][{}][{}] Deleted {} alarms", tenantId, entityType, entityId, task.getAlarms().size());
} }
int count = alarmService.deleteEntityAlarmRecords(tenantId, entityId);
log.debug("[{}][{}][{}] Deleted {} entity alarms", tenantId, entityType, entityId, count);
} }
@Override @Override

101
application/src/test/java/org/thingsboard/server/service/housekeeper/HousekeeperServiceTest.java

@ -31,6 +31,8 @@ import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration; import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.EventInfo;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
@ -77,6 +79,8 @@ import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.entity.EntityServiceRegistry; import org.thingsboard.server.dao.entity.EntityServiceRegistry;
import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
@ -145,6 +149,10 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
private ApiUsageStateDao apiUsageStateDao; private ApiUsageStateDao apiUsageStateDao;
@Autowired @Autowired
private EntityServiceRegistry entityServiceRegistry; private EntityServiceRegistry entityServiceRegistry;
@Autowired
private CustomerService customerService;
@Autowired
private DashboardService dashboardService;
@SpyBean @SpyBean
private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor; private TsHistoryDeletionTaskProcessor tsHistoryDeletionTaskProcessor;
@ -238,11 +246,67 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(device.getId()); verifyNoAlarms(device.getId());
}); });
} }
@Test
public void whenAssetIsDeleted_thenDeleteAllAlarms() throws Exception {
Asset asset = createAsset();
for (int i = 1; i <= 1000; i++) {
createAlarm(asset.getId());
}
doDelete("/api/asset/" + asset.getId()).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(asset.getId());
});
}
@Test
public void whenDashboardIsDeleted_thenDeleteAllAlarms() throws Exception {
Dashboard dashboard = createDashboard();
for (int i = 1; i <= 1000; i++) {
createAlarm(dashboard.getId());
}
doDelete("/api/dashboard/" + dashboard.getId()).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(dashboard.getId());
});
}
@Test
public void whenCustomerIsDeleted_thenDeleteAllAlarms() throws Exception {
Customer customer = createCustomer();
for (int i = 1; i <= 1000; i++) {
createAlarm(customer.getId());
}
doDelete("/api/customer/" + customer.getId()).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(customer.getId());
});
}
@Test
public void whenUserIsDeleted_thenDeleteAllAlarms() throws Exception {
UserId userId = customerUserId;
for (int i = 1; i <= 1000; i++) {
createAlarm(userId);
}
doDelete("/api/user/" + userId).andExpect(status().isOk());
await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
verifyNoAlarms(userId);
});
}
@Test @Test
public void whenTenantIsDeleted_thenDeleteAllEntitiesAndCleanUpRelatedData() throws Exception { public void whenTenantIsDeleted_thenDeleteAllEntitiesAndCleanUpRelatedData() throws Exception {
loginDifferentTenant(); loginDifferentTenant();
@ -335,7 +399,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
int attempts = 2; int attempts = 2;
await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> {
for (int i = 0; i <= attempts; i++) { for (int i = 0; i <= attempts; i++) {
int attempt = i; int attempt = i;
verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY,
@ -345,7 +409,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
assertThat(getTimeseriesHistory(device.getId())).isNotEmpty(); assertThat(getTimeseriesHistory(device.getId())).isNotEmpty();
doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any());
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(getTimeseriesHistory(device.getId())).isEmpty(); assertThat(getTimeseriesHistory(device.getId())).isEmpty();
}); });
} }
@ -379,7 +443,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
int attempts = 2; int attempts = 2;
await().atMost(30, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).untilAsserted(() -> {
for (int i = 0; i <= attempts; i++) { for (int i = 0; i <= attempts; i++) {
int attempt = i; int attempt = i;
verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, verify(housekeeperReprocessingService).submitForReprocessing(argThat(getTaskMatcher(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY,
@ -393,7 +457,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any()); doCallRealMethod().when(tsHistoryDeletionTaskProcessor).process(any());
someExecutor.shutdown(); someExecutor.shutdown();
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(getTimeseriesHistory(device.getId())).isEmpty(); assertThat(getTimeseriesHistory(device.getId())).isEmpty();
}); });
} }
@ -409,7 +473,7 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
doDelete("/api/device/" + device.getId()).andExpect(status().isOk()); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
int maxAttempts = 5; int maxAttempts = 5;
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> { await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> {
for (int i = 1; i <= maxAttempts; i++) { for (int i = 1; i <= maxAttempts; i++) {
verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, i); verifyTaskProcessing(device.getId(), HousekeeperTaskType.DELETE_TS_HISTORY, i);
} }
@ -479,7 +543,6 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
eventService.saveAsync(event); eventService.saveAsync(event);
await().atMost(10, TimeUnit.SECONDS) await().atMost(10, TimeUnit.SECONDS)
.until(() -> !getEvents(entityId).isEmpty()); .until(() -> !getEvents(entityId).isEmpty());
} }
private void createRelation(DeviceId to, AssetId from) { private void createRelation(DeviceId to, AssetId from) {
@ -502,14 +565,14 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty(); assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty();
} }
private void createAlarm(DeviceId deviceId) { private void createAlarm(EntityId entityId) {
Alarm alarm = doPost("/api/alarm", Alarm.builder() doPost("/api/alarm", Alarm.builder()
.tenantId(tenantId) .tenantId(tenantId)
.originator(deviceId) .originator(entityId)
.severity(AlarmSeverity.CRITICAL) .severity(AlarmSeverity.CRITICAL)
.type("test alarm for " + deviceId + " " + RandomStringUtils.randomAlphabetic(10)) .type("test alarm for " + entityId + " " + RandomStringUtils.randomAlphabetic(10))
.build(), Alarm.class); .build(), Alarm.class);
assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, deviceId, 0, null, 10)).isNotEmpty(); assertThat(alarmService.findAlarmIdsByOriginatorId(tenantId, entityId, 0, null, 10)).isNotEmpty();
} }
private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception { private TsKvEntry getLatestTelemetry(EntityId entityId) throws Exception {
@ -534,6 +597,20 @@ public class HousekeeperServiceTest extends AbstractControllerTest {
return doPost("/api/asset", asset, Asset.class); return doPost("/api/asset", asset, Asset.class);
} }
private Customer createCustomer() {
Customer customer = new Customer();
customer.setTenantId(tenantId);
customer.setTitle(StringUtils.randomAlphabetic(10));
return customerService.saveCustomer(customer);
}
private Dashboard createDashboard() {
Dashboard dashboard = new Dashboard();
dashboard.setTenantId(tenantId);
dashboard.setTitle(StringUtils.randomAlphabetic(10));
return dashboardService.saveDashboard(dashboard);
}
private RuleChainMetaData createRuleChain() { private RuleChainMetaData createRuleChain() {
RuleChain ruleChain = new RuleChain(); RuleChain ruleChain = new RuleChain();
ruleChain.setTenantId(tenantId); ruleChain.setTenantId(tenantId);

7
common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java

@ -37,13 +37,11 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
/**
* Created by ashvayka on 11.05.17.
*/
@Schema @Schema
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ -52,6 +50,9 @@ import java.util.UUID;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, HasCustomerId { public class Alarm extends BaseData<AlarmId> implements HasName, HasTenantId, HasCustomerId {
@Serial
private static final long serialVersionUID = -1935800187424953611L;
@Schema(description = "JSON object with Tenant Id", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "JSON object with Tenant Id", accessMode = Schema.AccessMode.READ_ONLY)
private TenantId tenantId; private TenantId tenantId;

7
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmComment.java

@ -30,11 +30,17 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
@Schema @Schema
@Data @Data
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor
public class AlarmComment extends BaseData<AlarmCommentId> implements HasName { public class AlarmComment extends BaseData<AlarmCommentId> implements HasName {
@Serial
private static final long serialVersionUID = -5454905526404017592L;
@Schema(description = "JSON object with Alarm id.", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "JSON object with Alarm id.", accessMode = Schema.AccessMode.READ_ONLY)
private AlarmId alarmId; private AlarmId alarmId;
@Schema(description = "JSON object with User id.", accessMode = Schema.AccessMode.READ_ONLY) @Schema(description = "JSON object with User id.", accessMode = Schema.AccessMode.READ_ONLY)
@ -85,4 +91,5 @@ public class AlarmComment extends BaseData<AlarmCommentId> implements HasName {
this.comment = alarmComment.getComment(); this.comment = alarmComment.getComment();
this.userId = alarmComment.getUserId(); this.userId = alarmComment.getUserId();
} }
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java

@ -22,9 +22,6 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
/**
* Created by ashvayka on 11.05.17.
*/
@Data @Data
@Builder @Builder
@AllArgsConstructor @AllArgsConstructor

3
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSeverity.java

@ -15,9 +15,6 @@
*/ */
package org.thingsboard.server.common.data.alarm; package org.thingsboard.server.common.data.alarm;
/**
* Created by ashvayka on 11.05.17.
*/
public enum AlarmSeverity { public enum AlarmSeverity {
CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE; CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE;

3
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatus.java

@ -15,9 +15,6 @@
*/ */
package org.thingsboard.server.common.data.alarm; package org.thingsboard.server.common.data.alarm;
/**
* Created by ashvayka on 11.05.17.
*/
public enum AlarmStatus { public enum AlarmStatus {
ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK; ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK;

9
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -17,7 +17,6 @@ package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Function;
import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -68,7 +67,6 @@ import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import java.util.ArrayList; import java.util.ArrayList;
@ -98,7 +96,6 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
private final TenantService tenantService; private final TenantService tenantService;
private final AlarmDao alarmDao; private final AlarmDao alarmDao;
private final EntityService entityService; private final EntityService entityService;
private final DataValidator<Alarm> alarmDataValidator;
@TransactionalEventListener(classes = AlarmTypesCacheEvictEvent.class) @TransactionalEventListener(classes = AlarmTypesCacheEvictEvent.class)
@Override @Override
@ -444,12 +441,6 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
} }
} }
private <T> T getAndUpdate(TenantId tenantId, AlarmId alarmId, Function<Alarm, T> function) {
validateId(alarmId, "Alarm id should be specified!");
Alarm entity = alarmDao.findAlarmById(tenantId, alarmId.getId());
return function.apply(entity);
}
@Override @Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) { public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {
return Optional.ofNullable(findAlarmById(tenantId, new AlarmId(entityId.getId()))); return Optional.ofNullable(findAlarmById(tenantId, new AlarmId(entityId.getId())));

3
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java

@ -32,9 +32,6 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
/**
* Created by Valerii Sosliuk on 5/21/2017.
*/
public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> { public interface AlarmRepository extends JpaRepository<AlarmEntity, UUID> {
@Query("SELECT a FROM AlarmEntity a WHERE a.originatorId = :originatorId AND a.type = :alarmType ORDER BY a.startTs DESC") @Query("SELECT a FROM AlarmEntity a WHERE a.originatorId = :originatorId AND a.type = :alarmType ORDER BY a.startTs DESC")

21
pom.xml

@ -38,8 +38,7 @@
<pkg.implementationTitle>${project.name}</pkg.implementationTitle> <pkg.implementationTitle>${project.name}</pkg.implementationTitle>
<pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder> <pkg.unixLogFolder>/var/log/${pkg.name}</pkg.unixLogFolder>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder> <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<spring-boot.version>3.4.7</spring-boot.version> <spring-boot.version>3.4.8</spring-boot.version>
<tomcat.version>10.1.43</tomcat.version> <!-- to fix CVE-2025-52520 and CVE-2025-53506. TODO: remove when fixed in spring-boot-dependencies -->
<javax.xml.bind-api.version>2.4.0-b180830.0359</javax.xml.bind-api.version> <javax.xml.bind-api.version>2.4.0-b180830.0359</javax.xml.bind-api.version>
<jedis.version>5.1.5</jedis.version> <jedis.version>5.1.5</jedis.version>
<jjwt.version>0.12.5</jjwt.version> <jjwt.version>0.12.5</jjwt.version>
@ -52,6 +51,7 @@
<commons-io.version>2.16.1</commons-io.version> <commons-io.version>2.16.1</commons-io.version>
<commons-logging.version>1.3.1</commons-logging.version> <commons-logging.version>1.3.1</commons-logging.version>
<commons-csv.version>1.10.0</commons-csv.version> <commons-csv.version>1.10.0</commons-csv.version>
<nimbus-jose-jwt.version>10.0.2</nimbus-jose-jwt.version> <!-- to fix CVE-2023-52428, CVE-2025-53864. TODO: remove when fixed in spring-security-oauth2-client and mockserver-netty -->
<apache-httpclient.version>4.5.14</apache-httpclient.version> <apache-httpclient.version>4.5.14</apache-httpclient.version>
<joda-time.version>2.12.7</joda-time.version> <joda-time.version>2.12.7</joda-time.version>
<auth0-jwt.version>4.4.0</auth0-jwt.version> <auth0-jwt.version>4.4.0</auth0-jwt.version>
@ -121,7 +121,6 @@
<dbunit.version>2.7.3</dbunit.version> <dbunit.version>2.7.3</dbunit.version>
<java-websocket.version>1.5.6</java-websocket.version> <java-websocket.version>1.5.6</java-websocket.version>
<mock-server.version>5.15.0</mock-server.version> <mock-server.version>5.15.0</mock-server.version>
<nimbus-jose-jwt.version>9.37.2</nimbus-jose-jwt.version> <!-- to fix CVE-2023-52428. TODO: remove when fixed in mockserver-netty -->
<spring-test-dbunit.version>1.3.0</spring-test-dbunit.version> <!-- 2016 --> <spring-test-dbunit.version>1.3.0</spring-test-dbunit.version> <!-- 2016 -->
<takari-cpsuite.version>1.2.7</takari-cpsuite.version> <!-- 2015 --> <takari-cpsuite.version>1.2.7</takari-cpsuite.version> <!-- 2015 -->
<jeasy.version>5.0.0</jeasy.version> <jeasy.version>5.0.0</jeasy.version>
@ -1131,21 +1130,6 @@
<artifactId>jaxb-api</artifactId> <artifactId>jaxb-api</artifactId>
<version>${javax.xml.bind-api.version}</version> <version>${javax.xml.bind-api.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
@ -1782,7 +1766,6 @@
<groupId>com.nimbusds</groupId> <groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId> <artifactId>nimbus-jose-jwt</artifactId>
<version>${nimbus-jose-jwt.version}</version> <version>${nimbus-jose-jwt.version}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.mock-server</groupId> <groupId>org.mock-server</groupId>

3
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java

@ -42,9 +42,6 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery;
import java.util.Collection; import java.util.Collection;
/**
* Created by ashvayka on 02.04.18.
*/
public interface RuleEngineAlarmService { public interface RuleEngineAlarmService {
/* /*

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

@ -144,11 +144,6 @@
<artifactId>mockserver-netty</artifactId> <artifactId>mockserver-netty</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.mock-server</groupId> <groupId>org.mock-server</groupId>
<artifactId>mockserver-client-java</artifactId> <artifactId>mockserver-client-java</artifactId>

10
ui-ngx/src/app/core/auth/auth.actions.ts

@ -18,6 +18,7 @@ import { Action } from '@ngrx/store';
import { AuthUser, User } from '@shared/models/user.model'; import { AuthUser, User } from '@shared/models/user.model';
import { AuthPayload } from '@core/auth/auth.models'; import { AuthPayload } from '@core/auth/auth.models';
import { UserSettings } from '@shared/models/user-settings.models'; import { UserSettings } from '@shared/models/user-settings.models';
import { TrendzSettings } from "@shared/models/trendz-settings.models";
export enum AuthActionTypes { export enum AuthActionTypes {
AUTHENTICATED = '[Auth] Authenticated', AUTHENTICATED = '[Auth] Authenticated',
@ -31,6 +32,7 @@ export enum AuthActionTypes {
UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section', UPDATE_OPENED_MENU_SECTION = '[Preferences] Update Opened Menu Section',
PUT_USER_SETTINGS = '[Preferences] Put user settings', PUT_USER_SETTINGS = '[Preferences] Put user settings',
DELETE_USER_SETTINGS = '[Preferences] Delete user settings', DELETE_USER_SETTINGS = '[Preferences] Delete user settings',
UPDATE_TRENDZ_SETTINGS = '[Auth] Update Trendz Settings',
} }
export class ActionAuthAuthenticated implements Action { export class ActionAuthAuthenticated implements Action {
@ -97,7 +99,13 @@ export class ActionPreferencesDeleteUserSettings implements Action {
constructor(readonly payload: Array<NestedKeyOf<UserSettings>>) {} constructor(readonly payload: Array<NestedKeyOf<UserSettings>>) {}
} }
export class ActionAuthUpdateTrendzSettings implements Action {
readonly type = AuthActionTypes.UPDATE_TRENDZ_SETTINGS;
constructor(readonly payload: TrendzSettings) {}
}
export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated | export type AuthActions = ActionAuthAuthenticated | ActionAuthUnauthenticated |
ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository | ActionAuthLoadUser | ActionAuthUpdateUserDetails | ActionAuthUpdateLastPublicDashboardId | ActionAuthUpdateHasRepository |
ActionPreferencesUpdateOpenedMenuSection | ActionPreferencesPutUserSettings | ActionPreferencesDeleteUserSettings | ActionPreferencesUpdateOpenedMenuSection | ActionPreferencesPutUserSettings | ActionPreferencesDeleteUserSettings |
ActionAuthUpdateAuthUser | ActionUpdateMobileQrCodeEnabled; ActionAuthUpdateAuthUser | ActionUpdateMobileQrCodeEnabled | ActionAuthUpdateTrendzSettings;

3
ui-ngx/src/app/core/auth/auth.reducer.ts

@ -99,6 +99,9 @@ export const authReducer = (
action.payload.forEach(path => unset(userSettings, path)); action.payload.forEach(path => unset(userSettings, path));
return { ...state, ...{ userSettings }}; return { ...state, ...{ userSettings }};
case AuthActionTypes.UPDATE_TRENDZ_SETTINGS:
return { ...state, trendzSettings: action.payload };
default: default:
return state; return state;
} }

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.ts

@ -179,7 +179,7 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces
renderer: this.renderer, renderer: this.renderer,
componentType: CalculatedFieldArgumentPanelComponent, componentType: CalculatedFieldArgumentPanelComponent,
hostView: this.viewContainerRef, hostView: this.viewContainerRef,
preferredPlacement: isExists ? 'left' : 'right', preferredPlacement: isExists ? ['left', 'leftTop', 'leftBottom'] : ['topRight', 'right', 'rightTop'],
context: ctx, context: ctx,
isModal: true isModal: true
}); });

5
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -1187,8 +1187,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} }
openAlarmAssigneePanel($event: Event, entity: AlarmInfo) { openAlarmAssigneePanel($event: Event, entity: AlarmInfo) {
if ($event) { $event?.stopPropagation();
$event.stopPropagation(); if (entity.id.id === NULL_UUID) {
return
} }
const target = $event.target || $event.currentTarget; const target = $event.target || $event.currentTarget;
const config = new OverlayConfig(); const config = new OverlayConfig();

6
ui-ngx/src/app/modules/home/components/widget/lib/rpc/single-switch-widget.component.scss

@ -41,9 +41,9 @@ $switchColorDisabled: var(--tb-single-switch-color-disabled, #D5D7E5);
} }
> div.tb-single-switch-title-panel { > div.tb-single-switch-title-panel {
position: absolute; position: absolute;
top: 12px; top: 0;
left: 12px; left: 0;
right: 12px; right: 0;
z-index: 2; z-index: 2;
} }
.tb-single-switch-content { .tb-single-switch-content {

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.html

@ -74,8 +74,7 @@
</div> </div>
<ng-template #searchNotEmpty> <ng-template #searchNotEmpty>
<span> <span>
{{ translate.get('entity.no-alias-matching', {{ 'entity.no-alias-matching' | translate : {alias: (searchText | truncate: true: 6: &apos;...&apos;)} }}
{alias: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span> </span>
</ng-template> </ng-template>
<span> <span>

53
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/alias/entity-alias-select.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -26,18 +26,17 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, mergeMap, share, tap } from 'rxjs/operators'; import { map, mergeMap, share, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { EntityAlias } from '@shared/models/alias.models'; import { EntityAlias } from '@shared/models/alias.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { MatAutocomplete } from '@angular/material/autocomplete';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { EntityAliasSelectCallbacks } from './entity-alias-select.component.models'; import { EntityAliasSelectCallbacks } from './entity-alias-select.component.models';
import { ENTER } from '@angular/cdk/keycodes'; import { ENTER } from '@angular/cdk/keycodes';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'tb-entity-alias-select', selector: 'tb-entity-alias-select',
@ -47,11 +46,7 @@ import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EntityAliasSelectComponent), useExisting: forwardRef(() => EntityAliasSelectComponent),
multi: true multi: true
}/*, }]
{
provide: ErrorStateMatcher,
useExisting: EntityAliasSelectComponent
}*/]
}) })
export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, ErrorStateMatcher { export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit, ErrorStateMatcher {
@ -72,7 +67,6 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
showLabel: boolean; showLabel: boolean;
@ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete; @ViewChild('entityAliasAutocomplete') entityAliasAutocomplete: MatAutocomplete;
@ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger;
@Input() @Input()
@coerceBoolean() @coerceBoolean()
@ -93,21 +87,18 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
@ViewChild('entityAliasInput', {static: true}) entityAliasInput: ElementRef; @ViewChild('entityAliasInput', {static: true}) entityAliasInput: ElementRef;
entityAliasList: Array<EntityAlias> = [];
filteredEntityAliases: Observable<Array<EntityAlias>>; filteredEntityAliases: Observable<Array<EntityAlias>>;
searchText = ''; searchText = '';
private dirty = false; private dirty = false;
private entityAliasList: Array<EntityAlias> = [];
private propagateChange = (_v: any) => { }; private propagateChange = (_v: any) => { };
constructor(@SkipSelf() private errorStateMatcher: ErrorStateMatcher, constructor(@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private entityService: EntityService, private entityService: EntityService,
public translate: TranslateService, private fb: FormBuilder,
public truncate: TruncatePipe, private destroyRef: DestroyRef) {
private fb: FormBuilder) {
this.selectEntityAliasFormGroup = this.fb.group({ this.selectEntityAliasFormGroup = this.fb.group({
entityAlias: [null] entityAlias: [null]
}); });
@ -121,15 +112,7 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
} }
ngOnInit() { ngOnInit() {
const entityAliases = this.aliasController.getEntityAliases(); this.loadEntityAliases();
for (const aliasId of Object.keys(entityAliases)) {
if (this.allowedEntityTypes && this.allowedEntityTypes.length) {
if (!this.entityService.filterAliasByEntityTypes(entityAliases[aliasId], this.allowedEntityTypes)) {
continue;
}
}
this.entityAliasList.push(entityAliases[aliasId]);
}
this.filteredEntityAliases = this.selectEntityAliasFormGroup.get('entityAlias').valueChanges this.filteredEntityAliases = this.selectEntityAliasFormGroup.get('entityAlias').valueChanges
.pipe( .pipe(
@ -149,6 +132,12 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
mergeMap(name => this.fetchEntityAliases(name) ), mergeMap(name => this.fetchEntityAliases(name) ),
share() share()
); );
this.aliasController.entityAliasesChanged.pipe(
takeUntilDestroyed(this.destroyRef),
).subscribe(() => {
this.loadEntityAliases();
});
} }
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -271,4 +260,18 @@ export class EntityAliasSelectComponent implements ControlValueAccessor, OnInit,
); );
} }
} }
private loadEntityAliases(): void {
this.entityAliasList = [];
const entityAliases = this.aliasController.getEntityAliases();
for (const aliasId of Object.keys(entityAliases)) {
if (this.allowedEntityTypes?.length) {
if (!this.entityService.filterAliasByEntityTypes(entityAliases[aliasId], this.allowedEntityTypes)) {
continue;
}
}
this.entityAliasList.push(entityAliases[aliasId]);
}
this.dirty = true;
}
} }

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.html

@ -66,8 +66,7 @@
</div> </div>
<ng-template #searchNotEmpty> <ng-template #searchNotEmpty>
<span> <span>
{{ translate.get('filter.no-filter-matching', {{ 'filter.no-filter-matching' | translate : {filter: (searchText | truncate: true: 6: &apos;...&apos;)} }}
{filter: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span> </span>
</ng-template> </ng-template>
<span> <span>

77
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/filter/filter-select.component.ts

@ -14,31 +14,27 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core'; import { Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective, FormGroupDirective,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
NgForm NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, mergeMap, share, tap } from 'rxjs/operators'; import { map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { TruncatePipe } from '@shared/pipe/truncate.pipe'; import { MatAutocomplete } from '@angular/material/autocomplete';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { ENTER } from '@angular/cdk/keycodes'; import { ENTER } from '@angular/cdk/keycodes';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { FilterSelectCallbacks } from './filter-select.component.models'; import { FilterSelectCallbacks } from './filter-select.component.models';
import { Filter } from '@shared/models/query/query.models'; import { Filter } from '@shared/models/query/query.models';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({ @Component({
selector: 'tb-filter-select', selector: 'tb-filter-select',
@ -54,7 +50,7 @@ import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-
useExisting: FilterSelectComponent useExisting: FilterSelectComponent
}] }]
}) })
export class FilterSelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, ErrorStateMatcher { export class FilterSelectComponent implements ControlValueAccessor, OnInit, ErrorStateMatcher {
selectFilterFormGroup: UntypedFormGroup; selectFilterFormGroup: UntypedFormGroup;
@ -81,40 +77,27 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
subscriptSizing: SubscriptSizing = 'fixed'; subscriptSizing: SubscriptSizing = 'fixed';
@ViewChild('filterAutocomplete') filterAutocomplete: MatAutocomplete; @ViewChild('filterAutocomplete') filterAutocomplete: MatAutocomplete;
@ViewChild('autocomplete', { read: MatAutocompleteTrigger }) autoCompleteTrigger: MatAutocompleteTrigger;
private requiredValue: boolean;
get tbRequired(): boolean {
return this.requiredValue;
}
@Input() @Input()
set tbRequired(value: boolean) { @coerceBoolean()
this.requiredValue = coerceBooleanProperty(value); tbRequired: boolean;
}
@Input() @Input()
disabled: boolean; disabled: boolean;
@ViewChild('filterInput', {static: true}) filterInput: ElementRef; @ViewChild('filterInput', {static: true}) filterInput: ElementRef;
filterList: Array<Filter> = [];
filteredFilters: Observable<Array<Filter>>; filteredFilters: Observable<Array<Filter>>;
searchText = ''; searchText = '';
private dirty = false; private dirty = false;
private filterList: Array<Filter> = [];
private propagateChange = (_v: any) => { };
private creatingFilter = false; constructor(@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
private fb: UntypedFormBuilder,
private propagateChange = (v: any) => { }; private destroyRef: DestroyRef) {
constructor(private store: Store<AppState>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public translate: TranslateService,
public truncate: TruncatePipe,
private fb: UntypedFormBuilder) {
this.selectFilterFormGroup = this.fb.group({ this.selectFilterFormGroup = this.fb.group({
filter: [null] filter: [null]
}); });
@ -124,19 +107,16 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
this.propagateChange = fn; this.propagateChange = fn;
} }
registerOnTouched(fn: any): void { registerOnTouched(_fn: any): void {
} }
ngOnInit() { ngOnInit() {
const filters = this.aliasController.getFilters(); this.loadFilters();
for (const filterId of Object.keys(filters)) {
this.filterList.push(filters[filterId]);
}
this.filteredFilters = this.selectFilterFormGroup.get('filter').valueChanges this.filteredFilters = this.selectFilterFormGroup.get('filter').valueChanges
.pipe( .pipe(
tap(value => { tap(value => {
let modelValue; let modelValue: Filter;
if (typeof value === 'string' || !value) { if (typeof value === 'string' || !value) {
modelValue = null; modelValue = null;
} else { } else {
@ -151,6 +131,12 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
mergeMap(name => this.fetchFilters(name) ), mergeMap(name => this.fetchFilters(name) ),
share() share()
); );
this.aliasController.filtersChanged.pipe(
takeUntilDestroyed(this.destroyRef),
).subscribe(() => {
this.loadFilters();
});
} }
isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -159,8 +145,6 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
} }
ngAfterViewInit(): void {}
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
@ -227,7 +211,7 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
} }
textIsNotEmpty(text: string): boolean { textIsNotEmpty(text: string): boolean {
return (text && text != null && text.length > 0) ? true : false; return text?.length > 0;
} }
filterEnter($event: KeyboardEvent) { filterEnter($event: KeyboardEvent) {
@ -242,7 +226,6 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
createFilter($event: Event, filter: string, focusOnCancel = true) { createFilter($event: Event, filter: string, focusOnCancel = true) {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
this.creatingFilter = true;
if (this.callbacks && this.callbacks.createFilter) { if (this.callbacks && this.callbacks.createFilter) {
this.callbacks.createFilter(filter).subscribe((newFilter) => { this.callbacks.createFilter(filter).subscribe((newFilter) => {
if (!newFilter) { if (!newFilter) {
@ -253,7 +236,6 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
}, 0); }, 0);
} }
} else { } else {
this.filterList.push(newFilter);
this.modelValue = newFilter.id; this.modelValue = newFilter.id;
this.selectFilterFormGroup.get('filter').patchValue(newFilter, {emitEvent: true}); this.selectFilterFormGroup.get('filter').patchValue(newFilter, {emitEvent: true});
this.propagateChange(this.modelValue); this.propagateChange(this.modelValue);
@ -262,4 +244,13 @@ export class FilterSelectComponent implements ControlValueAccessor, OnInit, Afte
); );
} }
} }
private loadFilters(): void {
this.filterList = [];
const filters = this.aliasController.getFilters();
for (const filterId of Object.keys(filters)) {
this.filterList.push(filters[filterId]);
}
this.dirty = true;
}
} }

2
ui-ngx/src/app/modules/home/pages/admin/oauth2/domains/domain.component.html

@ -34,7 +34,7 @@
<div class="tb-form-row tb-standard-fields no-border no-padding"> <div class="tb-form-row tb-standard-fields no-border no-padding">
<mat-form-field class="flex"> <mat-form-field class="flex">
<mat-label translate>admin.oauth2.redirect-url-template</mat-label> <mat-label translate>admin.oauth2.redirect-url-template</mat-label>
<input matInput [value]="redirectURI()" readonly> <input matInput [value]="redirectURI()" readonly [disabled]="!isEdit && !isAdd">
<tb-copy-button <tb-copy-button
matSuffix matSuffix
miniButton="false" miniButton="false"

7
ui-ngx/src/app/modules/home/pages/admin/trendz-settings.component.ts

@ -21,6 +21,9 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TrendzSettingsService } from '@core/http/trendz-settings.service'; import { TrendzSettingsService } from '@core/http/trendz-settings.service';
import { TrendzSettings } from '@shared/models/trendz-settings.models'; import { TrendzSettings } from '@shared/models/trendz-settings.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Store } from "@ngrx/store";
import { AppState } from "@core/core.state";
import { ActionAuthUpdateTrendzSettings } from "@core/auth/auth.actions";
@Component({ @Component({
selector: 'tb-trendz-settings', selector: 'tb-trendz-settings',
@ -31,7 +34,8 @@ export class TrendzSettingsComponent extends PageComponent implements OnInit, Ha
trendzSettingsForm: FormGroup; trendzSettingsForm: FormGroup;
constructor(private fb: FormBuilder, constructor(protected store: Store<AppState>,
private fb: FormBuilder,
private trendzSettingsService: TrendzSettingsService, private trendzSettingsService: TrendzSettingsService,
private destroyRef: DestroyRef) { private destroyRef: DestroyRef) {
super(); super();
@ -93,6 +97,7 @@ export class TrendzSettingsComponent extends PageComponent implements OnInit, Ha
this.trendzSettingsService.saveTrendzSettings(trendzSettings) this.trendzSettingsService.saveTrendzSettings(trendzSettings)
.subscribe(() => { .subscribe(() => {
this.setTrendzSettings(trendzSettings); this.setTrendzSettings(trendzSettings);
this.store.dispatch(new ActionAuthUpdateTrendzSettings(trendzSettings))
}) })
} }
} }

2
ui-ngx/src/app/shared/components/json-object-edit.component.scss

@ -16,7 +16,7 @@
.tb-json-object { .tb-json-object {
position: relative; position: relative;
.fill-height { &.fill-height {
height: 100%; height: 100%;
} }

Loading…
Cancel
Save