Browse Source

Merge branch 'develop/3.5.2' into feature/basic-widget-config

pull/8997/head
Igor Kulikov 3 years ago
parent
commit
26a4509be5
  1. 2
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  2. 38
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  3. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java
  4. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  5. 8
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  6. 47
      application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
  7. 32
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  8. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  9. 18
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  10. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
  11. 4
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  12. 4
      common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
  13. 18
      common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
  14. 6
      common/data/src/main/java/org/thingsboard/server/common/data/User.java
  15. 6
      common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
  16. 1
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  17. 4
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  18. 21
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  19. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  20. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  21. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  22. 8
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
  23. 10
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  24. 40
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java
  25. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  26. 2
      ui-ngx/src/app/core/public-api.ts
  27. 50
      ui-ngx/src/app/core/services/script/node-script-test.service.ts
  28. 19
      ui-ngx/src/app/core/ws/public-api.ts
  29. 4
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  30. 4
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  31. 29
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  32. 37
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  33. 2
      ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html
  34. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  35. 7
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html
  36. 5
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  37. 7
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  38. 10
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  39. 1
      ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss
  40. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html
  41. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts
  42. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html
  43. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.ts
  44. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html
  45. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts
  46. 1
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  47. 7
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  48. 4
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  49. 1
      ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts
  50. 10
      ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-config.resolver.ts
  51. 2
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-config.resolver.ts
  52. 33
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts
  53. 4
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html
  54. 8
      ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts
  55. 9
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  56. 24
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  57. 1
      ui-ngx/src/app/shared/components/js-func.component.html
  58. 59
      ui-ngx/src/app/shared/components/unit-input.component.ts
  59. 2
      ui-ngx/src/app/shared/models/notification.models.ts
  60. 9
      ui-ngx/src/app/shared/models/rule-node.models.ts
  61. 40
      ui-ngx/src/app/shared/models/unit.models.ts
  62. 420
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  63. 2013
      ui-ngx/src/assets/model/units.json

2
application/src/main/data/json/system/widget_bundles/input_widgets.json

@ -498,7 +498,7 @@
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}", "dataKeySettingsSchema": "{}",
"settingsDirective": "tb-update-json-attribute-widget-settings", "settingsDirective": "tb-update-json-attribute-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"widgetMode\":\"ATTRIBUTE\",\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
} }
} }
] ]

38
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.alarm.AlarmComment;
import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmCommentType;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -36,9 +37,13 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@ -210,6 +215,39 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
return alarmInfo; return alarmInfo;
} }
@Override
public void unassignUserAlarms(TenantId tenantId, User user, long unassignTs) {
AlarmQueryV2 alarmQuery = AlarmQueryV2.builder().assigneeId(user.getId()).pageLink(new TimePageLink(Integer.MAX_VALUE)).build();
try {
List<AlarmInfo> alarms = alarmService.findAlarmsV2(tenantId, alarmQuery).get(30, TimeUnit.SECONDS).getData();
for (AlarmInfo alarm : alarms) {
AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(tenantId, alarm.getId(), getOrDefault(unassignTs));
if (!result.isSuccessful()) {
continue;
}
if (result.isModified()) {
AlarmComment alarmComment = AlarmComment.builder()
.alarmId(alarm.getId())
.type(AlarmCommentType.SYSTEM)
.comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned because user %s - was deleted",
(user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName()))
.put("userId", user.getId().toString())
.put("subtype", "ASSIGN"))
.build();
try {
alarmCommentService.saveAlarmComment(alarm, alarmComment, user);
} catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e);
}
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user);
}
}
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new RuntimeException(e);
}
}
@Override @Override
public Boolean delete(Alarm alarm, User user) { public Boolean delete(Alarm alarm, User user) {
TenantId tenantId = alarm.getTenantId(); TenantId tenantId = alarm.getTenantId();

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

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.UserId;
public interface TbAlarmService { public interface TbAlarmService {
@ -37,5 +38,7 @@ public interface TbAlarmService {
AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException; AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException;
void unassignUserAlarms(TenantId tenantId, User user, long unassignTs);
Boolean delete(Alarm alarm, User user); Boolean delete(Alarm alarm, User user);
} }

3
application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmService;
import org.thingsboard.server.service.security.system.SystemSecurityService; import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -43,6 +44,7 @@ import static org.thingsboard.server.controller.UserController.ACTIVATE_URL_PATT
public class DefaultUserService extends AbstractTbEntityService implements TbUserService { public class DefaultUserService extends AbstractTbEntityService implements TbUserService {
private final UserService userService; private final UserService userService;
private final TbAlarmService tbAlarmService;
private final MailService mailService; private final MailService mailService;
private final SystemSecurityService systemSecurityService; private final SystemSecurityService systemSecurityService;
@ -80,6 +82,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
UserId userId = tbUser.getId(); UserId userId = tbUser.getId();
try { try {
tbAlarmService.unassignUserAlarms(tenantId, tbUser, System.currentTimeMillis());
userService.deleteUser(tenantId, userId); userService.deleteUser(tenantId, userId);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, userId, tbUser, notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, userId, tbUser,
user, ActionType.DELETED, true, null, customerId.toString()); user, ActionType.DELETED, true, null, customerId.toString());

8
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -27,14 +27,18 @@ import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
@ -55,9 +59,11 @@ import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource
public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService { public class DefaultTbResourceService extends AbstractTbEntityService implements TbResourceService {
private final ResourceService resourceService; private final ResourceService resourceService;
private final WidgetTypeService widgetTypeService;
public DefaultTbResourceService(ResourceService resourceService) { public DefaultTbResourceService(ResourceService resourceService, WidgetTypeService widgetTypeService) {
this.resourceService = resourceService; this.resourceService = resourceService;
this.widgetTypeService = widgetTypeService;
} }
@Override @Override

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

@ -32,6 +32,7 @@ import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@ -39,6 +40,7 @@ import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.alarm.AlarmDao; import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -529,6 +531,51 @@ public class AlarmControllerTest extends AbstractControllerTest {
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED); tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED);
} }
@Test
public void testUnassignAlarmOnUserRemoving() throws Exception {
loginDifferentTenant();
User user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setTenantId(tenantId);
user.setEmail("tenantForAssign@thingsboard.org");
User savedUser = createUser(user, "password");
Device device = createDevice("Different tenant device", "default", "differentTenantTest");
Alarm alarm = Alarm.builder()
.type(TEST_ALARM_TYPE)
.tenantId(savedDifferentTenant.getId())
.originator(device.getId())
.severity(AlarmSeverity.MAJOR)
.build();
alarm = doPost("/api/alarm", alarm, Alarm.class);
Assert.assertNotNull(alarm);
alarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(alarm);
Mockito.reset(tbClusterService, auditLogService);
long beforeAssignmentTs = System.currentTimeMillis();
doPost("/api/alarm/" + alarm.getId() + "/assign/" + savedUser.getId().getId()).andExpect(status().isOk());
AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertEquals(savedUser.getId(), foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
beforeAssignmentTs = System.currentTimeMillis();
Mockito.reset(tbClusterService, auditLogService);
doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk());
foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class);
Assert.assertNotNull(foundAlarm);
Assert.assertNull(foundAlarm.getAssigneeId());
Assert.assertTrue(foundAlarm.getAssignTs() >= beforeAssignmentTs);
}
@Test @Test
public void testFindAlarmsViaCustomerUser() throws Exception { public void testFindAlarmsViaCustomerUser() throws Exception {
loginCustomerUser(); loginCustomerUser();

32
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

@ -38,6 +38,8 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -216,6 +218,36 @@ public class TbResourceControllerTest extends AbstractControllerTest {
.andExpect(statusReason(containsString(msgErrorNoFound("Resource", resourceIdStr)))); .andExpect(statusReason(containsString(msgErrorNoFound("Resource", resourceIdStr))));
} }
@Test
public void testShoudNotDeleteTbResourceIfAssignedToWidgetType() throws Exception {
TbResource resource = new TbResource();
resource.setResourceType(ResourceType.JKS);
resource.setTitle("My first resource");
resource.setFileName(DEFAULT_FILE_NAME);
resource.setData(TEST_DATA);
TbResource savedResource = save(resource);
Mockito.reset(tbClusterService, auditLogService);
String resourceIdStr = savedResource.getId().getId().toString();
//create widget type
WidgetsBundle widgetsBundle = new WidgetsBundle();
widgetsBundle.setTitle("My widgets bundle");
WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(JacksonUtil.fromString(String.format("{ \"resources\": [{\"url\":{\"entityType\":\"TB_RESOURCE\",\"id\":\"%s\"},\"isModule\":true}]}", savedResource.getId()), JsonNode.class));
doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
doDelete("/api/resource/" + resourceIdStr)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Following widget types uses current resource: ["
+ widgetType .getName()+ "]")));
}
@Test @Test
public void testFindTenantTbResources() throws Exception { public void testFindTenantTbResources() throws Exception {

3
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.dao.widget; package org.thingsboard.server.dao.widget;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetType;
@ -40,6 +41,8 @@ public interface WidgetTypeService extends EntityDaoService {
List<WidgetTypeInfo> findWidgetTypesInfosByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias); List<WidgetTypeInfo> findWidgetTypesInfosByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);
List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(TenantId tenantId, TbResourceId tbResourceId);
WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias); WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias);
void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias); void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);

18
common/data/src/main/java/org/thingsboard/server/common/data/Customer.java

@ -35,9 +35,9 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId, E
@NoXss @NoXss
@Length(fieldName = "title") @Length(fieldName = "title")
@ApiModelProperty(position = 3, value = "Title of the customer", example = "Company A") @ApiModelProperty(position = 3, required = true, value = "Title of the customer", example = "Company A")
private String title; private String title;
@ApiModelProperty(position = 5, required = true, value = "JSON object with Tenant Id") @ApiModelProperty(position = 5, value = "JSON object with Tenant Id")
private TenantId tenantId; private TenantId tenantId;
@Getter @Setter @Getter @Setter
@ -89,43 +89,43 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId, E
return super.getCreatedTime(); return super.getCreatedTime();
} }
@ApiModelProperty(position = 6, required = true, value = "Country", example = "US") @ApiModelProperty(position = 6, value = "Country", example = "US")
@Override @Override
public String getCountry() { public String getCountry() {
return super.getCountry(); return super.getCountry();
} }
@ApiModelProperty(position = 7, required = true, value = "State", example = "NY") @ApiModelProperty(position = 7, value = "State", example = "NY")
@Override @Override
public String getState() { public String getState() {
return super.getState(); return super.getState();
} }
@ApiModelProperty(position = 8, required = true, value = "City", example = "New York") @ApiModelProperty(position = 8, value = "City", example = "New York")
@Override @Override
public String getCity() { public String getCity() {
return super.getCity(); return super.getCity();
} }
@ApiModelProperty(position = 9, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400") @ApiModelProperty(position = 9, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@Override @Override
public String getAddress() { public String getAddress() {
return super.getAddress(); return super.getAddress();
} }
@ApiModelProperty(position = 10, required = true, value = "Address Line 2", example = "") @ApiModelProperty(position = 10, value = "Address Line 2", example = "")
@Override @Override
public String getAddress2() { public String getAddress2() {
return super.getAddress2(); return super.getAddress2();
} }
@ApiModelProperty(position = 11, required = true, value = "Zip code", example = "10004") @ApiModelProperty(position = 11, value = "Zip code", example = "10004")
@Override @Override
public String getZip() { public String getZip() {
return super.getZip(); return super.getZip();
} }
@ApiModelProperty(position = 12, required = true, value = "Phone number", example = "+1(415)777-7777") @ApiModelProperty(position = 12, value = "Phone number", example = "+1(415)777-7777")
@Override @Override
public String getPhone() { public String getPhone() {
return super.getPhone(); return super.getPhone();

2
common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java

@ -85,7 +85,7 @@ public class DashboardInfo extends BaseData<DashboardId> implements HasName, Has
this.tenantId = tenantId; this.tenantId = tenantId;
} }
@ApiModelProperty(position = 4, value = "Title of the dashboard.") @ApiModelProperty(position = 4, required = true, value = "Title of the dashboard.")
public String getTitle() { public String getTitle() {
return title; return title;
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/Device.java

@ -146,7 +146,7 @@ public class Device extends BaseDataWithAdditionalInfo<DeviceId> implements HasL
this.name = name; this.name = name;
} }
@ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor") @ApiModelProperty(position = 6, value = "Device Profile Name", example = "Temperature Sensor")
public String getType() { public String getType() {
return type; return type;
} }
@ -155,7 +155,7 @@ public class Device extends BaseDataWithAdditionalInfo<DeviceId> implements HasL
this.type = type; this.type = type;
} }
@ApiModelProperty(position = 7, required = true, value = "Label that may be used in widgets", example = "Room 234 Sensor") @ApiModelProperty(position = 7, value = "Label that may be used in widgets", example = "Room 234 Sensor")
public String getLabel() { public String getLabel() {
return label; return label;
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java

@ -40,7 +40,7 @@ public class EntityView extends BaseDataWithAdditionalInfo<EntityViewId>
private static final long serialVersionUID = 5582010124562018986L; private static final long serialVersionUID = 5582010124562018986L;
@ApiModelProperty(position = 7, required = true, value = "JSON object with the referenced Entity Id (Device or Asset).") @ApiModelProperty(position = 7, value = "JSON object with the referenced Entity Id (Device or Asset).")
private EntityId entityId; private EntityId entityId;
private TenantId tenantId; private TenantId tenantId;
private CustomerId customerId; private CustomerId customerId;
@ -52,7 +52,7 @@ public class EntityView extends BaseDataWithAdditionalInfo<EntityViewId>
@Length(fieldName = "type") @Length(fieldName = "type")
@ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor") @ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor")
private String type; private String type;
@ApiModelProperty(position = 8, required = true, value = "Set of telemetry and attribute keys to expose via Entity View.") @ApiModelProperty(position = 8, value = "Set of telemetry and attribute keys to expose via Entity View.")
private TelemetryEntityView keys; private TelemetryEntityView keys;
@ApiModelProperty(position = 9, value = "Represents the start time of the interval that is used to limit access to target device telemetry. Customer will not be able to see entity telemetry that is outside the specified interval;") @ApiModelProperty(position = 9, value = "Represents the start time of the interval that is used to limit access to target device telemetry. Customer will not be able to see entity telemetry that is outside the specified interval;")
private long startTimeMs; private long startTimeMs;

18
common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java

@ -34,14 +34,14 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId, HasTi
@Length(fieldName = "title") @Length(fieldName = "title")
@NoXss @NoXss
@ApiModelProperty(position = 3, value = "Title of the tenant", example = "Company A") @ApiModelProperty(position = 3, required = true, value = "Title of the tenant", example = "Company A")
private String title; private String title;
@NoXss @NoXss
@Length(fieldName = "region") @Length(fieldName = "region")
@ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America") @ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America")
private String region; private String region;
@ApiModelProperty(position = 6, required = true, value = "JSON object with Tenant Profile Id") @ApiModelProperty(position = 6, value = "JSON object with Tenant Profile Id")
private TenantProfileId tenantProfileId; private TenantProfileId tenantProfileId;
public Tenant() { public Tenant() {
@ -111,43 +111,43 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId, HasTi
return super.getCreatedTime(); return super.getCreatedTime();
} }
@ApiModelProperty(position = 7, required = true, value = "Country", example = "US") @ApiModelProperty(position = 7, value = "Country", example = "US")
@Override @Override
public String getCountry() { public String getCountry() {
return super.getCountry(); return super.getCountry();
} }
@ApiModelProperty(position = 8, required = true, value = "State", example = "NY") @ApiModelProperty(position = 8, value = "State", example = "NY")
@Override @Override
public String getState() { public String getState() {
return super.getState(); return super.getState();
} }
@ApiModelProperty(position = 9, required = true, value = "City", example = "New York") @ApiModelProperty(position = 9, value = "City", example = "New York")
@Override @Override
public String getCity() { public String getCity() {
return super.getCity(); return super.getCity();
} }
@ApiModelProperty(position = 10, required = true, value = "Address Line 1", example = "42 Broadway Suite 12-400") @ApiModelProperty(position = 10, value = "Address Line 1", example = "42 Broadway Suite 12-400")
@Override @Override
public String getAddress() { public String getAddress() {
return super.getAddress(); return super.getAddress();
} }
@ApiModelProperty(position = 11, required = true, value = "Address Line 2", example = "") @ApiModelProperty(position = 11, value = "Address Line 2", example = "")
@Override @Override
public String getAddress2() { public String getAddress2() {
return super.getAddress2(); return super.getAddress2();
} }
@ApiModelProperty(position = 12, required = true, value = "Zip code", example = "10004") @ApiModelProperty(position = 12, value = "Zip code", example = "10004")
@Override @Override
public String getZip() { public String getZip() {
return super.getZip(); return super.getZip();
} }
@ApiModelProperty(position = 13, required = true, value = "Phone number", example = "+1(415)777-7777") @ApiModelProperty(position = 13, value = "Phone number", example = "+1(415)777-7777")
@Override @Override
public String getPhone() { public String getPhone() {
return super.getPhone(); return super.getPhone();

6
common/data/src/main/java/org/thingsboard/server/common/data/User.java

@ -129,7 +129,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.authority = authority; this.authority = authority;
} }
@ApiModelProperty(position = 8, required = true, value = "First name of the user", example = "John") @ApiModelProperty(position = 8, required = false, value = "First name of the user", example = "John")
public String getFirstName() { public String getFirstName() {
return firstName; return firstName;
} }
@ -138,7 +138,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.firstName = firstName; this.firstName = firstName;
} }
@ApiModelProperty(position = 9, required = true, value = "Last name of the user", example = "Doe") @ApiModelProperty(position = 9, required = false, value = "Last name of the user", example = "Doe")
public String getLastName() { public String getLastName() {
return lastName; return lastName;
} }
@ -147,7 +147,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
this.lastName = lastName; this.lastName = lastName;
} }
@ApiModelProperty(position = 10, required = true, value = "Phone number of the user", example = "38012345123") @ApiModelProperty(position = 10, required = false, value = "Phone number of the user", example = "38012345123")
public String getPhone() { public String getPhone() {
return phone; return phone;
} }

6
common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java

@ -131,7 +131,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.name = name; this.name = name;
} }
@ApiModelProperty(position = 6, required = true, value = "Asset type", example = "Building") @ApiModelProperty(position = 6, value = "Asset type", example = "Building")
public String getType() { public String getType() {
return type; return type;
} }
@ -140,7 +140,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.type = type; this.type = type;
} }
@ApiModelProperty(position = 7, required = true, value = "Label that may be used in widgets", example = "NY Building") @ApiModelProperty(position = 7, value = "Label that may be used in widgets", example = "NY Building")
public String getLabel() { public String getLabel() {
return label; return label;
} }
@ -149,7 +149,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
this.label = label; this.label = label;
} }
@ApiModelProperty(position = 8, required = true, value = "JSON object with Asset Profile Id.") @ApiModelProperty(position = 8, value = "JSON object with Asset Profile Id.")
public AssetProfileId getAssetProfileId() { public AssetProfileId getAssetProfileId() {
return assetProfileId; return assetProfileId;
} }

1
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -109,6 +109,7 @@ public class BaseResourceService extends AbstractCachedEntityService<ResourceInf
public void deleteResource(TenantId tenantId, TbResourceId resourceId) { public void deleteResource(TenantId tenantId, TbResourceId resourceId) {
log.trace("Executing deleteResource [{}] [{}]", tenantId, resourceId); log.trace("Executing deleteResource [{}] [{}]", tenantId, resourceId);
Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId); Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId);
resourceValidator.validateDelete(tenantId, resourceId);
resourceDao.removeById(tenantId, resourceId.getId()); resourceDao.removeById(tenantId, resourceId.getId());
} }

4
dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java

@ -22,6 +22,7 @@ import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.TenantEntityWithDataDao; import org.thingsboard.server.dao.TenantEntityWithDataDao;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
@ -82,6 +83,9 @@ public abstract class DataValidator<D extends BaseData<?>> {
return null; return null;
} }
public void validateDelete(TenantId tenantId, EntityId entityId) {
}
protected boolean isSameData(D existentData, D actualData) { protected boolean isSameData(D existentData, D actualData) {
return actualData.getId() != null && existentData.getId().equals(actualData.getId()); return actualData.getId() != null && existentData.getId().equals(actualData.getId());
} }

21
dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java

@ -20,14 +20,21 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.resource.TbResourceDao; import org.thingsboard.server.dao.resource.TbResourceDao;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
import java.util.List;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.EntityType.TB_RESOURCE; import static org.thingsboard.server.common.data.EntityType.TB_RESOURCE;
@ -37,6 +44,9 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
@Autowired @Autowired
private TbResourceDao resourceDao; private TbResourceDao resourceDao;
@Autowired
private WidgetTypeDao widgetTypeDao;
@Autowired @Autowired
private TenantService tenantService; private TenantService tenantService;
@ -77,4 +87,15 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
} }
} }
} }
@Override
public void validateDelete(TenantId tenantId, EntityId resourceId) {
List<WidgetTypeDetails> widgets = widgetTypeDao.findWidgetTypesInfosByTenantIdAndResourceId(tenantId.getId(),
resourceId.getId());
if (!widgets.isEmpty()) {
List<String> widgetNames = widgets.stream().map(BaseWidgetType::getName).collect(Collectors.toList());
throw new DataValidationException(String.format("Following widget types uses current resource: %s", widgetNames));
}
}
} }

3
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -69,7 +69,8 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
private static final Map<EntityType, String> entityTableMap = new HashMap<>(); private static final Map<EntityType, String> entityTableMap = new HashMap<>();
private static final Map<EntityType, String> entityNameColumns = new HashMap<>(); private static final Map<EntityType, String> entityNameColumns = new HashMap<>();
private static final String SELECT_PHONE = " CASE WHEN entity.entity_type = 'TENANT' THEN (select phone from tenant where id = entity_id)" + private static final String SELECT_PHONE = " CASE WHEN entity.entity_type = 'TENANT' THEN (select phone from tenant where id = entity_id)" +
" WHEN entity.entity_type = 'CUSTOMER' THEN (select phone from customer where id = entity_id) END as phone"; " WHEN entity.entity_type = 'CUSTOMER' THEN (select phone from customer where id = entity_id)" +
" WHEN entity.entity_type = 'USER' THEN (select phone from tb_user where id = entity_id) END as phone";
private static final String SELECT_ZIP = " CASE WHEN entity.entity_type = 'TENANT' THEN (select zip from tenant where id = entity_id)" + private static final String SELECT_ZIP = " CASE WHEN entity.entity_type = 'TENANT' THEN (select zip from tenant where id = entity_id)" +
" WHEN entity.entity_type = 'CUSTOMER' THEN (select zip from customer where id = entity_id) END as zip"; " WHEN entity.entity_type = 'CUSTOMER' THEN (select zip from customer where id = entity_id) END as zip";
private static final String SELECT_ADDRESS_2 = " CASE WHEN entity.entity_type = 'TENANT'" + private static final String SELECT_ADDRESS_2 = " CASE WHEN entity.entity_type = 'TENANT'" +

5
dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java

@ -77,6 +77,11 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndBundleAliasAndAlias(tenantId, bundleAlias, alias)); return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndBundleAliasAndAlias(tenantId, bundleAlias, alias));
} }
@Override
public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndResourceId(tenantId, tbResourceId));
}
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.WIDGET_TYPE; return EntityType.WIDGET_TYPE;

7
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java

@ -46,4 +46,11 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
WidgetTypeEntity findWidgetTypeByTenantIdAndBundleAliasAndAlias(@Param("tenantId") UUID tenantId, WidgetTypeEntity findWidgetTypeByTenantIdAndBundleAliasAndAlias(@Param("tenantId") UUID tenantId,
@Param("bundleAlias") String bundleAlias, @Param("bundleAlias") String bundleAlias,
@Param("alias") String alias); @Param("alias") String alias);
@Query(value = "SELECT * FROM widget_type wt " +
"WHERE wt.tenant_id = :tenantId AND cast(wt.descriptor as json) ->> 'resources' LIKE LOWER(CONCAT('%', :resourceId, '%'))",
nativeQuery = true)
List<WidgetTypeDetailsEntity> findWidgetTypesInfosByTenantIdAndResourceId(@Param("tenantId") UUID tenantId,
@Param("resourceId") UUID resourceId);
} }

8
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java

@ -83,4 +83,12 @@ public interface WidgetTypeDao extends Dao<WidgetTypeDetails> {
*/ */
WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias); WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias);
/**
* Find widget types infos by tenantId and resourceId in descriptor.
*
* @param tenantId the tenantId
* @param tbResourceId the resourceId
* @return the list of widget types infos objects
*/
List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId);
} }

10
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -21,6 +21,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetType;
@ -37,6 +38,7 @@ import java.util.Optional;
public class WidgetTypeServiceImpl implements WidgetTypeService { public class WidgetTypeServiceImpl implements WidgetTypeService {
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_RESOURCE_ID = "Incorrect resourceId ";
public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias "; public static final String INCORRECT_BUNDLE_ALIAS = "Incorrect bundleAlias ";
@Autowired @Autowired
private WidgetTypeDao widgetTypeDao; private WidgetTypeDao widgetTypeDao;
@ -96,6 +98,14 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return widgetTypeDao.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias); return widgetTypeDao.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias);
} }
@Override
public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(TenantId tenantId, TbResourceId tbResourceId) {
log.trace("Executing findWidgetTypesInfosByTenantIdAndResourceId, tenantId [{}], tbResourceId [{}]", tenantId, tbResourceId);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validateId(tbResourceId, INCORRECT_RESOURCE_ID + tbResourceId);
return widgetTypeDao.findWidgetTypesInfosByTenantIdAndResourceId(tenantId.getId(), tbResourceId.getId());
}
@Override @Override
public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias) { public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias) {
log.trace("Executing findWidgetTypeByTenantIdBundleAliasAndAlias, tenantId [{}], bundleAlias [{}], alias [{}]", tenantId, bundleAlias, alias); log.trace("Executing findWidgetTypeByTenantIdBundleAliasAndAlias, tenantId [{}], bundleAlias [{}], alias [{}]", tenantId, bundleAlias, alias);

40
dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -70,6 +71,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
@ -79,6 +81,7 @@ import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity;
import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.sql.relation.RelationRepository; import org.thingsboard.server.dao.sql.relation.RelationRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -105,6 +108,8 @@ public class EntityServiceTest extends AbstractServiceTest {
@Autowired @Autowired
AssetService assetService; AssetService assetService;
@Autowired @Autowired
UserService userService;
@Autowired
AttributesService attributesService; AttributesService attributesService;
@Autowired @Autowired
DeviceService deviceService; DeviceService deviceService;
@ -227,6 +232,41 @@ public class EntityServiceTest extends AbstractServiceTest {
Assert.assertEquals(0, count); Assert.assertEquals(0, count);
} }
@Test
public void testCountHierarchicalUserEntitiesByQuery() throws InterruptedException {
List<User> users = new ArrayList<>();
createTestUserRelations(tenantId, users);
RelationsQueryFilter filter = new RelationsQueryFilter();
filter.setRootEntity(tenantId);
filter.setDirection(EntitySearchDirection.FROM);
EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, null);
List<EntityKey> entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "phone"));
EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, null, null);
PageData<EntityData> entityDataByQuery = entityService.findEntityDataByQuery(tenantId, new CustomerId(CustomerId.NULL_UUID), query);
List<EntityData> data = entityDataByQuery.getData();
Assert.assertEquals(data.size(), 5);
data.forEach(entityData -> Assert.assertNotNull(entityData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("phone")));
}
private void createTestUserRelations(TenantId tenantId, List<User> users) {
for (int i = 0; i < ENTITY_COUNT; i++) {
User user = new User();
user.setTenantId(tenantId);
user.setAuthority(Authority.TENANT_ADMIN);
user.setEmail(StringUtils.randomAlphabetic(10) + "@gmail.com");
user.setPhone(StringUtils.randomNumeric(10));
user = userService.saveUser(user);
users.add(user);
createRelation(tenantId, "Contains", tenantId, user.getId());
}
}
@Test @Test
public void testCountEdgeEntitiesByQuery() throws InterruptedException { public void testCountEdgeEntitiesByQuery() throws InterruptedException {
List<Edge> edges = new ArrayList<>(); List<Edge> edges = new ArrayList<>();

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

2
ui-ngx/src/app/core/public-api.ts

@ -18,7 +18,7 @@ export * from './api/public-api';
export * from './http/public-api'; export * from './http/public-api';
export * from './local-storage/local-storage.service'; export * from './local-storage/local-storage.service';
export * from './services/public-api'; export * from './services/public-api';
export * from './ws/telemetry-websocket.service'; export * from './ws/public-api';
export * from './core.state'; export * from './core.state';
export * from './core.module'; export * from './core.module';
export * from './utils'; export * from './utils';

50
ui-ngx/src/app/core/services/script/node-script-test.service.ts

@ -23,8 +23,8 @@ import {
NodeScriptTestDialogComponent, NodeScriptTestDialogComponent,
NodeScriptTestDialogData NodeScriptTestDialogData
} from '@shared/components/dialog/node-script-test-dialog.component'; } from '@shared/components/dialog/node-script-test-dialog.component';
import { sortObjectKeys } from '@core/utils';
import { ScriptLanguage } from '@shared/models/rule-node.models'; import { ScriptLanguage } from '@shared/models/rule-node.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -37,56 +37,52 @@ export class NodeScriptTestService {
testNodeScript(script: string, scriptType: string, functionTitle: string, testNodeScript(script: string, scriptType: string, functionTitle: string,
functionName: string, argNames: string[], ruleNodeId: string, helpId?: string, functionName: string, argNames: string[], ruleNodeId: string, helpId?: string,
scriptLang?: ScriptLanguage): Observable<string> { scriptLang?: ScriptLanguage, debugEventBody?: DebugRuleNodeEventBody): Observable<string> {
if (ruleNodeId) { if (ruleNodeId && !debugEventBody) {
return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe( return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe(
switchMap((debugIn) => { switchMap((debugIn) => {
let msg: any;
let metadata: {[key: string]: string};
let msgType: string;
if (debugIn) {
if (debugIn.data) {
try {
msg = JSON.parse(debugIn.data);
} catch (e) {}
}
if (debugIn.metadata) {
try {
metadata = JSON.parse(debugIn.metadata);
} catch (e) {}
}
msgType = debugIn.msgType;
}
return this.openTestScriptDialog(script, scriptType, functionTitle, return this.openTestScriptDialog(script, scriptType, functionTitle,
functionName, argNames, msg, metadata, msgType, helpId, scriptLang); functionName, argNames, debugIn, helpId, scriptLang);
}) })
); );
} else { } else {
return this.openTestScriptDialog(script, scriptType, functionTitle, return this.openTestScriptDialog(script, scriptType, functionTitle,
functionName, argNames, null, null, null, helpId, scriptLang); functionName, argNames, debugEventBody, helpId, scriptLang);
} }
} }
private openTestScriptDialog(script: string, scriptType: string, private openTestScriptDialog(script: string, scriptType: string, functionTitle: string, functionName: string,
functionTitle: string, functionName: string, argNames: string[], argNames: string[], eventBody: DebugRuleNodeEventBody, helpId?: string,
msg?: any, metadata?: {[key: string]: string}, msgType?: string, helpId?: string,
scriptLang?: ScriptLanguage): Observable<string> { scriptLang?: ScriptLanguage): Observable<string> {
let msg: any;
let metadata: {[key: string]: string};
let msgType: string;
if (eventBody && eventBody.data) {
try {
msg = JSON.parse(eventBody.data);
} catch (e) {}
}
if (!msg) { if (!msg) {
msg = { msg = {
temperature: 22.4, temperature: 22.4,
humidity: 78 humidity: 78
}; };
} }
if (eventBody && eventBody.metadata) {
try {
metadata = JSON.parse(eventBody.metadata);
} catch (e) {}
}
if (!metadata) { if (!metadata) {
metadata = { metadata = {
deviceName: 'Test Device', deviceName: 'Test Device',
deviceType: 'default', deviceType: 'default',
ts: new Date().getTime() + '' ts: new Date().getTime() + ''
}; };
} else {
metadata = sortObjectKeys(metadata);
} }
if (!msgType) { if (eventBody && eventBody.msgType) {
msgType = eventBody.msgType;
} else {
msgType = 'POST_TELEMETRY_REQUEST'; msgType = 'POST_TELEMETRY_REQUEST';
} }
return this.dialog.open<NodeScriptTestDialogComponent, NodeScriptTestDialogData, string>(NodeScriptTestDialogComponent, return this.dialog.open<NodeScriptTestDialogComponent, NodeScriptTestDialogData, string>(NodeScriptTestDialogComponent,

19
ui-ngx/src/app/core/ws/public-api.ts

@ -0,0 +1,19 @@
///
/// 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.
///
export * from './notification-websocket.service';
export * from './telemetry-websocket.service';
export * from './websocket.service';

4
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -217,7 +217,7 @@
<mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 48) + 'px', <mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (cellActionDescriptors.length * 48) + 'px',
maxWidth: (cellActionDescriptors.length * 48) + 'px', maxWidth: (cellActionDescriptors.length * 48) + 'px',
width: (cellActionDescriptors.length * 48) + 'px' }"> width: (cellActionDescriptors.length * 48) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> <div [fxHide]="cellActionDescriptors.length !== 1" fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button [disabled]="(isLoading$ | async) || !actionDescriptor.isEnabled(entity)" <button mat-icon-button [disabled]="(isLoading$ | async) || !actionDescriptor.isEnabled(entity)"
*ngFor="let actionDescriptor of cellActionDescriptors" *ngFor="let actionDescriptor of cellActionDescriptors"
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}" matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
@ -228,7 +228,7 @@
{{actionDescriptor.icon}}</mat-icon> {{actionDescriptor.icon}}</mat-icon>
</button> </button>
</div> </div>
<div fxHide fxShow.lt-lg *ngIf="cellActionDescriptors.length"> <div fxHide [fxShow.lt-lg]="cellActionDescriptors.length !== 1" *ngIf="cellActionDescriptors.length">
<button mat-icon-button <button mat-icon-button
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu"> [matMenuTriggerFor]="cellActionsMenu">

4
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -637,6 +637,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} }
} }
cellActionDescriptorsUpdated() {
this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors];
}
headerCellStyle(column: EntityColumn<BaseData<HasId>>) { headerCellStyle(column: EntityColumn<BaseData<HasId>>) {
const index = this.entitiesTableConfig.columns.indexOf(column); const index = this.entitiesTableConfig.columns.indexOf(column);
let res = this.headerCellStyleCache[index]; let res = this.headerCellStyleCache[index];

29
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -21,7 +21,7 @@ import {
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { DebugEventType, Event, EventType, FilterEventBody } from '@shared/models/event.models'; import { DebugEventType, Event, EventBody, EventType, FilterEventBody } from '@shared/models/event.models';
import { TimePageLink } from '@shared/models/page/page-link'; import { TimePageLink } from '@shared/models/page/page-link';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@ -41,7 +41,7 @@ import {
} from '@home/components/event/event-content-dialog.component'; } from '@home/components/event/event-content-dialog.component';
import { isEqual, sortObjectKeys } from '@core/utils'; import { isEqual, sortObjectKeys } from '@core/utils';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { import {
EVENT_FILTER_PANEL_DATA, EVENT_FILTER_PANEL_DATA,
@ -61,6 +61,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
set eventType(eventType: EventType | DebugEventType) { set eventType(eventType: EventType | DebugEventType) {
if (this.eventTypeValue !== eventType) { if (this.eventTypeValue !== eventType) {
this.eventTypeValue = eventType; this.eventTypeValue = eventType;
this.updateCellAction();
this.updateColumns(true); this.updateColumns(true);
this.updateFilterColumns(); this.updateFilterColumns();
} }
@ -84,7 +85,9 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private debugEventTypes: Array<DebugEventType> = null, private debugEventTypes: Array<DebugEventType> = null,
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef,
public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>) {
super(); super();
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
@ -120,6 +123,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.updateColumns(); this.updateColumns();
this.updateCellAction();
this.updateFilterColumns(); this.updateFilterColumns();
this.headerActionDescriptors.push({ this.headerActionDescriptors.push({
@ -348,6 +352,25 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
} }
} }
updateCellAction() {
this.cellActionDescriptors = [];
switch (this.eventType) {
case DebugEventType.DEBUG_RULE_NODE:
if (this.testButtonLabel) {
this.cellActionDescriptors.push({
name: this.translate.instant('rulenode.test-with-this-message', {test: this.translate.instant(this.testButtonLabel)}),
icon: 'bug_report',
isEnabled: (entity) => entity.body.type === 'IN',
onAction: ($event, entity) => {
this.debugEventSelected.next(entity.body);
}
});
}
break;
}
this.getTable()?.cellActionDescriptorsUpdated();
}
showContent($event: MouseEvent, content: string, title: string, contentType: ContentType = null, sortKeys = false): void { showContent($event: MouseEvent, content: string, title: string, contentType: ContentType = null, sortKeys = false): void {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();

37
ui-ngx/src/app/modules/home/components/event/event-table.component.ts

@ -17,10 +17,10 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component, EventEmitter,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit, Output,
ViewChild, ViewChild,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
@ -32,9 +32,10 @@ import { EntitiesTableComponent } from '@home/components/entity/entities-table.c
import { EventTableConfig } from './event-table-config'; import { EventTableConfig } from './event-table-config';
import { EventService } from '@core/http/event.service'; import { EventService } from '@core/http/event.service';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { DebugEventType, EventType } from '@shared/models/event.models'; import { DebugEventType, EventBody, EventType } from '@shared/models/event.models';
import { Overlay } from '@angular/cdk/overlay'; import { Overlay } from '@angular/cdk/overlay';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { isNotEmptyStr } from '@core/utils';
@Component({ @Component({
selector: 'tb-event-table', selector: 'tb-event-table',
@ -59,6 +60,10 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
dirtyValue = false; dirtyValue = false;
entityIdValue: EntityId; entityIdValue: EntityId;
get active(): boolean {
return this.activeValue;
}
@Input() @Input()
set active(active: boolean) { set active(active: boolean) {
if (this.activeValue !== active) { if (this.activeValue !== active) {
@ -83,6 +88,28 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
} }
} }
private functionTestButtonLabelValue: string;
get functionTestButtonLabel(): string {
return this.functionTestButtonLabelValue;
}
@Input()
set functionTestButtonLabel(value: string) {
if (isNotEmptyStr(value)) {
this.functionTestButtonLabelValue = value;
} else {
this.functionTestButtonLabelValue = '';
}
if (this.eventTableConfig) {
this.eventTableConfig.testButtonLabel = this.functionTestButtonLabel;
this.eventTableConfig.updateCellAction();
}
}
@Output()
debugEventSelected = new EventEmitter<EventBody>();
@ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent; @ViewChild(EntitiesTableComponent, {static: true}) entitiesTable: EntitiesTableComponent;
eventTableConfig: EventTableConfig; eventTableConfig: EventTableConfig;
@ -114,7 +141,9 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.debugEventTypes, this.debugEventTypes,
this.overlay, this.overlay,
this.viewContainerRef, this.viewContainerRef,
this.cd this.cd,
this.functionTestButtonLabel,
this.debugEventSelected
); );
} }

2
ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html

@ -35,7 +35,7 @@
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
<button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value && !disabled && editProfileEnabled" <button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value?.id && !disabled && editProfileEnabled"
type="button" type="button"
matSuffix mat-icon-button aria-label="Edit" matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'asset-profile.edit' | translate }}" matTooltip="{{ 'asset-profile.edit' | translate }}"

2
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html

@ -35,7 +35,7 @@
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled && editProfileEnabled" <button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value?.id && !disabled && editProfileEnabled"
type="button" type="button"
matSuffix mat-icon-button aria-label="Edit" matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'device-profile.edit' | translate }}" matTooltip="{{ 'device-profile.edit' | translate }}"

7
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html

@ -125,7 +125,7 @@
<mat-cell *matCellDef="let alarm" [ngStyle.gt-md]="{ minWidth: (alarmsDatasource.countCellButtonAction * 48) + 'px', <mat-cell *matCellDef="let alarm" [ngStyle.gt-md]="{ minWidth: (alarmsDatasource.countCellButtonAction * 48) + 'px',
maxWidth: (alarmsDatasource.countCellButtonAction * 48) + 'px', maxWidth: (alarmsDatasource.countCellButtonAction * 48) + 'px',
width: (alarmsDatasource.countCellButtonAction * 48) + 'px' }"> width: (alarmsDatasource.countCellButtonAction * 48) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> <div [fxHide]="showCellActionsMenu" fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-container *ngFor="let actionDescriptor of alarm.actionCellButtons; trackBy: trackByActionCellDescriptionId"> <ng-container *ngFor="let actionDescriptor of alarm.actionCellButtons; trackBy: trackByActionCellDescriptionId">
<span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span> <span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span>
<button mat-icon-button [disabled]="(isLoading$ | async) || !actionEnabled(alarm, actionDescriptor)" <button mat-icon-button [disabled]="(isLoading$ | async) || !actionEnabled(alarm, actionDescriptor)"
@ -137,7 +137,7 @@
</button> </button>
</ng-container> </ng-container>
</div> </div>
<div fxHide fxShow.lt-lg *ngIf="alarm.hasActions"> <div fxHide [fxShow.lt-lg]="showCellActionsMenu" *ngIf="alarm.hasActions">
<button mat-icon-button <button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();" (click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu"> [matMenuTriggerFor]="cellActionsMenu">
@ -160,7 +160,8 @@
<mat-row [ngClass]="{'mat-row-select': enableSelection, <mat-row [ngClass]="{'mat-row-select': enableSelection,
'mat-selected': alarmsDatasource.isSelected(alarm), 'mat-selected': alarmsDatasource.isSelected(alarm),
'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm), 'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm),
'invisible': alarmsDatasource.dataLoading}" 'invisible': alarmsDatasource.dataLoading,
'tb-pointer': hasRowAction}"
*matRowDef="let alarm; columns: displayedColumns; let row = index" *matRowDef="let alarm; columns: displayedColumns; let row = index"
[ngStyle]="rowStyle(alarm, row)" [ngStyle]="rowStyle(alarm, row)"
(click)="onRowClick($event, alarm)"></mat-row> (click)="onRowClick($event, alarm)"></mat-row>

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

@ -160,6 +160,7 @@ interface AlarmWidgetActionDescriptor extends TableCellButtonActionDescriptor {
}) })
export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit { export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -171,6 +172,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public displayPagination = true; public displayPagination = true;
public enableStickyHeader = true; public enableStickyHeader = true;
public enableStickyAction = false; public enableStickyAction = false;
public showCellActionsMenu = true;
public pageSizeOptions; public pageSizeOptions;
public pageLink: AlarmDataPageLink; public pageLink: AlarmDataPageLink;
public sortOrderProperty: string; public sortOrderProperty: string;
@ -180,6 +182,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public displayedColumns: string[] = []; public displayedColumns: string[] = [];
public alarmsDatasource: AlarmsDatasource; public alarmsDatasource: AlarmsDatasource;
public noDataDisplayMessageText: string; public noDataDisplayMessageText: string;
public hasRowAction: boolean;
private setCellButtonAction: boolean; private setCellButtonAction: boolean;
private cellContentCache: Array<any> = []; private cellContentCache: Array<any> = [];
@ -355,6 +358,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : false; this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : false;
this.showCellActionsMenu = isDefined(this.settings.showCellActionsMenu) ? this.settings.showCellActionsMenu : true;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
let enableFilter; let enableFilter;
if (isDefined(this.settings.enableFilter)) { if (isDefined(this.settings.enableFilter)) {
@ -493,6 +497,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
} }
this.setCellButtonAction = !!(actionCellDescriptors.length + this.ctx.actionsApi.getActionDescriptors('actionCellButton').length); this.setCellButtonAction = !!(actionCellDescriptors.length + this.ctx.actionsApi.getActionDescriptors('actionCellButton').length);
this.hasRowAction = !!this.ctx.actionsApi.getActionDescriptors('rowClick').length;
if (this.setCellButtonAction) { if (this.setCellButtonAction) {
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');

7
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html

@ -55,7 +55,7 @@
<mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (entityDatasource.countCellButtonAction * 48) + 'px', <mat-cell *matCellDef="let entity" [ngStyle.gt-md]="{ minWidth: (entityDatasource.countCellButtonAction * 48) + 'px',
maxWidth: (entityDatasource.countCellButtonAction * 48) + 'px', maxWidth: (entityDatasource.countCellButtonAction * 48) + 'px',
width: (entityDatasource.countCellButtonAction * 48) + 'px' }"> width: (entityDatasource.countCellButtonAction * 48) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> <div [fxHide]="showCellActionsMenu && entityDatasource.countCellButtonAction !== 1" fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-container *ngFor="let actionDescriptor of entity.actionCellButtons; trackBy: trackByActionCellDescriptionId"> <ng-container *ngFor="let actionDescriptor of entity.actionCellButtons; trackBy: trackByActionCellDescriptionId">
<span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span> <span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span>
<button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
@ -67,7 +67,7 @@
</button> </button>
</ng-container> </ng-container>
</div> </div>
<div fxHide fxShow.lt-lg *ngIf="entity.hasActions"> <div fxHide [fxShow.lt-lg]="showCellActionsMenu && entityDatasource.countCellButtonAction !== 1" *ngIf="entity.hasActions">
<button mat-icon-button <button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();" (click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu"> [matMenuTriggerFor]="cellActionsMenu">
@ -88,7 +88,8 @@
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: enableStickyHeader"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity), <mat-row [ngClass]="{'tb-current-entity': entityDatasource.isCurrentEntity(entity),
'invisible': entityDatasource.dataLoading}" 'invisible': entityDatasource.dataLoading,
'tb-pointer': hasRowAction}"
*matRowDef="let entity; columns: displayedColumns; let row = index" *matRowDef="let entity; columns: displayedColumns; let row = index"
[ngStyle]="rowStyle(entity, row)" [ngStyle]="rowStyle(entity, row)"
(click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row> (click)="onRowClick($event, entity)" (dblclick)="onRowClick($event, entity, true)"></mat-row>

10
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -22,6 +22,7 @@ import {
Injector, Injector,
Input, Input,
NgZone, NgZone,
OnDestroy,
OnInit, OnInit,
StaticProvider, StaticProvider,
ViewChild, ViewChild,
@ -78,8 +79,8 @@ import {
getColumnDefaultVisibility, getColumnDefaultVisibility,
getColumnSelectionAvailability, getColumnSelectionAvailability,
getColumnWidth, getColumnWidth,
getHeaderTitle,
getEntityValue, getEntityValue,
getHeaderTitle,
getRowStyleInfo, getRowStyleInfo,
getTableCellButtonActions, getTableCellButtonActions,
noDataMessage, noDataMessage,
@ -129,7 +130,7 @@ interface EntitiesTableWidgetSettings extends TableWidgetSettings {
templateUrl: './entities-table-widget.component.html', templateUrl: './entities-table-widget.component.html',
styleUrls: ['./entities-table-widget.component.scss', './table-widget.scss'] styleUrls: ['./entities-table-widget.component.scss', './table-widget.scss']
}) })
export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { export class EntitiesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -141,6 +142,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public displayPagination = true; public displayPagination = true;
public enableStickyHeader = true; public enableStickyHeader = true;
public enableStickyAction = true; public enableStickyAction = true;
public showCellActionsMenu = true;
public pageSizeOptions; public pageSizeOptions;
public pageLink: EntityDataPageLink; public pageLink: EntityDataPageLink;
public sortOrderProperty: string; public sortOrderProperty: string;
@ -150,6 +152,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public displayedColumns: string[] = []; public displayedColumns: string[] = [];
public entityDatasource: EntityDatasource; public entityDatasource: EntityDatasource;
public noDataDisplayMessageText: string; public noDataDisplayMessageText: string;
public hasRowAction: boolean;
private setCellButtonAction: boolean; private setCellButtonAction: boolean;
private cellContentCache: Array<any> = []; private cellContentCache: Array<any> = [];
@ -278,6 +281,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length; this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length;
this.hasRowAction = !!this.ctx.actionsApi.getActionDescriptors('rowClick').length || !!this.ctx.actionsApi.getActionDescriptors('rowDoubleClick').length;
if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) { if (this.settings.entitiesTitle && this.settings.entitiesTitle.length) {
this.entitiesTitlePattern = this.utils.customTranslation(this.settings.entitiesTitle, this.settings.entitiesTitle); this.entitiesTitlePattern = this.utils.customTranslation(this.settings.entitiesTitle, this.settings.entitiesTitle);
} else { } else {
@ -290,6 +295,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true;
this.showCellActionsMenu = isDefined(this.settings.showCellActionsMenu) ? this.settings.showCellActionsMenu : true;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
this.rowStylesInfo = getRowStyleInfo(this.settings, 'entity, ctx'); this.rowStylesInfo = getRowStyleInfo(this.settings, 'entity, ctx');

1
ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss

@ -72,6 +72,7 @@
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
outline: none; outline: none;
cursor: pointer;
&.tb-horizontal { &.tb-horizontal {
width: 95%; width: 95%;

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

@ -47,10 +47,13 @@
<mat-slide-toggle class="mat-slide" formControlName="enableSelection"> <mat-slide-toggle class="mat-slide" formControlName="enableSelection">
{{ 'widgets.table.enable-alarms-selection' | translate }} {{ 'widgets.table.enable-alarms-selection' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div class="tb-form-row"> <div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide" formControlName="enableStickyAction"> <mat-slide-toggle class="mat-slide" formControlName="enableStickyAction">
{{ 'widgets.table.enable-sticky-action' | translate }} {{ 'widgets.table.enable-sticky-action' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showCellActionsMenu">
{{ 'widgets.table.show-cell-actions-menu-mobile' | translate }}
</mat-slide-toggle>
</div> </div>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>widgets.table.table-buttons</div> <div class="tb-form-panel-title" translate>widgets.table.table-buttons</div>

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

@ -47,6 +47,7 @@ export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent
enableFilter: true, enableFilter: true,
enableStickyHeader: true, enableStickyHeader: true,
enableStickyAction: true, enableStickyAction: true,
showCellActionsMenu: true,
reserveSpaceForHiddenAction: 'true', reserveSpaceForHiddenAction: 'true',
displayDetails: true, displayDetails: true,
allowAcknowledgment: true, allowAcknowledgment: true,
@ -69,6 +70,7 @@ export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent
enableFilter: [settings.enableFilter, []], enableFilter: [settings.enableFilter, []],
enableStickyHeader: [settings.enableStickyHeader, []], enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []], enableStickyAction: [settings.enableStickyAction, []],
showCellActionsMenu: [settings.showCellActionsMenu, []],
reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []],
displayDetails: [settings.displayDetails, []], displayDetails: [settings.displayDetails, []],
allowAcknowledgment: [settings.allowAcknowledgment, []], allowAcknowledgment: [settings.allowAcknowledgment, []],

5
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html

@ -62,10 +62,13 @@
{{ 'widgets.table.display-entity-type' | translate }} {{ 'widgets.table.display-entity-type' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-row"> <div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide" formControlName="enableStickyAction"> <mat-slide-toggle class="mat-slide" formControlName="enableStickyAction">
{{ 'widgets.table.enable-sticky-action' | translate }} {{ 'widgets.table.enable-sticky-action' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showCellActionsMenu">
{{ 'widgets.table.show-cell-actions-menu-mobile' | translate }}
</mat-slide-toggle>
</div> </div>
<mat-form-field fxFlex subscriptSizing="dynamic"> <mat-form-field fxFlex subscriptSizing="dynamic">
<mat-label translate>widgets.table.hidden-cell-button-display-mode</mat-label> <mat-label translate>widgets.table.hidden-cell-button-display-mode</mat-label>

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.ts

@ -45,6 +45,7 @@ export class EntitiesTableWidgetSettingsComponent extends WidgetSettingsComponen
enableSelectColumnDisplay: true, enableSelectColumnDisplay: true,
enableStickyHeader: true, enableStickyHeader: true,
enableStickyAction: true, enableStickyAction: true,
showCellActionsMenu: true,
reserveSpaceForHiddenAction: 'true', reserveSpaceForHiddenAction: 'true',
displayEntityName: true, displayEntityName: true,
entityNameColumnTitle: '', entityNameColumnTitle: '',
@ -66,6 +67,7 @@ export class EntitiesTableWidgetSettingsComponent extends WidgetSettingsComponen
enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []], enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []],
enableStickyHeader: [settings.enableStickyHeader, []], enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []], enableStickyAction: [settings.enableStickyAction, []],
showCellActionsMenu: [settings.showCellActionsMenu, []],
reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []],
displayEntityName: [settings.displayEntityName, []], displayEntityName: [settings.displayEntityName, []],
entityNameColumnTitle: [settings.entityNameColumnTitle, []], entityNameColumnTitle: [settings.entityNameColumnTitle, []],

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html

@ -39,6 +39,9 @@
<mat-slide-toggle class="mat-slide" formControlName="enableStickyAction"> <mat-slide-toggle class="mat-slide" formControlName="enableStickyAction">
{{ 'widgets.table.enable-sticky-action' | translate }} {{ 'widgets.table.enable-sticky-action' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showCellActionsMenu">
{{ 'widgets.table.show-cell-actions-menu-mobile' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex subscriptSizing="dynamic"> <mat-form-field fxFlex subscriptSizing="dynamic">
<mat-label translate>widgets.table.hidden-cell-button-display-mode</mat-label> <mat-label translate>widgets.table.hidden-cell-button-display-mode</mat-label>
<mat-select formControlName="reserveSpaceForHiddenAction"> <mat-select formControlName="reserveSpaceForHiddenAction">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts

@ -44,6 +44,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
enableSelectColumnDisplay: true, enableSelectColumnDisplay: true,
enableStickyHeader: true, enableStickyHeader: true,
enableStickyAction: true, enableStickyAction: true,
showCellActionsMenu: true,
reserveSpaceForHiddenAction: 'true', reserveSpaceForHiddenAction: 'true',
showTimestamp: true, showTimestamp: true,
showMilliseconds: false, showMilliseconds: false,
@ -63,6 +64,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []], enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []],
enableStickyHeader: [settings.enableStickyHeader, []], enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []], enableStickyAction: [settings.enableStickyAction, []],
showCellActionsMenu: [settings.showCellActionsMenu, []],
reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []],
showTimestamp: [settings.showTimestamp, []], showTimestamp: [settings.showTimestamp, []],
showMilliseconds: [settings.showMilliseconds, []], showMilliseconds: [settings.showMilliseconds, []],

1
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -33,6 +33,7 @@ export interface TableWidgetSettings {
enableSearch: boolean; enableSearch: boolean;
enableSelectColumnDisplay: boolean; enableSelectColumnDisplay: boolean;
enableStickyAction: boolean; enableStickyAction: boolean;
showCellActionsMenu: boolean;
enableStickyHeader: boolean; enableStickyHeader: boolean;
displayPagination: boolean; displayPagination: boolean;
defaultPageSize: number; defaultPageSize: number;

7
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html

@ -67,7 +67,7 @@
<mat-cell *matCellDef="let row" [ngStyle.gt-md]="{ minWidth: (source.timeseriesDatasource.countCellButtonAction * 48) + 'px', <mat-cell *matCellDef="let row" [ngStyle.gt-md]="{ minWidth: (source.timeseriesDatasource.countCellButtonAction * 48) + 'px',
maxWidth: (source.timeseriesDatasource.countCellButtonAction * 48) + 'px', maxWidth: (source.timeseriesDatasource.countCellButtonAction * 48) + 'px',
width: (source.timeseriesDatasource.countCellButtonAction * 48) + 'px' }"> width: (source.timeseriesDatasource.countCellButtonAction * 48) + 'px' }">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> <div [fxHide]="showCellActionsMenu && source.timeseriesDatasource.countCellButtonAction !== 1" fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-container *ngFor="let actionDescriptor of row.actionCellButtons; trackBy: trackByActionCellDescriptionId"> <ng-container *ngFor="let actionDescriptor of row.actionCellButtons; trackBy: trackByActionCellDescriptionId">
<span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span> <span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span>
<button *ngIf="actionDescriptor.icon" <button *ngIf="actionDescriptor.icon"
@ -79,7 +79,7 @@
</button> </button>
</ng-container> </ng-container>
</div> </div>
<div fxHide fxShow.lt-lg *ngIf="row.hasActions"> <div fxHide [fxShow.lt-lg]="showCellActionsMenu && source.timeseriesDatasource.countCellButtonAction !== 1" *ngIf="row.hasActions">
<button mat-icon-button <button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();" (click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu"> [matMenuTriggerFor]="cellActionsMenu">
@ -99,7 +99,8 @@
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: enableStickyHeader"></mat-header-row> <mat-header-row *matHeaderRowDef="source.displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<mat-row *matRowDef="let row; columns: source.displayedColumns; let rowIndex = index" <mat-row [ngClass]="{'tb-pointer': hasRowAction}"
*matRowDef="let row; columns: source.displayedColumns; let rowIndex = index"
[ngStyle]="rowStyle(source, row, rowIndex)" [ngStyle]="rowStyle(source, row, rowIndex)"
(click)="onRowClick($event, row)"></mat-row> (click)="onRowClick($event, row)"></mat-row>
</table> </table>

4
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -154,6 +154,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
public displayPagination = true; public displayPagination = true;
public enableStickyHeader = true; public enableStickyHeader = true;
public enableStickyAction = true; public enableStickyAction = true;
public showCellActionsMenu = true;
public pageSizeOptions; public pageSizeOptions;
public textSearchMode = false; public textSearchMode = false;
public hidePageSize = false; public hidePageSize = false;
@ -161,6 +162,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
public sources: TimeseriesTableSource[]; public sources: TimeseriesTableSource[];
public sourceIndex: number; public sourceIndex: number;
public noDataDisplayMessageText: string; public noDataDisplayMessageText: string;
public hasRowAction: boolean;
private setCellButtonAction: boolean; private setCellButtonAction: boolean;
private cellContentCache: Array<any> = []; private cellContentCache: Array<any> = [];
@ -300,12 +302,14 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction]; this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length; this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length;
this.hasRowAction = !!this.ctx.actionsApi.getActionDescriptors('rowClick').length;
this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true; this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true; this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true;
this.showCellActionsMenu = isDefined(this.settings.showCellActionsMenu) ? this.settings.showCellActionsMenu : true;
this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false; this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false;
this.useEntityLabel = isDefined(this.widgetConfig.settings.useEntityLabel) ? this.widgetConfig.settings.useEntityLabel : false; this.useEntityLabel = isDefined(this.widgetConfig.settings.useEntityLabel) ? this.widgetConfig.settings.useEntityLabel : false;
this.showTimestamp = this.settings.showTimestamp !== false; this.showTimestamp = this.settings.showTimestamp !== false;

1
ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts

@ -80,6 +80,7 @@ export interface IEntitiesTableComponent {
exitFilterMode(): void; exitFilterMode(): void;
resetSortAndFilter(update?: boolean, preserveTimewindow?: boolean): void; resetSortAndFilter(update?: boolean, preserveTimewindow?: boolean): void;
columnsUpdated(resetData?: boolean): void; columnsUpdated(resetData?: boolean): void;
cellActionDescriptorsUpdated(): void;
headerCellStyle(column: EntityColumn<BaseData<HasId>>): any; headerCellStyle(column: EntityColumn<BaseData<HasId>>): any;
clearCellCache(col: number, row: number): void; clearCellCache(col: number, row: number): void;
cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number): any; cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number): any;

10
ui-ngx/src/app/modules/home/pages/notification/inbox/inbox-table-config.resolver.ts

@ -39,6 +39,7 @@ import {
} from '@home/pages/notification/inbox/inbox-notification-dialog.component'; } from '@home/pages/notification/inbox/inbox-notification-dialog.component';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
import { UtilsService } from '@core/services/utils.service';
@Injectable() @Injectable()
export class InboxTableConfigResolver implements Resolve<EntityTableConfig<Notification>> { export class InboxTableConfigResolver implements Resolve<EntityTableConfig<Notification>> {
@ -48,7 +49,8 @@ export class InboxTableConfigResolver implements Resolve<EntityTableConfig<Notif
constructor(private notificationService: NotificationService, constructor(private notificationService: NotificationService,
private translate: TranslateService, private translate: TranslateService,
private dialog: MatDialog, private dialog: MatDialog,
private datePipe: DatePipe) { private datePipe: DatePipe,
private utilsService: UtilsService) {
this.config.entityType = EntityType.NOTIFICATION; this.config.entityType = EntityType.NOTIFICATION;
this.config.detailsPanelEnabled = false; this.config.detailsPanelEnabled = false;
@ -92,8 +94,10 @@ export class InboxTableConfigResolver implements Resolve<EntityTableConfig<Notif
new DateEntityTableColumn<Notification>('createdTime', 'common.created-time', this.datePipe, '170px'), new DateEntityTableColumn<Notification>('createdTime', 'common.created-time', this.datePipe, '170px'),
new EntityTableColumn<Notification>('type', 'notification.type', '10%', (notification) => new EntityTableColumn<Notification>('type', 'notification.type', '10%', (notification) =>
this.translate.instant(NotificationTemplateTypeTranslateMap.get(notification.type).name)), this.translate.instant(NotificationTemplateTypeTranslateMap.get(notification.type).name)),
new EntityTableColumn<Notification>('subject', 'notification.subject', '30%'), new EntityTableColumn<Notification>('subject', 'notification.subject', '30%',
new EntityTableColumn<Notification>('text', 'notification.message', '60%') (entity) => this.utilsService.customTranslation(entity.subject, entity.subject)),
new EntityTableColumn<Notification>('text', 'notification.message', '60%',
(entity) => this.utilsService.customTranslation(entity.text, entity.text))
); );
} }

2
ui-ngx/src/app/modules/home/pages/notification/rule/rule-table-config.resolver.ts

@ -81,7 +81,7 @@ export class RuleTableConfigResolver implements Resolve<EntityTableConfig<Notifi
(rule) => this.translate.instant(TriggerTypeTranslationMap.get(rule.triggerType)) || '', (rule) => this.translate.instant(TriggerTypeTranslationMap.get(rule.triggerType)) || '',
() => ({}), true), () => ({}), true),
new EntityTableColumn<NotificationRule>('additionalConfig.description', 'notification.description', '30%', new EntityTableColumn<NotificationRule>('additionalConfig.description', 'notification.description', '30%',
(target) => target.additionalConfig.description || '', (target) => target.additionalConfig?.description || '',
() => ({}), false) () => ({}), false)
); );
} }

33
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts

@ -18,14 +18,22 @@ import {
AfterViewInit, AfterViewInit,
Component, Component,
ComponentRef, ComponentRef,
EventEmitter,
forwardRef, forwardRef,
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output,
ViewChild, ViewChild,
ViewContainerRef ViewContainerRef
} from '@angular/core'; } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { import {
IRuleNodeConfigurationComponent, IRuleNodeConfigurationComponent,
RuleNodeConfiguration, RuleNodeConfiguration,
@ -76,6 +84,12 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
@Input() @Input()
ruleChainType: RuleChainType; ruleChainType: RuleChainType;
@Output()
initRuleNode = new EventEmitter<void>();
@Output()
changeScript = new EventEmitter<void>();
nodeDefinitionValue: RuleNodeDefinition; nodeDefinitionValue: RuleNodeDefinition;
@Input() @Input()
@ -85,6 +99,7 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
if (this.nodeDefinitionValue) { if (this.nodeDefinitionValue) {
this.validateDefinedDirective(); this.validateDefinedDirective();
} }
setTimeout(() => this.initRuleNode.emit());
} }
} }
@ -98,8 +113,11 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
changeSubscription: Subscription; changeSubscription: Subscription;
changeScriptSubscription: Subscription;
definedConfigComponent: IRuleNodeConfigurationComponent;
private definedConfigComponentRef: ComponentRef<IRuleNodeConfigurationComponent>; private definedConfigComponentRef: ComponentRef<IRuleNodeConfigurationComponent>;
private definedConfigComponent: IRuleNodeConfigurationComponent;
private configuration: RuleNodeConfiguration; private configuration: RuleNodeConfiguration;
@ -127,6 +145,14 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
if (this.definedConfigComponentRef) { if (this.definedConfigComponentRef) {
this.definedConfigComponentRef.destroy(); this.definedConfigComponentRef.destroy();
} }
if (this.changeSubscription) {
this.changeSubscription.unsubscribe();
this.changeSubscription = null;
}
if (this.changeScriptSubscription) {
this.changeScriptSubscription.unsubscribe();
this.changeScriptSubscription = null;
}
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -199,6 +225,9 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
this.changeSubscription = this.definedConfigComponent.configurationChanged.subscribe((configuration) => { this.changeSubscription = this.definedConfigComponent.configurationChanged.subscribe((configuration) => {
this.updateModel(configuration); this.updateModel(configuration);
}); });
if (this.definedConfigComponent?.changeScript) {
this.changeScriptSubscription = this.definedConfigComponent.changeScript.subscribe(() => this.changeScript.emit());
}
} }
} }

4
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.html

@ -50,7 +50,9 @@
[ruleNodeId]="ruleNode.ruleNodeId?.id" [ruleNodeId]="ruleNode.ruleNodeId?.id"
[ruleChainId]="ruleChainId" [ruleChainId]="ruleChainId"
[ruleChainType]="ruleChainType" [ruleChainType]="ruleChainType"
[nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition"> [nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition"
(initRuleNode)="initRuleNode.emit($event)"
(changeScript)="changeScript.emit($event)">
</tb-rule-node-config> </tb-rule-node-config>
<div formGroupName="additionalInfo" fxLayout="column" class="description-block"> <div formGroupName="additionalInfo" fxLayout="column" class="description-block">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">

8
ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -55,6 +55,12 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
@Input() @Input()
isAdd = false; isAdd = false;
@Output()
initRuleNode = new EventEmitter<void>();
@Output()
changeScript = new EventEmitter<void>();
ruleNodeType = RuleNodeType; ruleNodeType = RuleNodeType;
entityType = EntityType; entityType = EntityType;

9
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -111,7 +111,9 @@
[ruleChainId]="ruleChain.id?.id" [ruleChainId]="ruleChain.id?.id"
[ruleChainType]="ruleChainType" [ruleChainType]="ruleChainType"
[isEdit]="true" [isEdit]="true"
[isReadOnly]="false"> [isReadOnly]="false"
(initRuleNode)="onRuleNodeInit()"
(changeScript)="switchToFirstTab()">
</tb-rule-node> </tb-rule-node>
</mat-tab> </mat-tab>
<mat-tab *ngIf="editingRuleNode.ruleNodeId" label="{{ 'rulenode.events' | translate }}" #eventsTab="matTab"> <mat-tab *ngIf="editingRuleNode.ruleNodeId" label="{{ 'rulenode.events' | translate }}" #eventsTab="matTab">
@ -119,7 +121,10 @@
[defaultEventType]="debugEventTypes.DEBUG_RULE_NODE" [defaultEventType]="debugEventTypes.DEBUG_RULE_NODE"
[active]="eventsTab.isActive" [active]="eventsTab.isActive"
[tenantId]="ruleChain.tenantId.id" [tenantId]="ruleChain.tenantId.id"
[entityId]="editingRuleNode.ruleNodeId"></tb-event-table> [entityId]="editingRuleNode.ruleNodeId"
[functionTestButtonLabel]="ruleNodeTestButtonLabel"
(debugEventSelected)="onDebugEventSelected($event)">
</tb-event-table>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'rulenode.help' | translate }}"> <mat-tab label="{{ 'rulenode.help' | translate }}">
<div class="tb-rule-node-help"> <div class="tb-rule-node-help">

24
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -87,7 +87,7 @@ import { DialogComponent } from '@shared/components/dialog.component';
import { MatMenuTrigger } from '@angular/material/menu'; import { MatMenuTrigger } from '@angular/material/menu';
import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service'; import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service';
import { Hotkey } from 'angular2-hotkeys'; import { Hotkey } from 'angular2-hotkeys';
import { DebugEventType, EventType } from '@shared/models/event.models'; import { DebugEventType, DebugRuleNodeEventBody, EventType } from '@shared/models/event.models';
import { MatMiniFabButton } from '@angular/material/button'; import { MatMiniFabButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
@ -153,6 +153,7 @@ export class RuleChainPageComponent extends PageComponent
editingRuleNodeAllowCustomLabels = false; editingRuleNodeAllowCustomLabels = false;
editingRuleNodeLinkLabels: {[label: string]: LinkLabel}; editingRuleNodeLinkLabels: {[label: string]: LinkLabel};
editingRuleNodeSourceRuleChainId: string; editingRuleNodeSourceRuleChainId: string;
ruleNodeTestButtonLabel: string;
@ViewChild('tbRuleNode') ruleNodeComponent: RuleNodeDetailsComponent; @ViewChild('tbRuleNode') ruleNodeComponent: RuleNodeDetailsComponent;
@ViewChild('tbRuleNodeLink') ruleNodeLinkComponent: RuleNodeLinkComponent; @ViewChild('tbRuleNodeLink') ruleNodeLinkComponent: RuleNodeLinkComponent;
@ -1277,6 +1278,27 @@ export class RuleChainPageComponent extends PageComponent
this.editingRuleNodeLink = deepClone(edge); this.editingRuleNodeLink = deepClone(edge);
} }
onDebugEventSelected(debugEventBody: DebugRuleNodeEventBody) {
const ruleNodeConfigComponent = this.ruleNodeComponent.ruleNodeConfigComponent;
const ruleNodeConfigDefinedComponent = ruleNodeConfigComponent.definedConfigComponent;
if (ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript && ruleNodeConfigDefinedComponent.testScript) {
ruleNodeConfigDefinedComponent.testScript(debugEventBody);
}
}
onRuleNodeInit() {
const ruleNodeConfigDefinedComponent = this.ruleNodeComponent.ruleNodeConfigComponent.definedConfigComponent;
if (this.ruleNodeComponent.ruleNodeConfigComponent.useDefinedDirective() && ruleNodeConfigDefinedComponent.hasScript) {
this.ruleNodeTestButtonLabel = ruleNodeConfigDefinedComponent.testScriptLabel;
} else {
this.ruleNodeTestButtonLabel = '';
}
}
switchToFirstTab() {
this.selectedRuleNodeTabIndex = 0;
}
saveRuleNode() { saveRuleNode() {
this.ruleNodeComponent.validate(); this.ruleNodeComponent.validate();
if (this.ruleNodeComponent.ruleNodeFormGroup.valid) { if (this.ruleNodeComponent.ruleNodeFormGroup.valid) {

1
ui-ngx/src/app/shared/components/js-func.component.html

@ -27,6 +27,7 @@
<button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyJs()"> <button type='button' *ngIf="!disabled" mat-button class="tidy" (click)="beautifyJs()">
{{'js-func.tidy' | translate }} {{'js-func.tidy' | translate }}
</button> </button>
<ng-content select="[toolbarSuffixButton]"></ng-content>
<div *ngIf="helpId" [tb-help-popup]="helpId"></div> <div *ngIf="helpId" [tb-help-popup]="helpId"></div>
<fieldset style="width: initial"> <fieldset style="width: initial">
<div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" <div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"

59
ui-ngx/src/app/shared/components/unit-input.component.ts

@ -15,11 +15,14 @@
/// ///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of, shareReplay, switchMap } from 'rxjs';
import { searchUnits, Unit, unitBySymbol, units } from '@shared/models/unit.models'; import { searchUnits, Unit, unitBySymbol } from '@shared/models/unit.models';
import { map, mergeMap, startWith, tap } from 'rxjs/operators'; import { map, mergeMap, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ResourcesService } from '@core/services/resources.service';
const unitsModels = '/assets/model/units.json';
@Component({ @Component({
selector: 'tb-unit-input', selector: 'tb-unit-input',
@ -51,13 +54,12 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
private dirty = false; private dirty = false;
private translatedUnits: Array<Unit> = units.map(u => ({symbol: u.symbol, private fetchUnits$: Observable<Array<Unit>> = null;
name: this.translate.instant(u.name),
tags: u.tags}));
private propagateChange = (_val: any) => {}; private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder, constructor(private fb: FormBuilder,
private resourcesService: ResourcesService,
private translate: TranslateService) { private translate: TranslateService) {
} }
@ -68,22 +70,22 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
tap(value => { tap(value => {
this.updateView(value); this.updateView(value);
}), }),
startWith<string | Unit>(''),
map(value => (value as Unit)?.symbol ? (value as Unit).symbol : (value ? value as string : '')), map(value => (value as Unit)?.symbol ? (value as Unit).symbol : (value ? value as string : '')),
mergeMap(symbol => this.fetchUnits(symbol) ) mergeMap(symbol => this.fetchUnits(symbol))
); );
} }
writeValue(symbol?: string): void { writeValue(symbol?: string): void {
this.searchText = ''; this.searchText = '';
this.modelValue = symbol; this.modelValue = symbol;
let res: Unit | string = null; of(symbol).pipe(
if (symbol) { switchMap(value => value
const unit = unitBySymbol(symbol); ? this.unitsConstant().pipe(map(units => unitBySymbol(units, value) ?? value))
res = unit ? unit : symbol; : of(null))
} ).subscribe(result => {
this.unitsFormControl.patchValue(res, {emitEvent: false}); this.unitsFormControl.patchValue(result, {emitEvent: false});
this.dirty = true; this.dirty = true;
});
} }
onFocus() { onFocus() {
@ -114,12 +116,9 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
fetchUnits(searchText?: string): Observable<Array<Unit | string>> { fetchUnits(searchText?: string): Observable<Array<Unit | string>> {
this.searchText = searchText; this.searchText = searchText;
const result = searchUnits(this.translatedUnits, searchText); return this.unitsConstant().pipe(
if (result.length) { map(unit => searchUnits(unit, searchText))
return of(result); );
} else {
return of([]);
}
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -145,4 +144,18 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
this.unitInput.nativeElement.focus(); this.unitInput.nativeElement.focus();
}, 0); }, 0);
} }
private unitsConstant(): Observable<Array<Unit>> {
if (this.fetchUnits$ === null) {
this.fetchUnits$ = this.resourcesService.loadJsonResource<Array<Unit>>(unitsModels).pipe(
map(units => units.map(u => ({
symbol: u.symbol,
name: this.translate.instant(u.name),
tags: u.tags
}))),
shareReplay(1)
);
}
return this.fetchUnits$;
}
} }

2
ui-ngx/src/app/shared/models/notification.models.ts

@ -116,7 +116,7 @@ export interface NotificationRule extends Omit<BaseData<NotificationRuleId>, 'la
triggerType: TriggerType; triggerType: TriggerType;
triggerConfig: NotificationRuleTriggerConfig; triggerConfig: NotificationRuleTriggerConfig;
recipientsConfig: NotificationRuleRecipientConfig; recipientsConfig: NotificationRuleRecipientConfig;
additionalConfig: {description: string}; additionalConfig?: {description: string};
} }
export type NotificationRuleTriggerConfig = Partial<AlarmNotificationRuleTriggerConfig & DeviceInactivityNotificationRuleTriggerConfig & export type NotificationRuleTriggerConfig = Partial<AlarmNotificationRuleTriggerConfig & DeviceInactivityNotificationRuleTriggerConfig &

9
ui-ngx/src/app/shared/models/rule-node.models.ts

@ -26,6 +26,8 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { RuleChainType } from '@shared/models/rule-chain.models'; import { RuleChainType } from '@shared/models/rule-chain.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
import { TranslateService } from '@ngx-translate/core';
export interface RuleNodeConfiguration { export interface RuleNodeConfiguration {
[key: string]: any; [key: string]: any;
@ -71,10 +73,14 @@ export interface RuleNodeConfigurationDescriptor {
export interface IRuleNodeConfigurationComponent { export interface IRuleNodeConfigurationComponent {
ruleNodeId: string; ruleNodeId: string;
ruleChainId: string; ruleChainId: string;
hasScript: boolean;
testScriptLabel?: string;
changeScript?: EventEmitter<void>;
ruleChainType: RuleChainType; ruleChainType: RuleChainType;
configuration: RuleNodeConfiguration; configuration: RuleNodeConfiguration;
configurationChanged: Observable<RuleNodeConfiguration>; configurationChanged: Observable<RuleNodeConfiguration>;
validate(); validate();
testScript? (debugEventBody?: DebugRuleNodeEventBody);
[key: string]: any; [key: string]: any;
} }
@ -87,6 +93,8 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple
ruleChainId: string; ruleChainId: string;
hasScript: boolean = false;
ruleChainType: RuleChainType; ruleChainType: RuleChainType;
configurationValue: RuleNodeConfiguration; configurationValue: RuleNodeConfiguration;
@ -499,3 +507,4 @@ export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): str
} }
return 'ruleEngine'; return 'ruleEngine';
} }

40
ui-ngx/src/app/shared/models/unit.models.ts

@ -20,45 +20,7 @@ export interface Unit {
tags: string[]; tags: string[];
} }
export const units: Array<Unit> = [ export const unitBySymbol = (_units: Array<Unit>, symbol: string): Unit => _units.find(u => u.symbol === symbol);
{
name: 'unit.celsius',
symbol: '°C',
tags: ['temperature']
},
{
name: 'unit.kelvin',
symbol: 'K',
tags: ['temperature']
},
{
name: 'unit.fahrenheit',
symbol: '°F',
tags: ['temperature']
},
{
name: 'unit.percentage',
symbol: '%',
tags: ['percentage']
},
{
name: 'unit.second',
symbol: 's',
tags: ['time']
},
{
name: 'unit.minute',
symbol: 'min',
tags: ['time']
},
{
name: 'unit.hour',
symbol: 'h',
tags: ['time']
}
];
export const unitBySymbol = (symbol: string): Unit => units.find(u => u.symbol === symbol);
const searchUnitTags = (unit: Unit, searchText: string): boolean => const searchUnitTags = (unit: Unit, searchText: string): boolean =>
!!unit.tags.find(t => t.toUpperCase().includes(searchText.toUpperCase())); !!unit.tags.find(t => t.toUpperCase().includes(searchText.toUpperCase()));

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

@ -3474,7 +3474,8 @@
"output": "Output", "output": "Output",
"test": "Test", "test": "Test",
"help": "Help", "help": "Help",
"reset-debug-mode": "Reset debug mode in all nodes" "reset-debug-mode": "Reset debug mode in all nodes",
"test-with-this-message": "{{test}} with this message"
}, },
"timezone": { "timezone": {
"timezone": "Timezone", "timezone": "Timezone",
@ -3877,13 +3878,423 @@
"ago": "ago" "ago": "ago"
}, },
"unit": { "unit": {
"millimeter": "Millimeter",
"centimeter": "Centimeter",
"angstrom": "Angstrom",
"nanometer": "Nanometer",
"micrometer": "Micrometer",
"meter": "Meter",
"kilometer": "Kilometer",
"inch": "Inch",
"foot": "Foot",
"yard": "Yard",
"mile": "Mile",
"nautical-mile": "Nautical Mile",
"astronomical-unit": "Astronomical Unit",
"reciprocal-metre": "Reciprocal Metre",
"meter-per-meter": "Meter per meter",
"steradian": "Steradian",
"thou": "Thou",
"barleycorn": "Barleycorn",
"hand": "Hand",
"chain": "Chain",
"furlong": "Furlong",
"league": "League",
"fathom": "Fathom",
"cable": "Cable",
"link": "Link",
"rod": "Rod",
"nanogram": "Nanogram",
"microgram": "Microgram",
"milligram": "Milligram",
"gram": "Gram",
"kilogram": "Kilogram",
"tonne": "Tonne",
"ounce": "Ounce",
"pound": "Pound",
"stone": "Stone",
"hundredweight-count": "Hundredweight count",
"short-tons": "Short tons",
"dalton": "Dalton",
"grain": "Grain",
"drachm": "Drachm",
"quarter": "Quarter",
"slug": "Slug",
"carat": "Carat",
"cubic-millimeter": "Cubic Millimeter",
"cubic-centimeter": "Cubic Centimeter",
"cubic-meter": "Cubic Meter/s",
"cubic-kilometer": "Cubic Kilometers",
"microliter": "Microliter",
"milliliter": "Milliliter",
"liter": "Liter",
"hectoliter": "Hectolitre",
"cubic-inch": "Cubic Inch",
"cubic-foot": "Cubic Foot",
"cubic-yard": "Cubic Yards",
"fluid-ounce": "Fluid Ounce",
"pint": "Pint",
"quart": "Quart",
"gallon": "Gallon",
"oil-barrels": "Oil Barrels",
"cubic-meter-per-kilogram": "Cubic Meter per Kilogram",
"gill": "Gill",
"hogshead": "Hogshead",
"teaspoon": "Teaspoon",
"tablespoon": "Tablespoon",
"cup": "Cup",
"celsius": "Celsius", "celsius": "Celsius",
"kelvin": "Kelvin", "kelvin": "Kelvin",
"rankine": "Rankine",
"fahrenheit": "Fahrenheit", "fahrenheit": "Fahrenheit",
"percentage": "Percentage", "percent": "Percent",
"meter-per-second": "Meter per Second",
"kilometer-per-hour": "Kilometer per Hour",
"foot-per-second": "Foot per Second",
"mile-per-hour": "Mile per Hour",
"knot": "Knot",
"millimeters-per-minute": "Millimeters per minute",
"kilometer-per-hour-squared": "Kilometer per hour squared",
"foot-per-second-squared": "Foot per second squared",
"pascal": "Pascal",
"kilopascal": "Kilopascal",
"megapascal": "Megapascal",
"gigapascal": "Gigapascal",
"millibar": "Millibar",
"bar": "Bar",
"kilobar": "Kilobar",
"newton": "Newton",
"newton-meter": "Newton meter",
"foot-pounds": "Foot-pounds",
"inch-pounds": "Inch-pounds",
"newton-per-meter": "Newton per meter",
"atmospheres": "Atmospheres",
"pounds-per-square-inch": "Pounds per Square Inch",
"torr": "Torr",
"inches-of-mercury": "Inches of Mercury",
"pascal-per-square-meter": "Pascal per Square Meter",
"pound-per-square-inch": "Pound per Square Inch",
"newton-per-square-meter": "Newton per Square Meter",
"kilogram-force-per-square-meter": "Kilogram-force per Square Meter",
"pascal-per-square-centimeter": "Pascal per Square Centimeter",
"ton-force-per-square-inch": "Ton-force per Square Inch",
"kilonewton-per-square-meter": "Kilonewton per Square Meter",
"newton-per-square-millimeter": "Newton per Square Millimeter",
"microjoule": "Microjoule",
"millijoule": "Millijoule",
"joule": "Joule",
"kilojoule": "Kilojoule",
"megajoule": "Megajoule",
"gigajoule": "Gigajoule",
"watt-hour": "Watt-hour",
"kilowatt-hour": "Kilowatt-hour",
"electron-volts": "Electron volts",
"joules-per-coulomb": "Joules per Coulomb",
"british-thermal-unit": "British Thermal Units",
"foot-pound": "Foot-pound",
"calorie": "Calorie",
"small-calorie": "Small Calorie",
"kilocalorie": "Kilocalorie",
"joule-per-kelvin": "Joule per Kelvin",
"joule-per-kilogram-kelvin": "Joule per Kilogram-Kelvin",
"joule-per-kilogram": "Joule per Kilogram",
"watt-per-meter-kelvin": "Watt per Meter-Kelvin",
"joule-per-cubic-meter": "Joule per Cubic Meter",
"therm": "Therm",
"electric-dipole-moment": "Electric Dipole Moment",
"magnetic-dipole-moment": "Magnetic Dipole Moment",
"debye": "Debye",
"coulomb-per-square-meter-per-volt": "Coulomb per Square Meter per Volt",
"milliwatt": "Milliwatt",
"microwatt": "Microwatt",
"watt": "Watt",
"kilowatt": "Kilowatt",
"megawatt": "Megawatt",
"gigawatt": "Gigawatt",
"metric-horsepower": "Metric Horsepower",
"milliwatt-per-square-centimeter": "Milliwatts per square centimeter",
"watt-per-square-centimeter": "Watts per square centimeter",
"kilowatt-per-square-centimeter": "Kilowatts per square centimeter",
"milliwatt-per-square-meter": "Milliwatts per square meter",
"watt-per-square-meter": "Watts per square meter",
"kilowatt-per-square-meter": "Kilowatts per square meter",
"watt-per-square-inch": "Watts per square inch",
"kilowatt-per-square-inch": "Kilowatts per square inch",
"horsepower": "Horsepower",
"btu-per-hour": "British thermal units/hour",
"coulomb": "Coulomb",
"millicoulomb": "Millicoulombs",
"microcoulomb": "Microcoulomb",
"picocoulomb": "Picocoulomb",
"coulomb-per-meter": "Coulomb per meter",
"coulomb-per-cubic-meter": "Coulomb per Cubic Meter",
"coulomb-per-square-meter": "Coulomb per Square Meter",
"square-millimeter": "Square Millimeter",
"square-centimeter": "Square Centimeter",
"square-meter": "Square Meter",
"hectare": "Hectare",
"square-kilometer": "Square Kilometer",
"square-inch": "Square Inch",
"square-foot": "Square Foot",
"square-yard": "Square Yard",
"acre": "Acre",
"square-mile": "Square Mile",
"are": "Are",
"barn": "Barn",
"circular-inch": "Circular Inch",
"milliampere-hour": "Milliampere-hour",
"milliampere-hour-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, milliampere-hour, milliampere-hours, mAh",
"ampere-hours": "Ampere-hours",
"ampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, ampere, ampere-hours, Ah",
"kiloampere-hours": "Kiloampere-hours",
"kiloampere-hours-tags": "electric current, current flow, electric charge, current capacity, flow of electricity, electrical flow, kiloampere-hours, kiloampere-hour, kAh",
"nanoampere": "Nanoampere",
"nanoampere-tags": "current, amperes, nanoampere, nA",
"picoampere": "Picoampere",
"picoampere-tags": "current, amperes, picoampere, pA",
"microampere": "Microampere",
"microampere-tags": "electric current, microampere, microamperes, μA",
"milliampere": "Milliampere",
"milliampere-tags": "electric current, milliampere, milliamperes, mA",
"ampere": "Ampere",
"ampere-tags": "electric current, current flow, flow of electricity, electrical flow, ampere, amperes, amperage, A",
"kiloamperes": "Kiloamperes",
"kiloamperes-tags": "electric current, current flow, kiloamperes, kA",
"microampere-per-square-centimeter": "Microampere per square centimeter",
"microampere-per-square-centimeter-tags": "Current density, microampere per square centimeter, µA/cm²",
"ampere-per-square-meter": "Ampere per Square Meter",
"ampere-per-square-meter-tags": "current density, current per unit area, ampere per square meter, A/m²",
"ampere-per-meter": "Ampere per Meter",
"ampere-per-meter-tags": "magnetic field strength, magnetic field intensity, ampere per meter, A/m",
"oersted": "Oersted",
"oersted-tags": "magnetic field, oersted, Oe",
"bohr-magneton": "Bohr Magneton",
"bohr-magneton-tags": "atomic physics, magnetic moment, bohr magneton, μB",
"ampere-meter-squared": "Ampere-Meter Squared",
"ampere-meter-squared-tags": "magnetic moment, dipole moment, ampere-meter squared, A·m²",
"ampere-meter": "Ampere-Meter",
"ampere-meter-tags": "magnetic field, current loop, ampere-meter, A·m",
"nanovolt": "Nanovolt",
"picovolt": "Picovolt",
"millivolts": "Millivolts",
"microvolts": "Microvolts",
"volt": "Volt",
"kilovolts": "Kilovolts",
"dbmV": "dBmV",
"volt-meter": "Volt-Meter",
"kilovolt-meter": "Kilovolt-Meter",
"megavolt-meter": "Megavolt-Meter",
"microvolt-meter": "Microvolt-Meter",
"millivolt-meter": "Millivolt-Meter",
"nanovolt-meter": "Nanovolt-Meter",
"ohm": "Ohm",
"microohm": "Microohm",
"milliohm": "Milliohm",
"kilohm": "Kilohm",
"megohm": "Megohm",
"gigohm": "Gigohm",
"hertz": "Hertz",
"kilohertz": "Kilohertz",
"megahertz": "Megahertz",
"gigahertz": "Gigahertz",
"rpm": "Revolutions Per Minute",
"candela-per-square-meter": "Candela per square meter",
"candela": "Candela",
"lumen": "Lumen",
"lux": "Lux",
"foot-candle": "Foot-candle",
"lumen-per-square-meter": "Lumen per square meter",
"lux-second": "Lux second",
"lumen-second": "Lumen second",
"lumens-per-watt": "Lumens per watt",
"absorbance": "Absorbance",
"mole": "Mole",
"nanomole": "Nanomole",
"micromole": "MicroMole",
"millimole": "Millimole",
"kilomole": "Kilomole",
"mole-per-cubic-meter": "Mole per Cubic Meter",
"rssi": "RSSI",
"ppm": "Parts Per Million",
"ppb": "Parts Per Billion",
"micrograms-per-cubic-meter": "Micrograms per Cubic Meter",
"aqi": "AQI",
"gram-per-cubic-meter": "Gram per cubic meter",
"gram-per-kilogram": "Specific Humidity",
"millimeters-per-second": "Millimeters per second",
"neper": "Neper",
"bel": "Bel",
"decibel": "Decibel",
"meters-per-second-squared": "Meters per second squared",
"becquerel": "Becquerel",
"curie": "Curie",
"gray": "Gray",
"sievert": "Sievert",
"roentgen": "Roentgen",
"cps": "Counts per Second",
"rad": "Rad",
"rem": "Rem",
"dps": "Disintegrations per second",
"rutherford": "Rutherford",
"coulombs-per-kilogram": "Coulombs per kilogram",
"becquerels-per-cubic-meter": "Becquerels per cubic meter",
"curies-per-liter": "Curies per liter",
"becquerels-per-second": "Becquerels per second",
"curies-per-second": "Curies per second",
"gy-per-second": "Gray per Second",
"watt-per-steradian": "Watt per Steradian",
"watt-per-square-metre-steradian": "Watt per Square Metre-Steradian",
"ph-level": "pH Level",
"turbidity": "Turbidity",
"mg-per-liter": "Milligrams per liter",
"microsiemens-per-centimeter": "Microsiemens per centimeter",
"millisiemens-per-meter": "Millisiemens per meter",
"siemens-per-meter": "Siemens per meter",
"kilogram-per-cubic-meter": "Kilogram per cubic meter",
"gram-per-cubic-centimeter": "Gram per cubic centimeter",
"kilogram-per-square-meter": "Kilogram per square metre",
"milligram-per-milliliter": "Milligram per milliliter",
"pound-per-cubic-foot": "Pound per cubic foot",
"ounces-per-cubic-inch": "Ounces per cubic inch",
"tons-per-cubic-yard": "Tons per cubic yard",
"particle-density": "Particle density",
"kilometers-per-liter": "Kilometers per liter",
"miles-per-gallon": "Miles per gallon",
"liters-per-100-km": "Liters per 100 km",
"gallons-per-mile": "Gallons per mile",
"liters-per-hour": "Liters per hour",
"gallons-per-hour": "Gallons per hour",
"beats-per-minute": "Beats per minute",
"millimeters-of-mercury": "Millimeters of mercury",
"milligrams-per-deciliter": "Milligrams per deciliter",
"g-force": "G-force",
"kilonewton": "Kilonewton",
"kilogram-force": "Kilogram-Force",
"pound-force": "Pound-Force",
"kilopound-force": "Kilopound-Force",
"dyne": "Dyne",
"poundal": "Poundal",
"kip": "Kip",
"gal": "Gal",
"gravity": "Gravity",
"hectopascal": "Hectopascal",
"atmosphere": "Atmosphere",
"millibars": "Millibars",
"inch-of-mercury": "One inch of mercury",
"richter-scale": "Richter Scale",
"second": "Second", "second": "Second",
"minute": "Minute", "minute": "Minute",
"hour": "Hour" "hour": "Hour",
"day": "Day",
"week": "Week",
"month": "Month",
"year": "Year",
"cubic-foot-per-minute": "Cubic Foot Per Minute",
"cubic-meters-per-hour": "Cubic Meters Per Hour",
"cubic-meters-per-second": "Cubic Meters Per Second",
"liter-per-second": "Liter Per Second",
"liter-per-minute": "Liter Per Minute",
"gallons-per-minute": "Gallons Per Minute",
"cubic-foot-per-second": "Cubic foot per second",
"milliliters-per-minute": "Milliliters per minute",
"bit": "Bit",
"byte": "Byte",
"kilobyte": "Kilobyte",
"megabyte": "Megabyte",
"gigabyte": "Gigabyte",
"terabyte": "Terabyte",
"petabyte": "Petabyte",
"exabyte": "Exabyte",
"zettabyte": "Zettabyte",
"yottabyte": "Yottabyte",
"bit-per-second": "Bit per second",
"kilobit-per-second": "Kilobit per second",
"megabit-per-second": "Megabit per second",
"gigabit-per-second": "Gigabit per second",
"terabit-per-second": "Terabit per second",
"byte-per-second": "Byte per second",
"kilobyte-per-second": "Kilobyte per second",
"megabyte-per-second": "Megabyte per second",
"gigabyte-per-second": "Gigabyte per second",
"degree": "Degree",
"radian": "Radian",
"gradian": "Gradian",
"mil": "Mil",
"revolution": "Revolution",
"siemens": "Siemens",
"millisiemens": "Millisiemens",
"microsiemens": "Microsiemens",
"kilosiemens": "Kilosiemens",
"megasiemens": "Megasiemens",
"gigasiemens": "Gigasiemens",
"farad": "Farad",
"millifarad": "Millifarad",
"microfarad": "Microfarad",
"nanofarad": "Nanofarad",
"picofarad": "Picofarad",
"kilofarad": "Kilofarad",
"megafarad": "Megafarad",
"gigafarad": "Gigafarad",
"terfarad": "Terfarad",
"farad-per-meter": "Farad per Meter",
"tesla": "Tesla",
"gauss": "Gauss",
"kilogauss": "Kilogauss",
"millitesla": "Millitesla",
"microtesla": "Microtesla",
"nanotesla": "Nanotesla",
"kilotesla": "Kilotesla",
"megatesla": "Megatesla",
"millitesla-square-meters": "millitesla square meters",
"gamma": "Gamma",
"lambda": "Lambda",
"square-meter-per-second": "Square meter per second",
"square-centimeter-per-second": "Square centimeter per second",
"stoke": "Stoke",
"centistokes": "Centistokes",
"square-foot-per-second": "Square foot per second",
"square-inch-per-second": "Square inch per second",
"pascal-second": "Pascal-second",
"centipoise": "Centipoise",
"poise": "Poise",
"reynolds": "Reynolds",
"pound-per-foot-hour": "Pound per foot-hour",
"newton-second-per-square-meter": "Newton second per square meter",
"dyne-second-per-square-centimeter": "Dyne second per square centimeter",
"kilogram-per-meter-second": "Kilogram per meter-second",
"tesla-square-meters": "Tesla square meters",
"maxwell": "Maxwell",
"tesla-per-meter": "Tesla per Meter",
"gauss-per-centimeter": "Gauss per Centimeter",
"weber": "Weber",
"microweber": "Microweber",
"milliweber": "Milliweber",
"gauss-square-centimeter": "Gauss-Square Centimeter",
"kilogauss-square-centimeter": "Kilogauss-Square Centimeter",
"henry": "Henry",
"millihenry": "Millihenry",
"microhenry": "Microhenry",
"nanohenry": "Nanohenry",
"henry-per-meter": "Henry per Meter",
"tesla-meter-per-ampere": "Tesla Meter per Ampere",
"gauss-per-oersted": "Gauss per Oersted",
"kilogram-per-mole": "Kilogram per mole",
"gram-per-mole": "Gram per mole",
"milligram-per-mole": "Milligram per mole",
"joule-per-mole": "Joule per Mole",
"joule-per-mole-kelvin": "Joule per Mole-Kelvin",
"millivolts-per-meter": "Millivolts per meter",
"volts-per-meter": "Volts per meter",
"kilovolts-per-meter": "Kilovolts per meter",
"radian-per-second": "Radian per second",
"radian-per-second-squared": "Radian per second squared",
"revolutions-per-minute-per-second": "Angular acceleration",
"revolutions-per-minute-per-second-squared": "Angular Acceleration",
"deg-per-second": "deg/s",
"degrees-brix": "Degrees Brix",
"katal": "Katal",
"katal-per-cubic-metre": "Katal per Cubic Metre"
}, },
"user": { "user": {
"user": "User", "user": "User",
@ -5319,7 +5730,8 @@
"pagination": "Pagination", "pagination": "Pagination",
"rows": "Rows", "rows": "Rows",
"timeseries-column-error": "At least one timeseries column should be specified", "timeseries-column-error": "At least one timeseries column should be specified",
"table-tabs": "Table tabs" "table-tabs": "Table tabs",
"show-cell-actions-menu-mobile": "Show cell actions dropdown menu in mobile mode"
}, },
"value-source": { "value-source": {
"value-source": "Value source", "value-source": "Value source",

2013
ui-ngx/src/assets/model/units.json

File diff suppressed because it is too large
Loading…
Cancel
Save