Browse Source

merged with master

pull/9266/head
dashevchenko 3 years ago
parent
commit
3972aa2b2d
  1. 40
      .github/workflows/check-configuration-files.yml
  2. 12
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  3. 2
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  4. 2
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  5. 20
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  6. 7
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java
  7. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java
  8. 7
      application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java
  10. 80
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  11. 642
      application/src/main/resources/thingsboard.yml
  12. 65
      application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
  13. 36
      application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java
  14. 37
      application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
  15. 8
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  16. 64
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java
  17. 31
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  18. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java
  19. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java
  20. 2
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  21. 140
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  22. 46
      dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java
  23. 4
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java
  24. 25
      dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java
  25. 3
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java
  26. 9
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  27. 50
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  28. 2
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java
  29. 2
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java
  30. 5
      dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java
  31. 3
      dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java
  32. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java
  33. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  34. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java
  35. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java
  36. 8
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java
  37. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java
  38. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java
  39. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java
  40. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java
  41. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java
  42. 3
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  43. 3
      dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDao.java
  44. 8
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  45. 11
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmCommentServiceTest.java
  46. 52
      dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java
  47. 1
      pull_request_template.md
  48. 26
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java
  49. 122
      tools/src/main/python/check_yml_file.py
  50. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html
  51. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts
  52. 1
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html
  53. 5
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html
  54. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts
  55. 13
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  56. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  57. 6
      ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html
  58. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html
  59. 8
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html
  60. 11
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss
  61. 48
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts
  62. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts
  63. 6
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html
  64. 72
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss
  65. 75
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts
  66. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts
  67. 32
      ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html
  68. 88
      ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.scss
  69. 73
      ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts
  70. 2
      ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.models.ts
  71. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html
  72. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.html
  73. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts
  74. 13
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html
  75. 1
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.ts
  76. 16
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html
  77. 35
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  78. 54
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts
  79. 13
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html
  80. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts
  81. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts
  82. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html
  83. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html
  84. 12
      ui-ngx/src/assets/locale/locale.constant-en_US.json

40
.github/workflows/check-configuration-files.yml

@ -0,0 +1,40 @@
#
# Copyright © 2016-2023 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.
#
name: Check configuration files
on:
push:
branches:
- master
pull_request:
paths:
- 'application/src/main/resources/thingsboard.yml'
jobs:
build:
name: Check thingsboard.yml file
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10.2"
architecture: "x64"
env:
AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache
- name: Run Verification Script
run: python3 tools/src/main/python/check_yml_file.py

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

@ -286,7 +286,7 @@ public class AlarmController extends BaseController {
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get());
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)));
}
@ApiOperation(value = "Get All Alarms (getAllAlarms)",
@ -335,9 +335,9 @@ public class AlarmController extends BaseController {
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
if (getCurrentUser().isCustomerUser()) {
return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get());
return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)));
} else {
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get());
return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)));
}
}
@ -402,7 +402,7 @@ public class AlarmController extends BaseController {
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(entityId, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(entityId, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)));
}
@ApiOperation(value = "Get All Alarms (getAllAlarmsV2)",
@ -461,9 +461,9 @@ public class AlarmController extends BaseController {
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
if (getCurrentUser().isCustomerUser()) {
return checkNotNull(alarmService.findCustomerAlarmsV2(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
return checkNotNull(alarmService.findCustomerAlarmsV2(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)));
} else {
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)));
}
}

2
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -153,7 +153,7 @@ public class AssetController extends BaseController {
checkParameter(ASSET_ID, strAssetId);
AssetId assetId = new AssetId(toUUID(strAssetId));
Asset asset = checkAssetId(assetId, Operation.DELETE);
tbAssetService.delete(asset, getCurrentUser()).get();
tbAssetService.delete(asset, getCurrentUser());
}
@ApiOperation(value = "Assign asset to customer (assignAssetToCustomer)",

2
application/src/main/java/org/thingsboard/server/controller/DeviceController.java

@ -229,7 +229,7 @@ public class DeviceController extends BaseController {
checkParameter(DEVICE_ID, strDeviceId);
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId, Operation.DELETE);
tbDeviceService.delete(device, getCurrentUser()).get();
tbDeviceService.delete(device, getCurrentUser());
}
@ApiOperation(value = "Assign device to customer (assignDeviceToCustomer)",

20
application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java

@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.TenantId;
@ -46,7 +45,6 @@ import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Slf4j
public abstract class AbstractTbEntityService {
@ -63,26 +61,22 @@ public abstract class AbstractTbEntityService {
protected EdgeService edgeService;
@Autowired
protected AlarmService alarmService;
@Autowired @Lazy
@Autowired
@Lazy
protected AlarmSubscriptionService alarmSubscriptionService;
@Autowired
protected CustomerService customerService;
@Autowired
protected TbClusterService tbClusterService;
@Autowired(required = false) @Lazy
@Autowired(required = false)
@Lazy
private EntitiesVersionControlService vcService;
protected ListenableFuture<Void> removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) {
ListenableFuture<PageData<AlarmInfo>> alarmsFuture =
protected void removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) {
PageData<AlarmInfo> alarms =
alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false));
ListenableFuture<List<AlarmId>> alarmIdsFuture = Futures.transform(alarmsFuture, page ->
page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor);
return Futures.transform(alarmIdsFuture, ids -> {
ids.stream().map(alarmId -> alarmService.deleteAlarm(tenantId, alarmId)).collect(Collectors.toList());
return null;
}, dbExecutor);
alarms.getData().stream().map(AlarmInfo::getId).forEach(alarmId -> alarmService.delAlarm(tenantId, alarmId));
}
protected <T> T checkNotNull(T reference) throws ThingsboardException {

7
application/src/main/java/org/thingsboard/server/service/entitiy/asset/DefaultTbAssetService.java

@ -15,9 +15,9 @@
*/
package org.thingsboard.server.service.entitiy.asset;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
@ -72,15 +72,16 @@ public class DefaultTbAssetService extends AbstractTbEntityService implements Tb
}
@Override
public ListenableFuture<Void> delete(Asset asset, User user) {
@Transactional
public void delete(Asset asset, User user) {
ActionType actionType = ActionType.DELETED;
TenantId tenantId = asset.getTenantId();
AssetId assetId = asset.getId();
try {
removeAlarmsByEntityId(tenantId, assetId);
assetService.deleteAsset(tenantId, assetId);
notificationEntityService.logEntityAction(tenantId, assetId, asset, asset.getCustomerId(), actionType, user, assetId.toString());
tbClusterService.broadcastEntityStateChangeEvent(tenantId, assetId, ComponentLifecycleEvent.DELETED);
return removeAlarmsByEntityId(tenantId, assetId);
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET), actionType, user, e,
assetId.toString());

3
application/src/main/java/org/thingsboard/server/service/entitiy/asset/TbAssetService.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.service.entitiy.asset;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
@ -28,7 +27,7 @@ public interface TbAssetService {
Asset save(Asset asset, User user) throws Exception;
ListenableFuture<Void> delete(Asset asset, User user);
void delete(Asset asset, User user);
Asset assignAssetToCustomer(TenantId tenantId, AssetId assetId, Customer customer, User user) throws ThingsboardException;

7
application/src/main/java/org/thingsboard/server/service/entitiy/device/DefaultTbDeviceService.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
@ -91,15 +92,15 @@ public class DefaultTbDeviceService extends AbstractTbEntityService implements T
}
@Override
public ListenableFuture<Void> delete(Device device, User user) {
@Transactional
public void delete(Device device, User user) {
TenantId tenantId = device.getTenantId();
DeviceId deviceId = device.getId();
try {
removeAlarmsByEntityId(tenantId, deviceId);
deviceService.deleteDevice(tenantId, deviceId);
notificationEntityService.notifyDeleteDevice(tenantId, deviceId, device.getCustomerId(), device,
user, deviceId.toString());
return removeAlarmsByEntityId(tenantId, deviceId);
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.DEVICE), ActionType.DELETED,
user, e, deviceId.toString());

2
application/src/main/java/org/thingsboard/server/service/entitiy/device/TbDeviceService.java

@ -35,7 +35,7 @@ public interface TbDeviceService {
Device saveDeviceWithCredentials(Device device, DeviceCredentials deviceCredentials, User user) throws ThingsboardException;
ListenableFuture<Void> delete(Device device, User user);
void delete(Device device, User user);
Device assignDeviceToCustomer(TenantId tenantId, DeviceId deviceId, Customer customer, User user) throws ThingsboardException;

80
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -53,7 +53,6 @@ import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService;
@ -115,31 +114,6 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return withWsCallback(alarmService.unassignAlarm(tenantId, alarmId, assignTs));
}
@Override
public Alarm createOrUpdateAlarm(Alarm alarm) {
AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled());
if (result.isSuccessful()) {
onAlarmUpdated(result);
AlarmSeverity oldSeverity = result.getOldSeverity();
if (oldSeverity != null && !oldSeverity.equals(result.getAlarm().getSeverity())) {
AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm severity was updated from %s to %s", oldSeverity, result.getAlarm().getSeverity())))
.build();
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, null);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
}
}
if (result.isCreated()) {
apiUsageClient.report(alarm.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT);
}
return result.getAlarm();
}
@Override
public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) {
AlarmApiCallResult result = alarmService.delAlarm(tenantId, alarmId);
@ -147,26 +121,6 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return result.isSuccessful();
}
@Override
public ListenableFuture<Boolean> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) {
ListenableFuture<AlarmApiCallResult> result = Futures.immediateFuture(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs));
Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor);
return Futures.transform(result, AlarmApiCallResult::isSuccessful, wsCallBackExecutor);
}
@Override
public ListenableFuture<Boolean> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) {
AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details);
return Futures.transform(Futures.immediateFuture(result), AlarmApiCallResult::isSuccessful, wsCallBackExecutor);
}
@Override
public ListenableFuture<AlarmOperationResult> clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) {
AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details);
Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor);
return Futures.immediateFuture(new AlarmOperationResult(result));
}
@Override
public ListenableFuture<Alarm> findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId) {
return alarmService.findAlarmByIdAsync(tenantId, alarmId);
@ -183,22 +137,22 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query) {
public PageData<AlarmInfo> findAlarms(TenantId tenantId, AlarmQuery query) {
return alarmService.findAlarms(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
public PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
return alarmService.findCustomerAlarms(tenantId, customerId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
public PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return alarmService.findAlarmsV2(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
public PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return alarmService.findCustomerAlarmsV2(tenantId, customerId, query);
}
@ -218,23 +172,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
}
@Override
public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmService.findLatestByOriginatorAndType(tenantId, originator, type);
}
@Deprecated
private void onAlarmUpdated(AlarmOperationResult result) {
wsCallBackExecutor.submit(() -> {
AlarmInfo alarm = new AlarmInfo(result.getAlarm());
TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) {
forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY);
}, () -> {
return TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm);
});
}
});
public Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type);
}
@Override
@ -248,10 +187,9 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
TenantId tenantId = alarm.getTenantId();
for (EntityId entityId : result.getPropagatedEntitiesList()) {
forwardToSubscriptionManagerService(tenantId, entityId, subscriptionManagerService -> {
subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY);
}, () -> {
return TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm);
});
subscriptionManagerService.onAlarmUpdate(tenantId, entityId, alarm, TbCallback.EMPTY);
}, () -> TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, alarm)
);
}
notificationRuleProcessor.process(AlarmTrigger.builder()
.tenantId(tenantId)

642
application/src/main/resources/thingsboard.yml

File diff suppressed because it is too large

65
application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java

@ -41,6 +41,8 @@ import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.alarm.AlarmDao;
@ -48,6 +50,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
@ -806,10 +809,14 @@ public class AlarmControllerTest extends AbstractControllerTest {
}
private AlarmInfo createAlarm(String type) throws Exception {
return createAlarm(type, customerDevice.getId(), customerId);
}
private AlarmInfo createAlarm(String type, EntityId originatorId, CustomerId customerId) throws Exception {
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.customerId(customerId)
.originator(customerDevice.getId())
.originator(originatorId)
.severity(AlarmSeverity.CRITICAL)
.type(type)
.build();
@ -963,4 +970,60 @@ public class AlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundTypes.isEmpty());
}
@Test
public void testDeleteAlarmTypesAfterDeletingAlarmOriginator() throws Exception {
loginTenantAdmin();
List<Device> devices = new ArrayList<>();
List<String> types = new ArrayList<>();
for (int i = 0; i < 13; i++) {
Device device = new Device();
device.setName("Test_device_" + i);
devices.add(device = doPost("/api/device", device, Device.class));
types.add(createAlarm(TEST_ALARM_TYPE + i, device.getId(), null).getType());
}
devices.sort(Comparator.comparing(Device::getName));
Collections.sort(types);
List<String> foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference<PageData<EntitySubtype>>() {
})
.getData()
.stream()
.map(EntitySubtype::getType)
.sorted()
.collect(Collectors.toList());
Assert.assertEquals(13, foundTypes.size());
Assert.assertEquals(types, foundTypes);
for (int i = 0; i < 12; i++) {
doDelete("/api/device/" + devices.remove(0).getId()).andExpect(status().isOk());
types.remove(0);
}
foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference<PageData<EntitySubtype>>() {
})
.getData()
.stream()
.map(EntitySubtype::getType)
.collect(Collectors.toList());
Assert.assertEquals(types.size(), foundTypes.size());
Assert.assertEquals(types, foundTypes);
doDelete("/api/device/" + devices.get(0).getId()).andExpect(status().isOk());
foundTypes = doGetTyped("/api/alarm/types?pageSize=1024&page=0", new TypeReference<PageData<EntitySubtype>>() {
})
.getData()
.stream()
.map(EntitySubtype::getType)
.sorted()
.collect(Collectors.toList());
Assert.assertTrue(foundTypes.isEmpty());
}
}

36
application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java

@ -34,6 +34,9 @@ import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
@ -291,6 +294,39 @@ public class AssetControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString(msgErrorNoFound("Asset", assetIdStr))));
}
@Test
public void testDeleteAssetWithAlarmsAndAlarmTypes() throws Exception {
Asset asset = new Asset();
asset.setName("My asset");
asset.setType("default");
Asset savedAsset = doPost("/api/asset", asset, Asset.class);
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.originator(savedAsset.getId())
.severity(AlarmSeverity.CRITICAL)
.type("test_type")
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
doDelete("/api/asset/" + savedAsset.getId().getId().toString())
.andExpect(status().isOk());
String assetIdStr = savedAsset.getId().getId().toString();
doGet("/api/asset/" + assetIdStr)
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Asset", assetIdStr))));
doGet("/api/alarm/info/" + alarm.getId())
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Alarm", alarm.getId().getId().toString()))));
}
@Test
public void testDeleteAssetAssignedToEntityView() throws Exception {
Asset asset1 = new Asset();

37
application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java

@ -48,6 +48,10 @@ import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
@ -504,6 +508,39 @@ public class DeviceControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString(msgErrorNoFound("Device", savedDeviceId.getId().toString()))));
}
@Test
public void testDeleteDeviceWithAlarmsAndAlarmTypes() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
Alarm alarm = Alarm.builder()
.tenantId(tenantId)
.originator(savedDevice.getId())
.severity(AlarmSeverity.CRITICAL)
.type("test_type")
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
doDelete("/api/device/" + savedDevice.getId().getId().toString())
.andExpect(status().isOk());
String DeviceIdStr = savedDevice.getId().getId().toString();
doGet("/api/device/" + DeviceIdStr)
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Device", DeviceIdStr))));
doGet("/api/device/info/" + alarm.getId())
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Device", alarm.getId().getId().toString()))));
}
@Test
public void testSaveDeviceWithEmptyType() throws Exception {
Device device = new Device();

8
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -230,8 +230,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr);
await().atMost(10, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get();
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType);
long ts = System.currentTimeMillis();
await().atMost(15, TimeUnit.SECONDS)
@ -313,8 +313,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, attr);
await().atMost(10, TimeUnit.SECONDS)
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get() != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType).get();
.until(() -> alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType) != null);
Alarm alarm = alarmSubscriptionService.findLatestByOriginatorAndType(tenantId, device.getId(), alarmType);
getWsClient().waitForUpdate(true);
Notification notification = getWsClient().getLastDataUpdate().getUpdate();

64
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java

@ -1,64 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.alarm;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.EntityId;
import java.util.Collections;
import java.util.List;
@Builder
@Data
@AllArgsConstructor
@Deprecated
public class AlarmOperationResult {
private final Alarm alarm;
private final boolean successful;
private final boolean created;
private final AlarmSeverity oldSeverity;
private final List<EntityId> propagatedEntitiesList;
public AlarmOperationResult(Alarm alarm, boolean successful) {
this(alarm, successful, Collections.emptyList());
}
public AlarmOperationResult(Alarm alarm, boolean successful, List<EntityId> propagatedEntitiesList) {
this(alarm, successful, false, null, propagatedEntitiesList);
}
public AlarmOperationResult(Alarm alarm, boolean successful, boolean created, List<EntityId> propagatedEntitiesList) {
this.alarm = alarm;
this.successful = successful;
this.created = created;
this.propagatedEntitiesList = propagatedEntitiesList;
this.oldSeverity = null;
}
//Temporary while we have not removed the AlarmOperationResult.
public AlarmOperationResult(AlarmApiCallResult result) {
this.alarm = result.getAlarm() != null ? new Alarm(result.getAlarm()) : null;
this.successful = result.isSuccessful() && (result.isCreated() || result.isModified());
this.created = result.isCreated();
this.oldSeverity = result.getOldSeverity();
this.propagatedEntitiesList = result.getPropagatedEntitiesList();
}
}

31
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -81,27 +81,6 @@ public interface AlarmService extends EntityDaoService {
void delAlarmTypes(TenantId tenantId, Set<String> types);
/*
* Legacy API, before 3.5.
*/
@Deprecated(since = "3.5.0", forRemoval = true)
AlarmOperationResult createOrUpdateAlarm(Alarm alarm);
@Deprecated(since = "3.5.0", forRemoval = true)
AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled);
@Deprecated(since = "3.5.0", forRemoval = true)
ListenableFuture<AlarmOperationResult> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);
@Deprecated(since = "3.5.0", forRemoval = true)
ListenableFuture<AlarmOperationResult> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs);
@Deprecated(since = "3.5.0", forRemoval = true)
AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId);
@Deprecated(since = "3.5.0", forRemoval = true)
ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
// Other API
Alarm findAlarmById(TenantId tenantId, AlarmId alarmId);
@ -109,13 +88,13 @@ public interface AlarmService extends EntityDaoService {
AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId);
ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query);
PageData<AlarmInfo> findAlarms(TenantId tenantId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus, String assigneeId);
@ -129,6 +108,8 @@ public interface AlarmService extends EntityDaoService {
void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId);
void deleteEntityAlarmRecordsByTenantId(TenantId tenantId);
long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query);
PageData<EntitySubtype> findAlarmTypesByTenantId(TenantId tenantId, PageLink pageLink);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java

@ -36,4 +36,6 @@ public interface DeviceCredentialsService {
void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials);
void deleteDeviceCredentialsByDeviceId(TenantId tenantId, DeviceId deviceId);
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java

@ -76,6 +76,8 @@ public interface EntityViewService extends EntityDaoService {
List<EntityView> findEntityViewsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId);
boolean existsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId);
void deleteEntityView(TenantId tenantId, EntityViewId entityViewId);
void deleteEntityViewsByTenantId(TenantId tenantId);

2
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java

@ -86,6 +86,8 @@ public interface AlarmDao extends Dao<Alarm> {
void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId);
void deleteEntityAlarmRecordsByTenantId(TenantId tenantId);
AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled);
AlarmApiCallResult updateAlarm(AlarmUpdateRequest request);

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

@ -18,7 +18,6 @@ package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Function;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -162,51 +161,11 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
return result;
}
@Override
public AlarmOperationResult createOrUpdateAlarm(Alarm alarm) {
return createOrUpdateAlarm(alarm, true);
}
@Override
public AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled) {
alarmDataValidator.validate(alarm, Alarm::getTenantId);
try {
if (alarm.getStartTs() == 0L) {
alarm.setStartTs(System.currentTimeMillis());
}
if (alarm.getEndTs() == 0L) {
alarm.setEndTs(alarm.getStartTs());
}
alarm.setCustomerId(entityService.fetchEntityCustomerId(alarm.getTenantId(), alarm.getOriginator()).orElse(null));
if (alarm.getId() == null) {
// Atomic update and return alarm + assignee.
Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType());
if (existing == null || existing.getStatus().isCleared()) {
if (!alarmCreationEnabled) {
throw new ApiUsageLimitsExceededException("Alarms creation is disabled");
}
return createAlarm(alarm);
} else {
return updateAlarm(existing, alarm);
}
} else {
return updateAlarm(alarm);
}
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmDao.findLatestActiveByOriginatorAndType(tenantId, originator, type);
}
@Override
public ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) {
return alarmDao.findLatestByOriginatorAndTypeAsync(tenantId, originator, type);
}
@Override
public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId,
AlarmDataQuery query, Collection<EntityId> orderedEntityIds) {
@ -248,27 +207,6 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
}
}
@Override
@Transactional
public AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId) {
log.debug("Deleting Alarm Id: {}", alarmId);
Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId());
if (alarm == null) {
return new AlarmOperationResult(alarm, false);
}
AlarmOperationResult result = new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm)));
deleteEntityRelations(tenantId, alarm.getId());
alarmDao.removeById(tenantId, alarm.getUuidId());
return result;
}
private AlarmOperationResult createAlarm(Alarm alarm) throws InterruptedException, ExecutionException {
log.debug("New Alarm : {}", alarm);
Alarm saved = alarmDao.save(alarm.getTenantId(), alarm);
List<EntityId> propagatedEntitiesList = createEntityAlarmRecords(saved);
return new AlarmOperationResult(saved, true, true, propagatedEntitiesList);
}
private List<EntityId> createEntityAlarmRecords(Alarm alarm) throws ExecutionException, InterruptedException {
Set<EntityId> propagatedEntitiesSet = new LinkedHashSet<>();
propagatedEntitiesSet.add(alarm.getOriginator());
@ -299,61 +237,6 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
return relations.map(EntityRelation::getFrom).collect(Collectors.toCollection(LinkedHashSet::new));
}
private AlarmOperationResult updateAlarm(Alarm update) {
alarmDataValidator.validate(update, Alarm::getTenantId);
return getAndUpdate(update.getTenantId(), update.getId(),
(alarm) -> alarm == null ? null : updateAlarm(alarm, update));
}
private AlarmOperationResult updateAlarm(Alarm oldAlarm, Alarm newAlarm) {
boolean propagationEnabled = !oldAlarm.isPropagate() && newAlarm.isPropagate();
boolean propagationToOwnerEnabled = !oldAlarm.isPropagateToOwner() && newAlarm.isPropagateToOwner();
boolean propagationToTenantEnabled = !oldAlarm.isPropagateToTenant() && newAlarm.isPropagateToTenant();
AlarmSeverity oldAlarmSeverity = oldAlarm.getSeverity();
Alarm result = alarmDao.save(newAlarm.getTenantId(), merge(oldAlarm, newAlarm));
List<EntityId> propagatedEntitiesList;
if (propagationEnabled || propagationToOwnerEnabled || propagationToTenantEnabled) {
try {
propagatedEntitiesList = createEntityAlarmRecords(result);
} catch (InterruptedException | ExecutionException e) {
log.warn("Failed to update alarm relations [{}]", result, e);
throw new RuntimeException(e);
}
} else {
propagatedEntitiesList = new ArrayList<>(getPropagationEntityIds(result));
}
return new AlarmOperationResult(result, true, false, oldAlarmSeverity, propagatedEntitiesList);
}
@Override
public ListenableFuture<AlarmOperationResult> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTime) {
Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId());
if (alarm == null || alarm.getStatus().isAck()) {
return Futures.immediateFuture(new AlarmOperationResult(alarm, false));
} else {
alarm.setAcknowledged(true);
alarm.setAckTs(ackTime);
alarm = alarmDao.save(alarm.getTenantId(), alarm);
return Futures.immediateFuture(new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))));
}
}
@Override
public ListenableFuture<AlarmOperationResult> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTime) {
Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId());
if (alarm == null || alarm.getStatus().isCleared()) {
return Futures.immediateFuture(new AlarmOperationResult(alarm, false));
} else {
alarm.setCleared(true);
alarm.setClearTs(clearTime);
if (details != null) {
alarm.setDetails(details);
}
alarm = alarmDao.save(alarm.getTenantId(), alarm);
return Futures.immediateFuture(new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))));
}
}
@Override
public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime) {
var result = withPropagated(alarmDao.assignAlarm(tenantId, alarmId, assigneeId, assignTime));
@ -396,23 +279,23 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query) {
return Futures.immediateFuture(alarmDao.findAlarms(tenantId, query));
public PageData<AlarmInfo> findAlarms(TenantId tenantId, AlarmQuery query) {
return alarmDao.findAlarms(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query));
public PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) {
return alarmDao.findCustomerAlarms(tenantId, customerId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findAlarmsV2(tenantId, query));
public PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return alarmDao.findAlarmsV2(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findCustomerAlarmsV2(tenantId, customerId, query));
public PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return alarmDao.findCustomerAlarmsV2(tenantId, customerId, query);
}
@Override
@ -440,9 +323,16 @@ public class BaseAlarmService extends AbstractCachedEntityService<TenantId, Page
@Override
public void deleteEntityAlarmRelations(TenantId tenantId, EntityId entityId) {
log.trace("Executing deleteEntityAlarms [{}]", entityId);
alarmDao.deleteEntityAlarmRecords(tenantId, entityId);
}
@Override
public void deleteEntityAlarmRecordsByTenantId(TenantId tenantId) {
log.trace("Executing deleteEntityAlarmRecordsByTenantId [{}]", tenantId);
alarmDao.deleteEntityAlarmRecordsByTenantId(tenantId);
}
@Override
public long countAlarmsByQuery(TenantId tenantId, CustomerId customerId, AlarmCountQuery query) {
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);

46
dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java

@ -26,7 +26,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
@ -205,21 +204,25 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
@Override
@Transactional
public void deleteAsset(TenantId tenantId, AssetId assetId) {
log.trace("Executing deleteAsset [{}]", assetId);
validateId(assetId, INCORRECT_ASSET_ID + assetId);
deleteEntityRelations(tenantId, assetId);
Asset asset = assetDao.findById(tenantId, assetId.getId());
List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityId(asset.getTenantId(), assetId);
if (entityViews != null && !entityViews.isEmpty()) {
if (entityViewService.existsByTenantIdAndEntityId(tenantId, assetId)) {
throw new DataValidationException("Can't delete asset that has entity views!");
}
Asset asset = assetDao.findById(tenantId, assetId.getId());
alarmService.deleteEntityAlarmRelations(tenantId, assetId);
deleteAsset(tenantId, asset);
}
private void deleteAsset(TenantId tenantId, Asset asset) {
log.trace("Executing deleteAsset [{}]", asset.getId());
relationService.deleteEntityRelations(tenantId, asset.getAssetProfileId());
assetDao.removeById(tenantId, asset.getUuidId());
publishEvictEvent(new AssetCacheEvictEvent(asset.getTenantId(), asset.getName(), null));
countService.publishCountEntityEvictEvent(tenantId, EntityType.ASSET);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(assetId).build());
assetDao.removeById(tenantId, assetId.getId());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(asset.getId()).build());
}
@Override
@ -438,21 +441,20 @@ public class BaseAssetService extends AbstractCachedEntityService<AssetCacheKey,
return assetDao.findAssetsByTenantIdAndEdgeIdAndType(tenantId.getId(), edgeId.getId(), type, pageLink);
}
private PaginatedRemover<TenantId, Asset> tenantAssetsRemover =
new PaginatedRemover<TenantId, Asset>() {
private final PaginatedRemover<TenantId, Asset> tenantAssetsRemover = new PaginatedRemover<>() {
@Override
protected PageData<Asset> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return assetDao.findAssetsByTenantId(id.getId(), pageLink);
}
@Override
protected PageData<Asset> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return assetDao.findAssetsByTenantId(id.getId(), pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Asset entity) {
deleteAsset(tenantId, new AssetId(entity.getId().getId()));
}
};
@Override
protected void removeEntity(TenantId tenantId, Asset asset) {
deleteAsset(tenantId, asset);
}
};
private PaginatedRemover<CustomerId, Asset> customerAssetsUnasigner = new PaginatedRemover<CustomerId, Asset>() {
private final PaginatedRemover<CustomerId, Asset> customerAssetsUnasigner = new PaginatedRemover<CustomerId, Asset>() {
@Override
protected PageData<Asset> findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) {

4
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardDao.java

@ -18,6 +18,8 @@ package org.thingsboard.server.dao.dashboard;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.TenantEntityDao;
@ -40,4 +42,6 @@ public interface DashboardDao extends Dao<Dashboard>, TenantEntityDao, Exportabl
List<Dashboard> findByTenantIdAndTitle(UUID tenantId, String title);
PageData<DashboardId> findIdsByTenantId(TenantId tenantId, PageLink pageLink);
}

25
dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java

@ -364,19 +364,18 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb
return dashboardDao.findByTenantIdAndTitle(tenantId.getId(), title);
}
private PaginatedRemover<TenantId, DashboardInfo> tenantDashboardsRemover =
new PaginatedRemover<TenantId, DashboardInfo>() {
@Override
protected PageData<DashboardInfo> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return dashboardInfoDao.findDashboardsByTenantId(id.getId(), pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, DashboardInfo entity) {
deleteDashboard(tenantId, new DashboardId(entity.getUuidId()));
}
};
private final PaginatedRemover<TenantId, DashboardId> tenantDashboardsRemover = new PaginatedRemover<>() {
@Override
protected PageData<DashboardId> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return dashboardDao.findIdsByTenantId(id, pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, DashboardId dashboardId) {
deleteDashboard(tenantId, dashboardId);
}
};
@Override
public Optional<HasId<?>> findEntity(TenantId tenantId, EntityId entityId) {

3
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsDao.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.device;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.Dao;
@ -53,4 +54,6 @@ public interface DeviceCredentialsDao extends Dao<DeviceCredentials> {
*/
DeviceCredentials findByCredentialsId(TenantId tenantId, String credentialsId);
DeviceCredentials removeByDeviceId(TenantId tenantId, DeviceId deviceId);
}

9
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java

@ -400,4 +400,13 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
publishEvictEvent(new DeviceCredentialsEvictEvent(deviceCredentials.getCredentialsId(), null));
}
@Override
public void deleteDeviceCredentialsByDeviceId(TenantId tenantId, DeviceId deviceId) {
log.trace("Executing deleteDeviceCredentialsByDeviceId [{}]", deviceId);
DeviceCredentials credentials = deviceCredentialsDao.removeByDeviceId(tenantId, deviceId);
if (credentials != null) {
publishEvictEvent(new DeviceCredentialsEvictEvent(credentials.getCredentialsId(), null));
}
}
}

50
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -316,26 +316,27 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
@Transactional
@Override
public void deleteDevice(final TenantId tenantId, final DeviceId deviceId) {
log.trace("Executing deleteDevice [{}]", deviceId);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
Device device = deviceDao.findById(tenantId, deviceId.getId());
DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), null);
List<EntityView> entityViews = entityViewService.findEntityViewsByTenantIdAndEntityId(device.getTenantId(), deviceId);
if (entityViews != null && !entityViews.isEmpty()) {
if (entityViewService.existsByTenantIdAndEntityId(tenantId, deviceId)) {
throw new DataValidationException("Can't delete device that has entity views!");
}
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, deviceId);
if (deviceCredentials != null) {
deviceCredentialsService.deleteDeviceCredentials(tenantId, deviceCredentials);
}
deleteEntityRelations(tenantId, deviceId);
deviceDao.removeById(tenantId, deviceId.getId());
Device device = deviceDao.findById(tenantId, deviceId.getId());
alarmService.deleteEntityAlarmRelations(tenantId, deviceId);
deleteDevice(tenantId, device);
}
private void deleteDevice(TenantId tenantId, Device device) {
log.trace("Executing deleteDevice [{}]", device.getId());
deviceCredentialsService.deleteDeviceCredentialsByDeviceId(tenantId, device.getId());
relationService.deleteEntityRelations(tenantId, device.getId());
deviceDao.removeById(tenantId, device.getUuidId());
DeviceCacheEvictEvent deviceCacheEvictEvent = new DeviceCacheEvictEvent(device.getTenantId(), device.getId(), device.getName(), null);
publishEvictEvent(deviceCacheEvictEvent);
countService.publishCountEntityEvictEvent(tenantId, EntityType.DEVICE);
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(deviceId).build());
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entityId(device.getId()).build());
}
@Override
@ -650,21 +651,20 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
deleteDevice(tenantId, (DeviceId) id);
}
private PaginatedRemover<TenantId, Device> tenantDevicesRemover =
new PaginatedRemover<>() {
private final PaginatedRemover<TenantId, Device> tenantDevicesRemover = new PaginatedRemover<>() {
@Override
protected PageData<Device> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return deviceDao.findDevicesByTenantId(id.getId(), pageLink);
}
@Override
protected PageData<Device> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return deviceDao.findDevicesByTenantId(id.getId(), pageLink);
}
@Override
protected void removeEntity(TenantId tenantId, Device entity) {
deleteDevice(tenantId, new DeviceId(entity.getUuidId()));
}
};
@Override
protected void removeEntity(TenantId tenantId, Device device) {
deleteDevice(tenantId, device);
}
};
private PaginatedRemover<CustomerId, Device> customerDeviceUnasigner = new PaginatedRemover<CustomerId, Device>() {
private final PaginatedRemover<CustomerId, Device> customerDeviceUnasigner = new PaginatedRemover<CustomerId, Device>() {
@Override
protected PageData<Device> findEntities(TenantId tenantId, CustomerId id, PageLink pageLink) {

2
dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java

@ -74,9 +74,7 @@ public abstract class AbstractEntityService {
}
protected void deleteEntityRelations(TenantId tenantId, EntityId entityId) {
log.trace("Executing deleteEntityRelations [{}]", entityId);
relationService.deleteEntityRelations(tenantId, entityId);
log.trace("Executing deleteEntityAlarms [{}]", entityId);
alarmService.deleteEntityAlarmRelations(tenantId, entityId);
}

2
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewDao.java

@ -148,6 +148,8 @@ public interface EntityViewDao extends Dao<EntityView>, ExportableEntityDao<Enti
List<EntityView> findEntityViewsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
/**
* Find tenants entity view types.
*

5
dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java

@ -316,6 +316,11 @@ public class EntityViewServiceImpl extends AbstractCachedEntityService<EntityVie
EntityViewCacheValue::getEntityViews, v -> new EntityViewCacheValue(null, v), true);
}
@Override
public boolean existsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId) {
return entityViewDao.existsByTenantIdAndEntityId(tenantId.getId(), entityId.getId());
}
@Override
@Transactional
public void deleteEntityView(TenantId tenantId, EntityViewId entityViewId) {

3
dao/src/main/java/org/thingsboard/server/dao/service/PaginatedRemover.java

@ -15,12 +15,11 @@
*/
package org.thingsboard.server.dao.service;
import org.thingsboard.server.common.data.id.IdBased;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
public abstract class PaginatedRemover<I, D extends IdBased<?>> {
public abstract class PaginatedRemover<I, D> {
private static final int DEFAULT_LIMIT = 100;

4
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/EntityAlarmRepository.java

@ -34,4 +34,8 @@ public interface EntityAlarmRepository extends JpaRepository<EntityAlarmEntity,
@Modifying
@Query("DELETE FROM EntityAlarmEntity e where e.entityId = :entityId")
void deleteByEntityId(@Param("entityId") UUID entityId);
@Transactional
void deleteByTenantId(UUID tenantId);
}

6
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -310,10 +310,14 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
@Override
public void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId) {
log.trace("[{}] Try to delete entity alarm records using [{}]", tenantId, entityId);
entityAlarmRepository.deleteByEntityId(entityId.getId());
}
@Override
public void deleteEntityAlarmRecordsByTenantId(TenantId tenantId) {
entityAlarmRepository.deleteByTenantId(tenantId.getId());
}
@Override
public AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) {
AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation());

3
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardRepository.java

@ -40,4 +40,7 @@ public interface DashboardRepository extends JpaRepository<DashboardEntity, UUID
@Query("SELECT externalId FROM DashboardEntity WHERE id = :id")
UUID getExternalIdById(@Param("id") UUID id);
@Query("SELECT d.id FROM DashboardEntity d WHERE d.tenantId = :tenantId")
Page<UUID> findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable);
}

5
dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java

@ -80,6 +80,11 @@ public class JpaDashboardDao extends JpaAbstractDao<DashboardEntity, Dashboard>
return DaoUtil.convertDataList(dashboardRepository.findByTenantIdAndTitle(tenantId, title));
}
@Override
public PageData<DashboardId> findIdsByTenantId(TenantId tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(dashboardRepository.findIdsByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink)).map(DashboardId::new));
}
@Override
public EntityType getEntityType() {
return EntityType.DASHBOARD;

8
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceCredentialsRepository.java

@ -16,6 +16,9 @@
package org.thingsboard.server.dao.sql.device;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.DeviceCredentialsEntity;
import java.util.UUID;
@ -28,4 +31,9 @@ public interface DeviceCredentialsRepository extends JpaRepository<DeviceCredent
DeviceCredentialsEntity findByDeviceId(UUID deviceId);
DeviceCredentialsEntity findByCredentialsId(String credentialsId);
@Transactional
@Query(value = "DELETE FROM device_credentials WHERE device_id = :deviceId RETURNING *", nativeQuery = true)
DeviceCredentialsEntity deleteByDeviceId(@Param("deviceId") UUID deviceId);
}

7
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java

@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.dao.DaoUtil;
@ -66,4 +67,10 @@ public class JpaDeviceCredentialsDao extends JpaAbstractDao<DeviceCredentialsEnt
public DeviceCredentials findByCredentialsId(TenantId tenantId, String credentialsId) {
return DaoUtil.getData(deviceCredentialsRepository.findByCredentialsId(credentialsId));
}
@Override
public DeviceCredentials removeByDeviceId(TenantId tenantId, DeviceId deviceId) {
return DaoUtil.getData(deviceCredentialsRepository.deleteByDeviceId(deviceId.getId()));
}
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java

@ -118,6 +118,8 @@ public interface EntityViewRepository extends JpaRepository<EntityViewEntity, UU
List<EntityViewEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId);
boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId);
@Query("SELECT DISTINCT ev.type FROM EntityViewEntity ev WHERE ev.tenantId = :tenantId")
List<String> findTenantEntityViewTypes(@Param("tenantId") UUID tenantId);

5
dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java

@ -165,6 +165,11 @@ public class JpaEntityViewDao extends JpaAbstractDao<EntityViewEntity, EntityVie
entityViewRepository.findAllByTenantIdAndEntityId(tenantId, entityId));
}
@Override
public boolean existsByTenantIdAndEntityId(UUID tenantId, UUID entityId) {
return entityViewRepository.existsByTenantIdAndEntityId(tenantId, entityId);
}
@Override
public ListenableFuture<List<EntitySubtype>> findTenantEntityViewTypesAsync(UUID tenantId) {
return service.submit(() -> convertTenantEntityTypesToDto(tenantId, EntityType.ENTITY_VIEW, entityViewRepository.findTenantEntityViewTypes(tenantId)));

7
dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserCredentialsDao.java

@ -19,6 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.UserCredentialsEntity;
@ -62,4 +63,10 @@ public class JpaUserCredentialsDao extends JpaAbstractDao<UserCredentialsEntity,
public UserCredentials findByResetToken(TenantId tenantId, String resetToken) {
return DaoUtil.getData(userCredentialsRepository.findByResetToken(resetToken));
}
@Override
public void removeByUserId(TenantId tenantId, UserId userId) {
userCredentialsRepository.removeByUserId(userId.getId());
}
}

5
dao/src/main/java/org/thingsboard/server/dao/sql/user/UserCredentialsRepository.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.sql.user;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.UserCredentialsEntity;
import java.util.UUID;
@ -30,4 +31,8 @@ public interface UserCredentialsRepository extends JpaRepository<UserCredentials
UserCredentialsEntity findByActivateToken(String activateToken);
UserCredentialsEntity findByResetToken(String resetToken);
@Transactional
void removeByUserId(UUID userId);
}

3
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -245,7 +245,8 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
tenantDao.removeById(tenantId, tenantId.getId());
publishEvictEvent(new TenantEvictEvent(tenantId, true));
eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(TenantId.SYS_TENANT_ID).entityId(tenantId).build());
deleteEntityRelations(tenantId, tenantId);
relationService.deleteEntityRelations(tenantId, tenantId);
alarmService.deleteEntityAlarmRecordsByTenantId(tenantId);
}
@Override

3
dao/src/main/java/org/thingsboard/server/dao/user/UserCredentialsDao.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.user;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.Dao;
@ -58,4 +59,6 @@ public interface UserCredentialsDao extends Dao<UserCredentials> {
*/
UserCredentials findByResetToken(TenantId tenantId, String resetToken);
void removeByUserId(TenantId tenantId, UserId userId);
}

8
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -252,10 +252,10 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
UserId userId = user.getId();
log.trace("[{}] Executing deleteUser [{}]", tenantId, userId);
validateId(userId, INCORRECT_USER_ID + userId);
UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId());
userCredentialsDao.removeById(tenantId, userCredentials.getUuidId());
userCredentialsDao.removeByUserId(tenantId, userId);
userAuthSettingsDao.removeByUserId(userId);
deleteEntityRelations(tenantId, userId);
userDao.removeById(tenantId, userId.getId());
eventPublisher.publishEvent(new UserCredentialsInvalidationEvent(userId));
countService.publishCountEntityEvictEvent(tenantId, EntityType.USER);
@ -446,8 +446,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
}
@Override
protected void removeEntity(TenantId tenantId, User entity) {
deleteUser(tenantId, entity);
protected void removeEntity(TenantId tenantId, User user) {
deleteUser(tenantId, user);
}
};

11
dao/src/test/java/org/thingsboard/server/dao/service/AlarmCommentServiceTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentInfo;
import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.TenantId;
@ -60,11 +61,12 @@ public class AlarmCommentServiceTest extends AbstractServiceTest {
@Before
public void before() {
alarm = Alarm.builder().tenantId(tenantId).originator(new AssetId(Uuids.timeBased()))
alarm = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder()
.tenantId(tenantId)
.originator(new AssetId(Uuids.timeBased()))
.type(TEST_ALARM)
.severity(AlarmSeverity.CRITICAL)
.startTs(System.currentTimeMillis()).build();
alarm = alarmService.createOrUpdateAlarm(alarm).getAlarm();
.startTs(System.currentTimeMillis()).build()).getAlarm();
user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
@ -77,10 +79,9 @@ public class AlarmCommentServiceTest extends AbstractServiceTest {
@After
public void after() {
alarmService.deleteAlarm(tenantId, alarm.getId());
alarmService.delAlarm(tenantId, alarm.getId());
}
@Test
public void testCreateAndFetchAlarmComment() throws ExecutionException, InterruptedException {
AlarmComment alarmComment = AlarmComment.builder().alarmId(alarm.getId())

52
dao/src/test/java/org/thingsboard/server/dao/service/AlarmServiceTest.java

@ -146,7 +146,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -157,7 +157,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -171,7 +171,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -182,7 +182,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -195,7 +195,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_ACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -206,7 +206,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -218,7 +218,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.CLEARED_ACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -245,11 +245,11 @@ public class AlarmServiceTest extends AbstractServiceTest {
// Check child relation
PageData<AlarmInfo> alarms = alarmService.findAlarmsV2(tenantId, AlarmQueryV2.builder()
.affectedEntityId(childId)
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
.severityList(Arrays.asList(AlarmSeverity.CRITICAL))
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -261,7 +261,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -276,7 +276,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -288,7 +288,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -302,7 +302,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.ACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -314,7 +314,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.ACTIVE, AlarmSearchStatus.UNACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -327,7 +327,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.statusList(Arrays.asList(AlarmSearchStatus.CLEARED, AlarmSearchStatus.ACK)).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -371,7 +371,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.fetchOriginator(true)
.pageLink(new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, alarms.getData().get(0));
@ -466,7 +466,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(10, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(tenantAlarm, new AlarmInfo(alarms.getData().get(0)));
@ -752,7 +752,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(10, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarmsInfoData.getData());
Assert.assertEquals(1, alarmsInfoData.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0)));
@ -763,7 +763,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(10, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarmsInfoData.getData());
Assert.assertEquals(1, alarmsInfoData.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0)));
@ -774,7 +774,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(10, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarmsInfoData.getData());
Assert.assertEquals(1, alarmsInfoData.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0)));
@ -899,7 +899,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
@ -910,12 +910,12 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(1, alarms.getData().size());
Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0)));
Assert.assertTrue("Alarm was not deleted when expected", alarmService.deleteAlarm(tenantId, created.getId()).isSuccessful());
Assert.assertTrue("Alarm was not deleted when expected", alarmService.delAlarm(tenantId, created.getId()).isSuccessful());
Alarm fetched = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get();
@ -926,7 +926,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
@ -936,7 +936,7 @@ public class AlarmServiceTest extends AbstractServiceTest {
.status(AlarmStatus.ACTIVE_UNACK).pageLink(
new TimePageLink(1, 0, "",
new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis())
).build()).get();
).build());
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());

1
pull_request_template.md

@ -26,6 +26,7 @@ Put your PR description here instead of this sentence.
- [ ] If new dependency was added: the dependency tree is checked for conflicts.
- [ ] If new service was added: the service is marked with corresponding @TbCoreComponent, @TbRuleEngineComponent, @TbTransportComponent, etc.
- [ ] If new REST API was added: the RestClient.java was updated, issue for [Python REST client](https://github.com/thingsboard/thingsboard-python-rest-client) is created.
- [ ] If new yml property was added: make sure a description is added (above or near the property).

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

@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import java.util.Collection;
@ -70,21 +69,6 @@ public interface RuleEngineAlarmService {
AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs);
/*
* Legacy API, before 3.5.
*/
@Deprecated(since = "3.5", forRemoval = true)
Alarm createOrUpdateAlarm(Alarm alarm);
@Deprecated(since = "3.5", forRemoval = true)
ListenableFuture<Boolean> ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs);
@Deprecated(since = "3.5", forRemoval = true)
ListenableFuture<Boolean> clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs);
@Deprecated(since = "3.5", forRemoval = true)
ListenableFuture<AlarmOperationResult> clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs);
// Other API
Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId);
@ -94,7 +78,7 @@ public interface RuleEngineAlarmService {
Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
ListenableFuture<Alarm> findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type);
AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId);
@ -102,13 +86,13 @@ public interface RuleEngineAlarmService {
return Futures.immediateFuture(findAlarmInfoById(tenantId, alarmId));
}
ListenableFuture<PageData<AlarmInfo>> findAlarms(TenantId tenantId, AlarmQuery query);
PageData<AlarmInfo> findAlarms(TenantId tenantId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId);

122
tools/src/main/python/check_yml_file.py

@ -0,0 +1,122 @@
#
# Copyright © 2016-2023 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
import re
def extract_properties_with_comments(yaml_file_path):
properties = {}
with open(yaml_file_path, 'r') as file:
lines = file.readlines()
index = 0
key_level_map = {0 : ''}
parse_line('', '', key_level_map, 0, index, lines, properties)
return properties
def parse_line(table_name, comment, key_level_map, parent_line_level, index, lines, properties):
if index >= len(lines):
return
line = lines[index]
line_level = (len(line) - len(line.lstrip())) if line.strip() else 0
line = line.strip()
# if line is empty - parse next line
if not line:
index = index + 1
parse_line(table_name, comment, key_level_map, line_level, index, lines, properties)
# if line is a comment - save comment and parse next line
else:
if line_level == 0:
key_level_map = {0 : ''}
if line.startswith('#'):
if line_level == 0:
table_name = line.lstrip('#')
elif line_level == parent_line_level:
comment = comment + '\n' + line.lstrip('#')
else:
comment = line.lstrip('#')
index = index + 1
parse_line(table_name, comment, key_level_map, line_level, index, lines, properties)
else:
# Check if it's a property line
if ':' in line:
# clean comment if level was changed
if line_level != parent_line_level:
comment = ''
key, value = line.split(':', 1)
if key.startswith('- '):
key = key.lstrip('- ')
key_level_map[line_level] = key
value = value.strip()
if value.split('#')[0]:
current_key = ''
for k in key_level_map.keys():
if k <= line_level:
current_key = ((current_key + '.') if current_key else '') + key_level_map[k]
properties[current_key] = (value, comment, table_name)
comment = ''
index = index + 1
parse_line(table_name, comment, key_level_map, line_level, index, lines, properties)
def extract_property_info(properties):
rows = []
for property_name, value in properties.items():
if '#' in value[0]:
value_parts = value[0].split('#')
comment = value_parts[1]
else:
comment = value[1]
pattern = r'\"\$\{(.*?)\:(.*?)\}\"'
match = re.match(pattern, value[0])
if match is not None:
rows.append((property_name, match.group(1), match.group(2), comment, value[2]))
else:
rows.append((property_name, "", value[0].split('#')[0], comment, value[2]))
return rows
def check_descriptions(properties):
variables_without_description = []
for row in properties:
# Extract information from the tuple
property_name, env_variable, default_value, comment, table_name = row
if comment == '' or len(comment) < 5 :
variables_without_description.append(property_name)
return variables_without_description
if __name__ == '__main__':
sys. setrecursionlimit(10000)
# path to the YAML file
input_yaml_file = "application/src/main/resources/thingsboard.yml"
# Parse yml file to map where key is property key path with '.' separator
# and value is an object (env_name_with_default_value, comment, table_name)
properties = extract_properties_with_comments(input_yaml_file)
# Extract property information (extract env name, default value and comment nearby property)
property_info = extract_property_info(properties)
# Check all properies have descriptions
variables_without_desc = check_descriptions(property_info)
if len(variables_without_desc) > 0:
print(f"There are some yml properties without valid description: (total {len(variables_without_desc)}) {variables_without_desc}.")
exit(1)
else:
print("All yml properties have valid description.")

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.html

@ -61,7 +61,7 @@
</tb-font-settings>
</div>
<div class="tb-color-field">
<tb-color-settings formControlName="color">
<tb-color-settings formControlName="color" settingsKey="{{ (index+1) }}. {{ aggregatedValueCardKeyPositionTranslationMap.get(keyRowFormGroup.get('position').value) | translate }}">
</tb-color-settings>
</div>
<div class="tb-arrow-field">

4
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-key-row.component.ts

@ -87,6 +87,9 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn
@Input()
keyName: string;
@Input()
index: number;
@Output()
keyRemoved = new EventEmitter();
@ -225,6 +228,7 @@ export class AggregatedDataKeyRowComponent implements ControlValueAccessor, OnIn
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false});
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false});
this.updateModel();
this.cd.markForCheck();
}
});
}

1
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-data-keys-panel.component.html

@ -32,6 +32,7 @@
<div *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-aggregated-data-key-row fxFlex
[formControl]="keyControl"
[index]="$index"
[datasourceType]="datasourceType"
[keyName]="keyName"
(keyRemoved)="removeKey($index)">

5
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.html

@ -29,6 +29,11 @@
</tb-datasources>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.aggregated-value-card.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showTitle">
{{ 'widget-config.title' | translate }}

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/aggregated-value-card-basic-config.component.ts

@ -119,6 +119,7 @@ export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigCo
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
autoScale: [settings.autoScale, []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.title, []],
titleFont: [configData.config.titleFont, []],
@ -172,6 +173,7 @@ export class AggregatedValueCardBasicConfigComponent extends BasicWidgetConfigCo
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.autoScale = config.autoScale;
this.widgetConfig.config.settings.showSubtitle = config.showSubtitle;
this.widgetConfig.config.settings.subtitle = config.subtitle;
this.widgetConfig.config.settings.subtitleFont = config.subtitleFont;

13
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html

@ -40,6 +40,11 @@
{{ valueCardLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.value-card.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.value-card.label' | translate }}
@ -51,7 +56,7 @@
<tb-font-settings formControlName="labelFont"
[previewText]="valueCardWidgetConfigForm.get('label').value">
</tb-font-settings>
<tb-color-settings formControlName="labelColor">
<tb-color-settings formControlName="labelColor" settingsKey="{{'widgets.value-card.label' | translate }}">
</tb-color-settings>
</div>
</div>
@ -69,7 +74,7 @@
[color]="valueCardWidgetConfigForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>
<tb-color-settings formControlName="iconColor">
<tb-color-settings formControlName="iconColor" settingsKey="{{'widgets.value-card.icon' | translate }}">
</tb-color-settings>
</div>
</div>
@ -84,7 +89,7 @@
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
<tb-color-settings formControlName="valueColor" settingsKey="{{'widgets.value-card.value' | translate }}">
</tb-color-settings>
</div>
</div>
@ -97,7 +102,7 @@
<tb-font-settings formControlName="dateFont"
[previewText]="datePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="dateColor">
<tb-color-settings formControlName="dateColor" settingsKey="{{'widgets.value-card.date' | translate }}">
</tb-color-settings>
</div>
</div>

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts

@ -116,6 +116,7 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
layout: [settings.layout, []],
autoScale: [settings.autoScale, []],
showLabel: [settings.showLabel, []],
label: [getLabel(configData.config.datasources), []],
@ -154,6 +155,7 @@ export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.autoScale = config.autoScale;
this.widgetConfig.config.settings.showLabel = config.showLabel;
setLabel(config.label, this.widgetConfig.config.datasources);

6
ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/battery-level-basic-config.component.html

@ -91,18 +91,18 @@
[autoScale]="batteryLevelWidgetConfigForm.get('autoScaleValueSize').value"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
<tb-color-settings formControlName="valueColor" settingsKey="{{'widgets.battery-level.value' | translate }}">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.battery-level.battery-level-color' | translate }}</div>
<tb-color-settings formControlName="batteryLevelColor">
<tb-color-settings formControlName="batteryLevelColor" settingsKey="{{'widgets.battery-level.battery-level-color' | translate }}">
</tb-color-settings>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.battery-level.battery-shape-color' | translate }}</div>
<tb-color-settings formControlName="batteryShapeColor">
<tb-color-settings formControlName="batteryShapeColor" settingsKey="{{'widgets.battery-level.battery-shape-color' | translate }}">
</tb-color-settings>
</div>
<div class="tb-form-row space-between">

4
ui-ngx/src/app/modules/home/components/widget/config/basic/weather/wind-speed-direction-basic-config.component.html

@ -49,7 +49,7 @@
[previewText]="centerValuePreviewFn">
</tb-font-settings>
<tb-color-settings *ngIf="!windSpeedDirectionWidgetConfigForm.get('centerValueKey').value"
formControlName="centerValueColor">
formControlName="centerValueColor" settingsKey="{{'widgets.wind-speed-direction.wind-direction' | translate }}">
</tb-color-settings>
</div>
</div>
@ -71,7 +71,7 @@
[previewText]="centerValuePreviewFn">
</tb-font-settings>
<tb-color-settings *ngIf="windSpeedDirectionWidgetConfigForm.get('centerValueKey').value"
formControlName="centerValueColor">
formControlName="centerValueColor" settingsKey="{{'widgets.wind-speed-direction.center-value' | translate }}">
</tb-color-settings>
</div>
</div>

8
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.html

@ -21,7 +21,9 @@
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<ng-container *ngTemplateOutlet="subtitleTpl"></ng-container>
</div>
<ng-container *ngTemplateOutlet="valuesTpl"></ng-container>
<ng-container *ngIf="showValues">
<ng-container *ngTemplateOutlet="valuesTpl"></ng-container>
</ng-container>
<ng-container *ngTemplateOutlet="chartTpl"></ng-container>
<ng-container *ngTemplateOutlet="dateTpl"></ng-container>
</div>
@ -31,8 +33,8 @@
[style]="subtitleStyle" [style.color]="subtitleColor">{{ subtitle$ | async }}</div>
</ng-template>
<ng-template #valuesTpl>
<div *ngIf="showValues" class="tb-aggregated-value-card-values">
<div class="tb-aggregated-value-card-values-container">
<div #valueCardValues class="tb-aggregated-value-card-values">
<div #valueCardValueContainer class="tb-aggregated-value-card-values-container">
<div class="tb-aggregated-value-card-values-section left">
<ng-container *ngIf="values[aggregatedValueCardKeyPosition.leftTop]">
<ng-container *ngTemplateOutlet="valueTpl; context:{value: values[aggregatedValueCardKeyPosition.leftTop]}"></ng-container>

11
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.scss

@ -21,7 +21,7 @@
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
gap: 2px;
padding: 20px 24px 24px;
> div:not(.tb-value-card-overlay) {
z-index: 1;
@ -45,10 +45,17 @@
min-height: 0;
overflow: hidden;
}
.tb-aggregated-value-card-values {
padding: 8px 0;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.tb-aggregated-value-card-values-container {
width: 100%;
height: 100%;
padding: 8px 0;
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) fit-content(100%) minmax(0, 1fr);
.tb-aggregated-value-card-values-section {

48
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card-widget.component.ts

@ -20,7 +20,9 @@ import {
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild
} from '@angular/core';
@ -43,22 +45,31 @@ import {
overlayStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { DatePipe } from '@angular/common';
import { TbFlot } from '@home/components/widget/lib/flot-widget';
import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models';
import { DataKey } from '@shared/models/widget.models';
import { formatNumberValue, formatValue, isDefined } from '@core/utils';
import { map } from 'rxjs/operators';
import { ResizeObserver } from '@juggle/resize-observer';
const valuesLayoutHeight = 66;
const valuesLayoutVerticalPadding = 16;
@Component({
selector: 'tb-aggregated-value-card-widget',
templateUrl: './aggregated-value-card-widget.component.html',
styleUrls: ['./aggregated-value-card-widget.component.scss']
})
export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit {
export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('chartElement', {static: false}) chartElement: ElementRef;
@ViewChild('valueCardValues', {static: false})
valueCardValues: ElementRef<HTMLElement>;
@ViewChild('valueCardValueContainer', {static: false})
valueCardValueContainer: ElementRef<HTMLElement>;
aggregatedValueCardKeyPosition = AggregatedValueCardKeyPosition;
settings: AggregatedValueCardWidgetSettings;
@ -96,7 +107,9 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
tickMin$: Observable<string>;
tickMax$: Observable<string>;
constructor(private date: DatePipe,
private panelResize$: ResizeObserver;
constructor(private renderer: Renderer2,
private cd: ChangeDetectorRef) {
}
@ -174,6 +187,22 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
(this.flotDataKey?.units || this.ctx.units))
));
}
if (this.settings.autoScale && this.showValues) {
this.renderer.setStyle(this.valueCardValueContainer.nativeElement, 'height', valuesLayoutHeight + 'px');
this.renderer.setStyle(this.valueCardValueContainer.nativeElement, 'overflow', 'visible');
this.renderer.setStyle(this.valueCardValueContainer.nativeElement, 'position', 'absolute');
this.panelResize$ = new ResizeObserver(() => {
this.onValueCardValuesResize();
});
this.panelResize$.observe(this.valueCardValues.nativeElement);
this.onValueCardValuesResize();
}
}
ngOnDestroy() {
if (this.panelResize$) {
this.panelResize$.disconnect();
}
}
public onInit() {
@ -249,4 +278,17 @@ export class AggregatedValueCardWidgetComponent implements OnInit, AfterViewInit
}
}
private onValueCardValuesResize() {
const panelWidth = this.valueCardValues.nativeElement.getBoundingClientRect().width;
const panelHeight = this.valueCardValues.nativeElement.getBoundingClientRect().height - valuesLayoutVerticalPadding;
const targetWidth = panelWidth;
const minAspect = 0.25;
const aspect = Math.min(panelHeight / targetWidth, minAspect);
const targetHeight = targetWidth * aspect;
const scale = targetHeight / valuesLayoutHeight;
const width = targetWidth / scale;
this.renderer.setStyle(this.valueCardValueContainer.nativeElement, 'width', width + 'px');
this.renderer.setStyle(this.valueCardValueContainer.nativeElement, 'transform', `scale(${scale})`);
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/cards/aggregated-value-card.models.ts

@ -32,6 +32,7 @@ import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
export interface AggregatedValueCardWidgetSettings {
autoScale: boolean;
showSubtitle: boolean;
subtitle: string;
subtitleFont: Font;
@ -112,6 +113,7 @@ export const getTsValueByLatestDataKey = (latestData: Array<DatasourceData>, dat
};
export const aggregatedValueCardDefaultSettings: AggregatedValueCardWidgetSettings = {
autoScale: true,
showSubtitle: true,
subtitle: '${entityName}',
subtitleFont: {

6
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html

@ -15,12 +15,13 @@
limitations under the License.
-->
<div class="tb-value-card-panel" [class]="this.layout" [style]="backgroundStyle">
<div #valueCardPanel class="tb-value-card-panel" [class]="this.layout" [style]="backgroundStyle">
<div class="tb-value-card-overlay" [style]="overlayStyle"></div>
<div class="tb-value-card-title-panel">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
</div>
<ng-container [ngSwitch]="layout">
<div #valueCardContent class="tb-value-card-content" [class]="this.layout">
<ng-container [ngSwitch]="layout">
<ng-template [ngSwitchCase]="valueCardLayout.square">
<ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
@ -49,6 +50,7 @@
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</ng-template>
</ng-container>
</div>
</div>
<ng-template #iconWithLabelTpl>
<div class="tb-value-card-icon-row">

72
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss

@ -19,24 +19,15 @@
height: 100%;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 24px;
&.horizontal, &.horizontal_reversed {
padding: 0 24px;
}
> div:not(.tb-value-card-overlay), > tb-icon {
z-index: 1;
}
&.square {
justify-content: space-evenly;
gap: 0;
}
&.horizontal {
flex-direction: row;
}
&.horizontal_reversed {
flex-direction: row-reverse;
}
.tb-value-card-overlay {
position: absolute;
top: 12px;
@ -51,27 +42,54 @@
right: 12px;
z-index: 2;
}
.tb-value-card-icon-row {
.tb-value-card-content {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: row;
flex-direction: column;
align-items: center;
gap: 8px;
}
&.horizontal_reversed {
.tb-value-card-icon-row {
justify-content: center;
gap: 16px;
&.square {
justify-content: space-evenly;
gap: 0;
}
&.horizontal {
flex-direction: row;
}
&.horizontal_reversed {
flex-direction: row-reverse;
}
.tb-value-card-icon-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
&.horizontal_reversed {
.tb-value-card-icon-row {
flex-direction: row-reverse;
}
.tb-value-card-label-row {
align-items: flex-end;
}
}
.tb-value-card-label-row {
align-items: flex-end;
display: flex;
flex-direction: column;
justify-content: center;
}
.tb-value-card-value {
white-space: nowrap;
}
}
.tb-value-card-label-row {
display: flex;
flex-direction: column;
justify-content: center;
}
.tb-value-card-value {
white-space: nowrap;
}
}
}

75
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts

@ -14,14 +14,25 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Input, OnInit, TemplateRef } from '@angular/core';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild
} from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { DatePipe } from '@angular/common';
import {
backgroundStyle,
ColorProcessor,
ComponentStyle, DateFormatProcessor,
ComponentStyle,
DateFormatProcessor,
getDataKey,
getLabel,
getSingleTsValue,
@ -32,13 +43,24 @@ import {
import { valueCardDefaultSettings, ValueCardLayout, ValueCardWidgetSettings } from './value-card-widget.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
import { Observable } from 'rxjs';
import { ResizeObserver } from '@juggle/resize-observer';
const squareLayoutSize = 160;
const squareLayoutPadding = 48;
const horizontalLayoutHeight = 80;
@Component({
selector: 'tb-value-card-widget',
templateUrl: './value-card-widget.component.html',
styleUrls: ['./value-card-widget.component.scss']
})
export class ValueCardWidgetComponent implements OnInit {
export class ValueCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('valueCardPanel', {static: false})
valueCardPanel: ElementRef<HTMLElement>;
@ViewChild('valueCardContent', {static: false})
valueCardContent: ElementRef<HTMLElement>;
settings: ValueCardWidgetSettings;
@ -73,11 +95,13 @@ export class ValueCardWidgetComponent implements OnInit {
backgroundStyle: ComponentStyle = {};
overlayStyle: ComponentStyle = {};
private panelResize$: ResizeObserver;
private horizontal = false;
private decimals = 0;
private units = '';
constructor(private date: DatePipe,
constructor(private renderer: Renderer2,
private widgetComponent: WidgetComponent,
private cd: ChangeDetectorRef) {
}
@ -122,6 +146,29 @@ export class ValueCardWidgetComponent implements OnInit {
this.overlayStyle = overlayStyle(this.settings.background.overlay);
}
public ngAfterViewInit() {
if (this.settings.autoScale) {
if (!this.horizontal) {
this.renderer.setStyle(this.valueCardContent.nativeElement, 'width', squareLayoutSize + 'px');
}
const height = this.horizontal ? horizontalLayoutHeight : squareLayoutSize;
this.renderer.setStyle(this.valueCardContent.nativeElement, 'height', height + 'px');
this.renderer.setStyle(this.valueCardContent.nativeElement, 'overflow', 'visible');
this.renderer.setStyle(this.valueCardContent.nativeElement, 'position', 'absolute');
this.panelResize$ = new ResizeObserver(() => {
this.onResize();
});
this.panelResize$.observe(this.valueCardPanel.nativeElement);
this.onResize();
}
}
ngOnDestroy() {
if (this.panelResize$) {
this.panelResize$.disconnect();
}
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
@ -146,4 +193,22 @@ export class ValueCardWidgetComponent implements OnInit {
this.dateColor.update(value);
this.cd.detectChanges();
}
private onResize() {
const panelWidth = this.valueCardPanel.nativeElement.getBoundingClientRect().width - squareLayoutPadding;
const panelHeight = this.valueCardPanel.nativeElement.getBoundingClientRect().height - (this.horizontal ? 0 : squareLayoutPadding);
let scale: number;
if (!this.horizontal) {
const size = Math.min(panelWidth, panelHeight);
scale = size / squareLayoutSize;
} else {
const targetWidth = panelWidth;
const aspect = Math.min(panelHeight / targetWidth, 0.25);
const targetHeight = targetWidth * aspect;
scale = targetHeight / horizontalLayoutHeight;
const width = targetWidth / scale;
this.renderer.setStyle(this.valueCardContent.nativeElement, 'width', width + 'px');
}
this.renderer.setStyle(this.valueCardContent.nativeElement, 'transform', `scale(${scale})`);
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts

@ -64,6 +64,7 @@ export const valueCardLayoutImages = new Map<ValueCardLayout, string>(
export interface ValueCardWidgetSettings {
layout: ValueCardLayout;
autoScale: boolean;
showLabel: boolean;
labelFont: Font;
labelColor: ColorSettings;
@ -83,6 +84,7 @@ export interface ValueCardWidgetSettings {
export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSettings => ({
layout: horizontal ? ValueCardLayout.horizontal : ValueCardLayout.square,
autoScale: true,
showLabel: true,
labelFont: {
family: 'Roboto',

32
ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.html

@ -15,23 +15,25 @@
limitations under the License.
-->
<div class="tb-count-panel" [class.tb-count-pointer]="hasCardClickAction" (click)="cardClick($event)">
<div class="tb-count-panel-column">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-count-panel-row">
<div class="tb-count-icon-panel" *ngIf="showIconBackground || showIcon"
[style]="{minWidth: iconBackgroundSize, minHeight: iconBackgroundSize}">
<div *ngIf="showIconBackground" class="tb-count-icon-background-panel">
<div [style]="iconBackgroundStyle" [style.background-color]="iconBackgroundColor.color"></div>
<div #countPanel class="tb-count-panel" [class.tb-count-pointer]="hasCardClickAction" (click)="cardClick($event)">
<div #countPanelContent class="tb-count-panel-content">
<div class="tb-count-panel-column">
<ng-container *ngTemplateOutlet="widgetTitlePanel"></ng-container>
<div class="tb-count-panel-row">
<div class="tb-count-icon-panel" *ngIf="showIconBackground || showIcon"
[style]="{minWidth: iconBackgroundSize, minHeight: iconBackgroundSize}">
<div *ngIf="showIconBackground" class="tb-count-icon-background-panel">
<div [style]="iconBackgroundStyle" [style.background-color]="iconBackgroundColor.color"></div>
</div>
<tb-icon *ngIf="showIcon" [style]="iconStyle" [style.color]="iconColor.color">{{ icon }}</tb-icon>
</div>
<div class="tb-count-label-value-panel" [class.tb-count-layout-row]="layout === countCardLayout.row">
<div *ngIf="showLabel" class="tb-count-label" [style]="labelStyle" [style.color]="labelColor.color">{{ label }}</div>
<div *ngIf="layout === countCardLayout.row" style="flex: 1; min-width: 12px;"></div>
<div [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>
</div>
<tb-icon *ngIf="showIcon" [style]="iconStyle" [style.color]="iconColor.color">{{ icon }}</tb-icon>
</div>
<div class="tb-count-label-value-panel" [class.tb-count-layout-row]="layout === countCardLayout.row">
<div *ngIf="showLabel" [style]="labelStyle" [style.color]="labelColor.color">{{ label }}</div>
<div *ngIf="layout === countCardLayout.row" style="flex: 1;"></div>
<div [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>
</div>
</div>
<tb-icon *ngIf="showChevron" [style]="chevronStyle">chevron_right</tb-icon>
</div>
<tb-icon *ngIf="showChevron" [style]="chevronStyle">chevron_right</tb-icon>
</div>

88
ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.scss

@ -17,55 +17,71 @@
.tb-count-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
justify-content: center;
padding: 12px;
&.tb-count-pointer {
cursor: pointer;
}
.tb-count-panel-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.tb-count-panel-row {
flex: 1;
.tb-count-panel-content {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.tb-count-icon-panel {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.mat-icon {
z-index: 1;
.tb-count-panel-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
}
}
.tb-count-icon-background-panel {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-count-label-value-panel {
flex: 1;
display: flex;
flex-direction: column;
place-content: flex-start space-around;
align-items: flex-start;
gap: 0;
&.tb-count-layout-row {
.tb-count-panel-row {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.tb-count-icon-panel {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.mat-icon {
z-index: 1;
}
}
.tb-count-icon-background-panel {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-count-label-value-panel {
flex: 1;
display: flex;
flex-direction: column;
place-content: flex-start space-around;
align-items: flex-start;
gap: 0;
overflow: hidden;
&.tb-count-layout-row {
flex-direction: row;
align-items: center;
}
.tb-count-label {
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
}
}
}

73
ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts

@ -14,7 +14,18 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Input, OnInit, TemplateRef } from '@angular/core';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewChild
} from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue } from '@core/utils';
import {
@ -24,20 +35,30 @@ import {
iconStyle,
textStyle
} from '@shared/models/widget-settings.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
import {
CountCardLayout,
countDefaultSettings,
CountWidgetSettings
} from '@home/components/widget/lib/count/count-widget.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
const layoutHeight = 36;
const layoutHeightWithTitle = 60;
const layoutPadding = 24;
@Component({
selector: 'tb-count-widget',
templateUrl: './count-widget.component.html',
styleUrls: ['./count-widget.component.scss']
})
export class CountWidgetComponent implements OnInit {
export class CountWidgetComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('countPanel', {static: false})
countPanel: ElementRef<HTMLElement>;
@ViewChild('countPanelContent', {static: false})
countPanelContent: ElementRef<HTMLElement>;
settings: CountWidgetSettings;
@ -79,7 +100,10 @@ export class CountWidgetComponent implements OnInit {
hasCardClickAction = false;
constructor(private widgetComponent: WidgetComponent,
private panelResize$: ResizeObserver;
private hasTitle = false;
constructor(private renderer: Renderer2,
private cd: ChangeDetectorRef) {
}
@ -120,6 +144,27 @@ export class CountWidgetComponent implements OnInit {
this.chevronStyle.color = this.settings.chevronColor;
this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
this.hasTitle = this.ctx.widgetConfig.showTitle;
}
public ngAfterViewInit() {
if (this.settings.autoScale) {
const height = this.hasTitle ? layoutHeightWithTitle : layoutHeight;
this.renderer.setStyle(this.countPanelContent.nativeElement, 'height', height + 'px');
this.renderer.setStyle(this.countPanelContent.nativeElement, 'overflow', 'visible');
this.renderer.setStyle(this.countPanelContent.nativeElement, 'position', 'absolute');
this.panelResize$ = new ResizeObserver(() => {
this.onResize();
});
this.panelResize$.observe(this.countPanel.nativeElement);
this.onResize();
}
}
ngOnDestroy() {
if (this.panelResize$) {
this.panelResize$.disconnect();
}
}
public onInit() {
@ -144,4 +189,24 @@ export class CountWidgetComponent implements OnInit {
public cardClick($event: Event) {
this.ctx.actionsApi.cardClick($event);
}
private onResize() {
const panelWidth = this.countPanel.nativeElement.getBoundingClientRect().width - layoutPadding;
const panelHeight = this.countPanel.nativeElement.getBoundingClientRect().height - layoutPadding;
const targetWidth = panelWidth;
let minAspect = 0.25;
if (this.settings.showChevron) {
minAspect -= 0.05;
}
if (this.hasTitle) {
minAspect += 0.15;
}
const aspect = Math.min(panelHeight / targetWidth, minAspect);
const targetHeight = targetWidth * aspect;
const height = this.hasTitle ? layoutHeightWithTitle : layoutHeight;
const scale = targetHeight / height;
const width = targetWidth / scale;
this.renderer.setStyle(this.countPanelContent.nativeElement, 'width', width + 'px');
this.renderer.setStyle(this.countPanelContent.nativeElement, 'transform', `scale(${scale})`);
}
}

2
ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.models.ts

@ -53,6 +53,7 @@ export const entityCountCardLayoutImages = new Map<CountCardLayout, string>(
export interface CountWidgetSettings {
layout: CountCardLayout;
autoScale: boolean;
showLabel: boolean;
label: string;
labelFont: Font;
@ -76,6 +77,7 @@ export interface CountWidgetSettings {
export const countDefaultSettings = (alarmElseEntity: boolean): CountWidgetSettings => ({
layout: CountCardLayout.column,
autoScale: true,
showLabel: true,
label: alarmElseEntity ? 'Total' : 'Devices',
labelFont: {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-key-settings.component.html

@ -36,7 +36,7 @@
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.aggregated-value-card.color' | translate }}</div>
<tb-color-settings formControlName="color">
<tb-color-settings formControlName="color" settingsKey="{{'widgets.aggregated-value-card.color' | translate }}">
</tb-color-settings>
</div>
<div class="tb-form-row">

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.html

@ -18,6 +18,11 @@
<ng-container [formGroup]="aggregatedValueCardWidgetSettingsForm">
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widgets.aggregated-value-card.aggregated-value-card-style</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.aggregated-value-card.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showSubtitle">
{{ 'widgets.aggregated-value-card.subtitle' | translate }}

1
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/aggregated-value-card-widget-settings.component.ts

@ -50,6 +50,7 @@ export class AggregatedValueCardWidgetSettingsComponent extends WidgetSettingsCo
protected onSettingsSet(settings: WidgetSettings) {
this.aggregatedValueCardWidgetSettingsForm = this.fb.group({
autoScale: [settings.autoScale, []],
showSubtitle: [settings.showSubtitle, []],
subtitle: [settings.subtitle, []],
subtitleFont: [settings.subtitleFont, []],

13
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-card-widget-settings.component.html

@ -28,6 +28,11 @@
{{ valueCardLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.value-card.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showLabel">
{{ 'widgets.value-card.label' | translate }}
@ -36,7 +41,7 @@
<tb-font-settings formControlName="labelFont"
[previewText]="label">
</tb-font-settings>
<tb-color-settings formControlName="labelColor">
<tb-color-settings formControlName="labelColor" settingsKey="{{'widgets.value-card.label' | translate }}">
</tb-color-settings>
</div>
</div>
@ -54,7 +59,7 @@
[color]="valueCardWidgetSettingsForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>
<tb-color-settings formControlName="iconColor">
<tb-color-settings formControlName="iconColor" settingsKey="{{'widgets.value-card.icon' | translate }}">
</tb-color-settings>
</div>
</div>
@ -64,7 +69,7 @@
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
<tb-color-settings formControlName="valueColor" settingsKey="{{'widgets.value-card.value' | translate }}">
</tb-color-settings>
</div>
</div>
@ -77,7 +82,7 @@
<tb-font-settings formControlName="dateFont"
[previewText]="datePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="dateColor">
<tb-color-settings formControlName="dateColor" settingsKey="{{'widgets.value-card.date' | translate }}">
</tb-color-settings>
</div>
</div>

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

@ -90,6 +90,7 @@ export class ValueCardWidgetSettingsComponent extends WidgetSettingsComponent {
protected onSettingsSet(settings: WidgetSettings) {
this.valueCardWidgetSettingsForm = this.fb.group({
layout: [settings.layout, []],
autoScale: [settings.autoScale, []],
showLabel: [settings.showLabel, []],
labelFont: [settings.labelFont, []],

16
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html

@ -40,6 +40,22 @@
<ng-container *ngTemplateOutlet="function"></ng-container>
</div>
<div class="tb-color-settings-panel-buttons">
<button *ngIf="settingsComponents?.length"
#copySettingsButton
mat-stroked-button
color="primary"
type="button"
[matMenuTriggerFor]="settingsSourcesMenu" [matMenuTriggerData]="{menuWidth: copySettingsButton._elementRef.nativeElement.clientWidth}">
{{ 'widgets.color.copy-color-settings-from' | translate }}
</button>
<mat-menu #settingsSourcesMenu="matMenu">
<ng-template matMenuContent let-menuWidth="menuWidth">
<div [style.min-width.px]="menuWidth">
<button mat-menu-item *ngFor="let comp of settingsComponents" (click)="copyColorSettings(comp)">{{ comp.settingsKey }}</button>
</div>
</ng-template>
</mat-menu>
<span fxFlex></span>
<button mat-button
color="primary"
type="button"

35
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts

@ -16,27 +16,14 @@
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import {
ColorRange,
ColorSettings,
ColorType,
colorTypeTranslations
} from '@shared/models/widget-settings.models';
import { ColorRange, ColorSettings, ColorType, colorTypeTranslations } from '@shared/models/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
AbstractControl,
FormControl,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup
} from '@angular/forms';
import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Datasource, DatasourceType } from '@shared/models/widget.models';
import { deepClone } from '@core/utils';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetService } from '@core/http/widget.service';
import { ColorSettingsComponent } from '@home/components/widget/lib/settings/common/color-settings.component';
@Component({
selector: 'tb-color-settings-panel',
@ -53,6 +40,9 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
@Input()
popover: TbPopoverComponent<ColorSettingsPanelComponent>;
@Input()
settingsComponents: ColorSettingsComponent[];
@Output()
colorSettingsApplied = new EventEmitter<ColorSettings>();
@ -121,6 +111,19 @@ export class ColorSettingsPanelComponent extends PageComponent implements OnInit
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
copyColorSettings(comp: ColorSettingsComponent) {
const sourceSettings = deepClone(comp.modelValue);
this.colorSettings = sourceSettings;
this.colorSettingsFormGroup.patchValue({
type: this.colorSettings.type,
color: this.colorSettings.color,
colorFunction: this.colorSettings.colorFunction
}, {emitEvent: false});
this.colorSettingsFormGroup.setControl('rangeList',
this.fb.array((this.colorSettings.rangeList || []).map(r => this.colorRangeControl(r))), {emitEvent: false});
this.colorSettingsFormGroup.markAsDirty();
}
cancel() {
this.popover?.hide();
}

54
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts

@ -14,7 +14,16 @@
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import {
Component,
forwardRef,
Injectable,
Input,
OnDestroy,
OnInit,
Renderer2,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ColorSettings, ColorType, ComponentStyle } from '@shared/models/widget-settings.models';
import { MatButton } from '@angular/material/button';
@ -23,6 +32,33 @@ import {
ColorSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/color-settings-panel.component';
@Injectable()
export class ColorSettingsComponentService {
private colorSettingsComponents = new Set<ColorSettingsComponent>();
constructor() {}
public registerColorSettingsComponent(comp: ColorSettingsComponent) {
this.colorSettingsComponents.add(comp);
}
public unregisterColorSettingsComponent(comp: ColorSettingsComponent) {
this.colorSettingsComponents.delete(comp);
}
public getOtherColorSettingsComponents(comp: ColorSettingsComponent): ColorSettingsComponent[] {
const result: ColorSettingsComponent[] = [];
for (const component of this.colorSettingsComponents.values()) {
if (component.settingsKey && component.modelValue && component !== comp) {
result.push(component);
}
}
return result;
}
}
@Component({
selector: 'tb-color-settings',
templateUrl: './color-settings.component.html',
@ -35,11 +71,14 @@ import {
}
]
})
export class ColorSettingsComponent implements OnInit, ControlValueAccessor {
export class ColorSettingsComponent implements OnInit, ControlValueAccessor, OnDestroy {
@Input()
disabled: boolean;
@Input()
settingsKey: string;
colorType = ColorType;
modelValue: ColorSettings;
@ -50,9 +89,15 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor {
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
private viewContainerRef: ViewContainerRef,
private colorSettingsComponentService: ColorSettingsComponentService) {}
ngOnInit(): void {
this.colorSettingsComponentService.registerColorSettingsComponent(this);
}
ngOnDestroy() {
this.colorSettingsComponentService.unregisterColorSettingsComponent(this);
}
registerOnChange(fn: any): void {
@ -81,7 +126,8 @@ export class ColorSettingsComponent implements OnInit, ControlValueAccessor {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
colorSettings: this.modelValue
colorSettings: this.modelValue,
settingsComponents: this.colorSettingsComponentService.getOtherColorSettingsComponents(this)
};
const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorSettingsPanelComponent, 'left', true, null,

13
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.html

@ -26,6 +26,11 @@
{{ countCardLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoScale">
{{ 'widgets.count.auto-scale' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.count.label' | translate }}
@ -37,7 +42,7 @@
<tb-font-settings formControlName="labelFont"
[previewText]="countWidgetConfigForm.get('label').value">
</tb-font-settings>
<tb-color-settings formControlName="labelColor">
<tb-color-settings formControlName="labelColor" settingsKey="{{'widgets.count.label' | translate }}">
</tb-color-settings>
</div>
</div>
@ -58,7 +63,7 @@
: ''"
formControlName="icon">
</tb-material-icon-select>
<tb-color-settings formControlName="iconColor">
<tb-color-settings formControlName="iconColor" settingsKey="{{'widgets.count.icon' | translate }}">
</tb-color-settings>
</div>
</div>
@ -71,7 +76,7 @@
<input matInput type="number" min="0" formControlName="iconBackgroundSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconBackgroundSizeUnit"></tb-css-unit-select>
<tb-color-settings formControlName="iconBackgroundColor">
<tb-color-settings formControlName="iconBackgroundColor" settingsKey="{{'widgets.count.icon-background' | translate }}">
</tb-color-settings>
</div>
</div>
@ -81,7 +86,7 @@
<tb-font-settings formControlName="valueFont"
previewText="10">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
<tb-color-settings formControlName="valueColor" settingsKey="{{'widgets.count.value' | translate }}">
</tb-color-settings>
</div>
</div>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/count-widget-settings.component.ts

@ -78,6 +78,8 @@ export class CountWidgetSettingsComponent extends PageComponent implements OnIni
this.countCardLayoutImageMap = this.alarmElseEntity ? alarmCountCardLayoutImages : entityCountCardLayoutImages;
this.countWidgetConfigForm = this.fb.group({
layout: [null, []],
autoScale: [null, []],
showLabel: [null, []],
label: [null, []],
labelFont: [null, []],

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/widget-settings-common.module.ts

@ -27,7 +27,10 @@ import {
} from '@home/components/widget/lib/settings/common/image-cards-select.component';
import { FontSettingsComponent } from '@home/components/widget/lib/settings/common/font-settings.component';
import { FontSettingsPanelComponent } from '@home/components/widget/lib/settings/common/font-settings-panel.component';
import { ColorSettingsComponent } from '@home/components/widget/lib/settings/common/color-settings.component';
import {
ColorSettingsComponent,
ColorSettingsComponentService
} from '@home/components/widget/lib/settings/common/color-settings.component';
import {
ColorSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/color-settings-panel.component';
@ -83,6 +86,9 @@ import {
LegendConfigComponent,
WidgetFontComponent,
CountWidgetSettingsComponent
],
providers: [
ColorSettingsComponentService
]
})
export class WidgetSettingsCommonModule {

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/battery-level-widget-settings.component.html

@ -40,18 +40,18 @@
[autoScale]="batteryLevelWidgetSettingsForm.get('autoScaleValueSize').value"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
<tb-color-settings formControlName="valueColor" settingsKey="{{'widgets.battery-level.value' | translate }}">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.battery-level.battery-level-color' | translate }}</div>
<tb-color-settings formControlName="batteryLevelColor">
<tb-color-settings formControlName="batteryLevelColor" settingsKey="{{'widgets.battery-level.battery-level-color' | translate }}">
</tb-color-settings>
</div>
<div class="tb-form-row space-between">
<div>{{ 'widgets.battery-level.battery-shape-color' | translate }}</div>
<tb-color-settings formControlName="batteryShapeColor">
<tb-color-settings formControlName="batteryShapeColor" settingsKey="{{'widgets.battery-level.battery-shape-color' | translate }}">
</tb-color-settings>
</div>
<div class="tb-form-row space-between">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/weather/wind-speed-direction-widget-settings.component.html

@ -36,7 +36,7 @@
[initialPreviewStyle]="{ color: windSpeedDirectionWidgetSettingsForm.get('centerValueColor').value?.color }"
[previewText]="centerValuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="centerValueColor">
<tb-color-settings formControlName="centerValueColor" settingsKey="{{ (hasCenterValue ? 'widgets.wind-speed-direction.center-value' : 'widgets.wind-speed-direction.wind-direction') | translate }}">
</tb-color-settings>
</div>
</div>

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

@ -5201,7 +5201,8 @@
"value-range": "Value range",
"from": "From",
"to": "To",
"color-function": "Color function"
"color-function": "Color function",
"copy-color-settings-from": "Copy color settings from"
},
"dashboard-state": {
"dashboard-state-settings": "Dashboard state settings",
@ -6039,7 +6040,8 @@
"icon": "Icon",
"value": "Value",
"date": "Date",
"value-card-style": "Value card style"
"value-card-style": "Value card style",
"auto-scale": "Auto scale"
},
"aggregated-value-card": {
"subtitle": "Subtitle",
@ -6060,7 +6062,8 @@
"remove-value": "Remove value",
"no-values": "No values configured",
"aggregation": "Aggregation",
"aggregated-value-card-style": "Aggregated value card style"
"aggregated-value-card-style": "Aggregated value card style",
"auto-scale": "Auto scale"
},
"alarm-count": {
"alarm-count-card-style": "Alarm count card style"
@ -6076,7 +6079,8 @@
"icon": "Icon",
"icon-background": "Icon background",
"value": "Value",
"chevron": "Chevron"
"chevron": "Chevron",
"auto-scale": "Auto scale"
},
"table": {
"common-table-settings": "Common Table Settings",

Loading…
Cancel
Save