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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because one or more lines are too long

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 { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { svgIcons } from '@shared/models/icon.models';
@Component({ @Component({
selector: 'tb-root', selector: 'tb-root',
@ -55,44 +56,14 @@ export class AppComponent implements OnInit {
} }
}); });
this.matIconRegistry.addSvgIconLiteral( for (const svgIcon of Object.keys(svgIcons)) {
'google-logo', this.matIconRegistry.addSvgIconLiteral(
this.domSanitizer.bypassSecurityTrustHtml( svgIcon,
'<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.domSanitizer.bypassSecurityTrustHtml(
) svgIcons[svgIcon]
); )
);
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>'
)
);
this.matIconRegistry.addSvgIcon('windows', this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/windows.svg')); this.matIconRegistry.addSvgIcon('windows', this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/windows.svg'));
this.matIconRegistry.addSvgIcon('macos', this.domSanitizer.bypassSecurityTrustResourceUrl('/assets/macos.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 './http/public-api';
export * from './local-storage/local-storage.service'; export * from './local-storage/local-storage.service';
export * from './services/public-api'; export * from './services/public-api';
export * from './ws/telemetry-websocket.service'; export * from './ws/public-api';
export * from './core.state'; export * from './core.state';
export * from './core.module'; export * from './core.module';
export * from './utils'; export * from './utils';

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

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

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

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

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

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

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

@ -0,0 +1,19 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
export * from './notification-websocket.service';
export * from './telemetry-websocket.service';
export * from './websocket.service';

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 ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.component'; import * as UnitInputComponent from '@shared/components/unit-input.component';
import * as MaterialIconsComponent from '@shared/components/material-icons.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 AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.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/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent, '@shared/components/unit-input.component': UnitInputComponent,
'@shared/components/material-icons.component': MaterialIconsComponent, '@shared/components/material-icons.component': MaterialIconsComponent,
'@shared/components/icon.component': TbIconComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent, '@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. limitations under the License.
--> -->
<form [formGroup]="widgetFormGroup" style="width: 1200px;"> <form class="tb-add-widget-dialog" [formGroup]="widgetFormGroup">
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2 translate>widget.add</h2> <h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span> <span fxFlex>: {{data.widgetInfo.widgetName}}</span>
@ -32,40 +32,37 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <div mat-dialog-content>
</mat-progress-bar> <tb-widget-config class="tb-absolute-fill"
<div mat-dialog-content style="padding: 0;"> [aliasController]="aliasController"
<fieldset [disabled]="isLoading$ | async" style="position: relative; height: 600px;"> [functionsOnly]="false"
<tb-widget-config [dashboard]="dashboard"
[aliasController]="aliasController" [widget]="widget"
[functionsOnly]="false" [widgetConfigMode]="widgetConfigMode"
[dashboard]="dashboard" [hideHeader]="widgetConfigMode === widgetConfigModes.basic"
[widget]="widget" isAdd
[widgetConfigMode]="widgetConfigMode" formControlName="widgetConfig">
[hideHeader]="widgetConfigMode === widgetConfigModes.basic" </tb-widget-config>
isAdd <tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
formControlName="widgetConfig"> [aliasController]="aliasController"
</tb-widget-config> [stateController]="stateController"
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill" [dashboardTimewindow]="dashboard.configuration.timewindow"
[aliasController]="aliasController" [widget]="widget"
[stateController]="stateController" [widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[dashboardTimewindow]="dashboard.configuration.timewindow" [previewWidth]="widgetConfig.typeParameters.previewWidth"
[widget]="widget" [previewHeight]="widgetConfig.typeParameters.previewHeight">
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"> <div class="tb-preview-panel-content">
<div class="tb-preview-panel-content"> <button mat-button
<button mat-button (click)="previewMode = false">
(click)="previewMode = false"> <mat-icon>chevron_left</mat-icon>
<mat-icon>chevron_left</mat-icon> {{ 'action.back' | translate }}
{{ 'action.back' | translate }} </button>
</button> </div>
</div> </tb-widget-preview>
</tb-widget-preview>
</fieldset>
</div> </div>
<div mat-dialog-actions fxLayoutAlign="space-between center"> <div mat-dialog-actions fxLayoutAlign="space-between center">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" (click)="cancel()"
cdkFocusInitial> cdkFocusInitial>
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
@ -74,14 +71,14 @@
<button *ngIf="!previewMode" <button *ngIf="!previewMode"
mat-button color="primary" mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid" [disabled]="widgetFormGroup.invalid"
(click)="previewMode = true" (click)="previewMode = true"
cdkFocusInitial> cdkFocusInitial>
{{ 'widget-config.preview' | translate }} {{ 'widget-config.preview' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
(click)="add()" (click)="add()"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid"> [disabled]="widgetFormGroup.invalid">
{{ 'action.add' | translate }} {{ 'action.add' | translate }}
</button> </button>
</div> </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. /// 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 { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; 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 { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models'; import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models';
@ -41,7 +41,8 @@ export interface AddWidgetDialogData {
selector: 'tb-add-widget-dialog', selector: 'tb-add-widget-dialog',
templateUrl: './add-widget-dialog.component.html', templateUrl: './add-widget-dialog.component.html',
providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/], providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/],
styleUrls: [] styleUrls: ['./add-widget-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
}) })
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget> export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher { implements OnInit, ErrorStateMatcher {

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

@ -60,7 +60,9 @@
[stateController]="stateController" [stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow" [dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget" [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> </tb-widget-preview>
</div> </div>
</fieldset> </fieldset>

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

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

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

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

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

@ -21,7 +21,7 @@ import {
EntityTableColumn, EntityTableColumn,
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { DebugEventType, Event, EventType, FilterEventBody } from '@shared/models/event.models'; import { DebugEventType, Event, EventBody, EventType, FilterEventBody } from '@shared/models/event.models';
import { TimePageLink } from '@shared/models/page/page-link'; import { TimePageLink } from '@shared/models/page/page-link';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@ -41,7 +41,7 @@ import {
} from '@home/components/event/event-content-dialog.component'; } from '@home/components/event/event-content-dialog.component';
import { isEqual, sortObjectKeys } from '@core/utils'; import { isEqual, sortObjectKeys } from '@core/utils';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ChangeDetectorRef, Injector, StaticProvider, ViewContainerRef } from '@angular/core'; import { ChangeDetectorRef, EventEmitter, Injector, StaticProvider, ViewContainerRef } from '@angular/core';
import { ComponentPortal } from '@angular/cdk/portal'; import { ComponentPortal } from '@angular/cdk/portal';
import { import {
EVENT_FILTER_PANEL_DATA, EVENT_FILTER_PANEL_DATA,
@ -61,6 +61,7 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
set eventType(eventType: EventType | DebugEventType) { set eventType(eventType: EventType | DebugEventType) {
if (this.eventTypeValue !== eventType) { if (this.eventTypeValue !== eventType) {
this.eventTypeValue = eventType; this.eventTypeValue = eventType;
this.updateCellAction();
this.updateColumns(true); this.updateColumns(true);
this.updateFilterColumns(); this.updateFilterColumns();
} }
@ -84,7 +85,9 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
private debugEventTypes: Array<DebugEventType> = null, private debugEventTypes: Array<DebugEventType> = null,
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef,
public testButtonLabel?: string,
private debugEventSelected?: EventEmitter<EventBody>) {
super(); super();
this.loadDataOnInit = false; this.loadDataOnInit = false;
this.tableTitle = ''; this.tableTitle = '';
@ -120,12 +123,12 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC};
this.updateColumns(); this.updateColumns();
this.updateCellAction();
this.updateFilterColumns(); this.updateFilterColumns();
this.headerActionDescriptors.push({ this.headerActionDescriptors.push({
name: this.translate.instant('event.clear-filter'), name: this.translate.instant('event.clear-filter'),
icon: 'mdi:filter-variant-remove', icon: 'mdi:filter-variant-remove',
isMdiIcon: true,
isEnabled: () => !isEqual(this.filterParams, {}), isEnabled: () => !isEqual(this.filterParams, {}),
onAction: ($event) => { onAction: ($event) => {
this.clearFiter($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 { showContent($event: MouseEvent, content: string, title: string, contentType: ContentType = null, sortKeys = false): void {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();

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

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

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

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

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

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

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

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

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

@ -26,7 +26,7 @@
line-height: 40px; line-height: 40px;
min-width: 200px; min-width: 200px;
border-bottom: none; border-bottom: none;
mat-icon { .mat-icon {
margin-right: 8px; margin-right: 8px;
margin-left: 0; 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"> <ng-container matColumnDef="icon">
<mat-header-cell *matHeaderCellDef style="width: 40px"> {{ 'widget-config.action-icon' | translate }} </mat-header-cell> <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-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> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="typeName"> <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 { import {
AlarmsTableBasicConfigComponent AlarmsTableBasicConfigComponent
} from '@home/components/widget/config/basic/alarm/alarms-table-basic-config.component'; } 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({ @NgModule({
declarations: [ declarations: [
@ -47,6 +50,7 @@ import {
TimeseriesTableBasicConfigComponent, TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent, FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent, AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
DataKeyRowComponent, DataKeyRowComponent,
DataKeysPanelComponent DataKeysPanelComponent
], ],
@ -63,6 +67,7 @@ import {
TimeseriesTableBasicConfigComponent, TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent, FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent, AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
DataKeyRowComponent, DataKeyRowComponent,
DataKeysPanelComponent DataKeysPanelComponent
] ]
@ -75,5 +80,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent, 'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent,
'tb-timeseries-table-basic-config': TimeseriesTableBasicConfigComponent, 'tb-timeseries-table-basic-config': TimeseriesTableBasicConfigComponent,
'tb-flot-basic-config': FlotBasicConfigComponent, '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 { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { import {
Datasource,
datasourcesHasAggregation, datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation, WidgetConfig, datasourcesHasOnlyComparisonAggregation,
WidgetConfig,
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component'; import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import { isUndefined } from '@core/utils'; import { isUndefined } from '@core/utils';
import { getLabel, setLabel } from '@home/components/widget/config/widget-settings.models';
@Component({ @Component({
selector: 'tb-simple-card-basic-config', selector: 'tb-simple-card-basic-config',
@ -67,7 +68,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.simpleCardWidgetConfigForm = this.fb.group({ this.simpleCardWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []], timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []], datasources: [configData.config.datasources, []],
label: [this.getDataKeyLabel(configData.config.datasources), []], label: [getLabel(configData.config.datasources), []],
labelPosition: [configData.config.settings?.labelPosition, []], labelPosition: [configData.config.settings?.labelPosition, []],
units: [configData.config.units, []], units: [configData.config.units, []],
decimals: [configData.config.decimals, []], decimals: [configData.config.decimals, []],
@ -83,7 +84,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow; this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow; this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources; 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.actions = config.actions;
this.widgetConfig.config.units = config.units; this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals; this.widgetConfig.config.decimals = config.decimals;
@ -95,25 +96,6 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
return this.widgetConfig; 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[] { private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = []; const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { 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> <mat-chip-listbox fxFlex>
<ng-container *ngFor="let actionSourceId of widgetActionSourceIds"> <ng-container *ngFor="let actionSourceId of widgetActionSourceIds">
<mat-chip *ngFor="let widgetAction of widgetActionsByActionSourceId(actionSourceId)"> <mat-chip *ngFor="let widgetAction of widgetActionsByActionSourceId(actionSourceId)">
<mat-icon matChipAvatar>{{ widgetAction.icon }}</mat-icon> <tb-icon matChipAvatar>{{ widgetAction.icon }}</tb-icon>
{{ widgetAction.name }} {{ widgetAction.name }}
</mat-chip> </mat-chip>
</ng-container> </ng-container>

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

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

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

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

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

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

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

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 <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <tb-icon>close</tb-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <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 <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <tb-icon>close</tb-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <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> </div>
<div *ngIf="editMode" fxLayout="row" class="tb-edit-buttons"> <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)="apply()"><tb-icon>check</tb-icon></button>
<button mat-icon-button (click)="cancelEdit()"><mat-icon>close</mat-icon></button> <button mat-icon-button (click)="cancelEdit()"><tb-icon>close</tb-icon></button>
</div> </div>
</div> </div>
<ng-template #docLinkTemplate> <ng-template #docLinkTemplate>
@ -47,7 +47,7 @@
<div fxFlex class="tb-link"> <div fxFlex class="tb-link">
<div class="tb-link-container"> <div class="tb-link-container">
<div class="tb-link-icon-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>
<div class="tb-link-text">{{ docLink.name }}</div> <div class="tb-link-text">{{ docLink.name }}</div>
</div> </div>
@ -57,13 +57,13 @@
matTooltip="{{ 'action.edit' | translate }}" matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="switchToEditMode()"> (click)="switchToEditMode()">
<mat-icon>edit</mat-icon> <tb-icon>edit</tb-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}" matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="delete()"> (click)="delete()">
<mat-icon>delete</mat-icon> <tb-icon>delete</tb-icon>
</button> </button>
</div> </div>
</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" matTooltipPosition="above"
mat-icon-button mat-icon-button
(click)="edit()"> (click)="edit()">
<mat-icon>edit</mat-icon> <tb-icon>edit</tb-icon>
</button> </button>
</div> </div>
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize"> <mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
@ -32,7 +32,7 @@
[href]="docLink.link" target="_blank"> [href]="docLink.link" target="_blank">
<div class="tb-link-container"> <div class="tb-link-container">
<div class="tb-link-icon-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>
<div class="tb-link-text">{{ docLink.name }}</div> <div class="tb-link-text">{{ docLink.name }}</div>
</div> </div>
@ -43,7 +43,7 @@
matTooltip="{{ 'widgets.documentation.add-link' | translate }}" matTooltip="{{ 'widgets.documentation.add-link' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="addLink()"> (click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon> <tb-icon class="tb-add-icon">add</tb-icon>
</div> </div>
</mat-grid-tile> </mat-grid-tile>
</mat-grid-list> </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 <button mat-icon-button
(click)="close()" (click)="close()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <tb-icon class="material-icons">close</tb-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <div mat-dialog-content>
@ -60,7 +60,7 @@
matTooltip="{{ 'action.drag' | translate }}" matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
class="tb-drag-handle"> class="tb-drag-handle">
<mat-icon>drag_indicator</mat-icon> <tb-icon>drag_indicator</tb-icon>
</div> </div>
</ng-template> </ng-template>
</div> </div>
@ -71,7 +71,7 @@
matTooltip="{{ (mode === 'docs' ? 'widgets.documentation.add-link' : 'widgets.quick-links.add-link') | translate }}" matTooltip="{{ (mode === 'docs' ? 'widgets.documentation.add-link' : 'widgets.quick-links.add-link') | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="addLink()"> (click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon> <tb-icon class="tb-add-icon">add</tb-icon>
</div> </div>
<ng-container *ngIf="addMode"> <ng-container *ngIf="addMode">
<ng-container [ngSwitch]="mode"> <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; letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76); color: rgba(0, 0, 0, 0.76);
.mat-icon { .mat-icon {
vertical-align: bottom;
margin-right: 10px; margin-right: 10px;
color: rgba(0, 0, 0, 0.54); color: rgba(0, 0, 0, 0.54);
font-size: 20px; 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()" (focusin)="onFocus()"
required required
[matAutocomplete]="linkAutocomplete"> [matAutocomplete]="linkAutocomplete">
<mat-icon matPrefix *ngIf="quickLink && !quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon> <tb-icon matPrefix *ngIf="quickLink" color="primary">{{ quickLink.icon }}</tb-icon>
<mat-icon matPrefix *ngIf="quickLink && quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon>
<button *ngIf="editQuickLinkFormGroup.get('link').value && !disabled" <button *ngIf="editQuickLinkFormGroup.get('link').value && !disabled"
type="button" type="button"
matSuffix mat-icon-button aria-label="Clear" matSuffix mat-icon-button aria-label="Clear"
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <tb-icon>close</tb-icon>
</button> </button>
<mat-autocomplete <mat-autocomplete
class="tb-autocomplete tb-quick-links" class="tb-autocomplete tb-quick-links"
#linkAutocomplete="matAutocomplete" #linkAutocomplete="matAutocomplete"
[displayWith]="displayLinkFn"> [displayWith]="displayLinkFn">
<mat-option *ngFor="let link of filteredLinks | async" [value]="link"> <mat-option *ngFor="let link of filteredLinks | async" [value]="link">
<mat-icon *ngIf="!link.isMdiIcon">{{ link.icon }}</mat-icon> <tb-icon>{{ link.icon }}</tb-icon>
<mat-icon *ngIf="link.isMdiIcon" [svgIcon]="link.icon"></mat-icon>
<span [innerHTML]="link.name | highlight:searchText"></span> <span [innerHTML]="link.name | highlight:searchText"></span>
</mat-option> </mat-option>
<mat-option *ngIf="!(filteredLinks | async)?.length" [value]="null"> <mat-option *ngIf="!(filteredLinks | async)?.length" [value]="null">
@ -58,8 +56,8 @@
</div> </div>
</div> </div>
<div *ngIf="editMode" fxLayout="row" class="tb-edit-buttons"> <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)="apply()"><tb-icon>check</tb-icon></button>
<button mat-icon-button (click)="cancelEdit()"><mat-icon>close</mat-icon></button> <button mat-icon-button (click)="cancelEdit()"><tb-icon>close</tb-icon></button>
</div> </div>
</div> </div>
<ng-template #quickLinkTemplate> <ng-template #quickLinkTemplate>
@ -67,8 +65,7 @@
<div fxFlex class="tb-link"> <div fxFlex class="tb-link">
<div class="tb-link-container"> <div class="tb-link-container">
<div class="tb-link-icon-container"> <div class="tb-link-icon-container">
<mat-icon *ngIf="!quickLink?.isMdiIcon" color="primary">{{ quickLink?.icon }}</mat-icon> <tb-icon color="primary">{{ quickLink?.icon }}</tb-icon>
<mat-icon *ngIf="quickLink?.isMdiIcon" color="primary" [svgIcon]="quickLink?.icon"></mat-icon>
</div> </div>
<div class="tb-link-text">{{ displayLinkFn(quickLink) }}</div> <div class="tb-link-text">{{ displayLinkFn(quickLink) }}</div>
</div> </div>
@ -78,13 +75,13 @@
matTooltip="{{ 'action.edit' | translate }}" matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="switchToEditMode()"> (click)="switchToEditMode()">
<mat-icon>edit</mat-icon> <tb-icon>edit</tb-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}" matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="delete()"> (click)="delete()">
<mat-icon>delete</mat-icon> <tb-icon>delete</tb-icon>
</button> </button>
</div> </div>
</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" matTooltipPosition="above"
mat-icon-button mat-icon-button
(click)="edit()"> (click)="edit()">
<mat-icon>edit</mat-icon> <tb-icon>edit</tb-icon>
</button> </button>
</div> </div>
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize"> <mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
@ -32,8 +32,7 @@
[routerLink]="quickLink.path"> [routerLink]="quickLink.path">
<div class="tb-link-container"> <div class="tb-link-container">
<div class="tb-link-icon-container"> <div class="tb-link-icon-container">
<mat-icon *ngIf="!quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon> <tb-icon color="primary">{{ quickLink.icon }}</tb-icon>
<mat-icon *ngIf="quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon>
</div> </div>
<div class="tb-link-text">{{ (quickLink.fullName || quickLink.name) | translate }}</div> <div class="tb-link-text">{{ (quickLink.fullName || quickLink.name) | translate }}</div>
</div> </div>
@ -44,7 +43,7 @@
matTooltip="{{ 'widgets.quick-links.add-link' | translate }}" matTooltip="{{ 'widgets.quick-links.add-link' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="addLink()"> (click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon> <tb-icon class="tb-add-icon">add</tb-icon>
</div> </div>
</mat-grid-tile> </mat-grid-tile>
</mat-grid-list> </mat-grid-list>

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

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

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)" (focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)"> (blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix> <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> <ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon"> <img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template> </ng-template>
@ -63,7 +63,7 @@
(focus)="key.isFocused = true; focusInputElement($event)" (focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)"> (blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix> <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> <ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon"> <img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template> </ng-template>
@ -99,7 +99,7 @@
(blur)="key.isFocused = false; inputChanged(source, key)" (blur)="key.isFocused = false; inputChanged(source, key)"
/> />
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix> <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> <ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon"> <img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template> </ng-template>
@ -123,7 +123,7 @@
[labelPosition]="key.settings.slideToggleLabelPosition" [labelPosition]="key.settings.slideToggleLabelPosition"
(change)="inputChanged(source, key)"> (change)="inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon"> <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> <ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon"> <img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template> </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)"> <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> <span>{{translatedName}}</span>
</a> </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-list rowHeight="170px" [cols]="sectionPlaces(section).length">
<mat-grid-tile *ngFor="let place of sectionPlaces(section)"> <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)"> <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> <tb-icon matButtonIcon class="tb-mat-96">{{place.icon}}</tb-icon>
<mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
<span translate>{{place.name}}</span> <span translate>{{place.name}}</span>
</a> </a>
</mat-grid-tile> </mat-grid-tile>

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

@ -55,7 +55,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
mat-icon { .mat-icon {
margin: auto; margin: auto;
} }
span.mdc-button__label { 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 }}" matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="onActionButtonClick($event, column, actionDescriptor)"> (click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{ actionDescriptor.icon }}</mat-icon> <tb-icon>{{ actionDescriptor.icon }}</tb-icon>
</button> </button>
</ng-container> </ng-container>
</div> </div>
@ -88,14 +88,14 @@
<button mat-icon-button <button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();" (click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu"> [matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon> <tb-icon>more_vert</tb-icon>
</button> </button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> <mat-menu #cellActionsMenu="matMenu" xPosition="before">
<ng-container *ngFor="let actionDescriptor of actionCellButtonAction"> <ng-container *ngFor="let actionDescriptor of actionCellButtonAction">
<button mat-menu-item *ngIf="actionDescriptor.icon" <button mat-menu-item *ngIf="actionDescriptor.icon"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onActionButtonClick($event, column, actionDescriptor)"> (click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon> <tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
<span>{{ actionDescriptor.displayName }}</span> <span>{{ actionDescriptor.displayName }}</span>
</button> </button>
</ng-container> </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"> <mat-slide-toggle class="mat-slide" formControlName="enableSelection">
{{ 'widgets.table.enable-alarms-selection' | translate }} {{ 'widgets.table.enable-alarms-selection' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<div class="tb-form-row"> <div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide" formControlName="enableStickyAction"> <mat-slide-toggle class="mat-slide" formControlName="enableStickyAction">
{{ 'widgets.table.enable-sticky-action' | translate }} {{ 'widgets.table.enable-sticky-action' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showCellActionsMenu">
{{ 'widgets.table.show-cell-actions-menu-mobile' | translate }}
</mat-slide-toggle>
</div> </div>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>widgets.table.table-buttons</div> <div class="tb-form-panel-title" translate>widgets.table.table-buttons</div>

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

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

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

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

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

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

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

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

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

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

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