Browse Source

Merge remote-tracking branch 'upstream/develop/3.5.2' into feature/check-connectivity-device

pull/8938/head
Vladyslav_Prykhodko 3 years ago
parent
commit
1dff25518f
  1. 42
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 2
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  3. 38
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  4. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java
  5. 3
      application/src/main/java/org/thingsboard/server/service/entitiy/user/DefaultUserService.java
  6. 8
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  7. 47
      application/src/test/java/org/thingsboard/server/controller/AlarmControllerTest.java
  8. 32
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  9. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  10. 18
      common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
  11. 2
      common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
  12. 4
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  13. 4
      common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
  14. 18
      common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
  15. 6
      common/data/src/main/java/org/thingsboard/server/common/data/User.java
  16. 6
      common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java
  17. 1
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  18. 4
      dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java
  19. 21
      dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java
  20. 3
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  21. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  22. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  23. 8
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
  24. 10
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  25. 40
      dao/src/test/java/org/thingsboard/server/dao/service/EntityServiceTest.java
  26. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  27. 47
      ui-ngx/src/app/app.component.ts
  28. 2
      ui-ngx/src/app/core/public-api.ts
  29. 2
      ui-ngx/src/app/core/services/menu.models.ts
  30. 52
      ui-ngx/src/app/core/services/menu.service.ts
  31. 50
      ui-ngx/src/app/core/services/script/node-script-test.service.ts
  32. 19
      ui-ngx/src/app/core/ws/public-api.ts
  33. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  34. 63
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  35. 29
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.scss
  36. 7
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  37. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  38. 7
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  39. 4
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  40. 30
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  41. 37
      ui-ngx/src/app/modules/home/components/event/event-table.component.ts
  42. 2
      ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html
  43. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  44. 3
      ui-ngx/src/app/modules/home/components/router-tabs.component.html
  45. 2
      ui-ngx/src/app/modules/home/components/router-tabs.component.scss
  46. 2
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
  47. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  48. 28
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts
  49. 104
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  50. 276
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  51. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
  52. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html
  53. 365
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.models.ts
  54. 7
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html
  55. 5
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  56. 70
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html
  57. 70
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss
  58. 144
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts
  59. 127
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts
  60. 7
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  61. 10
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  62. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.html
  63. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html
  64. 10
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html
  65. 6
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html
  66. 6
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html
  67. 1
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss
  68. 19
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html
  69. 7
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html
  70. 1
      ui-ngx/src/app/modules/home/components/widget/lib/legend.component.scss
  71. 8
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
  72. 2
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html
  73. 3
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html
  74. 2
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss
  75. 6
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html
  76. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html
  77. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts
  78. 5
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.html
  79. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-widget-settings.component.ts
  80. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html
  81. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts
  82. 105
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html
  83. 85
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss
  84. 133
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  85. 30
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html
  86. 124
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts
  87. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html
  88. 82
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts
  89. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html
  90. 148
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts
  91. 47
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.html
  92. 67
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.scss
  93. 70
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts
  94. 91
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
  95. 51
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss
  96. 133
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts
  97. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.html
  98. 102
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts
  99. 43
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html
  100. 116
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.scss

42
application/src/main/data/json/system/widget_bundles/cards.json

@ -225,6 +225,48 @@
"settingsDirective": "tb-dashboard-state-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"syncParentStateParams\":true,\"defaultAutofillLayout\":true,\"defaultMargin\":0,\"defaultBackgroundColor\":\"#fff\"},\"title\":\"Dashboard state widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
}
},
{
"alias": "value_card",
"name": "Value card",
"image": null,
"description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.",
"descriptor": {
"type": "latest",
"sizeX": 2.5,
"sizeY": 2.5,
"resources": [],
"templateHtml": "<tb-value-card-widget \n [ctx]=\"ctx\">\n</tb-value-card-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px'\n };\n};\n\nself.onDestroy = function() {\n};\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-value-card-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}"
}
},
{
"alias": "horizontal_value_card",
"name": "Horizontal value card",
"image": null,
"description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.",
"descriptor": {
"type": "latest",
"sizeX": 5,
"sizeY": 1.5,
"resources": [],
"templateHtml": "<tb-value-card-widget \n [ctx]=\"ctx\">\n</tb-value-card-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '130px'\n };\n};\n\nself.onDestroy = function() {\n};\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-value-card-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Horizontal value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}"
}
}
]
}

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

47
ui-ngx/src/app/app.component.ts

@ -30,6 +30,7 @@ import { combineLatest } from 'rxjs';
import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service';
import { svgIcons } from '@shared/models/icon.models';
@Component({
selector: 'tb-root',
@ -55,44 +56,14 @@ export class AppComponent implements OnInit {
}
});
this.matIconRegistry.addSvgIconLiteral(
'google-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'github-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 32.7 32.7"><path d="M16.3 0C7.3 0 0 7.3 0 16.3c0 7.2 4.7 13.3 11.1 15.5.8.1 1.1-.4 1.1-.8v-2.8c-4.5 1-5.5-2.2-5.5-2.2-.7-1.9-1.8-2.4-1.8-2.4-1.5-1 .1-1 .1-1 1.6.1 2.5 1.7 2.5 1.7 1.5 2.5 3.8 1.8 4.7 1.4.1-1.1.6-1.8 1-2.2-3.6-.4-7.4-1.8-7.4-8.1 0-1.8.6-3.2 1.7-4.4-.2-.4-.7-2.1.2-4.3 0 0 1.4-.4 4.5 1.7 1.3-.4 2.7-.5 4.1-.5s2.8.2 4.1.5c3.1-2.1 4.5-1.7 4.5-1.7.9 2.2.3 3.9.2 4.3 1 1.1 1.7 2.6 1.7 4.4 0 6.3-3.8 7.6-7.4 8 .6.5 1.1 1.5 1.1 3v4.5c0 .4.3.9 1.1.8 6.5-2.2 11.1-8.3 11.1-15.5C32.6 7.3 25.3 0 16.3 0z" fill="#211c19"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'facebook-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 263 263"><path d="M263 131.5C263 58.9 204.1 0 131.5 0S0 58.9 0 131.5c0 65.6 48.1 120 110.9 129.9v-91.9H77.5v-38h33.4v-29c0-33 19.6-51.2 49.7-51.2 14.4 0 29.4 2.6 29.4 2.6v32.4h-16.5c-16.3 0-21.4 10.1-21.4 20.5v24.7h36.4l-5.8 38h-30.6v91.9c62.8-9.9 110.9-64.3 110.9-129.9z" fill="#1877f2"/><path d="M182.7 169.5l5.8-38H152v-24.7c0-10.4 5.1-20.5 21.4-20.5H190V53.9s-15-2.6-29.4-2.6c-30 0-49.7 18.2-49.7 51.2v29H77.5v38h33.4v91.9c6.7 1.1 13.6 1.6 20.5 1.6s13.9-.5 20.5-1.6v-91.9h30.8z" fill="#fff"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'apple-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 256 315"><path d="M213.803394,167.030943 C214.2452,214.609646 255.542482,230.442639 256,230.644727 C255.650812,231.761357 249.401383,253.208293 234.24263,275.361446 C221.138555,294.513969 207.538253,313.596333 186.113759,313.991545 C165.062051,314.379442 158.292752,301.507828 134.22469,301.507828 C110.163898,301.507828 102.642899,313.596301 82.7151126,314.379442 C62.0350407,315.16201 46.2873831,293.668525 33.0744079,274.586162 C6.07529317,235.552544 -14.5576169,164.286328 13.147166,116.18047 C26.9103111,92.2909053 51.5060917,77.1630356 78.2026125,76.7751096 C98.5099145,76.3877456 117.677594,90.4371851 130.091705,90.4371851 C142.497945,90.4371851 165.790755,73.5415029 190.277627,76.0228474 C200.528668,76.4495055 229.303509,80.1636878 247.780625,107.209389 C246.291825,108.132333 213.44635,127.253405 213.803394,167.030988 M174.239142,50.1987033 C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 163.259413,63.4970262 174.239142,50.1987249" fill="#000000"></path></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'queues-list',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' +
'<path fill="#fff" d="M9 4V2H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h5v-2H4V4h5z"/>' +
'<path fill="#fff" d="M7 18V6h2v12H7zM11 6v12h2V6h-2zM15 20v2h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5v2h5v16h-5z"/>' +
'<path fill="#fff" d="M15 18V6h2v12h-2z"/>' +
'</svg>'
)
);
for (const svgIcon of Object.keys(svgIcons)) {
this.matIconRegistry.addSvgIconLiteral(
svgIcon,
this.domSanitizer.bypassSecurityTrustHtml(
svgIcons[svgIcon]
)
);
}
this.matIconRegistry.addSvgIcon('windows', this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/windows.svg'));
this.matIconRegistry.addSvgIcon('macos', this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/macos.svg'));

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';

2
ui-ngx/src/app/core/services/menu.models.ts

@ -24,7 +24,6 @@ export interface MenuSection extends HasUUID{
type: MenuSectionType;
path: string;
icon: string;
isMdiIcon?: boolean;
pages?: Array<MenuSection>;
opened?: boolean;
disabled?: boolean;
@ -39,6 +38,5 @@ export interface HomeSection {
export interface HomeSectionPlace {
name: string;
icon: string;
isMdiIcon?: boolean;
path: string;
}

52
ui-ngx/src/app/core/services/menu.service.ts

@ -111,8 +111,7 @@ export class MenuService {
name: 'tenant-profile.tenant-profiles',
type: 'link',
path: '/tenantProfiles',
icon: 'mdi:alpha-t-box',
isMdiIcon: true
icon: 'mdi:alpha-t-box'
},
{
id: 'resources',
@ -133,8 +132,7 @@ export class MenuService {
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split',
isMdiIcon: true
icon: 'mdi:rhombus-split'
}
]
},
@ -144,7 +142,6 @@ export class MenuService {
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: 'notification_inbox',
@ -176,8 +173,7 @@ export class MenuService {
fullName: 'notification.notification-templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw',
isMdiIcon: true
icon: 'mdi:message-draw'
},
{
id: 'notification_rules',
@ -185,8 +181,7 @@ export class MenuService {
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog',
isMdiIcon: true
icon: 'mdi:message-cog'
}
]
},
@ -218,8 +213,7 @@ export class MenuService {
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge',
isMdiIcon: true
icon: 'mdi:message-badge'
},
{
id: 'queues',
@ -250,16 +244,14 @@ export class MenuService {
name: 'admin.2fa.2fa',
type: 'link',
path: '/security-settings/2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true
icon: 'mdi:two-factor-authentication'
},
{
id: 'oauth2',
name: 'admin.oauth2.oauth2',
type: 'link',
path: '/security-settings/oauth2',
icon: 'mdi:shield-account',
isMdiIcon: true
icon: 'mdi:shield-account'
}
]
}
@ -281,7 +273,6 @@ export class MenuService {
{
name: 'tenant-profile.tenant-profiles',
icon: 'mdi:alpha-t-box',
isMdiIcon: true,
path: '/tenantProfiles'
},
]
@ -327,7 +318,6 @@ export class MenuService {
{
name: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true,
path: '/settings/2fa'
},
{
@ -361,8 +351,7 @@ export class MenuService {
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline',
isMdiIcon: true
icon: 'mdi:alert-outline'
},
{
id: 'dashboards',
@ -413,16 +402,14 @@ export class MenuService {
name: 'device-profile.device-profiles',
type: 'link',
path: '/profiles/deviceProfiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true
icon: 'mdi:alpha-d-box'
},
{
id: 'asset_profiles',
name: 'asset-profile.asset-profiles',
type: 'link',
path: '/profiles/assetProfiles',
icon: 'mdi:alpha-a-box',
isMdiIcon: true
icon: 'mdi:alpha-a-box'
}
]
},
@ -513,8 +500,7 @@ export class MenuService {
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split',
isMdiIcon: true
icon: 'mdi:rhombus-split'
}
]
},
@ -524,7 +510,6 @@ export class MenuService {
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: 'notification_inbox',
@ -556,8 +541,7 @@ export class MenuService {
fullName: 'notification.notification-templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw',
isMdiIcon: true
icon: 'mdi:message-draw'
},
{
id: 'notification_rules',
@ -565,8 +549,7 @@ export class MenuService {
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog',
isMdiIcon: true
icon: 'mdi:message-cog'
}
]
},
@ -598,8 +581,7 @@ export class MenuService {
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge',
isMdiIcon: true
icon: 'mdi:message-badge'
},
{
id: 'repository_settings',
@ -673,7 +655,6 @@ export class MenuService {
{
name: 'asset-profile.asset-profiles',
icon: 'mdi:alpha-a-box',
isMdiIcon: true,
path: '/profiles/assetProfiles'
}
]
@ -689,7 +670,6 @@ export class MenuService {
{
name: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true,
path: '/profiles/deviceProfiles'
},
{
@ -814,8 +794,7 @@ export class MenuService {
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline',
isMdiIcon: true
icon: 'mdi:alert-outline'
},
{
id: 'dashboards',
@ -874,7 +853,6 @@ export class MenuService {
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: 'notification_inbox',

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';

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -182,6 +182,7 @@ import * as ToggleHeaderComponent from '@shared/components/toggle-header.compone
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.component';
import * as MaterialIconsComponent from '@shared/components/material-icons.component';
import * as TbIconComponent from '@shared/components/icon.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -484,6 +485,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent,
'@shared/components/material-icons.component': MaterialIconsComponent,
'@shared/components/icon.component': TbIconComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent,

63
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="widgetFormGroup" style="width: 1200px;">
<form class="tb-add-widget-dialog" [formGroup]="widgetFormGroup">
<mat-toolbar color="primary">
<h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span>
@ -32,40 +32,37 @@
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content style="padding: 0;">
<fieldset [disabled]="isLoading$ | async" style="position: relative; height: 600px;">
<tb-widget-config
[aliasController]="aliasController"
[functionsOnly]="false"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
isAdd
formControlName="widgetConfig">
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
</fieldset>
<div mat-dialog-content>
<tb-widget-config class="tb-absolute-fill"
[aliasController]="aliasController"
[functionsOnly]="false"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
isAdd
formControlName="widgetConfig">
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
</div>
<div mat-dialog-actions fxLayoutAlign="space-between center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
@ -74,14 +71,14 @@
<button *ngIf="!previewMode"
mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
[disabled]="widgetFormGroup.invalid"
(click)="previewMode = true"
cdkFocusInitial>
{{ 'widget-config.preview' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid">
[disabled]="widgetFormGroup.invalid">
{{ 'action.add' | translate }}
</button>
</div>

29
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.scss

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../scss/constants';
.tb-add-widget-dialog {
.mat-mdc-dialog-content {
padding: 0;
position: relative;
}
@media #{$mat-gt-xs} {
width: 1200px;
.mat-mdc-dialog-content {
height: 600px;
}
}
}

7
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -14,12 +14,12 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { Component, Inject, OnInit, SkipSelf, ViewEncapsulation } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models';
@ -41,7 +41,8 @@ export interface AddWidgetDialogData {
selector: 'tb-add-widget-dialog',
templateUrl: './add-widget-dialog.component.html',
providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/],
styleUrls: []
styleUrls: ['./add-widget-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher {

4
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html

@ -60,7 +60,9 @@
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
</tb-widget-preview>
</div>
</fieldset>

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

@ -86,8 +86,7 @@
matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event)">
<mat-icon *ngIf="actionDescriptor.isMdiIcon" [svgIcon]="actionDescriptor.icon"></mat-icon>
<mat-icon *ngIf="!actionDescriptor.isMdiIcon">{{actionDescriptor.icon}}</mat-icon>
<tb-icon>{{actionDescriptor.icon}}</tb-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()"
matTooltip="{{ 'action.refresh' | translate }}"
@ -218,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 }}"
@ -229,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];

30
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,12 +123,12 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.updateColumns();
this.updateCellAction();
this.updateFilterColumns();
this.headerActionDescriptors.push({
name: this.translate.instant('event.clear-filter'),
icon: 'mdi:filter-variant-remove',
isMdiIcon: true,
isEnabled: () => !isEqual(this.filterParams, {}),
onAction: ($event) => {
this.clearFiter($event);
@ -349,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 }}"

3
ui-ngx/src/app/modules/home/components/router-tabs.component.html

@ -24,8 +24,7 @@
#rla="routerLinkActive"
mat-tab-link
[active]="rla.isActive">
<mat-icon *ngIf="!tab.isMdiIcon && tab.icon !== null" class="tb-mat-18">{{tab.icon}}</mat-icon>
<mat-icon *ngIf="tab.isMdiIcon && tab.icon !== null" [svgIcon]="tab.icon" class="tb-mat-18"></mat-icon>
<tb-icon *ngIf="tab.icon !== null" class="tb-mat-18">{{tab.icon}}</tb-icon>
<span>{{tab.name | translate}}</span>
</a>
</nav>

2
ui-ngx/src/app/modules/home/components/router-tabs.component.scss

@ -26,7 +26,7 @@
line-height: 40px;
min-width: 200px;
border-bottom: none;
mat-icon {
.mat-icon {
margin-right: 8px;
margin-left: 0;
}

2
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html

@ -96,7 +96,7 @@
<ng-container matColumnDef="icon">
<mat-header-cell *matHeaderCellDef style="width: 40px"> {{ 'widget-config.action-icon' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let action" class="tb-icon-cell" style="width: 40px">
<mat-icon>{{ action.icon }}</mat-icon>
<tb-icon>{{ action.icon }}</tb-icon>
</mat-cell>
</ng-container>
<ng-container matColumnDef="typeName">

8
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -38,6 +38,9 @@ import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widge
import {
AlarmsTableBasicConfigComponent
} from '@home/components/widget/config/basic/alarm/alarms-table-basic-config.component';
import {
ValueCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/value-card-basic-config.component';
@NgModule({
declarations: [
@ -47,6 +50,7 @@ import {
TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
],
@ -63,6 +67,7 @@ import {
TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
]
@ -75,5 +80,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent,
'tb-timeseries-table-basic-config': TimeseriesTableBasicConfigComponent,
'tb-flot-basic-config': FlotBasicConfigComponent,
'tb-alarms-table-basic-config': AlarmsTableBasicConfigComponent
'tb-alarms-table-basic-config': AlarmsTableBasicConfigComponent,
'tb-value-card-basic-config': ValueCardBasicConfigComponent
};

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

@ -21,14 +21,15 @@ import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation, WidgetConfig,
datasourcesHasOnlyComparisonAggregation,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import { isUndefined } from '@core/utils';
import { getLabel, setLabel } from '@home/components/widget/config/widget-settings.models';
@Component({
selector: 'tb-simple-card-basic-config',
@ -67,7 +68,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.simpleCardWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
label: [this.getDataKeyLabel(configData.config.datasources), []],
label: [getLabel(configData.config.datasources), []],
labelPosition: [configData.config.settings?.labelPosition, []],
units: [configData.config.units, []],
decimals: [configData.config.decimals, []],
@ -83,7 +84,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources;
this.setDataKeyLabel(config.label, this.widgetConfig.config.datasources);
setLabel(config.label, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;
this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals;
@ -95,25 +96,6 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
return this.widgetConfig;
}
private getDataKeyLabel(datasources?: Datasource[]): string {
if (datasources && datasources.length) {
const dataKeys = datasources[0].dataKeys;
if (dataKeys && dataKeys.length) {
return dataKeys[0].label;
}
}
return '';
}
private setDataKeyLabel(label: string, datasources?: Datasource[]) {
if (datasources && datasources.length) {
const dataKeys = datasources[0].dataKeys;
if (dataKeys && dataKeys.length) {
dataKeys[0].label = label;
}
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {

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

@ -0,0 +1,104 @@
<!--
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.
-->
<ng-container [formGroup]="valueCardWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeyLabel
hideDataKeyColor
hideDataKeyUnits
hideDataKeyDecimals
formControlName="datasources">
</tb-datasources>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="{{ horizontal ? '3:1' : '7:5' }}"
[cols]="horizontal ? 2 : 4"
[colsLtMd]="horizontal ? 1 : 2"
label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of valueCardLayouts"
[value]="layout"
[image]="valueCardLayoutImageMap.get(layout)">
{{ valueCardLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.value-card.label' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="valueCardWidgetConfigForm.get('label').value">
</tb-font-settings>
<tb-color-settings formControlName="labelColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.value-card.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
[color]="valueCardWidgetConfigForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>
<tb-color-settings formControlName="iconColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widgets.value-card.value</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-unit-input fxFlex formControlName="units"></tb-unit-input>
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
</tb-color-settings>
</div>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showDate">
{{ 'widgets.value-card.date' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex formControlName="dateFormat"></tb-date-format-select>
<tb-font-settings formControlName="dateFont"
[previewText]="datePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="dateColor">
</tb-color-settings>
</div>
</div>
</ng-container>

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

@ -0,0 +1,276 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Injector } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils';
import {
DateFormatProcessor,
DateFormatSettings,
getLabel,
setLabel
} from '@home/components/widget/config/widget-settings.models';
import {
valueCardDefaultSettings,
ValueCardLayout,
valueCardLayoutImages,
valueCardLayouts,
valueCardLayoutTranslations,
ValueCardWidgetSettings
} from '@home/components/widget/lib/cards/value-card-widget.models';
@Component({
selector: 'tb-value-card-basic-config',
templateUrl: './value-card-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
public get displayTimewindowConfig(): boolean {
const datasources = this.valueCardWidgetConfigForm.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
public onlyHistoryTimewindow(): boolean {
const datasources = this.valueCardWidgetConfigForm.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
}
valueCardLayouts: ValueCardLayout[] = [];
valueCardLayoutTranslationMap = valueCardLayoutTranslations;
valueCardLayoutImageMap = valueCardLayoutImages;
horizontal = false;
valueCardWidgetConfigForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
datePreviewFn = this._datePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private cd: ChangeDetectorRef,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.valueCardWidgetConfigForm;
}
protected setupConfig(widgetConfig: WidgetConfigComponentData) {
const params = widgetConfig.typeParameters as any;
this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false;
this.valueCardLayouts = valueCardLayouts(this.horizontal);
super.setupConfig(widgetConfig);
}
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData, [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }]);
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: ValueCardWidgetSettings = {...valueCardDefaultSettings(this.horizontal), ...(configData.config.settings || {})};
this.valueCardWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
layout: [settings.layout, []],
showLabel: [settings.showLabel, []],
label: [getLabel(configData.config.datasources), []],
labelFont: [settings.labelFont, []],
labelColor: [settings.labelColor, []],
showIcon: [settings.showIcon, []],
iconSize: [settings.iconSize, [Validators.min(0)]],
iconSizeUnit: [settings.iconSizeUnit, []],
icon: [settings.icon, []],
iconColor: [settings.iconColor, []],
units: [configData.config.units, []],
decimals: [configData.config.decimals, []],
valueFont: [settings.valueFont, []],
valueColor: [settings.valueColor, []],
showDate: [settings.showDate, []],
dateFormat: [settings.dateFormat, []],
dateFont: [settings.dateFont, []],
dateColor: [settings.dateColor, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.showLabel = config.showLabel;
setLabel(config.label, this.widgetConfig.config.datasources);
this.widgetConfig.config.settings.labelFont = config.labelFont;
this.widgetConfig.config.settings.labelColor = config.labelColor;
this.widgetConfig.config.settings.showIcon = config.showIcon;
this.widgetConfig.config.settings.iconSize = config.iconSize;
this.widgetConfig.config.settings.iconSizeUnit = config.iconSizeUnit;
this.widgetConfig.config.settings.icon = config.icon;
this.widgetConfig.config.settings.iconColor = config.iconColor;
this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals;
this.widgetConfig.config.settings.valueFont = config.valueFont;
this.widgetConfig.config.settings.valueColor = config.valueColor;
this.widgetConfig.config.settings.showDate = config.showDate;
this.widgetConfig.config.settings.dateFormat = config.dateFormat;
this.widgetConfig.config.settings.dateFont = config.dateFont;
this.widgetConfig.config.settings.dateColor = config.dateColor;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['layout', 'showLabel', 'showIcon', 'showDate'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const layout: ValueCardLayout = this.valueCardWidgetConfigForm.get('layout').value;
const showLabel: boolean = this.valueCardWidgetConfigForm.get('showLabel').value;
const showIcon: boolean = this.valueCardWidgetConfigForm.get('showIcon').value;
const showDate: boolean = this.valueCardWidgetConfigForm.get('showDate').value;
const dateEnabled = ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout);
const iconEnabled = layout !== ValueCardLayout.simplified;
if (showLabel) {
this.valueCardWidgetConfigForm.get('label').enable();
this.valueCardWidgetConfigForm.get('labelFont').enable();
this.valueCardWidgetConfigForm.get('labelColor').enable();
} else {
this.valueCardWidgetConfigForm.get('label').disable();
this.valueCardWidgetConfigForm.get('labelFont').disable();
this.valueCardWidgetConfigForm.get('labelColor').disable();
}
if (iconEnabled) {
this.valueCardWidgetConfigForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.valueCardWidgetConfigForm.get('iconSize').enable();
this.valueCardWidgetConfigForm.get('iconSizeUnit').enable();
this.valueCardWidgetConfigForm.get('icon').enable();
this.valueCardWidgetConfigForm.get('iconColor').enable();
} else {
this.valueCardWidgetConfigForm.get('iconSize').disable();
this.valueCardWidgetConfigForm.get('iconSizeUnit').disable();
this.valueCardWidgetConfigForm.get('icon').disable();
this.valueCardWidgetConfigForm.get('iconColor').disable();
}
} else {
this.valueCardWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.valueCardWidgetConfigForm.get('iconSize').disable();
this.valueCardWidgetConfigForm.get('iconSizeUnit').disable();
this.valueCardWidgetConfigForm.get('icon').disable();
this.valueCardWidgetConfigForm.get('iconColor').disable();
}
if (dateEnabled) {
this.valueCardWidgetConfigForm.get('showDate').enable({emitEvent: false});
if (showDate) {
this.valueCardWidgetConfigForm.get('dateFormat').enable();
this.valueCardWidgetConfigForm.get('dateFont').enable();
this.valueCardWidgetConfigForm.get('dateColor').enable();
} else {
this.valueCardWidgetConfigForm.get('dateFormat').disable();
this.valueCardWidgetConfigForm.get('dateFont').disable();
this.valueCardWidgetConfigForm.get('dateColor').disable();
}
} else {
this.valueCardWidgetConfigForm.get('showDate').disable({emitEvent: false});
this.valueCardWidgetConfigForm.get('dateFormat').disable();
this.valueCardWidgetConfigForm.get('dateFont').disable();
this.valueCardWidgetConfigForm.get('dateColor').disable();
}
this.valueCardWidgetConfigForm.get('showIcon').updateValueAndValidity({emitEvent: false});
this.valueCardWidgetConfigForm.get('showDate').updateValueAndValidity({emitEvent: false});
this.valueCardWidgetConfigForm.get('label').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('labelFont').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('labelColor').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('iconSize').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('iconSizeUnit').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('icon').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('dateFormat').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('dateFont').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('dateColor').updateValueAndValidity({emitEvent});
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _valuePreviewFn(): string {
const units: string = this.valueCardWidgetConfigForm.get('units').value;
const decimals: number = this.valueCardWidgetConfigForm.get('decimals').value;
return formatValue(22, decimals, units, true);
}
private _datePreviewFn(): string {
const dateFormat: DateFormatSettings = this.valueCardWidgetConfigForm.get('dateFormat').value;
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat);
processor.update(Date.now());
return processor.formatted;
}
}

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html

@ -21,7 +21,7 @@
<mat-chip-listbox fxFlex>
<ng-container *ngFor="let actionSourceId of widgetActionSourceIds">
<mat-chip *ngFor="let widgetAction of widgetActionsByActionSourceId(actionSourceId)">
<mat-icon matChipAvatar>{{ widgetAction.icon }}</mat-icon>
<tb-icon matChipAvatar>{{ widgetAction.icon }}</tb-icon>
{{ widgetAction.name }}
</mat-chip>
</ng-container>

2
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html

@ -73,7 +73,7 @@
</div>
<div *ngIf="!hideDataKeyColor" style="padding: 3px;">
<div #keyColorButton class="tb-color-preview small box" (click)="openColorPickerPopup(key, $event, keyColorButton)">
<div class="tb-color-result" [ngStyle]="{background: key.color}"></div>
<div class="tb-color-result" [style]="{background: key.color}"></div>
</div>
</div>
<button *ngIf="!disabled"

365
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.models.ts

@ -0,0 +1,365 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { isDefinedAndNotNull, isNumber, isNumeric, parseFunction } from '@core/utils';
import { DataKey, Datasource, DatasourceData } from '@shared/models/widget.models';
import { Injector } from '@angular/core';
import { DatePipe, formatDate } from '@angular/common';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import { TranslateService } from '@ngx-translate/core';
export type ComponentStyle = {[klass: string]: any};
export const cssUnits = ['px', 'em', '%', 'rem', 'pt', 'pc', 'in', 'cm', 'mm', 'ex', 'ch', 'vw', 'vh', 'vmin', 'vmax'] as const;
type cssUnitTuple = typeof cssUnits;
export type cssUnit = cssUnitTuple[number];
export const fontWeights = ['normal', 'bold', 'bolder', 'lighter', '100', '200', '300', '400', '500', '600', '700', '800', '900'] as const;
type fontWeightTuple = typeof fontWeights;
export type fontWeight = fontWeightTuple[number];
export const fontWeightTranslations = new Map<fontWeight, string>(
[
['normal', 'widgets.widget-font.font-weight-normal'],
['bold', 'widgets.widget-font.font-weight-bold'],
['bolder', 'widgets.widget-font.font-weight-bolder'],
['lighter', 'widgets.widget-font.font-weight-lighter']
]
);
export const fontStyles = ['normal', 'italic', 'oblique'] as const;
type fontStyleTuple = typeof fontStyles;
export type fontStyle = fontStyleTuple[number];
export const fontStyleTranslations = new Map<fontStyle, string>(
[
['normal', 'widgets.widget-font.font-style-normal'],
['italic', 'widgets.widget-font.font-style-italic'],
['oblique', 'widgets.widget-font.font-style-oblique']
]
);
export const commonFonts = ['Roboto', 'monospace', 'sans-serif', 'serif'];
export interface Font {
size: number;
sizeUnit: cssUnit;
family: string;
weight: fontWeight;
style: fontStyle;
}
export enum ColorType {
constant = 'constant',
range = 'range',
function = 'function'
}
export const colorTypeTranslations = new Map<ColorType, string>(
[
[ColorType.constant, 'widgets.color.color-type-constant'],
[ColorType.range, 'widgets.color.color-type-range'],
[ColorType.function, 'widgets.color.color-type-function']
]
);
export interface ColorRange {
from?: number;
to?: number;
color: string;
}
export interface ColorSettings {
type: ColorType;
color: string;
rangeList?: ColorRange[];
colorFunction?: string;
}
export const constantColor = (color: string): ColorSettings => ({
type: ColorType.constant,
color,
colorFunction: 'var temperature = value;\n' +
'if (typeof temperature !== undefined) {\n' +
' var percent = (temperature + 60)/120 * 100;\n' +
' return tinycolor.mix(\'blue\', \'red\', percent).toHexString();\n' +
'}\n' +
'return \'blue\';'
});
type ValueColorFunction = (value: any) => string;
export abstract class ColorProcessor {
static fromSettings(color: ColorSettings): ColorProcessor {
switch (color.type) {
case ColorType.constant:
return new ConstantColorProcessor(color);
case ColorType.range:
return new RangeColorProcessor(color);
case ColorType.function:
return new FunctionColorProcessor(color);
}
}
color: string;
protected constructor(protected settings: ColorSettings) {
this.color = settings.color;
}
abstract update(value: any): void;
}
class ConstantColorProcessor extends ColorProcessor {
constructor(protected settings: ColorSettings) {
super(settings);
}
update(value: any): void {}
}
class RangeColorProcessor extends ColorProcessor {
constructor(protected settings: ColorSettings) {
super(settings);
}
update(value: any): void {
this.color = this.computeFromRange(value);
}
private computeFromRange(value: any): string {
if (this.settings.rangeList?.length && isDefinedAndNotNull(value) && isNumeric(value)) {
const num = Number(value);
for (const range of this.settings.rangeList) {
if ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)) {
return range.color;
}
}
}
return this.settings.color;
}
}
class FunctionColorProcessor extends ColorProcessor {
private readonly colorFunction: ValueColorFunction;
constructor(protected settings: ColorSettings) {
super(settings);
this.colorFunction = parseFunction(settings.colorFunction, ['value']);
}
update(value: any): void {
if (this.colorFunction) {
this.color = this.colorFunction(value) || this.settings.color;
}
}
}
export interface DateFormatSettings {
format?: string;
lastUpdateAgo?: boolean;
custom?: boolean;
}
export const simpleDateFormat = (format: string): DateFormatSettings => ({
format,
lastUpdateAgo: false,
custom: false
});
export const lastUpdateAgoDateFormat = (): DateFormatSettings => ({
format: null,
lastUpdateAgo: true,
custom: false
});
export const customDateFormat = (format: string): DateFormatSettings => ({
format,
lastUpdateAgo: false,
custom: true
});
export const dateFormats = ['MMM dd yyyy HH:mm', 'dd MMM yyyy HH:mm', 'yyyy MMM dd HH:mm',
'MM/dd/yyyy HH:mm', 'dd/MM/yyyy HH:mm', 'yyyy/MM/dd HH:mm:ss']
.map(f => simpleDateFormat(f)).concat([lastUpdateAgoDateFormat(), customDateFormat('EEE, MMMM dd, yyyy')]);
export const compareDateFormats = (df1: DateFormatSettings, df2: DateFormatSettings): boolean => {
if (df1 === df2) {
return true;
} else if (df1 && df2) {
if (df1.lastUpdateAgo && df2.lastUpdateAgo) {
return true;
} else if (df1.custom && df2.custom) {
return true;
} else if (!df1.lastUpdateAgo && !df2.lastUpdateAgo && !df1.custom && !df2.custom) {
return df1.format === df2.format;
}
}
return false;
};
export abstract class DateFormatProcessor {
static fromSettings($injector: Injector, settings: DateFormatSettings): DateFormatProcessor {
if (settings.lastUpdateAgo) {
return new LastUpdateAgoDateFormatProcessor($injector, settings);
} else {
return new SimpleDateFormatProcessor($injector, settings);
}
}
formatted = '';
protected constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
}
abstract update(ts: string | number | Date): void;
}
export class SimpleDateFormatProcessor extends DateFormatProcessor {
private datePipe: DatePipe;
constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
super($injector, settings);
this.datePipe = $injector.get(DatePipe);
}
update(ts: string| number | Date): void {
this.formatted = this.datePipe.transform(ts, this.settings.format);
}
}
export class LastUpdateAgoDateFormatProcessor extends DateFormatProcessor {
private dateAgoPipe: DateAgoPipe;
private translate: TranslateService;
constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
super($injector, settings);
this.dateAgoPipe = $injector.get(DateAgoPipe);
this.translate = $injector.get(TranslateService);
}
update(ts: string| number | Date): void {
this.formatted = this.translate.instant('date.last-update-n-ago-text',
{agoText: this.dateAgoPipe.transform(ts, {applyAgo: true, short: true, textPart: true})});
}
}
export enum BackgroundType {
image = 'image',
imageUrl = 'imageUrl',
color = 'color'
}
export interface OverlaySettings {
enabled: boolean;
color: string;
blur: number;
}
export interface BackgroundSettings {
type: BackgroundType;
imageBase64?: string;
imageUrl?: string;
color?: string;
overlay: OverlaySettings;
}
export const iconStyle = (size: number, sizeUnit: cssUnit): ComponentStyle => {
const iconSize = size + sizeUnit;
return {
width: iconSize,
height: iconSize,
fontSize: iconSize,
lineHeight: iconSize
};
};
export const textStyle = (font: Font, lineHeight = '1.5', letterSpacing = '0.25px'): ComponentStyle => ({
font: font.style + ' normal ' + font.weight + ' ' + (font.size+font.sizeUnit) + '/' + lineHeight + ' ' + font.family +
(font.family !== 'Roboto' ? ', Roboto' : ''),
letterSpacing
});
export const backgroundStyle = (background: BackgroundSettings): ComponentStyle => {
if (background.type === BackgroundType.color) {
return {
background: background.color
};
} else {
const imageUrl = background.type === BackgroundType.image ? background.imageBase64 : background.imageUrl;
return {
background: `url(${imageUrl}) no-repeat`,
backgroundSize: 'cover',
backgroundPosition: '50% 50%'
};
}
};
export const overlayStyle = (overlay: OverlaySettings): ComponentStyle => (
{
display: overlay.enabled ? 'block' : 'none',
background: overlay.color,
backdropFilter: `blur(${overlay.blur}px)`
}
);
export const getDataKey = (datasources?: Datasource[]): DataKey => {
if (datasources && datasources.length) {
const dataKeys = datasources[0].dataKeys;
if (dataKeys && dataKeys.length) {
return dataKeys[0];
}
}
return null;
};
export const getLabel = (datasources?: Datasource[]): string => {
const dataKey = getDataKey(datasources);
if (dataKey) {
return dataKey.label;
}
return '';
};
export const setLabel = (label: string, datasources?: Datasource[]): void => {
const dataKey = getDataKey(datasources);
if (dataKey) {
dataKey.label = label;
}
};
export const getSingleTsValue = (data: Array<DatasourceData>): [number, any] => {
if (data.length) {
const dsData = data[0];
if (dsData.data.length) {
return dsData.data[0];
}
}
return null;
};

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');

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

@ -0,0 +1,70 @@
<!--
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.
-->
<div class="tb-value-card-panel" [class]="this.layout" [style]="backgroundStyle">
<div class="tb-value-card-overlay" [style]="overlayStyle"></div>
<ng-container [ngSwitch]="layout">
<ng-template [ngSwitchCase]="valueCardLayout.square">
<ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.vertical">
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
<ng-container *ngTemplateOutlet="iconTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.centered">
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<div class="tb-value-card-icon-row">
<ng-container *ngTemplateOutlet="iconTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</div>
<ng-container *ngTemplateOutlet="dateTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.simplified">
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="layout === valueCardLayout.horizontal ||
layout === valueCardLayout.horizontal_reversed ? layout : ''">
<ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
<div fxFlex></div>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</ng-template>
</ng-container>
</div>
<ng-template #iconWithLabelTpl>
<div class="tb-value-card-icon-row">
<ng-container *ngTemplateOutlet="iconTpl"></ng-container>
<div class="tb-value-card-label-row">
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<ng-container *ngTemplateOutlet="dateTpl"></ng-container>
</div>
</div>
</ng-template>
<ng-template #iconTpl>
<tb-icon *ngIf="showIcon" [style]="iconStyle" [style.color]="iconColor.color">{{ icon }}</tb-icon>
</ng-template>
<ng-template #labelTpl>
<div *ngIf="showLabel" [style]="labelStyle" [style.color]="labelColor.color">{{ label }}</div>
</ng-template>
<ng-template #dateTpl>
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color">{{ dateFormat.formatted }}</div>
</ng-template>
<ng-template #valueTpl>
<div class="tb-value-card-value" [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>
</ng-template>

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

@ -0,0 +1,70 @@
/**
* 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.
*/
:host {
.tb-value-card-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 24px;
> div:not(.tb-value-card-overlay) {
z-index: 1;
}
&.square {
justify-content: space-evenly;
gap: 0;
}
&.horizontal {
flex-direction: row;
}
&.horizontal_reversed {
flex-direction: row-reverse;
}
.tb-value-card-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
.tb-value-card-icon-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
&.horizontal_reversed {
.tb-value-card-icon-row {
flex-direction: row-reverse;
}
.tb-value-card-label-row {
align-items: flex-end;
}
}
.tb-value-card-label-row {
display: flex;
flex-direction: column;
justify-content: center;
}
.tb-value-card-value {
white-space: nowrap;
}
}
}

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

@ -0,0 +1,144 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { DatePipe } from '@angular/common';
import {
backgroundStyle,
ColorProcessor,
ComponentStyle, DateFormatProcessor,
getDataKey,
getLabel,
getSingleTsValue,
iconStyle,
overlayStyle,
textStyle
} from '@home/components/widget/config/widget-settings.models';
import { valueCardDefaultSettings, ValueCardLayout, ValueCardWidgetSettings } from './value-card-widget.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
@Component({
selector: 'tb-value-card-widget',
templateUrl: './value-card-widget.component.html',
styleUrls: ['./value-card-widget.component.scss']
})
export class ValueCardWidgetComponent implements OnInit {
settings: ValueCardWidgetSettings;
valueCardLayout = ValueCardLayout;
@Input()
ctx: WidgetContext;
layout: ValueCardLayout;
showIcon = true;
icon = '';
iconStyle: ComponentStyle = {};
iconColor: ColorProcessor;
showLabel = true;
label = '';
labelStyle: ComponentStyle = {};
labelColor: ColorProcessor;
valueText = 'N/A';
valueStyle: ComponentStyle = {};
valueColor: ColorProcessor;
showDate = true;
dateFormat: DateFormatProcessor;
dateStyle: ComponentStyle = {};
dateColor: ColorProcessor;
backgroundStyle: ComponentStyle = {};
overlayStyle: ComponentStyle = {};
private horizontal = false;
private decimals = 0;
private units = '';
constructor(private date: DatePipe,
private widgetComponent: WidgetComponent,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
const params = this.widgetComponent.typeParameters as any;
this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false;
this.ctx.$scope.valueCardWidget = this;
this.settings = {...valueCardDefaultSettings(this.horizontal), ...this.ctx.settings};
this.decimals = this.ctx.decimals;
this.units = this.ctx.units;
const dataKey = getDataKey(this.ctx.datasources);
if (isDefinedAndNotNull(dataKey?.decimals)) {
this.decimals = dataKey.decimals;
}
if (dataKey?.units) {
this.units = dataKey.units;
}
this.layout = this.settings.layout;
this.showIcon = this.settings.showIcon;
this.icon = this.settings.icon;
this.iconStyle = iconStyle(this.settings.iconSize, this.settings.iconSizeUnit );
this.iconColor = ColorProcessor.fromSettings(this.settings.iconColor);
this.showLabel = this.settings.showLabel;
this.label = getLabel(this.ctx.datasources);
this.labelStyle = textStyle(this.settings.labelFont, '1.5', '0.25px');
this.labelColor = ColorProcessor.fromSettings(this.settings.labelColor);
this.valueStyle = textStyle(this.settings.valueFont, '100%', '0.13px');
this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
this.showDate = this.settings.showDate;
this.dateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.dateFormat);
this.dateStyle = textStyle(this.settings.dateFont, '1.33', '0.25px');
this.dateColor = ColorProcessor.fromSettings(this.settings.dateColor);
this.backgroundStyle = backgroundStyle(this.settings.background);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public onDataUpdated() {
const tsValue = getSingleTsValue(this.ctx.data);
let ts;
let value;
if (tsValue) {
ts = tsValue[0];
value = tsValue[1];
this.valueText = formatValue(value, this.decimals, this.units, true);
} else {
this.valueText = 'N/A';
}
this.dateFormat.update(ts);
this.iconColor.update(value);
this.labelColor.update(value);
this.valueColor.update(value);
this.dateColor.update(value);
this.cd.detectChanges();
}
}

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

@ -0,0 +1,127 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
BackgroundSettings,
BackgroundType,
ColorSettings,
constantColor,
cssUnit, DateFormatSettings,
Font, lastUpdateAgoDateFormat
} from '@home/components/widget/config/widget-settings.models';
export enum ValueCardLayout {
square = 'square',
vertical = 'vertical',
centered = 'centered',
simplified = 'simplified',
horizontal = 'horizontal',
horizontal_reversed = 'horizontal_reversed'
}
export const valueCardLayouts = (horizontal: boolean): ValueCardLayout[] => {
if (horizontal) {
return [ValueCardLayout.horizontal, ValueCardLayout.horizontal_reversed];
} else {
return [ValueCardLayout.square, ValueCardLayout.vertical, ValueCardLayout.centered, ValueCardLayout.simplified];
}
};
export const valueCardLayoutTranslations = new Map<ValueCardLayout, string>(
[
[ValueCardLayout.square, 'widgets.value-card.layout-square'],
[ValueCardLayout.vertical, 'widgets.value-card.layout-vertical'],
[ValueCardLayout.centered, 'widgets.value-card.layout-centered'],
[ValueCardLayout.simplified, 'widgets.value-card.layout-simplified'],
[ValueCardLayout.horizontal, 'widgets.value-card.layout-horizontal'],
[ValueCardLayout.horizontal_reversed, 'widgets.value-card.layout-horizontal-reversed']
]
);
export const valueCardLayoutImages = new Map<ValueCardLayout, string>(
[
[ValueCardLayout.square, 'assets/widget/value-card/square-layout.svg'],
[ValueCardLayout.vertical, 'assets/widget/value-card/vertical-layout.svg'],
[ValueCardLayout.centered, 'assets/widget/value-card/centered-layout.svg'],
[ValueCardLayout.simplified, 'assets/widget/value-card/simplified-layout.svg'],
[ValueCardLayout.horizontal, 'assets/widget/value-card/horizontal-layout.svg'],
[ValueCardLayout.horizontal_reversed, 'assets/widget/value-card/horizontal-reversed-layout.svg']
]
);
export interface ValueCardWidgetSettings {
layout: ValueCardLayout;
showLabel: boolean;
labelFont: Font;
labelColor: ColorSettings;
showIcon: boolean;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
iconColor: ColorSettings;
valueFont: Font;
valueColor: ColorSettings;
showDate: boolean;
dateFormat: DateFormatSettings;
dateFont: Font;
dateColor: ColorSettings;
background: BackgroundSettings;
}
export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSettings => ({
layout: horizontal ? ValueCardLayout.horizontal : ValueCardLayout.square,
showLabel: true,
labelFont: {
family: 'Roboto',
size: 16,
sizeUnit: 'px',
style: 'normal',
weight: '500'
},
labelColor: constantColor('rgba(0, 0, 0, 0.87)'),
showIcon: true,
icon: 'thermostat',
iconSize: 40,
iconSizeUnit: 'px',
iconColor: constantColor('#5469FF'),
valueFont: {
family: 'Roboto',
size: 52,
sizeUnit: 'px',
style: 'normal',
weight: '500'
},
valueColor: constantColor('rgba(0, 0, 0, 0.87)'),
showDate: true,
dateFormat: lastUpdateAgoDateFormat(),
dateFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '500'
},
dateColor: constantColor('rgba(0, 0, 0, 0.38)'),
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
});

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');

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.html

@ -22,7 +22,7 @@
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
<tb-icon>close</tb-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html

@ -22,7 +22,7 @@
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
<tb-icon>close</tb-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>

10
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html

@ -38,8 +38,8 @@
</div>
</div>
<div *ngIf="editMode" fxLayout="row" class="tb-edit-buttons">
<button mat-icon-button (click)="apply()"><mat-icon>check</mat-icon></button>
<button mat-icon-button (click)="cancelEdit()"><mat-icon>close</mat-icon></button>
<button mat-icon-button (click)="apply()"><tb-icon>check</tb-icon></button>
<button mat-icon-button (click)="cancelEdit()"><tb-icon>close</tb-icon></button>
</div>
</div>
<ng-template #docLinkTemplate>
@ -47,7 +47,7 @@
<div fxFlex class="tb-link">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon color="primary">{{ docLink.icon }}</mat-icon>
<tb-icon color="primary">{{ docLink.icon }}</tb-icon>
</div>
<div class="tb-link-text">{{ docLink.name }}</div>
</div>
@ -57,13 +57,13 @@
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="switchToEditMode()">
<mat-icon>edit</mat-icon>
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="delete()">
<mat-icon>delete</mat-icon>
<tb-icon>delete</tb-icon>
</button>
</div>
</div>

6
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html

@ -23,7 +23,7 @@
matTooltipPosition="above"
mat-icon-button
(click)="edit()">
<mat-icon>edit</mat-icon>
<tb-icon>edit</tb-icon>
</button>
</div>
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
@ -32,7 +32,7 @@
[href]="docLink.link" target="_blank">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon color="primary">{{ docLink.icon }}</mat-icon>
<tb-icon color="primary">{{ docLink.icon }}</tb-icon>
</div>
<div class="tb-link-text">{{ docLink.name }}</div>
</div>
@ -43,7 +43,7 @@
matTooltip="{{ 'widgets.documentation.add-link' | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
<tb-icon class="tb-add-icon">add</tb-icon>
</div>
</mat-grid-tile>
</mat-grid-list>

6
ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html

@ -22,7 +22,7 @@
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
<tb-icon class="material-icons">close</tb-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
@ -60,7 +60,7 @@
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above"
class="tb-drag-handle">
<mat-icon>drag_indicator</mat-icon>
<tb-icon>drag_indicator</tb-icon>
</div>
</ng-template>
</div>
@ -71,7 +71,7 @@
matTooltip="{{ (mode === 'docs' ? 'widgets.documentation.add-link' : 'widgets.quick-links.add-link') | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
<tb-icon class="tb-add-icon">add</tb-icon>
</div>
<ng-container *ngIf="addMode">
<ng-container [ngSwitch]="mode">

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

@ -23,6 +23,7 @@
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
.mat-icon {
vertical-align: bottom;
margin-right: 10px;
color: rgba(0, 0, 0, 0.54);
font-size: 20px;

19
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html

@ -25,21 +25,19 @@
(focusin)="onFocus()"
required
[matAutocomplete]="linkAutocomplete">
<mat-icon matPrefix *ngIf="quickLink && !quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon>
<mat-icon matPrefix *ngIf="quickLink && quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon>
<tb-icon matPrefix *ngIf="quickLink" color="primary">{{ quickLink.icon }}</tb-icon>
<button *ngIf="editQuickLinkFormGroup.get('link').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
<tb-icon>close</tb-icon>
</button>
<mat-autocomplete
class="tb-autocomplete tb-quick-links"
#linkAutocomplete="matAutocomplete"
[displayWith]="displayLinkFn">
<mat-option *ngFor="let link of filteredLinks | async" [value]="link">
<mat-icon *ngIf="!link.isMdiIcon">{{ link.icon }}</mat-icon>
<mat-icon *ngIf="link.isMdiIcon" [svgIcon]="link.icon"></mat-icon>
<tb-icon>{{ link.icon }}</tb-icon>
<span [innerHTML]="link.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredLinks | async)?.length" [value]="null">
@ -58,8 +56,8 @@
</div>
</div>
<div *ngIf="editMode" fxLayout="row" class="tb-edit-buttons">
<button mat-icon-button (click)="apply()"><mat-icon>check</mat-icon></button>
<button mat-icon-button (click)="cancelEdit()"><mat-icon>close</mat-icon></button>
<button mat-icon-button (click)="apply()"><tb-icon>check</tb-icon></button>
<button mat-icon-button (click)="cancelEdit()"><tb-icon>close</tb-icon></button>
</div>
</div>
<ng-template #quickLinkTemplate>
@ -67,8 +65,7 @@
<div fxFlex class="tb-link">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon *ngIf="!quickLink?.isMdiIcon" color="primary">{{ quickLink?.icon }}</mat-icon>
<mat-icon *ngIf="quickLink?.isMdiIcon" color="primary" [svgIcon]="quickLink?.icon"></mat-icon>
<tb-icon color="primary">{{ quickLink?.icon }}</tb-icon>
</div>
<div class="tb-link-text">{{ displayLinkFn(quickLink) }}</div>
</div>
@ -78,13 +75,13 @@
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="switchToEditMode()">
<mat-icon>edit</mat-icon>
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="delete()">
<mat-icon>delete</mat-icon>
<tb-icon>delete</tb-icon>
</button>
</div>
</div>

7
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html

@ -23,7 +23,7 @@
matTooltipPosition="above"
mat-icon-button
(click)="edit()">
<mat-icon>edit</mat-icon>
<tb-icon>edit</tb-icon>
</button>
</div>
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
@ -32,8 +32,7 @@
[routerLink]="quickLink.path">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon *ngIf="!quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon>
<mat-icon *ngIf="quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon>
<tb-icon color="primary">{{ quickLink.icon }}</tb-icon>
</div>
<div class="tb-link-text">{{ (quickLink.fullName || quickLink.name) | translate }}</div>
</div>
@ -44,7 +43,7 @@
matTooltip="{{ 'widgets.quick-links.add-link' | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
<tb-icon class="tb-add-icon">add</tb-icon>
</div>
</mat-grid-tile>
</mat-grid-list>

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%;

8
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html

@ -38,7 +38,7 @@
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
@ -63,7 +63,7 @@
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
@ -99,7 +99,7 @@
(blur)="key.isFocused = false; inputChanged(source, key)"
/>
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
@ -123,7 +123,7 @@
[labelPosition]="key.settings.slideToggleLabelPosition"
(change)="inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>

2
ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html

@ -16,6 +16,6 @@
-->
<a mat-raised-button color="primary" class="tb-nav-button" href="{{settings.path}}" (click)="navigate($event, settings.path)">
<mat-icon class="material-icons tb-mat-96">{{settings.icon}}</mat-icon>
<tb-icon class="tb-mat-96">{{settings.icon}}</tb-icon>
<span>{{translatedName}}</span>
</a>

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

@ -25,8 +25,7 @@
<mat-grid-list rowHeight="170px" [cols]="sectionPlaces(section).length">
<mat-grid-tile *ngFor="let place of sectionPlaces(section)">
<a mat-raised-button color="primary" class="tb-card-button" href="{{place.path}}" (click)="navigate($event, place.path)">
<mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
<mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
<tb-icon matButtonIcon class="tb-mat-96">{{place.icon}}</tb-icon>
<span translate>{{place.name}}</span>
</a>
</mat-grid-tile>

2
ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss

@ -55,7 +55,7 @@
display: flex;
flex-direction: column;
align-items: center;
mat-icon {
.mat-icon {
margin: auto;
}
span.mdc-button__label {

6
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html

@ -80,7 +80,7 @@
matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above"
(click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{ actionDescriptor.icon }}</mat-icon>
<tb-icon>{{ actionDescriptor.icon }}</tb-icon>
</button>
</ng-container>
</div>
@ -88,14 +88,14 @@
<button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
<tb-icon>more_vert</tb-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<ng-container *ngFor="let actionDescriptor of actionCellButtonAction">
<button mat-menu-item *ngIf="actionDescriptor.icon"
[disabled]="(isLoading$ | async)"
(click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
<tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
<span>{{ actionDescriptor.displayName }}</span>
</button>
</ng-container>

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, []],

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

@ -0,0 +1,105 @@
<!--
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.
-->
<div class="tb-color-settings-panel" [formGroup]="colorSettingsFormGroup">
<div class="tb-color-settings-title" translate>widgets.color.color-settings</div>
<div fxLayout="row">
<tb-toggle-select formControlName="type" fxFlex.xs fxFlex="70%">
<tb-toggle-option *ngFor="let type of colorTypes"
[value]="type">
{{ colorTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.color.color</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-color-settings-panel-body" [fxShow]="colorSettingsFormGroup.get('type').value === colorType.constant">
</div>
<div class="tb-color-settings-panel-body" [fxShow]="colorSettingsFormGroup.get('type').value === colorType.range">
<ng-container *ngTemplateOutlet="range"></ng-container>
</div>
<div class="tb-color-settings-panel-body" [fxShow]="colorSettingsFormGroup.get('type').value === colorType.function">
<ng-container *ngTemplateOutlet="function"></ng-container>
</div>
<div class="tb-color-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyColorSettings()"
[disabled]="colorSettingsFormGroup.invalid || !colorSettingsFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>
<ng-template #range>
<div fxFlex class="tb-color-ranges-panel">
<div class="tb-form-panel-title" translate>widgets.color.value-range</div>
<div class="tb-color-ranges" [formGroup]="colorSettingsFormGroup">
<div class="tb-form-row no-padding no-border" [formGroup]="rangeFormGroup" *ngFor="let rangeFormGroup of rangeListFormGroups; trackBy: trackByRange; let $index = index;">
<div fxFlex fxLayout="row" fxLayoutGap="24px">
<div fxFlex fxLayout="row" fxLayoutGap="12px" fxLayoutAlign="start center">
<div class="tb-value-range-text" translate>widgets.color.from</div>
<mat-form-field fxFlex appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput type="number" formControlName="from" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-value-range-text tb-value-range-text-to" translate>widgets.color.to</div>
<mat-form-field fxFlex appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput type="number" formControlName="to" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<button type="button"
mat-icon-button
class="tb-box-button"
(click)="removeRange($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</div>
<button class="tb-add-color-range"
mat-stroked-button
(click)="addRange()">
<mat-icon>add</mat-icon>
</button>
</div>
</ng-template>
<ng-template #function>
<div class="tb-form-panel no-padding no-border" [formGroup]="colorSettingsFormGroup">
<tb-js-func formControlName="colorFunction"
[functionArgs]="['value']"
[globalVariables]="functionScopeVariables"
functionTitle="{{ 'widgets.color.color-function' | translate }}"
helpId="widget/lib/card/value_color_fn">
</tb-js-func>
</div>
</ng-template>

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

@ -0,0 +1,85 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
.tb-color-settings-panel {
width: 500px;
height: 470px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-xs} {
width: 90vw;
}
.tb-color-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-color-ranges-panel {
flex: 1;
min-height: 0;
gap: 16px;
display: flex;
flex-direction: column;
}
.tb-color-ranges {
flex: 1;
gap: 12px;
display: flex;
flex-direction: column;
overflow: auto;
}
.tb-form-row {
height: auto;
.tb-value-range-text {
width: 64px;
font-size: 14px;
color: rgba(0, 0, 0, 0.38);
@media #{$mat-xs} {
width: auto;
}
&.tb-value-range-text-to {
text-align: center;
}
}
}
button.mat-mdc-button-base.tb-add-color-range {
&:not(:disabled) {
color: rgba(0, 0, 0, 0.54);
}
&:disabled {
color: rgba(0, 0, 0, 0.12);
}
}
.tb-color-settings-panel-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.tb-color-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

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

@ -0,0 +1,133 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import {
ColorRange,
ColorSettings,
ColorType,
colorTypeTranslations
} from '@home/components/widget/config/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
AbstractControl,
FormControl,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Datasource, DatasourceType } from '@shared/models/widget.models';
import { deepClone } from '@core/utils';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
selector: 'tb-color-settings-panel',
templateUrl: './color-settings-panel.component.html',
providers: [],
styleUrls: ['./color-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ColorSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
colorSettings: ColorSettings;
@Input()
popover: TbPopoverComponent<ColorSettingsPanelComponent>;
@Output()
colorSettingsApplied = new EventEmitter<ColorSettings>();
colorType = ColorType;
colorTypes = Object.keys(ColorType) as ColorType[];
colorTypeTranslationsMap = colorTypeTranslations;
colorSettingsFormGroup: UntypedFormGroup;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.colorSettingsFormGroup = this.fb.group(
{
type: [this.colorSettings?.type, []],
color: [this.colorSettings?.color, []],
rangeList: this.fb.array((this.colorSettings?.rangeList || []).map(r => this.colorRangeControl(r))),
colorFunction: [this.colorSettings?.colorFunction, []]
}
);
this.colorSettingsFormGroup.get('type').valueChanges.subscribe(() => {
setTimeout(() => {this.popover?.updatePosition();}, 0);
});
}
private colorRangeControl(range: ColorRange): AbstractControl {
return this.fb.group({
from: [range?.from, []],
to: [range?.to, []],
color: [range?.color, []]
});
}
get rangeListFormArray(): UntypedFormArray {
return this.colorSettingsFormGroup.get('rangeList') as UntypedFormArray;
}
get rangeListFormGroups(): FormGroup[] {
return this.rangeListFormArray.controls as FormGroup[];
}
trackByRange(index: number, rangeControl: AbstractControl): any {
return rangeControl;
}
removeRange(index: number) {
this.rangeListFormArray.removeAt(index);
this.colorSettingsFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
addRange() {
const newRange: ColorRange = {
color: 'rgba(0,0,0,0.87)'
};
this.rangeListFormArray.push(this.colorRangeControl(newRange));
this.colorSettingsFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
cancel() {
this.popover?.hide();
}
applyColorSettings() {
const colorSettings = this.colorSettingsFormGroup.value;
this.colorSettingsApplied.emit(colorSettings);
}
}

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

@ -0,0 +1,30 @@
<!--
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.
-->
<button type="button"
mat-stroked-button
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openColorSettingsPopup($event, matButton)">
<tb-icon matButtonIcon *ngIf="modelValue.type === colorType.function; else colorPreview">mdi:function-variant</tb-icon>
</button>
<ng-template #colorPreview>
<div class="tb-color-preview box" [ngClass]="{'disabled': disabled}">
<div class="tb-color-result" [style]="colorStyle"></div>
</div>
</ng-template>

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

@ -0,0 +1,124 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ColorSettings, ColorType, ComponentStyle } from '@home/components/widget/config/widget-settings.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import {
ColorSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/color-settings-panel.component';
@Component({
selector: 'tb-color-settings',
templateUrl: './color-settings.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ColorSettingsComponent),
multi: true
}
]
})
export class ColorSettingsComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
colorType = ColorType;
modelValue: ColorSettings;
colorStyle: ComponentStyle = {};
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.updateColorStyle();
}
writeValue(value: ColorSettings): void {
this.modelValue = value;
this.updateColorStyle();
}
openColorSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
colorSettings: this.modelValue
};
const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorSettingsPanelComponent, 'left', true, null,
ctx,
{},
{}, {}, true);
colorSettingsPanelPopover.tbComponentRef.instance.popover = colorSettingsPanelPopover;
colorSettingsPanelPopover.tbComponentRef.instance.colorSettingsApplied.subscribe((colorSettings) => {
colorSettingsPanelPopover.hide();
this.modelValue = colorSettings;
this.updateColorStyle();
this.propagateChange(this.modelValue);
});
}
}
private updateColorStyle() {
if (!this.disabled) {
let colors: string[] = [this.modelValue.color];
if (this.modelValue.type === ColorType.range && this.modelValue.rangeList?.length) {
const rangeColors = this.modelValue.rangeList.slice(0, Math.min(2, this.modelValue.rangeList.length)).map(r => r.color);
colors = colors.concat(rangeColors);
}
if (colors.length === 1) {
this.colorStyle = {backgroundColor: colors[0]};
} else {
const gradientValues: string[] = [];
const step = 100 / colors.length;
for (let i = 0; i < colors.length; i++) {
gradientValues.push(`${colors[i]} ${step*i}%`);
gradientValues.push(`${colors[i]} ${step*(i+1)}%`);
}
this.colorStyle = {background: `linear-gradient(90deg, ${gradientValues.join(', ')})`};
}
} else {
this.colorStyle = {};
}
}
}

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html

@ -0,0 +1,22 @@
<!--
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.
-->
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 100%;">
<mat-select [formControl]="cssUnitFormControl">
<mat-option *ngFor="let cssUnit of cssUnitsList" [value]="cssUnit">{{ cssUnit }}</mat-option>
</mat-select>
</mat-form-field>

82
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts

@ -0,0 +1,82 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { cssUnit, cssUnits } from '@home/components/widget/config/widget-settings.models';
@Component({
selector: 'tb-css-unit-select',
templateUrl: './css-unit-select.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CssUnitSelectComponent),
multi: true
}
]
})
export class CssUnitSelectComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
cssUnitsList = cssUnits;
cssUnitFormControl: UntypedFormControl;
modelValue: cssUnit;
private propagateChange = null;
constructor() {}
ngOnInit(): void {
this.cssUnitFormControl = new UntypedFormControl();
this.cssUnitFormControl.valueChanges.subscribe((value: cssUnit) => {
this.updateModel(value);
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.cssUnitFormControl.disable();
} else {
this.cssUnitFormControl.enable();
}
}
writeValue(value: cssUnit): void {
this.modelValue = value;
this.cssUnitFormControl.patchValue(this.modelValue, {emitEvent: false});
}
updateModel(value: cssUnit): void {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
}

26
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html

@ -0,0 +1,26 @@
<!--
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.
-->
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 100%;">
<mat-select [formControl]="dateFormatFormControl" [compareWith]="dateFormatsCompare" >
<mat-option *ngFor="let dateFormat of dateFormatList" [value]="dateFormat">{{ dateFormatDisplayValue(dateFormat) }}</mat-option>
</mat-select>
<button #customFormatButton
*ngIf="dateFormatFormControl.value?.custom" matSuffix mat-icon-button (click)="openDateFormatSettingsPopup($event, customFormatButton)">
<tb-icon>edit</tb-icon>
</button>
</mat-form-field>

148
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts

@ -0,0 +1,148 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewChild, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import {
compareDateFormats,
dateFormats,
DateFormatSettings
} from '@home/components/widget/config/widget-settings.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { deepClone } from '@core/utils';
import {
DateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/date-format-settings-panel.component';
@Component({
selector: 'tb-date-format-select',
templateUrl: './date-format-select.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DateFormatSelectComponent),
multi: true
}
]
})
export class DateFormatSelectComponent implements OnInit, ControlValueAccessor {
@ViewChild('customFormatButton', {static: false})
customFormatButton: MatButton;
@Input()
disabled: boolean;
dateFormatList = dateFormats;
dateFormatsCompare = compareDateFormats;
dateFormatFormControl: UntypedFormControl;
modelValue: DateFormatSettings;
private propagateChange = null;
private formatCache: {[format: string]: string} = {};
constructor(private translate: TranslateService,
private date: DatePipe,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
this.dateFormatFormControl = new UntypedFormControl();
this.dateFormatFormControl.valueChanges.subscribe((value: DateFormatSettings) => {
this.updateModel(value);
if (value?.custom) {
setTimeout(() => {
this.openDateFormatSettingsPopup(null, this.customFormatButton);
}, 0);
}
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.dateFormatFormControl.disable();
} else {
this.dateFormatFormControl.enable();
}
}
writeValue(value: DateFormatSettings): void {
this.modelValue = value;
this.dateFormatFormControl.patchValue(this.modelValue, {emitEvent: false});
}
updateModel(value: DateFormatSettings): void {
if (!compareDateFormats(this.modelValue, value)) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
dateFormatDisplayValue(value: DateFormatSettings): string {
if (value.custom) {
return this.translate.instant('date.custom-date');
} else if (value.lastUpdateAgo) {
return this.translate.instant('date.last-update-n-ago');
} else {
if (!this.formatCache[value.format]) {
this.formatCache[value.format] = this.date.transform(Date.now(), value.format);
}
return this.formatCache[value.format];
}
}
openDateFormatSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
dateFormat: deepClone(this.modelValue)
};
const dateFormatSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, DateFormatSettingsPanelComponent, 'top', true, null,
ctx,
{},
{}, {}, true);
dateFormatSettingsPanelPopover.tbComponentRef.instance.popover = dateFormatSettingsPanelPopover;
dateFormatSettingsPanelPopover.tbComponentRef.instance.dateFormatApplied.subscribe((dateFormat) => {
dateFormatSettingsPanelPopover.hide();
this.modelValue = dateFormat;
this.propagateChange(this.modelValue);
});
}
}
}

47
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.html

@ -0,0 +1,47 @@
<!--
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.
-->
<div class="tb-date-format-settings-panel">
<div class="tb-date-format-settings-title" translate>date.custom-date</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>date.format</div>
<mat-form-field class="tb-date-format-input" required fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput [formControl]="dateFormatFormControl" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix tb-help-popup="date/date-format" [tb-help-popup-style]="{width: '800px'}"></div>
</mat-form-field>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding date-format-preview">
<div class="fixed-title-width" translate>date.preview</div>
<div class="preview-text" fxFlex>{{ previewText }}</div>
</div>
<div class="tb-date-format-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyDateFormat()"
[disabled]="dateFormatFormControl.invalid || !dateFormatFormControl.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

67
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.scss

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
.tb-date-format-settings-panel {
width: 500px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-xs} {
width: 90vw;
}
.tb-date-format-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
}
&.date-format-preview {
align-items: flex-start;
.preview-text {
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.38);
}
}
.mat-mdc-form-field.tb-date-format-input {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
.mat-mdc-form-field-icon-suffix {
display: flex;
align-items: center;
line-height: normal;
}
}
}
}
.tb-date-format-settings-panel-buttons {
height: 60px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

70
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts

@ -0,0 +1,70 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { DateFormatSettings } from '@home/components/widget/config/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormControl, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DatePipe } from '@angular/common';
@Component({
selector: 'tb-date-format-settings-panel',
templateUrl: './date-format-settings-panel.component.html',
providers: [],
styleUrls: ['./date-format-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class DateFormatSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
dateFormat: DateFormatSettings;
@Input()
popover: TbPopoverComponent<DateFormatSettingsPanelComponent>;
@Output()
dateFormatApplied = new EventEmitter<DateFormatSettings>();
dateFormatFormControl: UntypedFormControl;
previewText = '';
constructor(private date: DatePipe,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.dateFormatFormControl = new UntypedFormControl(this.dateFormat.format, [Validators.required]);
this.dateFormatFormControl.valueChanges.subscribe((value: string) => {
this.previewText = this.date.transform(Date.now(), value);
});
this.previewText = this.date.transform(Date.now(), this.dateFormat.format);
}
cancel() {
this.popover?.hide();
}
applyDateFormat() {
this.dateFormat.format = this.dateFormatFormControl.value;
this.dateFormatApplied.emit(this.dateFormat);
}
}

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

@ -0,0 +1,91 @@
<!--
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.
-->
<div class="tb-font-settings-panel" [formGroup]="fontFormGroup">
<div class="tb-font-settings-title" translate>widgets.widget-font.font-settings</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.size</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="size" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="sizeUnit"></tb-css-unit-select>
</div>
</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.font-family</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput #familyInput
formControlName="family" placeholder="{{ 'widget-config.set' | translate }}"
[matAutocomplete]="familyAutocomplete">
<button *ngIf="fontFormGroup.get('family').value"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clearFamily()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
#familyAutocomplete="matAutocomplete"
class="tb-autocomplete"
panelWidth="fit-content">
<mat-option *ngFor="let family of filteredFontFamilies | async" [value]="family">
<span [innerHTML]="family | highlight:familySearchText:true:'ig'"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.font-weight</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="weight">
<mat-option *ngFor="let weight of fontWeightsList" [value]="weight">
{{ fontWeightTranslationsMap.has(weight) ? (fontWeightTranslationsMap.get(weight) | translate) : weight }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.font-style</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="style">
<mat-option *ngFor="let style of fontStylesList" [value]="style">
{{ fontStyleTranslationsMap.get(style) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding font-preview">
<div class="fixed-title-width" translate>widgets.widget-font.preview</div>
<div class="preview-text" fxFlex [style]="previewStyle">{{ previewText }}</div>
</div>
<div class="tb-font-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyFont()"
[disabled]="fontFormGroup.invalid || !fontFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

51
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss

@ -0,0 +1,51 @@
/**
* 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.
*/
.tb-font-settings-panel {
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
.tb-font-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
}
&.font-preview {
align-items: flex-start;
.preview-text {
max-height: 300px;
max-width: 400px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.tb-font-settings-panel-buttons {
height: 60px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

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

@ -0,0 +1,133 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import {
commonFonts,
ComponentStyle,
Font,
fontStyles,
fontStyleTranslations,
fontWeights,
fontWeightTranslations,
textStyle
} from '@home/components/widget/config/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Observable } from 'rxjs';
import { map, startWith, tap } from 'rxjs/operators';
@Component({
selector: 'tb-font-settings-panel',
templateUrl: './font-settings-panel.component.html',
providers: [],
styleUrls: ['./font-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class FontSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
font: Font;
@Input()
previewText = 'AaBbCcDd';
@Input()
popover: TbPopoverComponent<FontSettingsPanelComponent>;
@Output()
fontApplied = new EventEmitter<Font>();
@ViewChild('familyInput', {static: true}) familyInput: ElementRef;
fontWeightsList = fontWeights;
fontWeightTranslationsMap = fontWeightTranslations;
fontStylesList = fontStyles;
fontStyleTranslationsMap = fontStyleTranslations;
fontFormGroup: UntypedFormGroup;
filteredFontFamilies: Observable<Array<string>>;
familySearchText = '';
previewStyle: ComponentStyle = {};
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.fontFormGroup = this.fb.group(
{
size: [this.font?.size, [Validators.required, Validators.min(0)]],
sizeUnit: [this.font?.sizeUnit, [Validators.required]],
family: [this.font?.family, [Validators.required]],
weight: [this.font?.weight, [Validators.required]],
style: [this.font?.style, [Validators.required]]
}
);
if (this.font) {
this.previewStyle = textStyle(this.font, '1');
}
this.fontFormGroup.valueChanges.subscribe((value: Font) => {
if (this.fontFormGroup.valid) {
this.previewStyle = textStyle(value, '1');
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
});
this.filteredFontFamilies = this.fontFormGroup.get('family').valueChanges
.pipe(
startWith<string>(''),
tap((searchText) => { this.familySearchText = searchText || ''; }),
map(() => commonFonts.filter(f => f.toUpperCase().includes(this.familySearchText.toUpperCase())))
);
}
clearFamily() {
this.fontFormGroup.get('family').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.familyInput.nativeElement.blur();
this.familyInput.nativeElement.focus();
}, 0);
}
cancel() {
this.popover?.hide();
}
applyFont() {
const font = this.fontFormGroup.value;
this.fontApplied.emit(font);
}
}

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

@ -0,0 +1,25 @@
<!--
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.
-->
<button type="button"
mat-stroked-button
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openFontSettingsPopup($event, matButton)">
<tb-icon matButtonIcon>text_format</tb-icon>
</button>

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

@ -0,0 +1,102 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Font } from '@home/components/widget/config/widget-settings.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { FontSettingsPanelComponent } from '@home/components/widget/lib/settings/common/font-settings-panel.component';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-font-settings',
templateUrl: './font-settings.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FontSettingsComponent),
multi: true
}
]
})
export class FontSettingsComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
@Input()
previewText: string | (() => string);
private modelValue: Font;
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: Font): void {
this.modelValue = value;
}
openFontSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
font: this.modelValue
};
if (isDefinedAndNotNull(this.previewText)) {
const previewText = typeof this.previewText === 'string' ? this.previewText : this.previewText();
if (previewText) {
ctx.previewText = previewText;
}
}
const fontSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, FontSettingsPanelComponent, 'left', true, null,
ctx,
{},
{}, {}, true);
fontSettingsPanelPopover.tbComponentRef.instance.popover = fontSettingsPanelPopover;
fontSettingsPanelPopover.tbComponentRef.instance.fontApplied.subscribe((font) => {
fontSettingsPanelPopover.hide();
this.modelValue = font;
this.propagateChange(this.modelValue);
});
}
}
}

43
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html

@ -0,0 +1,43 @@
<!--
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.
-->
<div class="tb-image-cards-select tb-form-panel stroked no-padding">
<mat-expansion-panel class="tb-settings" [expanded]="expanded" disabled>
<mat-expansion-panel-header class="fill-width" fxLayout="row wrap">
<div fxFlex class="tb-form-row no-border" [class.expanded]="expanded">
<div class="fixed-title-width">{{ label }}</div>
<mat-form-field class="tb-image-cards-value-field" fxFlex appearance="outline" subscriptSizing="dynamic" (click)="toggleSelectPanel($event)">
<input readonly matInput [formControl]="valueFormControl" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix>{{ expanded ? 'expand_less' : 'expand_more' }}</mat-icon>
</mat-form-field>
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-grid-list class="tb-image-cards-options" [cols]="cols$ | async" [rowHeight]="rowHeight" gutterSize="8">
<mat-grid-tile *ngFor="let option of options" (click)="updateModel(option.value)">
<div class="tb-image-cards-option" [class.selected]="modelValue === option.value">
<div class="tb-image-cards-option-background"></div>
<div class="tb-image-cards-option-title">{{ option.name }}</div>
<div class="tb-image-cards-option-image-container">
<img class="tb-image-cards-option-image" src="{{ option.image }}"/>
</div>
</div>
</mat-grid-tile>
</mat-grid-list>
</ng-template>
</mat-expansion-panel>
</div>

116
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.scss

@ -0,0 +1,116 @@
/**
* 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.
*/
.tb-image-cards-select.tb-form-panel {
.tb-form-row {
transition: all .3s;
&.expanded {
padding: 11px 7px 11px 16px;
}
}
.tb-image-cards-value-field {
cursor: pointer;
user-select: none;
input {
cursor: pointer;
pointer-events: none;
}
}
.mat-expansion-panel {
&.tb-settings {
> .mat-expansion-panel-header {
height: auto;
.mat-content {
margin: 0;
}
.tb-form-row {
font-weight: normal;
font-size: 16px;
color: rgba(0, 0, 0, 0.87);
}
.mat-expansion-indicator {
display: none;
}
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0 16px 16px !important;
}
}
}
}
.tb-image-cards-option {
width: 100%;
height: 100%;
cursor: pointer;
padding: 8px 12px 12px 12px;
display: flex;
flex-direction: column;
gap: 8px;
align-items: start;
position: relative;
.tb-image-cards-option-background {
border-radius: 4px;
position: absolute;
top: 1px;
left: 1px;
right: 1px;
bottom: 1px;
background: rgba(0, 0, 0, 0.04);
}
&:before {
content: unset;
border-radius: 4px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.tb-image-cards-option-title {
z-index: 1;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
}
.tb-image-cards-option-image-container {
z-index: 1;
flex: 1;
width: 100%;
min-height: 0;
display: flex;
justify-content: center;
}
&.selected {
.tb-image-cards-option-background {
background: #305680;
opacity: 0.04;
}
&:before {
content: "";
border: 1px solid #305680;
opacity: 0.32;
}
.tb-image-cards-option-title {
font-size: 13px;
font-weight: 500;
color: #305680;
}
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save