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": "",
"dataKeySettingsSchema": "{}",
"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.AlarmCreateOrUpdateActiveRequest;
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.audit.ActionType;
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.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Service
@AllArgsConstructor
@ -210,6 +215,39 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
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
public Boolean delete(Alarm alarm, User user) {
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.AlarmInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
public interface TbAlarmService {
@ -37,5 +38,7 @@ public interface TbAlarmService {
AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException;
void unassignUserAlarms(TenantId tenantId, User user, long unassignTs);
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.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmService;
import org.thingsboard.server.service.security.system.SystemSecurityService;
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 {
private final UserService userService;
private final TbAlarmService tbAlarmService;
private final MailService mailService;
private final SystemSecurityService systemSecurityService;
@ -80,6 +82,7 @@ public class DefaultUserService extends AbstractTbEntityService implements TbUse
UserId userId = tbUser.getId();
try {
tbAlarmService.unassignUserAlarms(tenantId, tbUser, System.currentTimeMillis());
userService.deleteUser(tenantId, userId);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, customerId, userId, tbUser,
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.User;
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.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
import org.thingsboard.server.common.data.page.PageData;
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.resource.ResourceService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
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 {
private final ResourceService resourceService;
private final WidgetTypeService widgetTypeService;
public DefaultTbResourceService(ResourceService resourceService) {
public DefaultTbResourceService(ResourceService resourceService, WidgetTypeService widgetTypeService) {
this.resourceService = resourceService;
this.widgetTypeService = widgetTypeService;
}
@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.server.common.data.Device;
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.AlarmInfo;
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.id.AlarmId;
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.service.DaoSqlTest;
@ -529,6 +531,51 @@ public class AlarmControllerTest extends AbstractControllerTest {
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
public void testFindAlarmsViaCustomerUser() throws Exception {
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.PageLink;
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.service.DaoSqlTest;
@ -216,6 +218,36 @@ public class TbResourceControllerTest extends AbstractControllerTest {
.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
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;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
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<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(TenantId tenantId, TbResourceId tbResourceId);
WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias);
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
@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;
@ApiModelProperty(position = 5, required = true, value = "JSON object with Tenant Id")
@ApiModelProperty(position = 5, value = "JSON object with Tenant Id")
private TenantId tenantId;
@Getter @Setter
@ -89,43 +89,43 @@ public class Customer extends ContactBased<CustomerId> implements HasTenantId, E
return super.getCreatedTime();
}
@ApiModelProperty(position = 6, required = true, value = "Country", example = "US")
@ApiModelProperty(position = 6, value = "Country", example = "US")
@Override
public String getCountry() {
return super.getCountry();
}
@ApiModelProperty(position = 7, required = true, value = "State", example = "NY")
@ApiModelProperty(position = 7, value = "State", example = "NY")
@Override
public String getState() {
return super.getState();
}
@ApiModelProperty(position = 8, required = true, value = "City", example = "New York")
@ApiModelProperty(position = 8, value = "City", example = "New York")
@Override
public String 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
public String getAddress() {
return super.getAddress();
}
@ApiModelProperty(position = 10, required = true, value = "Address Line 2", example = "")
@ApiModelProperty(position = 10, value = "Address Line 2", example = "")
@Override
public String getAddress2() {
return super.getAddress2();
}
@ApiModelProperty(position = 11, required = true, value = "Zip code", example = "10004")
@ApiModelProperty(position = 11, value = "Zip code", example = "10004")
@Override
public String 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
public String 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;
}
@ApiModelProperty(position = 4, value = "Title of the dashboard.")
@ApiModelProperty(position = 4, required = true, value = "Title of the dashboard.")
public String getTitle() {
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;
}
@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() {
return type;
}
@ -155,7 +155,7 @@ public class Device extends BaseDataWithAdditionalInfo<DeviceId> implements HasL
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() {
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;
@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 TenantId tenantId;
private CustomerId customerId;
@ -52,7 +52,7 @@ public class EntityView extends BaseDataWithAdditionalInfo<EntityViewId>
@Length(fieldName = "type")
@ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor")
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;
@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;

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")
@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;
@NoXss
@Length(fieldName = "region")
@ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America")
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;
public Tenant() {
@ -111,43 +111,43 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId, HasTi
return super.getCreatedTime();
}
@ApiModelProperty(position = 7, required = true, value = "Country", example = "US")
@ApiModelProperty(position = 7, value = "Country", example = "US")
@Override
public String getCountry() {
return super.getCountry();
}
@ApiModelProperty(position = 8, required = true, value = "State", example = "NY")
@ApiModelProperty(position = 8, value = "State", example = "NY")
@Override
public String getState() {
return super.getState();
}
@ApiModelProperty(position = 9, required = true, value = "City", example = "New York")
@ApiModelProperty(position = 9, value = "City", example = "New York")
@Override
public String 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
public String getAddress() {
return super.getAddress();
}
@ApiModelProperty(position = 11, required = true, value = "Address Line 2", example = "")
@ApiModelProperty(position = 11, value = "Address Line 2", example = "")
@Override
public String getAddress2() {
return super.getAddress2();
}
@ApiModelProperty(position = 12, required = true, value = "Zip code", example = "10004")
@ApiModelProperty(position = 12, value = "Zip code", example = "10004")
@Override
public String 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
public String 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;
}
@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() {
return firstName;
}
@ -138,7 +138,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
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() {
return lastName;
}
@ -147,7 +147,7 @@ public class User extends BaseDataWithAdditionalInfo<UserId> implements HasName,
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() {
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;
}
@ApiModelProperty(position = 6, required = true, value = "Asset type", example = "Building")
@ApiModelProperty(position = 6, value = "Asset type", example = "Building")
public String getType() {
return type;
}
@ -140,7 +140,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
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() {
return label;
}
@ -149,7 +149,7 @@ public class Asset extends BaseDataWithAdditionalInfo<AssetId> implements HasLab
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() {
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) {
log.trace("Executing deleteResource [{}] [{}]", tenantId, resourceId);
Validator.validateId(resourceId, INCORRECT_RESOURCE_ID + resourceId);
resourceValidator.validateDelete(tenantId, resourceId);
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.EntityType;
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.dao.TenantEntityWithDataDao;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -82,6 +83,9 @@ public abstract class DataValidator<D extends BaseData<?>> {
return null;
}
public void validateDelete(TenantId tenantId, EntityId entityId) {
}
protected boolean isSameData(D existentData, D actualData) {
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.thingsboard.server.common.data.StringUtils;
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.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.model.ModelConstants;
import org.thingsboard.server.dao.resource.TbResourceDao;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
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;
@ -37,6 +44,9 @@ public class ResourceDataValidator extends DataValidator<TbResource> {
@Autowired
private TbResourceDao resourceDao;
@Autowired
private WidgetTypeDao widgetTypeDao;
@Autowired
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> entityNameColumns = new HashMap<>();
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)" +
" 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'" +

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));
}
@Override
public List<WidgetTypeDetails> findWidgetTypesInfosByTenantIdAndResourceId(UUID tenantId, UUID tbResourceId) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndResourceId(tenantId, tbResourceId));
}
@Override
public EntityType getEntityType() {
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,
@Param("bundleAlias") String bundleAlias,
@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);
/**
* 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.id.EntityId;
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.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetType;
@ -37,6 +38,7 @@ import java.util.Optional;
public class WidgetTypeServiceImpl implements WidgetTypeService {
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 ";
@Autowired
private WidgetTypeDao widgetTypeDao;
@ -96,6 +98,14 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
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
public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String 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.EntityType;
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.edge.Edge;
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.RelationEntityTypeFilter;
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.attributes.AttributesService;
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.sql.relation.RelationRepository;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.ArrayList;
import java.util.Arrays;
@ -105,6 +108,8 @@ public class EntityServiceTest extends AbstractServiceTest {
@Autowired
AssetService assetService;
@Autowired
UserService userService;
@Autowired
AttributesService attributesService;
@Autowired
DeviceService deviceService;
@ -227,6 +232,41 @@ public class EntityServiceTest extends AbstractServiceTest {
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
public void testCountEdgeEntitiesByQuery() throws InterruptedException {
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 './local-storage/local-storage.service';
export * from './services/public-api';
export * from './ws/telemetry-websocket.service';
export * from './ws/public-api';
export * from './core.state';
export * from './core.module';
export * from './utils';

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

@ -23,8 +23,8 @@ import {
NodeScriptTestDialogComponent,
NodeScriptTestDialogData
} from '@shared/components/dialog/node-script-test-dialog.component';
import { sortObjectKeys } from '@core/utils';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
@Injectable({
providedIn: 'root'
@ -37,56 +37,52 @@ export class NodeScriptTestService {
testNodeScript(script: string, scriptType: string, functionTitle: string,
functionName: string, argNames: string[], ruleNodeId: string, helpId?: string,
scriptLang?: ScriptLanguage): Observable<string> {
if (ruleNodeId) {
scriptLang?: ScriptLanguage, debugEventBody?: DebugRuleNodeEventBody): Observable<string> {
if (ruleNodeId && !debugEventBody) {
return this.ruleChainService.getLatestRuleNodeDebugInput(ruleNodeId).pipe(
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,
functionName, argNames, msg, metadata, msgType, helpId, scriptLang);
functionName, argNames, debugIn, helpId, scriptLang);
})
);
} else {
return this.openTestScriptDialog(script, scriptType, functionTitle,
functionName, argNames, null, null, null, helpId, scriptLang);
functionName, argNames, debugEventBody, helpId, scriptLang);
}
}
private openTestScriptDialog(script: string, scriptType: string,
functionTitle: string, functionName: string, argNames: string[],
msg?: any, metadata?: {[key: string]: string}, msgType?: string, helpId?: string,
private openTestScriptDialog(script: string, scriptType: string, functionTitle: string, functionName: string,
argNames: string[], eventBody: DebugRuleNodeEventBody, helpId?: 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) {
msg = {
temperature: 22.4,
humidity: 78
};
}
if (eventBody && eventBody.metadata) {
try {
metadata = JSON.parse(eventBody.metadata);
} catch (e) {}
}
if (!metadata) {
metadata = {
deviceName: 'Test Device',
deviceType: 'default',
ts: new Date().getTime() + ''
};
} else {
metadata = sortObjectKeys(metadata);
}
if (!msgType) {
if (eventBody && eventBody.msgType) {
msgType = eventBody.msgType;
} else {
msgType = 'POST_TELEMETRY_REQUEST';
}
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',
maxWidth: (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)"
*ngFor="let actionDescriptor of cellActionDescriptors"
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
@ -228,7 +228,7 @@
{{actionDescriptor.icon}}</mat-icon>
</button>
</div>
<div fxHide fxShow.lt-lg *ngIf="cellActionDescriptors.length">
<div fxHide [fxShow.lt-lg]="cellActionDescriptors.length !== 1" *ngIf="cellActionDescriptors.length">
<button mat-icon-button
(click)="$event.stopPropagation()"
[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>>) {
const index = this.entitiesTableConfig.columns.indexOf(column);
let res = this.headerCellStyleCache[index];

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

@ -21,7 +21,7 @@ import {
EntityTableColumn,
EntityTableConfig
} 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 { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
@ -41,7 +41,7 @@ import {
} from '@home/components/event/event-content-dialog.component';
import { isEqual, sortObjectKeys } from '@core/utils';
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 {
EVENT_FILTER_PANEL_DATA,
@ -61,6 +61,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
set eventType(eventType: EventType | DebugEventType) {
if (this.eventTypeValue !== eventType) {
this.eventTypeValue = eventType;
this.updateCellAction();
this.updateColumns(true);
this.updateFilterColumns();
}
@ -84,7 +85,9 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private debugEventTypes: Array<DebugEventType> = null,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) {
private cd: ChangeDetectorRef,
public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>) {
super();
this.loadDataOnInit = false;
this.tableTitle = '';
@ -120,6 +123,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.updateColumns();
this.updateCellAction();
this.updateFilterColumns();
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 {
if ($event) {
$event.stopPropagation();

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

@ -17,10 +17,10 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
Component, EventEmitter,
Input,
OnDestroy,
OnInit,
OnInit, Output,
ViewChild,
ViewContainerRef
} from '@angular/core';
@ -32,9 +32,10 @@ import { EntitiesTableComponent } from '@home/components/entity/entities-table.c
import { EventTableConfig } from './event-table-config';
import { EventService } from '@core/http/event.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 { Subscription } from 'rxjs';
import { isNotEmptyStr } from '@core/utils';
@Component({
selector: 'tb-event-table',
@ -59,6 +60,10 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
dirtyValue = false;
entityIdValue: EntityId;
get active(): boolean {
return this.activeValue;
}
@Input()
set active(active: boolean) {
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;
eventTableConfig: EventTableConfig;
@ -114,7 +141,9 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy {
this.debugEventTypes,
this.overlay,
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()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value && !disabled && editProfileEnabled"
<button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value?.id && !disabled && editProfileEnabled"
type="button"
matSuffix mat-icon-button aria-label="Edit"
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()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled && editProfileEnabled"
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value?.id && !disabled && editProfileEnabled"
type="button"
matSuffix mat-icon-button aria-label="Edit"
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',
maxWidth: (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">
<span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span>
<button mat-icon-button [disabled]="(isLoading$ | async) || !actionEnabled(alarm, actionDescriptor)"
@ -137,7 +137,7 @@
</button>
</ng-container>
</div>
<div fxHide fxShow.lt-lg *ngIf="alarm.hasActions">
<div fxHide [fxShow.lt-lg]="showCellActionsMenu" *ngIf="alarm.hasActions">
<button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu">
@ -160,7 +160,8 @@
<mat-row [ngClass]="{'mat-row-select': enableSelection,
'mat-selected': alarmsDatasource.isSelected(alarm),
'tb-current-entity': alarmsDatasource.isCurrentAlarm(alarm),
'invisible': alarmsDatasource.dataLoading}"
'invisible': alarmsDatasource.dataLoading,
'tb-pointer': hasRowAction}"
*matRowDef="let alarm; columns: displayedColumns; let row = index"
[ngStyle]="rowStyle(alarm, 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 {
@Input()
ctx: WidgetContext;
@ -171,6 +172,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public displayPagination = true;
public enableStickyHeader = true;
public enableStickyAction = false;
public showCellActionsMenu = true;
public pageSizeOptions;
public pageLink: AlarmDataPageLink;
public sortOrderProperty: string;
@ -180,6 +182,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
public displayedColumns: string[] = [];
public alarmsDatasource: AlarmsDatasource;
public noDataDisplayMessageText: string;
public hasRowAction: boolean;
private setCellButtonAction: boolean;
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.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
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;
let 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.hasRowAction = !!this.ctx.actionsApi.getActionDescriptors('rowClick').length;
if (this.setCellButtonAction) {
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',
maxWidth: (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">
<span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span>
<button mat-icon-button [disabled]="isLoading$ | async"
@ -67,7 +67,7 @@
</button>
</ng-container>
</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
(click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu">
@ -88,7 +88,8 @@
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: enableStickyHeader"></mat-header-row>
<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"
[ngStyle]="rowStyle(entity, 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,
Input,
NgZone,
OnDestroy,
OnInit,
StaticProvider,
ViewChild,
@ -78,8 +79,8 @@ import {
getColumnDefaultVisibility,
getColumnSelectionAvailability,
getColumnWidth,
getHeaderTitle,
getEntityValue,
getHeaderTitle,
getRowStyleInfo,
getTableCellButtonActions,
noDataMessage,
@ -129,7 +130,7 @@ interface EntitiesTableWidgetSettings extends TableWidgetSettings {
templateUrl: './entities-table-widget.component.html',
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()
ctx: WidgetContext;
@ -141,6 +142,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public displayPagination = true;
public enableStickyHeader = true;
public enableStickyAction = true;
public showCellActionsMenu = true;
public pageSizeOptions;
public pageLink: EntityDataPageLink;
public sortOrderProperty: string;
@ -150,6 +152,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
public displayedColumns: string[] = [];
public entityDatasource: EntityDatasource;
public noDataDisplayMessageText: string;
public hasRowAction: boolean;
private setCellButtonAction: boolean;
private cellContentCache: Array<any> = [];
@ -278,6 +281,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
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) {
this.entitiesTitlePattern = this.utils.customTranslation(this.settings.entitiesTitle, this.settings.entitiesTitle);
} else {
@ -290,6 +295,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : 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.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;
white-space: nowrap;
outline: none;
cursor: pointer;
&.tb-horizontal {
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">
{{ 'widgets.table.enable-alarms-selection' | translate }}
</mat-slide-toggle>
<div class="tb-form-row">
<div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide" formControlName="enableStickyAction">
{{ 'widgets.table.enable-sticky-action' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showCellActionsMenu">
{{ 'widgets.table.show-cell-actions-menu-mobile' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked">
<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,
enableStickyHeader: true,
enableStickyAction: true,
showCellActionsMenu: true,
reserveSpaceForHiddenAction: 'true',
displayDetails: true,
allowAcknowledgment: true,
@ -69,6 +70,7 @@ export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent
enableFilter: [settings.enableFilter, []],
enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []],
showCellActionsMenu: [settings.showCellActionsMenu, []],
reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []],
displayDetails: [settings.displayDetails, []],
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 }}
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide" formControlName="enableStickyAction">
{{ 'widgets.table.enable-sticky-action' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showCellActionsMenu">
{{ 'widgets.table.show-cell-actions-menu-mobile' | translate }}
</mat-slide-toggle>
</div>
<mat-form-field fxFlex subscriptSizing="dynamic">
<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,
enableStickyHeader: true,
enableStickyAction: true,
showCellActionsMenu: true,
reserveSpaceForHiddenAction: 'true',
displayEntityName: true,
entityNameColumnTitle: '',
@ -66,6 +67,7 @@ export class EntitiesTableWidgetSettingsComponent extends WidgetSettingsComponen
enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []],
enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []],
showCellActionsMenu: [settings.showCellActionsMenu, []],
reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []],
displayEntityName: [settings.displayEntityName, []],
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">
{{ 'widgets.table.enable-sticky-action' | translate }}
</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-label translate>widgets.table.hidden-cell-button-display-mode</mat-label>
<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,
enableStickyHeader: true,
enableStickyAction: true,
showCellActionsMenu: true,
reserveSpaceForHiddenAction: 'true',
showTimestamp: true,
showMilliseconds: false,
@ -63,6 +64,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []],
enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []],
showCellActionsMenu: [settings.showCellActionsMenu, []],
reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []],
showTimestamp: [settings.showTimestamp, []],
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;
enableSelectColumnDisplay: boolean;
enableStickyAction: boolean;
showCellActionsMenu: boolean;
enableStickyHeader: boolean;
displayPagination: boolean;
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',
maxWidth: (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">
<span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span>
<button *ngIf="actionDescriptor.icon"
@ -79,7 +79,7 @@
</button>
</ng-container>
</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
(click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu">
@ -99,7 +99,8 @@
</mat-cell>
</ng-container>
<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)"
(click)="onRowClick($event, row)"></mat-row>
</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 enableStickyHeader = true;
public enableStickyAction = true;
public showCellActionsMenu = true;
public pageSizeOptions;
public textSearchMode = false;
public hidePageSize = false;
@ -161,6 +162,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
public sources: TimeseriesTableSource[];
public sourceIndex: number;
public noDataDisplayMessageText: string;
public hasRowAction: boolean;
private setCellButtonAction: boolean;
private cellContentCache: Array<any> = [];
@ -300,12 +302,14 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
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.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : 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.useEntityLabel = isDefined(this.widgetConfig.settings.useEntityLabel) ? this.widgetConfig.settings.useEntityLabel : 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;
resetSortAndFilter(update?: boolean, preserveTimewindow?: boolean): void;
columnsUpdated(resetData?: boolean): void;
cellActionDescriptorsUpdated(): void;
headerCellStyle(column: EntityColumn<BaseData<HasId>>): any;
clearCellCache(col: number, row: number): void;
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';
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
import { UtilsService } from '@core/services/utils.service';
@Injectable()
export class InboxTableConfigResolver implements Resolve<EntityTableConfig<Notification>> {
@ -48,7 +49,8 @@ export class InboxTableConfigResolver implements Resolve<EntityTableConfig<Notif
constructor(private notificationService: NotificationService,
private translate: TranslateService,
private dialog: MatDialog,
private datePipe: DatePipe) {
private datePipe: DatePipe,
private utilsService: UtilsService) {
this.config.entityType = EntityType.NOTIFICATION;
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 EntityTableColumn<Notification>('type', 'notification.type', '10%', (notification) =>
this.translate.instant(NotificationTemplateTypeTranslateMap.get(notification.type).name)),
new EntityTableColumn<Notification>('subject', 'notification.subject', '30%'),
new EntityTableColumn<Notification>('text', 'notification.message', '60%')
new EntityTableColumn<Notification>('subject', 'notification.subject', '30%',
(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)) || '',
() => ({}), true),
new EntityTableColumn<NotificationRule>('additionalConfig.description', 'notification.description', '30%',
(target) => target.additionalConfig.description || '',
(target) => target.additionalConfig?.description || '',
() => ({}), false)
);
}

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

@ -18,14 +18,22 @@ import {
AfterViewInit,
Component,
ComponentRef,
EventEmitter,
forwardRef,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
ViewContainerRef
} 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 {
IRuleNodeConfigurationComponent,
RuleNodeConfiguration,
@ -76,6 +84,12 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
@Input()
ruleChainType: RuleChainType;
@Output()
initRuleNode = new EventEmitter<void>();
@Output()
changeScript = new EventEmitter<void>();
nodeDefinitionValue: RuleNodeDefinition;
@Input()
@ -85,6 +99,7 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
if (this.nodeDefinitionValue) {
this.validateDefinedDirective();
}
setTimeout(() => this.initRuleNode.emit());
}
}
@ -98,8 +113,11 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
changeSubscription: Subscription;
changeScriptSubscription: Subscription;
definedConfigComponent: IRuleNodeConfigurationComponent;
private definedConfigComponentRef: ComponentRef<IRuleNodeConfigurationComponent>;
private definedConfigComponent: IRuleNodeConfigurationComponent;
private configuration: RuleNodeConfiguration;
@ -127,6 +145,14 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
if (this.definedConfigComponentRef) {
this.definedConfigComponentRef.destroy();
}
if (this.changeSubscription) {
this.changeSubscription.unsubscribe();
this.changeSubscription = null;
}
if (this.changeScriptSubscription) {
this.changeScriptSubscription.unsubscribe();
this.changeScriptSubscription = null;
}
}
ngAfterViewInit(): void {
@ -199,6 +225,9 @@ export class RuleNodeConfigComponent implements ControlValueAccessor, OnInit, On
this.changeSubscription = this.definedConfigComponent.configurationChanged.subscribe((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"
[ruleChainId]="ruleChainId"
[ruleChainType]="ruleChainType"
[nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition">
[nodeDefinition]="ruleNode.component.configurationDescriptor.nodeDefinition"
(initRuleNode)="initRuleNode.emit($event)"
(changeScript)="changeScript.emit($event)">
</tb-rule-node-config>
<div formGroupName="additionalInfo" fxLayout="column" class="description-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.
///
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -55,6 +55,12 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O
@Input()
isAdd = false;
@Output()
initRuleNode = new EventEmitter<void>();
@Output()
changeScript = new EventEmitter<void>();
ruleNodeType = RuleNodeType;
entityType = EntityType;

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

@ -111,7 +111,9 @@
[ruleChainId]="ruleChain.id?.id"
[ruleChainType]="ruleChainType"
[isEdit]="true"
[isReadOnly]="false">
[isReadOnly]="false"
(initRuleNode)="onRuleNodeInit()"
(changeScript)="switchToFirstTab()">
</tb-rule-node>
</mat-tab>
<mat-tab *ngIf="editingRuleNode.ruleNodeId" label="{{ 'rulenode.events' | translate }}" #eventsTab="matTab">
@ -119,7 +121,10 @@
[defaultEventType]="debugEventTypes.DEBUG_RULE_NODE"
[active]="eventsTab.isActive"
[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 label="{{ 'rulenode.help' | translate }}">
<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 { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffer.service';
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 { TbPopoverService } from '@shared/components/popover.service';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
@ -153,6 +153,7 @@ export class RuleChainPageComponent extends PageComponent
editingRuleNodeAllowCustomLabels = false;
editingRuleNodeLinkLabels: {[label: string]: LinkLabel};
editingRuleNodeSourceRuleChainId: string;
ruleNodeTestButtonLabel: string;
@ViewChild('tbRuleNode') ruleNodeComponent: RuleNodeDetailsComponent;
@ViewChild('tbRuleNodeLink') ruleNodeLinkComponent: RuleNodeLinkComponent;
@ -1277,6 +1278,27 @@ export class RuleChainPageComponent extends PageComponent
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() {
this.ruleNodeComponent.validate();
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()">
{{'js-func.tidy' | translate }}
</button>
<ng-content select="[toolbarSuffixButton]"></ng-content>
<div *ngIf="helpId" [tb-help-popup]="helpId"></div>
<fieldset style="width: initial">
<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 { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { searchUnits, Unit, unitBySymbol, units } from '@shared/models/unit.models';
import { map, mergeMap, startWith, tap } from 'rxjs/operators';
import { ControlValueAccessor, FormBuilder, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of, shareReplay, switchMap } from 'rxjs';
import { searchUnits, Unit, unitBySymbol } from '@shared/models/unit.models';
import { map, mergeMap, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { ResourcesService } from '@core/services/resources.service';
const unitsModels = '/assets/model/units.json';
@Component({
selector: 'tb-unit-input',
@ -51,13 +54,12 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
private dirty = false;
private translatedUnits: Array<Unit> = units.map(u => ({symbol: u.symbol,
name: this.translate.instant(u.name),
tags: u.tags}));
private fetchUnits$: Observable<Array<Unit>> = null;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
constructor(private fb: FormBuilder,
private resourcesService: ResourcesService,
private translate: TranslateService) {
}
@ -68,22 +70,22 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
tap(value => {
this.updateView(value);
}),
startWith<string | Unit>(''),
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 {
this.searchText = '';
this.modelValue = symbol;
let res: Unit | string = null;
if (symbol) {
const unit = unitBySymbol(symbol);
res = unit ? unit : symbol;
}
this.unitsFormControl.patchValue(res, {emitEvent: false});
this.dirty = true;
of(symbol).pipe(
switchMap(value => value
? this.unitsConstant().pipe(map(units => unitBySymbol(units, value) ?? value))
: of(null))
).subscribe(result => {
this.unitsFormControl.patchValue(result, {emitEvent: false});
this.dirty = true;
});
}
onFocus() {
@ -114,12 +116,9 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
fetchUnits(searchText?: string): Observable<Array<Unit | string>> {
this.searchText = searchText;
const result = searchUnits(this.translatedUnits, searchText);
if (result.length) {
return of(result);
} else {
return of([]);
}
return this.unitsConstant().pipe(
map(unit => searchUnits(unit, searchText))
);
}
registerOnChange(fn: any): void {
@ -145,4 +144,18 @@ export class UnitInputComponent implements ControlValueAccessor, OnInit {
this.unitInput.nativeElement.focus();
}, 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;
triggerConfig: NotificationRuleTriggerConfig;
recipientsConfig: NotificationRuleRecipientConfig;
additionalConfig: {description: string};
additionalConfig?: {description: string};
}
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 { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { RuleChainType } from '@shared/models/rule-chain.models';
import { DebugRuleNodeEventBody } from '@shared/models/event.models';
import { TranslateService } from '@ngx-translate/core';
export interface RuleNodeConfiguration {
[key: string]: any;
@ -71,10 +73,14 @@ export interface RuleNodeConfigurationDescriptor {
export interface IRuleNodeConfigurationComponent {
ruleNodeId: string;
ruleChainId: string;
hasScript: boolean;
testScriptLabel?: string;
changeScript?: EventEmitter<void>;
ruleChainType: RuleChainType;
configuration: RuleNodeConfiguration;
configurationChanged: Observable<RuleNodeConfiguration>;
validate();
testScript? (debugEventBody?: DebugRuleNodeEventBody);
[key: string]: any;
}
@ -87,6 +93,8 @@ export abstract class RuleNodeConfigurationComponent extends PageComponent imple
ruleChainId: string;
hasScript: boolean = false;
ruleChainType: RuleChainType;
configurationValue: RuleNodeConfiguration;
@ -499,3 +507,4 @@ export function getRuleNodeHelpLink(component: RuleNodeComponentDescriptor): str
}
return 'ruleEngine';
}

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

@ -20,45 +20,7 @@ export interface Unit {
tags: string[];
}
export const units: Array<Unit> = [
{
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);
export const unitBySymbol = (_units: Array<Unit>, symbol: string): Unit => _units.find(u => u.symbol === symbol);
const searchUnitTags = (unit: Unit, searchText: string): boolean =>
!!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",
"test": "Test",
"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",
@ -3877,13 +3878,423 @@
"ago": "ago"
},
"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",
"kelvin": "Kelvin",
"rankine": "Rankine",
"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",
"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",
@ -5319,7 +5730,8 @@
"pagination": "Pagination",
"rows": "Rows",
"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",

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

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