Browse Source

Merge with master

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
f2c3a9f6af
  1. 2
      application/src/main/data/json/system/widget_types/unread_notifications.json
  2. 5
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  3. 20
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  4. 2
      pom.xml
  5. 1
      ui-ngx/src/app/core/auth/auth.service.ts
  6. 3
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  7. 4
      ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts
  8. 4
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts
  9. 6
      ui-ngx/src/app/modules/home/components/profile/device-profile.component.html
  10. 11
      ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts
  11. 10
      ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts
  12. 62
      ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts
  13. 1
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html
  14. 10
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts
  15. 5
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html
  16. 17
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts
  17. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html
  18. 13
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts
  19. 11
      ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts
  20. 11
      ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts
  21. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.html
  22. 40
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts
  23. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html
  24. 24
      ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts
  25. 1
      ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html
  26. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.scss
  27. 8
      ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts
  28. 2
      ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.models.ts
  29. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html
  30. 30
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts
  31. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html
  32. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html
  33. 46
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  34. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html
  35. 24
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss
  36. 116
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts
  37. 180
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  38. 44
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss
  39. 208
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  40. 131
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html
  41. 90
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss
  42. 228
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts
  43. 62
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html
  44. 163
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts
  45. 263
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html
  46. 27
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss
  47. 287
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  48. 362
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  49. 21
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss
  50. 243
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  51. 121
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html
  52. 24
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss
  53. 236
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts
  54. 41
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts
  55. 125
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html
  56. 8
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.scss
  57. 30
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts
  58. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html
  59. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts
  60. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts
  61. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts
  62. 127
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html
  63. 8
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html
  64. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts
  65. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  66. 34
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html
  67. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  68. 37
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  69. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts
  70. 7
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  71. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  72. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html
  73. 32
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  74. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  75. 18
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html
  76. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss
  77. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts
  78. 315
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  79. 0
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts
  80. 42
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe.ts
  81. 5
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html
  82. 18
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts
  83. 215
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts
  84. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.html
  85. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.ts
  86. 8
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html
  87. 32
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts
  88. 52
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  89. 4
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
  90. 44
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  91. 6
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html
  92. 17
      ui-ngx/src/app/modules/home/pipes/public-api.ts
  93. 2
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html
  94. 8
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss
  95. 48
      ui-ngx/src/app/shared/components/table/table-datasource.abstract.ts
  96. 20
      ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts
  97. 116
      ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts
  98. 2
      ui-ngx/src/app/shared/pipe/key-value-not-empty.pipe.ts
  99. 2
      ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md
  100. 77
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
application/src/main/data/json/system/widget_types/unread_notifications.json

@ -17,7 +17,7 @@
"settingsDirective": "tb-unread-notification-widget-settings", "settingsDirective": "tb-unread-notification-widget-settings",
"hasBasicMode": true, "hasBasicMode": true,
"basicModeDirective": "tb-unread-notification-basic-config", "basicModeDirective": "tb-unread-notification-basic-config",
"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\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{\"cardHtml\":\"<div class='card'>HTML code here</div>\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\",\"maxNotificationDisplay\":6,\"showCounter\":true,\"counterValueFont\":{\"family\":\"Roboto\",\"size\":14,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"\"},\"counterValueColor\":\"#fff\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"enableViewAll\":true,\"enableFilter\":true,\"enableMarkAsRead\":true},\"title\":\"Unread notification\",\"dropShadow\":true,\"configMode\":\"basic\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"#000000\",\"showTitleIcon\":true,\"iconSize\":\"22px\",\"titleIcon\":\"notifications\",\"iconColor\":\"#000000\",\"actions\":{},\"enableFullscreen\":false,\"borderRadius\":\"4px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}" "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\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{\"cardHtml\":\"<div class='card'>HTML code here</div>\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\",\"maxNotificationDisplay\":6,\"showCounter\":true,\"counterValueFont\":{\"family\":\"Roboto\",\"size\":12,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"600\",\"lineHeight\":\"\"},\"counterValueColor\":\"#fff\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"enableViewAll\":true,\"enableFilter\":true,\"enableMarkAsRead\":true},\"title\":\"Unread notification\",\"dropShadow\":true,\"configMode\":\"basic\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleColor\":\"#000000\",\"showTitleIcon\":true,\"iconSize\":\"22px\",\"titleIcon\":\"notifications\",\"iconColor\":\"#000000\",\"actions\":{},\"enableFullscreen\":false,\"borderRadius\":\"4px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}"
}, },
"tags": null "tags": null
} }

5
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -598,6 +598,11 @@ public class TbUtils {
if (len % 2 > 0) { if (len % 2 > 0) {
throw new IllegalArgumentException("Hex string must be even-length."); throw new IllegalArgumentException("Hex string must be even-length.");
} }
int radix = isHexadecimal(value);
if (radix != HEX_RADIX) {
throw new NumberFormatException("Value: \"" + value + "\" is not numeric or hexDecimal format!");
}
ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx); ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx);
for (int i = 0; i < hex.length(); i += 2) { for (int i = 0; i < hex.length(); i += 2) {
// Extract two characters from the hex string // Extract two characters from the hex string

20
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -668,6 +668,26 @@ public class TbUtilsTest {
Assertions.assertEquals(expectedBe, actualBe); Assertions.assertEquals(expectedBe, actualBe);
} }
@Test
public void hexToBytes_Test() {
String input = "0x01752B0367FA000500010488FFFFFFFFFFFFFFFF33";
byte[] expected = {1, 117, 43, 3, 103, -6, 0, 5, 0, 1, 4, -120, -1, -1, -1, -1, -1, -1, -1, -1, 51};
List<Byte> actual = TbUtils.hexToBytes(ctx, input);
Assertions.assertEquals(toList(expected), actual);
try {
input = "0x01752B0367FA000500010488FFFFFFFFFFFFFFFF3";
actual = TbUtils.hexToBytes(ctx, input);
} catch (IllegalArgumentException e) {
Assertions.assertTrue(e.getMessage().contains("Hex string must be even-length."));
}
try {
input = "0x01752B0367KA000500010488FFFFFFFFFFFFFFFF33";
actual = TbUtils.hexToBytes(ctx, input);
} catch (NumberFormatException e) {
Assertions.assertTrue(e.getMessage().contains("Value: \"" + input + "\" is not numeric or hexDecimal format!"));
}
}
@Test @Test
public void floatToHex_Test() { public void floatToHex_Test() {
Float value = 123456789.00f; Float value = 123456789.00f;

2
pom.xml

@ -83,7 +83,7 @@
<zookeeper.version>3.9.2</zookeeper.version> <zookeeper.version>3.9.2</zookeeper.version>
<protobuf.version>3.25.3</protobuf.version> <!-- A Major v4 does not support by the pubsub yet--> <protobuf.version>3.25.3</protobuf.version> <!-- A Major v4 does not support by the pubsub yet-->
<grpc.version>1.63.0</grpc.version> <grpc.version>1.63.0</grpc.version>
<tbel.version>1.2.1</tbel.version> <tbel.version>1.2.2</tbel.version>
<lombok.version>1.18.32</lombok.version> <lombok.version>1.18.32</lombok.version>
<paho.client.version>1.2.5</paho.client.version> <paho.client.version>1.2.5</paho.client.version>
<paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version> <paho.mqttv5.client.version>1.2.5</paho.mqttv5.client.version>

1
ui-ngx/src/app/core/auth/auth.service.ts

@ -624,6 +624,7 @@ export class AuthService {
private userForceFullscreen(authPayload: AuthPayload): boolean { private userForceFullscreen(authPayload: AuthPayload): boolean {
return (authPayload.authUser && authPayload.authUser.isPublic) || return (authPayload.authUser && authPayload.authUser.isPublic) ||
(authPayload.userDetails && authPayload.userDetails.additionalInfo && (authPayload.userDetails && authPayload.userDetails.additionalInfo &&
authPayload.userDetails.additionalInfo.defaultDashboardId &&
authPayload.userDetails.additionalInfo.defaultDashboardFullscreen && authPayload.userDetails.additionalInfo.defaultDashboardFullscreen &&
authPayload.userDetails.additionalInfo.defaultDashboardFullscreen === true); authPayload.userDetails.additionalInfo.defaultDashboardFullscreen === true);
} }

3
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -108,8 +108,7 @@
</mat-form-field> </mat-form-field>
<tb-device-profile-transport-configuration <tb-device-profile-transport-configuration
formControlName="transportConfiguration" formControlName="transportConfiguration"
isAdd="true" isAdd="true">
required>
</tb-device-profile-transport-configuration> </tb-device-profile-transport-configuration>
</form> </form>
</mat-step> </mat-step>

4
ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts

@ -327,7 +327,7 @@ export class AssetProfileAutocompleteComponent implements ControlValueAccessor,
} }
createAssetProfile($event: Event, profileName: string) { createAssetProfile($event: Event, profileName: string) {
$event.preventDefault(); $event.stopPropagation();
const assetProfile: AssetProfile = { const assetProfile: AssetProfile = {
name: profileName name: profileName
} as AssetProfile; } as AssetProfile;
@ -337,7 +337,7 @@ export class AssetProfileAutocompleteComponent implements ControlValueAccessor,
} }
editAssetProfile($event: Event) { editAssetProfile($event: Event) {
$event.preventDefault(); $event.stopPropagation();
this.assetProfileService.getAssetProfile(this.modelValue.id).subscribe( this.assetProfileService.getAssetProfile(this.modelValue.id).subscribe(
(assetProfile) => { (assetProfile) => {
this.openAssetProfileDialog(assetProfile, false); this.openAssetProfileDialog(assetProfile, false);

4
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts

@ -349,7 +349,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
} }
createDeviceProfile($event: Event, profileName: string) { createDeviceProfile($event: Event, profileName: string) {
$event.preventDefault(); $event.stopPropagation();
const deviceProfile: DeviceProfile = { const deviceProfile: DeviceProfile = {
name: profileName, name: profileName,
transportType: this.transportType transportType: this.transportType
@ -360,7 +360,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
} }
editDeviceProfile($event: Event) { editDeviceProfile($event: Event) {
$event.preventDefault(); $event.stopPropagation();
this.deviceProfileService.getDeviceProfile(this.modelValue.id).subscribe( this.deviceProfileService.getDeviceProfile(this.modelValue.id).subscribe(
(deviceProfile) => { (deviceProfile) => {
this.openDeviceProfileDialog(deviceProfile, false); this.openDeviceProfileDialog(deviceProfile, false);

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

@ -137,8 +137,7 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-device-profile-configuration <tb-device-profile-configuration
formControlName="configuration" formControlName="configuration">
required>
</tb-device-profile-configuration> </tb-device-profile-configuration>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
@ -151,8 +150,7 @@
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-device-profile-transport-configuration <tb-device-profile-transport-configuration
formControlName="transportConfiguration" formControlName="transportConfiguration"
[isAdd] = "isTransportTypeChanged" [isAdd] = "isTransportTypeChanged">
required>
</tb-device-profile-transport-configuration> </tb-device-profile-transport-configuration>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>

11
ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.ts

@ -28,7 +28,6 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { import {
CoapDeviceProfileTransportConfiguration, CoapDeviceProfileTransportConfiguration,
coapDeviceTypeTranslationMap, coapDeviceTypeTranslationMap,
@ -74,7 +73,6 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control
coapTransportConfigurationFormGroup: UntypedFormGroup; coapTransportConfigurationFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private requiredValue: boolean;
private transportPayloadTypeConfiguration = this.fb.group({ private transportPayloadTypeConfiguration = this.fb.group({
transportPayloadType: [TransportPayloadType.JSON, Validators.required], transportPayloadType: [TransportPayloadType.JSON, Validators.required],
@ -84,15 +82,6 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control
deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required] deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required]
}); });
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input() @Input()
disabled: boolean; disabled: boolean;

10
ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-configuration.component.ts

@ -18,7 +18,6 @@ import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { import {
DefaultDeviceProfileConfiguration, DefaultDeviceProfileConfiguration,
DeviceProfileConfiguration, DeviceProfileConfiguration,
@ -39,15 +38,6 @@ export class DefaultDeviceProfileConfigurationComponent implements ControlValueA
defaultDeviceProfileConfigurationFormGroup: UntypedFormGroup; defaultDeviceProfileConfigurationFormGroup: UntypedFormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input() @Input()
disabled: boolean; disabled: boolean;

62
ui-ngx/src/app/modules/home/components/profile/device/default-device-profile-transport-configuration.component.ts

@ -15,39 +15,42 @@
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import {
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { DefaultDeviceProfileTransportConfiguration, DeviceTransportType } from '@shared/models/device.models';
import {
DefaultDeviceProfileTransportConfiguration,
DeviceProfileTransportConfiguration,
DeviceTransportType
} from '@shared/models/device.models';
@Component({ @Component({
selector: 'tb-default-device-profile-transport-configuration', selector: 'tb-default-device-profile-transport-configuration',
templateUrl: './default-device-profile-transport-configuration.component.html', templateUrl: './default-device-profile-transport-configuration.component.html',
styleUrls: [], styleUrls: [],
providers: [{ providers: [
provide: NG_VALUE_ACCESSOR, {
useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent), provide: NG_VALUE_ACCESSOR,
multi: true useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent),
}] multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent),
multi: true
}
]
}) })
export class DefaultDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit { export class DefaultDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator {
defaultDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup; defaultDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -86,12 +89,17 @@ export class DefaultDeviceProfileTransportConfigurationComponent implements Cont
this.defaultDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); this.defaultDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false});
} }
private updateModel() { validate(c: UntypedFormControl): ValidationErrors | null {
let configuration: DeviceProfileTransportConfiguration = null; return (this.defaultDeviceProfileTransportConfigurationFormGroup.valid) ? null : {
if (this.defaultDeviceProfileTransportConfigurationFormGroup.valid) { configuration: {
configuration = this.defaultDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; valid: false
configuration.type = DeviceTransportType.DEFAULT; }
} }
}
private updateModel() {
const configuration = this.defaultDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
configuration.type = DeviceTransportType.DEFAULT;
this.propagateChange(configuration); this.propagateChange(configuration);
} }
} }

1
ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html

@ -19,7 +19,6 @@
<div [ngSwitch]="type"> <div [ngSwitch]="type">
<ng-template [ngSwitchCase]="deviceProfileType.DEFAULT"> <ng-template [ngSwitchCase]="deviceProfileType.DEFAULT">
<tb-default-device-profile-configuration <tb-default-device-profile-configuration
[required]="required"
formControlName="configuration"> formControlName="configuration">
</tb-default-device-profile-configuration> </tb-default-device-profile-configuration>
</ng-template> </ng-template>

10
ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.ts

@ -18,7 +18,6 @@ import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileConfiguration, DeviceProfileType } from '@shared/models/device.models'; import { DeviceProfileConfiguration, DeviceProfileType } from '@shared/models/device.models';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -42,15 +41,6 @@ export class DeviceProfileConfigurationComponent implements ControlValueAccessor
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input() @Input()
disabled: boolean; disabled: boolean;

5
ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html

@ -19,32 +19,27 @@
<div [ngSwitch]="transportType"> <div [ngSwitch]="transportType">
<ng-template [ngSwitchCase]="deviceTransportType.DEFAULT"> <ng-template [ngSwitchCase]="deviceTransportType.DEFAULT">
<tb-default-device-profile-transport-configuration <tb-default-device-profile-transport-configuration
[required]="required"
formControlName="configuration"> formControlName="configuration">
</tb-default-device-profile-transport-configuration> </tb-default-device-profile-transport-configuration>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.MQTT"> <ng-template [ngSwitchCase]="deviceTransportType.MQTT">
<tb-mqtt-device-profile-transport-configuration <tb-mqtt-device-profile-transport-configuration
[required]="required"
formControlName="configuration"> formControlName="configuration">
</tb-mqtt-device-profile-transport-configuration> </tb-mqtt-device-profile-transport-configuration>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.COAP"> <ng-template [ngSwitchCase]="deviceTransportType.COAP">
<tb-coap-device-profile-transport-configuration <tb-coap-device-profile-transport-configuration
[required]="required"
formControlName="configuration"> formControlName="configuration">
</tb-coap-device-profile-transport-configuration> </tb-coap-device-profile-transport-configuration>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.LWM2M"> <ng-template [ngSwitchCase]="deviceTransportType.LWM2M">
<tb-profile-lwm2m-device-transport-configuration <tb-profile-lwm2m-device-transport-configuration
[isAdd]="isAdd" [isAdd]="isAdd"
[required]="required"
formControlName="configuration"> formControlName="configuration">
</tb-profile-lwm2m-device-transport-configuration> </tb-profile-lwm2m-device-transport-configuration>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.SNMP"> <ng-template [ngSwitchCase]="deviceTransportType.SNMP">
<tb-snmp-device-profile-transport-configuration <tb-snmp-device-profile-transport-configuration
[required]="required"
formControlName="configuration"> formControlName="configuration">
</tb-snmp-device-profile-transport-configuration> </tb-snmp-device-profile-transport-configuration>
</ng-template> </ng-template>

17
ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts

@ -23,12 +23,10 @@ import {
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
ValidationErrors, ValidationErrors,
Validator, Validator
Validators
} from '@angular/forms'; } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileTransportConfiguration, DeviceTransportType } from '@shared/models/device.models'; import { DeviceProfileTransportConfiguration, DeviceTransportType } from '@shared/models/device.models';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
@ -55,15 +53,6 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
deviceProfileTransportConfigurationFormGroup: UntypedFormGroup; deviceProfileTransportConfigurationFormGroup: UntypedFormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -87,7 +76,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
ngOnInit() { ngOnInit() {
this.deviceProfileTransportConfigurationFormGroup = this.fb.group({ this.deviceProfileTransportConfigurationFormGroup = this.fb.group({
configuration: [null, Validators.required] configuration: [null]
}); });
this.deviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => { this.deviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
@ -110,7 +99,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
delete configuration.type; delete configuration.type;
} }
setTimeout(() => { setTimeout(() => {
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration});
}, 0); }, 0);
} }

2
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.html

@ -22,7 +22,7 @@
<div fxLayout="row" fxFlex fxLayoutAlign="start center"> <div fxLayout="row" fxFlex fxLayoutAlign="start center">
<div style="min-width:150px">{{ (serverFormGroup.get('bootstrapServerIs').value ? <div style="min-width:150px">{{ (serverFormGroup.get('bootstrapServerIs').value ?
'device-profile.lwm2m.bootstrap-server' : 'device-profile.lwm2m.lwm2m-server') | translate }}</div> 'device-profile.lwm2m.bootstrap-server' : 'device-profile.lwm2m.lwm2m-server') | translate }}</div>
<div *ngIf="!serverPanel.expanded" fxLayout="row" style="font-size:14px"> <div *ngIf="!serverPanel.expanded" fxLayout="row" style="font-size:14px" class="no-wrap">
<div style="margin-left:32px">{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }} <div style="margin-left:32px">{{ ('device-profile.lwm2m.short-id' | translate) + ': ' }}
<span style="font-style: italic">{{ serverFormGroup.get('shortServerId').value }}</span> <span style="font-style: italic">{{ serverFormGroup.get('shortServerId').value }}</span>
</div> </div>

13
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts

@ -25,7 +25,6 @@ import {
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { import {
ATTRIBUTE, ATTRIBUTE,
DEFAULT_EDRX_CYCLE, DEFAULT_EDRX_CYCLE,
@ -47,7 +46,7 @@ import {
ObjectIDVerTranslationMap ObjectIDVerTranslationMap
} from './lwm2m-profile-config.models'; } from './lwm2m-profile-config.models';
import { DeviceProfileService } from '@core/http/device-profile.service'; import { DeviceProfileService } from '@core/http/device-profile.service';
import { deepClone, isDefinedAndNotNull, isEmpty, isUndefined } from '@core/utils'; import { deepClone, isDefinedAndNotNull, isEmpty } from '@core/utils';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import _ from 'lodash'; import _ from 'lodash';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -77,7 +76,6 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
public disabled = false; public disabled = false;
public isTransportWasRunWithBootstrap = true; public isTransportWasRunWithBootstrap = true;
public isBootstrapServerUpdateEnable: boolean; public isBootstrapServerUpdateEnable: boolean;
private requiredValue: boolean;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
lwm2mDeviceProfileFormGroup: UntypedFormGroup; lwm2mDeviceProfileFormGroup: UntypedFormGroup;
@ -88,15 +86,6 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
sortFunction: (key: string, value: object) => object; sortFunction: (key: string, value: object) => object;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input() @Input()
isAdd: boolean; isAdd: boolean;

11
ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.ts

@ -29,7 +29,6 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state'; import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { import {
defaultAttributesSchema, defaultAttributesSchema,
defaultRpcRequestSchema, defaultRpcRequestSchema,
@ -70,16 +69,6 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control
mqttDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup; mqttDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input() @Input()
disabled: boolean; disabled: boolean;

11
ui-ngx/src/app/modules/home/components/profile/device/snmp/snmp-device-profile-transport-configuration.component.ts

@ -25,7 +25,6 @@ import {
Validator, Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { import {
DeviceTransportType, DeviceTransportType,
SnmpDeviceProfileTransportConfiguration SnmpDeviceProfileTransportConfiguration
@ -63,16 +62,6 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements OnInit,
snmpDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup; snmpDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input() @Input()
disabled: boolean; disabled: boolean;

2
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.html

@ -77,7 +77,7 @@
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="counterValueFont" <tb-font-settings formControlName="counterValueFont"
clearButton clearButton
[previewText]="unreadNotificationWidgetConfigForm.get('maxNotificationDisplay').value.toString()" [previewText]="countPreviewFn"
[initialPreviewStyle]="widgetConfig.config.titleStyle"> [initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings> </tb-font-settings>
<tb-color-input asBoxInput <tb-color-input asBoxInput

40
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/unread-notification-basic-config.component.ts

@ -38,6 +38,8 @@ export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigCom
unreadNotificationWidgetConfigForm: UntypedFormGroup; unreadNotificationWidgetConfigForm: UntypedFormGroup;
countPreviewFn = this._countPreviewFn.bind(this);
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent, protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder) {
@ -87,16 +89,6 @@ export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigCom
const showTitle: boolean = this.unreadNotificationWidgetConfigForm.get('showTitle').value; const showTitle: boolean = this.unreadNotificationWidgetConfigForm.get('showTitle').value;
const showIcon: boolean = this.unreadNotificationWidgetConfigForm.get('showIcon').value; const showIcon: boolean = this.unreadNotificationWidgetConfigForm.get('showIcon').value;
if (showTitle) {
this.unreadNotificationWidgetConfigForm.get('title').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleFont').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleColor').enable({emitEvent});
} else {
this.unreadNotificationWidgetConfigForm.get('title').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleFont').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleColor').disable({emitEvent});
}
if (showIcon) { if (showIcon) {
this.unreadNotificationWidgetConfigForm.get('iconSize').enable({emitEvent}); this.unreadNotificationWidgetConfigForm.get('iconSize').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').enable({emitEvent}); this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').enable({emitEvent});
@ -118,6 +110,30 @@ export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigCom
this.unreadNotificationWidgetConfigForm.get('counterValueColor').disable({emitEvent}); this.unreadNotificationWidgetConfigForm.get('counterValueColor').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterColor').disable({emitEvent}); this.unreadNotificationWidgetConfigForm.get('counterColor').disable({emitEvent});
} }
if (showTitle) {
this.unreadNotificationWidgetConfigForm.get('title').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleFont').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleColor').enable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('showCounter').enable({emitEvent: false});
this.unreadNotificationWidgetConfigForm.get('showIcon').enable({emitEvent: false});
} else {
this.unreadNotificationWidgetConfigForm.get('title').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleFont').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('titleColor').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.unreadNotificationWidgetConfigForm.get('iconSize').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconSizeUnit').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('icon').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('iconColor').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('showCounter').disable({emitEvent: false});
this.unreadNotificationWidgetConfigForm.get('counterValueFont').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterValueColor').disable({emitEvent});
this.unreadNotificationWidgetConfigForm.get('counterColor').disable({emitEvent});
}
} }
protected prepareOutputConfig(config: any): WidgetConfigComponentData { protected prepareOutputConfig(config: any): WidgetConfigComponentData {
@ -174,4 +190,8 @@ export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigCom
config.enableFullscreen = buttons.includes('fullscreen'); config.enableFullscreen = buttons.includes('fullscreen');
} }
private _countPreviewFn(): string {
return this.unreadNotificationWidgetConfigForm.get('maxNotificationDisplay').value?.toString() || '6';
}
} }

8
ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.html

@ -76,6 +76,14 @@
<div class="tb-small-label" translate>widgets.gauge.max-value-short</div> <div class="tb-small-label" translate>widgets.gauge.max-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="maxValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widgets.gauge.min-max-validation-text' | translate"
*ngIf="simpleGaugeWidgetConfigForm.get('maxValue').hasError('maxValue')"
class="material-icons tb-suffix-show-always tb-error">
warning
</mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>

24
ui-ngx/src/app/modules/home/components/widget/config/basic/gauge/digital-simple-gauge-basic-config.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; 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';
@ -109,7 +109,7 @@ export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigCom
showMinMax: [settings.showMinMax, []], showMinMax: [settings.showMinMax, []],
minValue: [settings.minValue, []], minValue: [settings.minValue, []],
maxValue: [settings.maxValue, []], maxValue: [settings.maxValue, [this.maxValueValidation()]],
minMaxFont: [settings.minMaxFont, []], minMaxFont: [settings.minMaxFont, []],
minMaxColor: [settings.minMaxFont?.color, []], minMaxColor: [settings.minMaxFont?.color, []],
@ -182,11 +182,29 @@ export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigCom
return this.widgetConfig; return this.widgetConfig;
} }
private maxValueValidation(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value: string = control.value;
if (value) {
if (value < control.parent?.get('minValue').value) {
return {maxValue: true};
}
}
return null;
};
}
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return ['gaugeType', 'showValue', 'showTitle', 'showMinMax']; return ['gaugeType', 'showValue', 'showTitle', 'showMinMax', 'minValue'];
} }
protected updateValidators(emitEvent: boolean, trigger?: string) { protected updateValidators(emitEvent: boolean, trigger?: string) {
if (trigger === 'minValue') {
this.simpleGaugeWidgetConfigForm.get('maxValue').updateValueAndValidity({emitEvent: true});
this.simpleGaugeWidgetConfigForm.get('maxValue').markAsTouched({onlySelf: true});
return;
}
const isDonut = this.simpleGaugeWidgetConfigForm.get('gaugeType').value === this.digitalGaugeType.donut; const isDonut = this.simpleGaugeWidgetConfigForm.get('gaugeType').value === this.digitalGaugeType.donut;
if (isDonut) { if (isDonut) {

1
ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.html

@ -32,6 +32,7 @@
<tb-notification [notification]="notification" <tb-notification [notification]="notification"
(markAsRead)="markAsRead($event)"> (markAsRead)="markAsRead($event)">
</tb-notification> </tb-notification>
<mat-divider *ngIf="!last" style="margin: 4px"></mat-divider>
</div> </div>
</section> </section>
</div> </div>

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

@ -60,6 +60,6 @@
width: 24px; width: 24px;
height: 22px; height: 22px;
background-color: green; background-color: green;
border-radius: 7px; border-radius: 5px;
} }
} }

8
ui-ngx/src/app/modules/home/components/widget/lib/cards/unread-notification-widget.component.ts

@ -153,9 +153,11 @@ export class UnreadNotificationWidgetComponent implements OnInit, OnDestroy {
this.ctx.widgetActions = [this.viewAllAction, this.filterAction, this.markAsReadAction]; this.ctx.widgetActions = [this.viewAllAction, this.filterAction, this.markAsReadAction];
this.viewAllAction.show = isDefined(this.settings.enableViewAll) ? this.settings.enableViewAll : true; this.viewAllAction.show = isDefined(this.settings.enableViewAll) ? this.settings.enableViewAll : true;
this.store.pipe(select(selectUserDetails), take(1)).subscribe( if (this.viewAllAction.show) {
user => this.viewAllAction.show = !user.additionalInfo?.defaultDashboardFullscreen this.store.pipe(select(selectUserDetails), take(1)).subscribe(
); user => this.viewAllAction.show = !user.additionalInfo?.defaultDashboardFullscreen
);
}
this.filterAction.show = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true; this.filterAction.show = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true;
this.markAsReadAction.show = isDefined(this.settings.enableMarkAsRead) ? this.settings.enableMarkAsRead : true; this.markAsReadAction.show = isDefined(this.settings.enableMarkAsRead) ? this.settings.enableMarkAsRead : true;

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

@ -35,7 +35,7 @@ export const unreadNotificationDefaultSettings: UnreadNotificationWidgetSettings
showCounter: true, showCounter: true,
counterValueFont: { counterValueFont: {
family: 'Roboto', family: 'Roboto',
size: 14, size: 12,
sizeUnit: 'px', sizeUnit: 'px',
style: 'normal', style: 'normal',
weight: '600', weight: '600',

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.html

@ -42,7 +42,7 @@
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="portErrorTooltip" [matTooltip]="brokerConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(brokerConfigFormGroup.get('port').hasError('required') || *ngIf="(brokerConfigFormGroup.get('port').hasError('required') ||
brokerConfigFormGroup.get('port').hasError('min') || brokerConfigFormGroup.get('port').hasError('min') ||
brokerConfigFormGroup.get('port').hasError('max')) && brokerConfigFormGroup.get('port').hasError('max')) &&

30
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy } from '@angular/core';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -35,8 +35,9 @@ import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { generateSecret } from '@core/utils'; import { generateSecret } from '@core/utils';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { SecurityConfigComponent } from '../security-config/security-config.component';
@Component({ @Component({
selector: 'tb-broker-config-control', selector: 'tb-broker-config-control',
@ -47,6 +48,7 @@ import { Subject } from 'rxjs';
CommonModule, CommonModule,
SharedModule, SharedModule,
SecurityConfigComponent, SecurityConfigComponent,
GatewayPortTooltipPipe,
], ],
providers: [ providers: [
{ {
@ -72,13 +74,14 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder, constructor(private fb: FormBuilder,
private cdr: ChangeDetectorRef,
private translate: TranslateService) { private translate: TranslateService) {
this.brokerConfigFormGroup = this.fb.group({ this.brokerConfigFormGroup = this.fb.group({
name: ['', []], name: ['', []],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
version: [5, []], version: [5, []],
clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]], clientId: ['tb_gw_' + generateSecret(5), [Validators.pattern(noLeadTrailSpacesRegex)]],
security: [] security: []
}); });
@ -88,19 +91,6 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid
}); });
} }
get portErrorTooltip(): string {
if (this.brokerConfigFormGroup.get('port').hasError('required')) {
return this.translate.instant('gateway.port-required');
} else if (
this.brokerConfigFormGroup.get('port').hasError('min') ||
this.brokerConfigFormGroup.get('port').hasError('max')
) {
return this.translate.instant('gateway.port-limits-error',
{min: PortLimits.MIN, max: PortLimits.MAX});
}
return '';
}
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
@ -119,7 +109,13 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid
} }
writeValue(brokerConfig: BrokerConfig): void { writeValue(brokerConfig: BrokerConfig): void {
this.brokerConfigFormGroup.patchValue(brokerConfig, {emitEvent: false}); const brokerConfigState = {
...brokerConfig,
version: brokerConfig.version || 5,
clientId: brokerConfig.clientId || 'tb_gw_' + generateSecret(5),
};
this.brokerConfigFormGroup.reset(brokerConfigState, {emitEvent: false});
this.cdr.markForCheck();
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {

12
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html

@ -39,8 +39,8 @@
<div class="tb-form-panel-title" translate>gateway.platform-side</div> <div class="tb-form-panel-title" translate>gateway.platform-side</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" <div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate> tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}">
gateway.key {{ 'gateway.key' | translate }}
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -97,8 +97,8 @@
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" <div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate> tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}">
gateway.value {{ 'gateway.value' | translate }}
</div> </div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap"> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap">
<input matInput required formControlName="value" <input matInput required formControlName="value"
@ -158,8 +158,8 @@
</div> </div>
<div class="tb-form-panel no-border no-padding" *ngIf="keysType === MappingKeysType.RPC_METHODS"> <div class="tb-form-panel no-border no-padding" *ngIf="keysType === MappingKeysType.RPC_METHODS">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.method-name' | translate }}" translate> <div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.method-name' | translate }}">
gateway.method-name {{ 'gateway.method-name' | translate }}
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html

@ -61,11 +61,11 @@
<table mat-table [dataSource]="dataSource"> <table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="column.def" *ngFor="let column of mappingColumns; let i = index"> <ng-container [matColumnDef]="column.def" *ngFor="let column of mappingColumns; let i = index">
<mat-header-cell *matHeaderCellDef class="table-value-column" <mat-header-cell *matHeaderCellDef class="table-value-column"
[class.request-column]="mappingType !== mappingTypeEnum.DATA"> [class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ column.title | translate }} {{ column.title | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column" <mat-cell tbTruncateWithTooltip *matCellDef="let mapping" class="table-value-column"
[class.request-column]="mappingType !== mappingTypeEnum.DATA"> [class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ mapping[column.def] }} {{ mapping[column.def] }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>

46
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts

@ -28,8 +28,8 @@ import {
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, take, takeUntil } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators';
import { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
@ -52,12 +52,13 @@ import {
RequestType, RequestType,
RequestTypesTranslationsMap RequestTypesTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { DataSource } from '@angular/cdk/collections';
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component'; import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({ @Component({
selector: 'tb-mapping-table', selector: 'tb-mapping-table',
@ -77,7 +78,7 @@ import { CommonModule } from '@angular/common';
} }
], ],
standalone: true, standalone: true,
imports: [CommonModule, SharedModule] imports: [CommonModule, SharedModule, TruncateWithTooltipDirective]
}) })
export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
@ -191,7 +192,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
$event.stopPropagation(); $event.stopPropagation();
} }
const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {}; const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {};
this.dialog.open<MappingDialogComponent, MappingInfo, MappingValue>(MappingDialogComponent, { this.dialog.open<MappingDialogComponent, MappingInfo, ConnectorMapping>(MappingDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
@ -206,7 +207,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
if (isDefinedAndNotNull(index)) { if (isDefinedAndNotNull(index)) {
this.mappingFormGroup.at(index).patchValue(res); this.mappingFormGroup.at(index).patchValue(res);
} else { } else {
this.mappingFormGroup.push(this.fb.group(res)); this.pushDataAsFormArrays([res]);
} }
this.mappingFormGroup.markAsDirty(); this.mappingFormGroup.markAsDirty();
} }
@ -222,7 +223,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
) )
); );
} }
this.dataSource.loadMappings(tableValue); this.dataSource.loadData(tableValue);
} }
deleteMapping($event: Event, index: number): void { deleteMapping($event: Event, index: number): void {
@ -310,33 +311,8 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
} }
} }
export class MappingDatasource implements DataSource<{[key: string]: any}> { export class MappingDatasource extends TbTableDatasource<MappingValue> {
constructor() {
private mappingSubject = new BehaviorSubject<Array<{[key: string]: any}>>([]); super();
constructor() {}
connect(): Observable<Array<{[key: string]: any}>> {
return this.mappingSubject.asObservable();
}
disconnect(): void {
this.mappingSubject.complete();
}
loadMappings(mappings: Array<{[key: string]: any}>): void {
this.mappingSubject.next(mappings);
}
isEmpty(): Observable<boolean> {
return this.mappingSubject.pipe(
map((mappings) => !mappings.length)
);
}
total(): Observable<number> {
return this.mappingSubject.pipe(
map((mappings) => mappings.length)
);
} }
} }

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html

@ -0,0 +1,28 @@
<!--
Copyright © 2016-2024 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-tab-group [formGroup]="basicFormGroup">
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.master-connections' | translate }}*">
<tb-modbus-master-table formControlName="master"></tb-modbus-master-table>
</mat-tab>
<mat-tab label="{{ 'gateway.server-config' | translate }}">
<tb-modbus-slave-config formControlName="slave"></tb-modbus-slave-config>
</mat-tab>
</mat-tab-group>

24
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2024 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 {
height: 100%;
}
:host ::ng-deep {
.mat-mdc-tab-body-content {
overflow: hidden !important;
}
}

116
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts

@ -0,0 +1,116 @@
///
/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, TemplateRef } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
import { ConnectorType, ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
@Component({
selector: 'tb-modbus-basic-config',
templateUrl: './modbus-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusSlaveConfigComponent,
ModbusMasterTableComponent,
EllipsisChipListDirective,
],
styleUrls: ['./modbus-basic-config.component.scss'],
})
export class ModbusBasicConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<any>;
basicFormGroup: FormGroup;
onChange: (value: ModbusBasicConfig) => void;
onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.basicFormGroup = this.fb.group({
master: [],
slave: [],
});
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: ModbusBasicConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(basicConfig: ModbusBasicConfig): void {
const editedBase = {
slave: basicConfig.slave ?? {},
master: basicConfig.master ?? {},
};
this.basicFormGroup.setValue(editedBase, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false}
};
}
}

180
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html

@ -0,0 +1,180 @@
<!--
Copyright © 2016-2024 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-modbus-keys-panel">
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel-title">{{ panelTitle | translate }}{{' (' + keysListFormArray.controls.length + ')'}}</div>
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="keysListFormArray.controls.length; else noKeys">
<div class="tb-form-panel no-border no-padding tb-flex no-flex row center fill-width"
*ngFor="let keyControl of keysListFormArray.controls; trackBy: trackByControlId; let $index = index; let last = last;">
<div class="tb-form-panel stroked tb-flex">
<ng-container [formGroup]="keyControl">
<mat-expansion-panel class="tb-settings" [expanded]="last">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<div class="title-container">
<span *ngIf="isMaster else tagName">
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
</span>
<ng-template #tagName>{{ keyControl.get('tag').value }}</ng-template>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.platform-side</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>
gateway.key
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="tag" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="keyControl.get('tag').hasError('required') &&
keyControl.get('tag').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.connector-side</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>
gateway.type
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of modbusDataTypes" [value]="type">{{ type }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="withFunctionCode" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.function-code</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="functionCode">
<mat-option
*ngFor="let code of functionCodesMap.get(keyControl.get('id').value) || defaultFunctionCodes"
[value]="code"
>
{{ ModbusFunctionCodeTranslationsMap.get(code) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.objects-count</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input
matInput
type="number"
min="1"
max="50000"
name="value"
formControlName="objectsCount"
placeholder="{{ 'gateway.set' | translate }}"
[readonly]="!editableDataTypes.includes(keyControl.get('type').value)"
/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.address</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" max="50000" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.address-required') | translate"
*ngIf="keyControl.get('address').hasError('required') &&
keyControl.get('address').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="isMaster" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.value</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="value" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="keyControl.get('value').hasError('required') &&
keyControl.get('value').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
</ng-template>
</mat-expansion-panel>
</ng-container>
</div>
<button type="button"
mat-icon-button
(click)="deleteKey($event, $index)"
[matTooltip]="deleteKeyTitle | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ addKeyTitle | translate }}
</button>
</div>
</div>
<ng-template #noKeys>
<div class="tb-flex no-flex center align-center key-panel">
<span class="tb-prompt" translate>{{ noKeysText }}</span>
</div>
</ng-template>
<div class="tb-flex flex-end">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyKeysData()"
[disabled]="keysListFormArray.invalid || !keysListFormArray.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

44
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2024 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-modbus-keys-panel {
width: 77vw;
max-width: 700px;
.title-container {
max-width: 11vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
}
.key-panel {
height: 500px;
overflow: auto;
}
.tb-form-panel {
.mat-mdc-icon-button {
width: 56px;
height: 56px;
padding: 16px;
color: rgba(0, 0, 0, 0.54);
}
}
}
}

208
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts

@ -0,0 +1,208 @@
///
/// Copyright © 2016-2024 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, OnDestroy, OnInit, Output } from '@angular/core';
import {
AbstractControl,
FormArray,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
ModbusDataType,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusValue,
ModbusValueKey,
noLeadTrailSpacesRegex,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { GatewayHelpLinkPipe } from '@home/components/widget/lib/gateway/pipes/gateway-help-link.pipe';
import { generateSecret } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-modbus-data-keys-panel',
templateUrl: './modbus-data-keys-panel.component.html',
styleUrls: ['./modbus-data-keys-panel.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
GatewayHelpLinkPipe,
]
})
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
@coerceBoolean()
@Input() isMaster = false;
@Input() panelTitle: string;
@Input() addKeyTitle: string;
@Input() deleteKeyTitle: string;
@Input() noKeysText: string;
@Input() keysType: ModbusValueKey;
@Input() values: ModbusValue[];
@Input() popover: TbPopoverComponent<ModbusDataKeysPanelComponent>;
@Output() keysDataApplied = new EventEmitter<Array<ModbusValue>>();
keysListFormArray: FormArray<UntypedFormGroup>;
modbusDataTypes = Object.values(ModbusDataType);
withFunctionCode = true;
functionCodesMap = new Map();
defaultFunctionCodes = [];
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING];
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
private destroy$ = new Subject<void>();
private readonly defaultReadFunctionCodes = [3, 4];
private readonly defaultWriteFunctionCodes = [5, 6, 15, 16];
private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16];
constructor(private fb: UntypedFormBuilder) {}
ngOnInit(): void {
this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TIMESERIES);
this.keysListFormArray = this.prepareKeysFormArray(this.values);
this.defaultFunctionCodes = this.getDefaultFunctionCodes();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
trackByControlId(_: number, keyControl: AbstractControl): string {
return keyControl.value.id;
}
addKey(): void {
const dataKeyFormGroup = this.fb.group({
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusDataType.BYTES, [Validators.required]],
address: [null, [Validators.required]],
objectsCount: [1, [Validators.required]],
functionCode: [{ value: this.getDefaultFunctionCodes()[0], disabled: !this.withFunctionCode }, [Validators.required]],
id: [{value: generateSecret(5), disabled: true}],
});
this.observeKeyDataType(dataKeyFormGroup);
this.keysListFormArray.push(dataKeyFormGroup);
}
deleteKey($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.keysListFormArray.removeAt(index);
this.keysListFormArray.markAsDirty();
}
cancel(): void {
this.popover.hide();
}
applyKeysData(): void {
this.keysDataApplied.emit(this.keysListFormArray.value);
}
private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
if (values) {
values.forEach(value => {
const dataKeyFormGroup = this.createDataKeyFormGroup(value);
this.observeKeyDataType(dataKeyFormGroup);
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(value.type));
keysControlGroups.push(dataKeyFormGroup);
});
}
return this.fb.array(keysControlGroups);
}
private createDataKeyFormGroup(modbusValue: ModbusValue): FormGroup {
const { tag, value, type, address, objectsCount, functionCode } = modbusValue;
return this.fb.group({
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{ value, disabled: !this.isMaster }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, [Validators.required]],
address: [address, [Validators.required]],
objectsCount: [objectsCount, [Validators.required]],
functionCode: [{ value: functionCode, disabled: !this.withFunctionCode }, [Validators.required]],
id: [{ value: generateSecret(5), disabled: true }],
});
}
private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.editableDataTypes.includes(dataType)) {
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
}
this.updateFunctionCodes(keyFormGroup, dataType);
});
}
private updateFunctionCodes(keyFormGroup: FormGroup, dataType: ModbusDataType): void {
const functionCodes = this.getFunctionCodes(dataType);
this.functionCodesMap.set(keyFormGroup.get('id').value, functionCodes);
if (!functionCodes.includes(keyFormGroup.get('functionCode').value)) {
keyFormGroup.get('functionCode').patchValue(functionCodes[0], {emitEvent: false});
}
}
private getFunctionCodes(dataType: ModbusDataType): number[] {
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return dataType === ModbusDataType.STRING
? this.stringAttrUpdatesWriteFunctionCodes
: this.defaultWriteFunctionCodes;
}
const functionCodes = [...this.defaultReadFunctionCodes];
if (dataType === ModbusDataType.BITS) {
const bitsFunctionCodes = [1, 2];
functionCodes.push(...bitsFunctionCodes);
functionCodes.sort();
}
if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
functionCodes.push(...this.defaultWriteFunctionCodes);
}
return functionCodes;
}
private getDefaultFunctionCodes(): number[] {
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return this.defaultWriteFunctionCodes;
}
if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
return [...this.defaultReadFunctionCodes, ...this.defaultWriteFunctionCodes];
}
return this.defaultReadFunctionCodes;
}
}

131
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html

@ -0,0 +1,131 @@
<!--
Copyright © 2016-2024 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-master-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-master-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-master-table-title">{{ 'gateway.servers-slaves' | translate}}</span>
</div>
<span fxFlex></span>
<button mat-icon-button
(click)="manageSlave($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-icon-button
(click)="enterFilterMode()"
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
</div>
</mat-toolbar>
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="textSearchMode">
<div class="mat-toolbar-tools">
<button mat-icon-button
matTooltip="{{ 'action.search' | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex>
<mat-label>&nbsp;</mat-label>
<input #searchInput matInput
[formControl]="textSearch"
placeholder="{{ 'common.enter-search' | translate }}"/>
</mat-form-field>
<button mat-icon-button (click)="exitFilterMode()"
matTooltip="{{ 'action.close' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="'name'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.name' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ mapping['name'] }}
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.client-communication-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(mapping['type']) }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}">
</mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index"
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
(click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
(click)="$event.stopPropagation()"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button
(click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let mapping; columns: ['name', 'type', 'actions']"></mat-row>
</table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">
<button mat-button class="connector"
(click)="manageSlave($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'gateway.add-slave' | translate }}</span>
</button>
</section>
</div>
<span [fxShow]="textSearchMode && (dataSource.isEmpty() | async)"
fxLayoutAlign="center center"
class="no-data-found" translate>
widget.no-data-found
</span>
</div>
</div>

90
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss

@ -0,0 +1,90 @@
/**
* Copyright © 2016-2024 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';
:host {
width: 100%;
height: 100%;
display: block;
.tb-master-table {
.tb-master-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
.mat-toolbar-tools{
min-height: auto;
}
.title-container{
overflow: hidden;
}
.tb-master-table-title {
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.table-container {
overflow: auto;
.mat-mdc-table {
table-layout: fixed;
min-width: 450px;
.table-value-column {
padding: 0 12px;
width: 38%;
}
}
}
}
}
.no-data-found {
height: calc(100% - 120px);
}
@media #{$mat-xs} {
.mat-toolbar {
height: auto;
min-height: 100px;
.tb-master-table-title{
padding-bottom: 5px;
width: 100%;
}
}
}
}
:host ::ng-deep {
mat-cell.tb-value-cell {
cursor: pointer;
.mat-icon {
height: 24px;
width: 24px;
font-size: 24px;
color: #757575
}
}
}

228
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts

@ -0,0 +1,228 @@
///
/// Copyright © 2016-2024 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 {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators';
import {
ControlValueAccessor,
FormArray,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
ModbusMasterConfig,
ModbusProtocolLabelsMap,
SlaveConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({
selector: 'tb-modbus-master-table',
templateUrl: './modbus-master-table.component.html',
styleUrls: ['./modbus-master-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true
}
],
standalone: true,
imports: [CommonModule, SharedModule]
})
export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
@ViewChild('searchInput') searchInputField: ElementRef;
textSearchMode = false;
dataSource: SlavesDatasource;
masterFormGroup: UntypedFormGroup;
textSearch = this.fb.control('', {nonNullable: true});
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
private onChange: (value: ModbusMasterConfig) => void = () => {};
private onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
constructor(
public translate: TranslateService,
public dialog: MatDialog,
private dialogService: DialogService,
private fb: FormBuilder,
private cdr: ChangeDetectorRef,
) {
this.masterFormGroup = this.fb.group({ slaves: this.fb.array([]) });
this.dataSource = new SlavesDatasource();
}
get slaves(): FormArray {
return this.masterFormGroup.get('slaves') as FormArray;
}
ngOnInit(): void {
this.masterFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateTableData(value.slaves);
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewInit(): void {
this.textSearch.valueChanges.pipe(
debounceTime(150),
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()),
takeUntil(this.destroy$)
).subscribe(text => this.updateTableData(this.slaves.value, text.trim()));
}
registerOnChange(fn: (value: ModbusMasterConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(master: ModbusMasterConfig): void {
this.slaves.clear();
this.pushDataAsFormArrays(master.slaves);
}
validate(): ValidationErrors | null {
return this.slaves.controls.length ? null : {
slavesFormGroup: {valid: false}
};
}
enterFilterMode(): void {
this.textSearchMode = true;
this.cdr.detectChanges();
const searchInput = this.searchInputField.nativeElement;
searchInput.focus();
searchInput.setSelectionRange(0, 0);
}
exitFilterMode(): void {
this.updateTableData(this.slaves.value);
this.textSearchMode = false;
this.textSearch.reset();
}
manageSlave($event: Event, index?: number): void {
if ($event) {
$event.stopPropagation();
}
const withIndex = isDefinedAndNotNull(index);
const value = withIndex ? this.slaves.at(index).value : {};
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value,
buttonTitle: withIndex ? 'action.add' : 'action.apply'
}
}).afterClosed()
.pipe(take(1), takeUntil(this.destroy$))
.subscribe(res => {
if (res) {
if (withIndex) {
this.slaves.at(index).patchValue(res);
} else {
this.slaves.push(this.fb.control(res));
}
this.masterFormGroup.markAsDirty();
}
});
}
deleteMapping($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('gateway.delete-slave-title'),
'',
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).pipe(take(1), takeUntil(this.destroy$)).subscribe((result) => {
if (result) {
this.slaves.removeAt(index);
this.masterFormGroup.markAsDirty();
}
});
}
private updateTableData(data: SlaveConfig[], textSearch?: string): void {
if (textSearch) {
data = data.filter(item =>
Object.values(item).some(value =>
value.toString().toLowerCase().includes(textSearch.toLowerCase())
)
);
}
this.dataSource.loadData(data);
}
private pushDataAsFormArrays(slaves: SlaveConfig[]): void {
if (slaves?.length) {
slaves.forEach((slave: SlaveConfig) => this.slaves.push(this.fb.control(slave)));
}
}
}
export class SlavesDatasource extends TbTableDatasource<SlaveConfig> {
constructor() {
super();
}
}

62
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html

@ -0,0 +1,62 @@
<!--
Copyright © 2016-2024 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-form-panel no-border no-padding" [formGroup]="securityConfigFormGroup">
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.hints.path-in-os' | translate }}</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.client-cert-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="certfile" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.private-key-path</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="keyfile" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.password</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="password" name="value" formControlName="password" placeholder="{{ 'gateway.set' | translate }}"/>
<div class="tb-flex no-gap align-center fill-height" matSuffix>
<tb-toggle-password class="tb-flex align-center fill-height"></tb-toggle-password>
</div>
</mat-form-field>
</div>
</div>
<div *ngIf="!isMaster" class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.server-hostname</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="server_hostname" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="isMaster" class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="reqclicert">
<mat-label>
{{ 'gateway.request-client-certificate' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>

163
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts

@ -0,0 +1,163 @@
///
/// Copyright © 2016-2024 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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
OnChanges,
OnDestroy
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
ModbusSecurity,
noLeadTrailSpacesRegex,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-modbus-security-config',
templateUrl: './modbus-security-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusSecurityConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusSecurityConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
]
})
export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy {
@coerceBoolean()
@Input() isMaster = false;
securityConfigFormGroup: UntypedFormGroup;
private disabled = false;
private onChange: (value: ModbusSecurity) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {
this.securityConfigFormGroup = this.fb.group({
certfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
reqclicert: [{value: false, disabled: true}],
});
this.observeValueChanges();
}
ngOnChanges(): void {
this.updateMasterEnabling();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: ModbusSecurity) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.securityConfigFormGroup.disable({emitEvent: false});
} else {
this.securityConfigFormGroup.enable({emitEvent: false});
}
this.updateMasterEnabling();
this.cdr.markForCheck();
}
validate(): ValidationErrors | null {
return this.securityConfigFormGroup.valid ? null : {
securityConfigFormGroup: { valid: false }
};
}
writeValue(securityConfig: ModbusSecurity): void {
const { certfile, password, keyfile, server_hostname } = securityConfig;
const securityState = {
certfile: certfile ?? '',
password: password ?? '',
keyfile: keyfile ?? '',
server_hostname: server_hostname ?? '',
reqclicert: !!securityConfig.reqclicert,
};
this.securityConfigFormGroup.reset(securityState, {emitEvent: false});
}
private updateMasterEnabling(): void {
if (this.isMaster) {
if (!this.disabled) {
this.securityConfigFormGroup.get('reqclicert').enable({emitEvent: false});
}
this.securityConfigFormGroup.get('server_hostname').disable({emitEvent: false});
} else {
if (!this.disabled) {
this.securityConfigFormGroup.get('server_hostname').enable({emitEvent: false});
}
this.securityConfigFormGroup.get('reqclicert').disable({emitEvent: false});
}
}
private observeValueChanges(): void {
this.securityConfigFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value: ModbusSecurity) => {
this.onChange(value);
this.onTouched();
});
}
}

263
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html

@ -0,0 +1,263 @@
<!--
Copyright © 2016-2024 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 [formGroup]="slaveConfigFormGroup" class="slave-container">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.enable' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<div class="slave-content tb-form-panel no-border no-padding padding-top" >
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>gateway.server-slave-config</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding padding-top">
<div *ngIf="protocolType !== ModbusProtocolType.Serial"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="slaveConfigFormGroup.get('host').hasError('required')
&& slaveConfigFormGroup.get('host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial else serialPort"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') ||
slaveConfigFormGroup.get('port').hasError('min') ||
slaveConfigFormGroup.get('port').hasError('max')) &&
slaveConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<ng-template #serialPort>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.port-required' | translate"
*ngIf="slaveConfigFormGroup.get('port').hasError('required') && slaveConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</ng-template>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate>
gateway.method
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="method">
<mat-option *ngFor="let method of protocolType === ModbusProtocolType.Serial ? modbusSerialMethodTypes : modbusMethodTypes"
[value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.unit-id-required') | translate"
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') &&
slaveConfigFormGroup.get('unitId').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceName" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') &&
slaveConfigFormGroup.get('deviceName').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-profile-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') &&
slaveConfigFormGroup.get('deviceType').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.poll-period</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType === ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.baudrate</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="baudrate">
<mat-option *ngFor="let rate of modbusBaudrates" [value]="rate">{{ rate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.byte-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="byteOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle fxLayoutAlign="center" [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()">
<mat-label>
{{ 'gateway.tls-connection' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-modbus-security-config formControlName="security"></tb-modbus-security-config>
</mat-expansion-panel>
</div>
<ng-container [formGroup]="slaveConfigFormGroup.get('identity')">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.vendor-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="vendorName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.product-code</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="productCode" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.vendor-url</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="vendorUrl" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.product-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="productName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.model-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="modelName" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</ng-container>
</div>
</mat-expansion-panel>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.values</div>
<tb-modbus-values formControlName="values"></tb-modbus-values>
</div>
</div>
</div>

27
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2024 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.
*/
$server-config-header-height: 132px;
:host {
.slave-content {
height: calc(100% - #{$server-config-header-height});
overflow: auto;
}
.slave-container {
display: inherit;
}
}

287
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts

@ -0,0 +1,287 @@
///
/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators,
} from '@angular/forms';
import {
ModbusBaudrates,
ModbusMethodLabelsMap,
ModbusMethodType,
ModbusOrderType,
ModbusProtocolLabelsMap,
ModbusProtocolType,
ModbusRegisterValues,
ModbusSerialMethodType,
ModbusSlave,
noLeadTrailSpacesRegex,
PortLimits,
SlaveConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { startWith, takeUntil } from 'rxjs/operators';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component';
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-modbus-slave-config',
templateUrl: './modbus-slave-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusSlaveConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusSlaveConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
],
styleUrls: ['./modbus-slave-config.component.scss'],
})
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
slaveConfigFormGroup: UntypedFormGroup;
showSecurityControl: FormControl<boolean>;
ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
ModbusMethodLabelsMap = ModbusMethodLabelsMap;
portLimits = PortLimits;
readonly modbusProtocolTypes = Object.values(ModbusProtocolType);
readonly modbusMethodTypes = Object.values(ModbusMethodType);
readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType);
readonly modbusOrderType = Object.values(ModbusOrderType);
readonly ModbusProtocolType = ModbusProtocolType;
readonly modbusBaudrates = ModbusBaudrates;
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host'];
private onChange: (value: SlaveConfig) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.showSecurityControl = this.fb.control(false);
this.slaveConfigFormGroup = this.fb.group({
type: [ModbusProtocolType.TCP],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [ModbusMethodType.SOCKET],
unitId: [0, [Validators.required]],
baudrate: [this.modbusBaudrates[0]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
pollPeriod: [5000],
sendDataToThingsBoard: [false],
byteOrder:[ModbusOrderType.BIG],
security: [],
identity: this.fb.group({
vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
productCode: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
vendorUrl: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
productName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
modelName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
}),
values: [],
});
this.observeValueChanges();
this.observeTypeChange();
this.observeFormEnable();
this.observeShowSecurity();
}
get isSlaveEnabled(): boolean {
return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value;
}
get protocolType(): ModbusProtocolType {
return this.slaveConfigFormGroup.get('type').value;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: SlaveConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.slaveConfigFormGroup.valid ? null : {
slaveConfigFormGroup: { valid: false }
};
}
writeValue(slaveConfig: ModbusSlave): void {
this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {}));
this.updateSlaveConfig(slaveConfig);
this.updateFormEnableState(slaveConfig.sendDataToThingsBoard);
}
private observeValueChanges(): void {
this.slaveConfigFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value: SlaveConfig) => {
if (value.type === ModbusProtocolType.Serial) {
value.port = value.serialPort;
delete value.serialPort;
}
this.onChange(value);
this.onTouched();
});
}
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => {
this.updateFormEnableState(this.isSlaveEnabled);
this.updateMethodType(type);
});
}
private updateMethodType(type: ModbusProtocolType): void {
if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) {
this.slaveConfigFormGroup.get('method').patchValue(
type === ModbusProtocolType.Serial
? ModbusSerialMethodType.ASCII
: ModbusMethodType.SOCKET,
{emitEvent: false}
);
}
}
private observeFormEnable(): void {
this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges
.pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$))
.subscribe(value => this.updateFormEnableState(value));
}
private updateFormEnableState(enabled: boolean): void {
if (enabled) {
this.slaveConfigFormGroup.enable({emitEvent: false});
this.showSecurityControl.enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.disable({emitEvent: false});
this.showSecurityControl.disable({emitEvent: false});
this.slaveConfigFormGroup.get('sendDataToThingsBoard').enable({emitEvent: false});
}
this.updateEnablingByProtocol(this.protocolType);
this.updateSecurityEnable(this.showSecurityControl.value);
}
private observeShowSecurity(): void {
this.showSecurityControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateSecurityEnable(value));
}
private updateSecurityEnable(securityEnabled: boolean): void {
if (securityEnabled && this.isSlaveEnabled && this.protocolType !== ModbusProtocolType.Serial) {
this.slaveConfigFormGroup.get('security').enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.get('security').disable({emitEvent: false});
}
}
private updateEnablingByProtocol(type: ModbusProtocolType): void {
const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys;
const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys;
if (this.isSlaveEnabled) {
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false }));
}
disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false }));
}
private updateSlaveConfig(slaveConfig: ModbusSlave): void {
const {
type = ModbusProtocolType.TCP,
method = ModbusMethodType.RTU,
unitId = 0,
deviceName = '',
deviceType = '',
pollPeriod = 5000,
sendDataToThingsBoard = false,
byteOrder = ModbusOrderType.BIG,
security = {},
identity = {
vendorName: '',
productCode: '',
vendorUrl: '',
productName: '',
modelName: '',
},
values = {} as ModbusRegisterValues,
baudrate = this.modbusBaudrates[0],
host = '',
port = null,
} = slaveConfig;
const slaveState: ModbusSlave = {
type,
method,
unitId,
deviceName,
deviceType,
pollPeriod,
sendDataToThingsBoard: !!sendDataToThingsBoard,
byteOrder,
security,
identity,
values,
baudrate,
host: type === ModbusProtocolType.Serial ? '' : host,
port: type === ModbusProtocolType.Serial ? null : port,
serialPort: (type === ModbusProtocolType.Serial ? port : '') as string,
};
this.slaveConfigFormGroup.setValue(slaveState, { emitEvent: false });
}
}

362
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html

@ -0,0 +1,362 @@
<!--
Copyright © 2016-2024 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="slaves-config-container">
<mat-toolbar color="primary">
<h2>{{ 'gateway.server-slave' | translate }}</h2>
<span fxFlex></span>
<div [tb-help]="modbusHelpLink"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.name-required') | translate"
*ngIf="slaveConfigFormGroup.get('name').hasError('required') &&
slaveConfigFormGroup.get('name').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="stroked tb-form-panel">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>gateway.server-connection</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of modbusProtocolTypes" [value]="type">{{ ModbusProtocolLabelsMap.get(type) }}</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel no-border no-padding padding-top">
<div *ngIf="protocolType !== ModbusProtocolType.Serial"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="host" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.host-required') | translate"
*ngIf="slaveConfigFormGroup.get('host').hasError('required')
&& slaveConfigFormGroup.get('host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial else serialPort"
class="tb-form-row column-xs"
fxLayoutAlign="space-between center"
>
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="{{portLimits.MIN}}" max="{{portLimits.MAX}}"
name="value" formControlName="port" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('port').hasError('required') ||
slaveConfigFormGroup.get('port').hasError('min') ||
slaveConfigFormGroup.get('port').hasError('max')) &&
slaveConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<ng-template #serialPort>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="serialPort" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.port-required' | translate"
*ngIf="slaveConfigFormGroup.get('serialPort').hasError('required') &&
slaveConfigFormGroup.get('serialPort').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</ng-template>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.framer-type' | translate }}" translate>
gateway.method
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="method">
<mat-option *ngFor="let method of protocolType === ModbusProtocolType.Serial ? modbusSerialMethodTypes : modbusMethodTypes"
[value]="method">{{ ModbusMethodLabelsMap.get(method) }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<ng-container *ngIf="protocolType === ModbusProtocolType.Serial">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.baudrate</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="baudrate">
<mat-option *ngFor="let rate of modbusBaudrates" [value]="rate">{{ rate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.bytesize</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="bytesize">
<mat-option *ngFor="let size of modbusByteSizes" [value]="size">{{ size }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.stopbits</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="stopbits" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.parity</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="parity">
<mat-option *ngFor="let parity of modbusParities" [value]="parity">{{ ModbusParityLabelsMap.get(parity) }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="strict">
<mat-label>
{{ 'gateway.strict' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</ng-container>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.unit-id</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="unitId" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.unit-id-required') | translate"
*ngIf="slaveConfigFormGroup.get('unitId').hasError('required') &&
slaveConfigFormGroup.get('unitId').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceName" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceName').hasError('required') &&
slaveConfigFormGroup.get('deviceName').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceType" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-profile-required') | translate"
*ngIf="slaveConfigFormGroup.get('deviceType').hasError('required') &&
slaveConfigFormGroup.get('deviceType').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange">
<mat-label>
{{ 'gateway.send-data-on-change' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-form-panel-title" translate>gateway.advanced-connection-settings</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.connection-timeout</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="timeout" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.byte-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="byteOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.word-order</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="wordOrder">
<mat-option *ngFor="let order of modbusOrderType" [value]="order">{{ order }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div *ngIf="protocolType !== ModbusProtocolType.Serial" class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="showSecurityControl.value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle fxLayoutAlign="center" [formControl]="showSecurityControl" class="mat-slide" (click)="$event.stopPropagation()">
<mat-label>
{{ 'gateway.tls-connection' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-modbus-security-config formControlName="security"></tb-modbus-security-config>
</mat-expansion-panel>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retries">
<mat-label>
{{ 'gateway.retries' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retryOnEmpty">
<mat-label>
{{ 'gateway.retries-on-empty' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="retryOnInvalid">
<mat-label>
{{ 'gateway.retries-on-invalid' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.poll-period</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-time</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptTimeMs" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-count</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptCount" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.wait-after-failed-attempts</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="waitAfterFailedAttemptsMs" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<div class="tb-form-panel stroked">
<tb-modbus-values [singleMode]="true" formControlName="values"></tb-modbus-values>
</div>
</div>
</div>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="slaveConfigFormGroup.invalid || !slaveConfigFormGroup.dirty">
{{ data.buttonTitle | translate }}
</button>
</div>
</div>

21
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2024 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 {
.slaves-config-container {
width: 80vw;
max-width: 900px;
}
}

243
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts

@ -0,0 +1,243 @@
///
/// Copyright © 2016-2024 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 { ChangeDetectionStrategy, Component, forwardRef, Inject, OnDestroy } from '@angular/core';
import {
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import {
ModbusBaudrates,
ModbusByteSizes,
ModbusMethodLabelsMap,
ModbusMethodType,
ModbusOrderType,
ModbusParity,
ModbusParityLabelsMap,
ModbusProtocolLabelsMap,
ModbusProtocolType,
ModbusSerialMethodType,
ModbusSlaveInfo,
noLeadTrailSpacesRegex,
PortLimits,
SlaveConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { ModbusValuesComponent } from '../modbus-values/modbus-values.component';
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component';
import { DialogComponent } from '@shared/components/dialog.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-modbus-slave-dialog',
templateUrl: './modbus-slave-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusSlaveDialogComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusSlaveDialogComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
],
styleUrls: ['./modbus-slave-dialog.component.scss'],
})
export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialogComponent, SlaveConfig> implements OnDestroy {
slaveConfigFormGroup: UntypedFormGroup;
showSecurityControl: FormControl<boolean>;
portLimits = PortLimits;
readonly modbusProtocolTypes = Object.values(ModbusProtocolType);
readonly modbusMethodTypes = Object.values(ModbusMethodType);
readonly modbusSerialMethodTypes = Object.values(ModbusSerialMethodType);
readonly modbusParities = Object.values(ModbusParity);
readonly modbusByteSizes = ModbusByteSizes;
readonly modbusBaudrates = ModbusBaudrates;
readonly modbusOrderType = Object.values(ModbusOrderType);
readonly ModbusProtocolType = ModbusProtocolType;
readonly ModbusParityLabelsMap = ModbusParityLabelsMap;
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap;
readonly modbusHelpLink =
'https://thingsboard.io/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters';
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host', 'wordOrder'];
private destroy$ = new Subject<void>();
constructor(
private fb: FormBuilder,
protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ModbusSlaveInfo,
public dialogRef: MatDialogRef<ModbusSlaveDialogComponent, SlaveConfig>,
) {
super(store, router, dialogRef);
this.showSecurityControl = this.fb.control(false);
this.initializeSlaveFormGroup();
this.updateSlaveFormGroup();
this.updateControlsEnabling(this.data.value.type);
this.observeTypeChange();
this.observeShowSecurity();
this.showSecurityControl.patchValue(!!this.data.value.security && !isEqual(this.data.value.security, {}));
}
get protocolType(): ModbusProtocolType {
return this.slaveConfigFormGroup.get('type').value;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
if (!this.slaveConfigFormGroup.valid) {
return;
}
const { values, type, serialPort, ...rest } = this.slaveConfigFormGroup.value;
const slaveResult = { ...rest, type, ...values };
if (type === ModbusProtocolType.Serial) {
slaveResult.port = serialPort;
}
this.dialogRef.close(slaveResult);
}
private initializeSlaveFormGroup(): void {
this.slaveConfigFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusProtocolType.TCP],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [ModbusMethodType.SOCKET, [Validators.required]],
baudrate: [this.modbusBaudrates[0]],
stopbits: [1],
bytesize: [ModbusByteSizes[0]],
parity: [ModbusParity.None],
strict: [true],
unitId: [0, [Validators.required]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
sendDataOnlyOnChange: [false],
timeout: [35],
byteOrder: [ModbusOrderType.BIG],
wordOrder: [ModbusOrderType.BIG],
retries: [true],
retryOnEmpty: [true],
retryOnInvalid: [true],
pollPeriod: [5000],
connectAttemptTimeMs: [5000],
connectAttemptCount: [5],
waitAfterFailedAttemptsMs: [300000],
values: [{}],
security: [{}],
});
}
private updateSlaveFormGroup(): void {
this.slaveConfigFormGroup.patchValue({
...this.data.value,
port: this.data.value.type === ModbusProtocolType.Serial ? null : this.data.value.port,
serialPort: this.data.value.type === ModbusProtocolType.Serial ? this.data.value.port : '',
values: {
attributes: this.data.value.attributes ?? [],
timeseries: this.data.value.timeseries ?? [],
attributeUpdates: this.data.value.attributeUpdates ?? [],
rpc: this.data.value.rpc ?? [],
}
});
}
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => {
this.updateControlsEnabling(type);
this.updateMethodType(type);
});
}
private updateMethodType(type: ModbusProtocolType): void {
if (this.slaveConfigFormGroup.get('method').value !== ModbusMethodType.RTU) {
this.slaveConfigFormGroup.get('method').patchValue(
type === ModbusProtocolType.Serial
? ModbusSerialMethodType.ASCII
: ModbusMethodType.SOCKET,
{emitEvent: false}
);
}
}
private updateControlsEnabling(type: ModbusProtocolType): void {
const [enableKeys, disableKeys] = type === ModbusProtocolType.Serial
? [this.serialSpecificControlKeys, this.tcpUdpSpecificControlKeys]
: [this.tcpUdpSpecificControlKeys, this.serialSpecificControlKeys];
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false }));
disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false }));
this.updateSecurityEnabling(this.showSecurityControl.value);
}
private observeShowSecurity(): void {
this.showSecurityControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateSecurityEnabling(value));
}
private updateSecurityEnabling(isEnabled: boolean): void {
if (isEnabled && this.protocolType !== ModbusProtocolType.Serial) {
this.slaveConfigFormGroup.get('security').enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.get('security').disable({emitEvent: false});
}
}
}

121
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html

@ -0,0 +1,121 @@
<!--
Copyright © 2016-2024 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 *ngIf="singleMode else multipleView">
<div [formGroup]="valuesFormGroup" class="tb-form-panel no-border no-padding padding-top" fxLayout="column">
<ng-container [ngTemplateOutlet]="singleView" [ngTemplateOutletContext]="{$implicit: null}"></ng-container>
</div>
</ng-container>
<ng-template #multipleView>
<mat-tab-group [formGroup]="valuesFormGroup">
<mat-tab *ngFor="let register of modbusRegisterTypes" label="{{ ModbusValuesTranslationsMap.get(register) | translate }}">
<div [formGroup]="valuesFormGroup.get(register)" class="tb-form-panel no-border no-padding padding-top" fxLayout="column">
<ng-container [ngTemplateOutlet]="singleView" [ngTemplateOutletContext]="{$implicit: register}"></ng-container>
</div>
</mat-tab>
</mat-tab-group>
</ng-template>
<ng-template #singleView let-register>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.attributes</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES, register).value" class="tb-flex">
<mat-chip *ngFor="let attribute of getValueGroup(ModbusValueKey.ATTRIBUTES, register).value">
{{ attribute.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
color="primary"
[disabled]="disabled"
#attributesButton
(click)="manageKeys($event, attributesButton, ModbusValueKey.ATTRIBUTES, register)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.timeseries</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox class="tb-flex" [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.TIMESERIES, register).value">
<mat-chip *ngFor="let telemetry of getValueGroup(ModbusValueKey.TIMESERIES, register).value">
{{ telemetry.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
color="primary"
[disabled]="disabled"
#telemetryButton
(click)="manageKeys($event, telemetryButton, ModbusValueKey.TIMESERIES, register)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.attribute-updates</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register).value" class="tb-flex">
<mat-chip *ngFor="let attributeUpdate of getValueGroup(ModbusValueKey.ATTRIBUTES_UPDATES, register).value">
{{ attributeUpdate.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
[disabled]="disabled"
color="primary"
#attributesUpdatesButton
(click)="manageKeys($event, attributesUpdatesButton, ModbusValueKey.ATTRIBUTES_UPDATES, register)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.rpc-requests</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="getValueGroup(ModbusValueKey.RPC_REQUESTS, register).value" class="tb-flex">
<mat-chip *ngFor="let rpcRequest of getValueGroup(ModbusValueKey.RPC_REQUESTS, register).value">
{{ rpcRequest.tag }}
</mat-chip>
<mat-chip class="mat-mdc-chip ellipsis-chip">
<label class="ellipsis-text"></label>
</mat-chip>
</mat-chip-listbox>
<button type="button"
mat-icon-button
color="primary"
[disabled]="disabled"
#rpcRequestsButton
(click)="manageKeys($event, rpcRequestsButton, ModbusValueKey.RPC_REQUESTS, register)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</ng-template>

24
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2024 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 {
.mat-mdc-tab-body-wrapper {
min-height: 320px;
}
}
::ng-deep .mdc-evolution-chip-set__chips {
align-items: center;
}

236
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts

@ -0,0 +1,236 @@
///
/// Copyright © 2016-2024 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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
OnDestroy,
OnInit,
Renderer2,
ViewContainerRef
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
ModbusKeysAddKeyTranslationsMap,
ModbusKeysDeleteKeyTranslationsMap,
ModbusKeysNoKeysTextTranslationsMap,
ModbusKeysPanelTitleTranslationsMap,
ModbusRegisterTranslationsMap,
ModbusRegisterType,
ModbusRegisterValues,
ModbusValue,
ModbusValueKey,
ModbusValues,
ModbusValuesState,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { ModbusDataKeysPanelComponent } from '../modbus-data-keys-panel/modbus-data-keys-panel.component';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-modbus-values',
templateUrl: './modbus-values.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusValuesComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusValuesComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
EllipsisChipListDirective,
],
styleUrls: ['./modbus-values.component.scss']
})
export class ModbusValuesComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
@coerceBoolean()
@Input() singleMode = false;
disabled = false;
modbusRegisterTypes: ModbusRegisterType[] = Object.values(ModbusRegisterType);
modbusValueKeys = Object.values(ModbusValueKey);
ModbusValuesTranslationsMap = ModbusRegisterTranslationsMap;
ModbusValueKey = ModbusValueKey;
valuesFormGroup: FormGroup;
private onChange: (value: ModbusValuesState) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private cdr: ChangeDetectorRef,
) {}
ngOnInit() {
this.initializeValuesFormGroup();
this.observeValuesChanges();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: ModbusValuesState) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(values: ModbusValuesState): void {
if (this.singleMode) {
this.valuesFormGroup.setValue(this.getSingleRegisterState(values as ModbusValues), { emitEvent: false });
} else {
const { holding_registers, coils_initializer, input_registers, discrete_inputs } = values as ModbusRegisterValues;
this.valuesFormGroup.setValue({
holding_registers: this.getSingleRegisterState(holding_registers),
coils_initializer: this.getSingleRegisterState(coils_initializer),
input_registers: this.getSingleRegisterState(input_registers),
discrete_inputs: this.getSingleRegisterState(discrete_inputs),
}, { emitEvent: false });
}
this.cdr.markForCheck();
}
validate(): ValidationErrors | null {
return this.valuesFormGroup.valid ? null : {
valuesFormGroup: {valid: false}
};
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.cdr.markForCheck();
}
getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType): FormGroup {
return register
? this.valuesFormGroup.get(register).get(valueKey) as FormGroup
: this.valuesFormGroup.get(valueKey) as FormGroup;
}
manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void {
$event.stopPropagation();
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
return;
}
const keysControl = this.getValueGroup(keysType, register);
const ctx = {
values: keysControl.value,
isMaster: !this.singleMode,
keysType,
panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType),
addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType),
deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType),
noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType)
};
const dataKeysPanelPopover = this.popoverService.displayPopover(
trigger,
this.renderer,
this.viewContainerRef,
ModbusDataKeysPanelComponent,
'leftBottom',
false,
null,
ctx,
{},
{},
{},
true
);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData: ModbusValue[]) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
this.cdr.markForCheck();
});
}
private initializeValuesFormGroup(): void {
const getValuesFormGroup = () => this.fb.group(this.modbusValueKeys.reduce((acc, key) => {
acc[key] = this.fb.control([[], []]);
return acc;
}, {}));
if (this.singleMode) {
this.valuesFormGroup = getValuesFormGroup();
} else {
this.valuesFormGroup = this.fb.group(
this.modbusRegisterTypes.reduce((registersAcc, register) => {
registersAcc[register] = getValuesFormGroup();
return registersAcc;
}, {})
);
}
}
private observeValuesChanges(): void {
this.valuesFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
private getSingleRegisterState(values: ModbusValues): ModbusValues {
return {
attributes: values?.attributes ?? [],
timeseries: values?.timeseries ?? [],
attributeUpdates: values?.attributeUpdates ?? [],
rpc: values?.rpc ?? [],
};
}
}

41
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts

@ -25,22 +25,28 @@ import {
Validator, Validator,
} from '@angular/forms'; } from '@angular/forms';
import { import {
ConnectorBaseConfig,
MappingType, MappingType,
MQTTBasicConfig,
RequestMappingData, RequestMappingData,
RequestType, RequestType,
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import {
BrokerConfigControlComponent,
MappingTableComponent,
SecurityConfigComponent,
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { isObject } from 'lodash'; import { isObject } from 'lodash';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
@Component({ @Component({
selector: 'tb-mqtt-basic-config', selector: 'tb-mqtt-basic-config',
@ -112,17 +118,18 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.onTouched = fn; this.onTouched = fn;
} }
writeValue(basicConfig: ConnectorBaseConfig): void { writeValue(basicConfig: MQTTBasicConfig): void {
const { broker, dataMapping = [], requestsMapping } = basicConfig;
const editedBase = { const editedBase = {
workers: { workers: broker && (broker.maxNumberOfWorkers || broker.maxMessageNumberPerWorker) ? {
maxNumberOfWorkers: basicConfig.broker?.maxNumberOfWorkers, maxNumberOfWorkers: broker.maxNumberOfWorkers,
maxMessageNumberPerWorker: basicConfig.broker?.maxMessageNumberPerWorker, maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker,
}, } : {},
dataMapping: basicConfig.dataMapping || [], dataMapping: dataMapping || [],
broker: basicConfig.broker || {}, broker: broker || {},
requestsMapping: Array.isArray(basicConfig.requestsMapping) requestsMapping: Array.isArray(requestsMapping)
? basicConfig.requestsMapping ? requestsMapping
: this.getRequestDataArray(basicConfig.requestsMapping), : this.getRequestDataArray(requestsMapping),
}; };
this.basicFormGroup.setValue(editedBase, {emitEvent: false}); this.basicFormGroup.setValue(editedBase, {emitEvent: false});

125
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html

@ -0,0 +1,125 @@
<!--
Copyright © 2016-2024 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-form-panel no-border no-padding padding-top" [formGroup]="serverConfigFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tbTruncateWithTooltip translate>gateway.server-url</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="url" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.server-url-required') | translate"
*ngIf="serverConfigFormGroup.get('url').hasError('required') &&
serverConfigFormGroup.get('url').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.opcua-timeout' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.timeout' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="timeoutInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.timeout-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('timeoutInMillis').hasError('required') ||
serverConfigFormGroup.get('timeoutInMillis').hasError('min')) &&
serverConfigFormGroup.get('timeoutInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tbTruncateWithTooltip translate>gateway.security</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="security">
<mat-option *ngFor="let version of securityPolicyTypes" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.scan-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.scan-period' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="scanPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.scan-period-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('scanPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('scanPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('scanPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.sub-check-period' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="subCheckPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.sub-check-period-error' | translate: {min: 10}"
*ngIf="(serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('subCheckPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="enableSubscriptions">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.enable-subscription' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.enable-subscription' | translate }}</div>
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="showMap">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.show-map' | translate }}">
{{ 'gateway.show-map' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<tb-security-config formControlName="identity"
[extendCertificatesModel]="true">
</tb-security-config>
</div>

8
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.scss → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.scss

@ -17,12 +17,4 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: block;
.server-conf-field-title {
min-width: 250px;
width: 30%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
} }

30
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.ts → ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts

@ -33,24 +33,27 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
@Component({ @Component({
selector: 'tb-server-config', selector: 'tb-opc-server-config',
templateUrl: './server-config.component.html', templateUrl: './opc-server-config.component.html',
styleUrls: ['./server-config.component.scss'], styleUrls: ['./opc-server-config.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ServerConfigComponent), useExisting: forwardRef(() => OpcServerConfigComponent),
multi: true multi: true
}, },
{ {
provide: NG_VALIDATORS, provide: NG_VALIDATORS,
useExisting: forwardRef(() => ServerConfigComponent), useExisting: forwardRef(() => OpcServerConfigComponent),
multi: true multi: true
} }
], ],
@ -59,9 +62,10 @@ import { takeUntil } from 'rxjs/operators';
CommonModule, CommonModule,
SharedModule, SharedModule,
SecurityConfigComponent, SecurityConfigComponent,
TruncateWithTooltipDirective,
] ]
}) })
export class ServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
securityPolicyTypes = SecurityPolicyTypes; securityPolicyTypes = SecurityPolicyTypes;
serverConfigFormGroup: UntypedFormGroup; serverConfigFormGroup: UntypedFormGroup;
@ -112,6 +116,16 @@ export class ServerConfigComponent implements ControlValueAccessor, Validator, O
} }
writeValue(serverConfig: ServerConfig): void { writeValue(serverConfig: ServerConfig): void {
this.serverConfigFormGroup.patchValue(serverConfig, {emitEvent: false}); const { timeoutInMillis, scanPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, showMap, security } = serverConfig;
const serverConfigState = {
...serverConfig,
timeoutInMillis: timeoutInMillis || 1000,
scanPeriodInMillis: scanPeriodInMillis || 1000,
enableSubscriptions: enableSubscriptions || true,
subCheckPeriodInMillis: subCheckPeriodInMillis || 10,
showMap: showMap || false,
security: security || SecurityPolicy.BASIC128,
};
this.serverConfigFormGroup.reset(serverConfigState, {emitEvent: false});
} }
} }

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.html

@ -20,7 +20,7 @@
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container> <ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'gateway.server' | translate }}*"> <mat-tab label="{{ 'gateway.server' | translate }}*">
<tb-server-config formControlName="server"></tb-server-config> <tb-opc-server-config formControlName="server"></tb-opc-server-config>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*"> <mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height"> <div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component.ts

@ -25,21 +25,29 @@ import {
Validator, Validator,
} from '@angular/forms'; } from '@angular/forms';
import { import {
ConnectorBaseConfig,
ConnectorType, ConnectorType,
MappingType, MappingType,
OPCBasicConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import {
BrokerConfigControlComponent,
MappingTableComponent,
SecurityConfigComponent,
ServerConfigComponent,
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component';
@Component({ @Component({
selector: 'tb-opc-ua-basic-config', selector: 'tb-opc-ua-basic-config',
@ -65,7 +73,7 @@ import { Subject } from 'rxjs';
WorkersConfigControlComponent, WorkersConfigControlComponent,
BrokerConfigControlComponent, BrokerConfigControlComponent,
MappingTableComponent, MappingTableComponent,
ServerConfigComponent, OpcServerConfigComponent,
], ],
styleUrls: ['./opc-ua-basic-config.component.scss'] styleUrls: ['./opc-ua-basic-config.component.scss']
}) })
@ -109,7 +117,7 @@ export class OpcUaBasicConfigComponent implements ControlValueAccessor, Validato
this.onTouched = fn; this.onTouched = fn;
} }
writeValue(basicConfig: ConnectorBaseConfig): void { writeValue(basicConfig: OPCBasicConfig): void {
const editedBase = { const editedBase = {
server: basicConfig.server || {}, server: basicConfig.server || {},
mapping: basicConfig.mapping || [], mapping: basicConfig.mapping || [],

26
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/public-api.ts

@ -1,26 +0,0 @@
///
/// Copyright © 2016-2024 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 './mapping-table/mapping-table.component';
export * from './device-info-table/device-info-table.component';
export * from './security-config/security-config.component';
export * from './server-config/server-config.component';
export * from './mapping-data-keys-panel/mapping-data-keys-panel.component';
export * from './type-value-panel/type-value-panel.component';
export * from './broker-config-control/broker-config-control.component';
export * from './workers-config-control/workers-config-control.component';
export * from './opc-ua-basic-config/opc-ua-basic-config.component';
export * from './mqtt-basic-config/mqtt-basic-config.component';

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts

@ -16,6 +16,7 @@
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
forwardRef, forwardRef,
Input, Input,
@ -87,7 +88,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {} constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}
ngOnInit(): void { ngOnInit(): void {
this.securityFormGroup = this.fb.group({ this.securityFormGroup = this.fb.group({
@ -127,6 +128,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On
} }
this.securityFormGroup.reset(securityInfo, {emitEvent: false}); this.securityFormGroup.reset(securityInfo, {emitEvent: false});
} }
this.cdr.markForCheck();
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {

127
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/server-config/server-config.component.html

@ -1,127 +0,0 @@
<!--
Copyright © 2016-2024 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-form-panel no-border no-padding padding-top" [formGroup]="serverConfigFormGroup">
<div class="server-settings">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" translate>gateway.server-url</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="url" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.server-url-required') | translate"
*ngIf="serverConfigFormGroup.get('url').hasError('required') &&
serverConfigFormGroup.get('url').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" tb-hint-tooltip-icon="{{ 'gateway.hints.opcua-timeout' | translate }}" translate>
gateway.timeout
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="timeoutInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.timeout-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('timeoutInMillis').hasError('required') ||
serverConfigFormGroup.get('timeoutInMillis').hasError('min')) &&
serverConfigFormGroup.get('timeoutInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" translate>gateway.security</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="security">
<mat-option *ngFor="let version of securityPolicyTypes" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" tb-hint-tooltip-icon="{{ 'gateway.hints.scan-period' | translate }}" translate>
gateway.scan-period
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="scanPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.scan-period-error' | translate: {min: 1000}"
*ngIf="(serverConfigFormGroup.get('scanPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('scanPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('scanPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="server-conf-field-title" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}" translate>
gateway.sub-check-period
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value"
formControlName="subCheckPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.sub-check-period-error' | translate: {min: 10}"
*ngIf="(serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('subCheckPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('subCheckPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="enableSubscriptions">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.enable-subscription' | translate }}">
{{ 'gateway.enable-subscription' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="showMap">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.show-map' | translate }}">
{{ 'gateway.show-map' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<tb-security-config formControlName="identity"
[extendCertificatesModel]="true">
</tb-security-config>
</div>

8
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.html

@ -18,8 +18,8 @@
<div class="tb-form-panel no-border no-padding padding-top" [formGroup]="workersConfigFormGroup"> <div class="tb-form-panel no-border no-padding padding-top" [formGroup]="workersConfigFormGroup">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50" <div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}" translate> tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}">
gateway.max-number-of-workers <div tbTruncateWithTooltip>{{ 'gateway.max-number-of-workers' | translate }}</div>
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -40,8 +40,8 @@
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50" <div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}" translate> tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}">
gateway.max-messages-queue-for-worker <div tbTruncateWithTooltip>{{ 'gateway.max-messages-queue-for-worker' | translate }}</div>
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">

12
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts

@ -25,7 +25,9 @@ import {
FormBuilder, FormBuilder,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormGroup, ValidationErrors, Validator, UntypedFormGroup,
ValidationErrors,
Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
@ -33,6 +35,7 @@ import { CommonModule } from '@angular/common';
import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-workers-config-control', selector: 'tb-workers-config-control',
@ -42,6 +45,7 @@ import { takeUntil } from 'rxjs/operators';
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
TruncateWithTooltipDirective,
], ],
providers: [ providers: [
{ {
@ -91,7 +95,11 @@ export class WorkersConfigControlComponent implements OnDestroy, ControlValueAcc
} }
writeValue(workersConfig: WorkersConfig): void { writeValue(workersConfig: WorkersConfig): void {
this.workersConfigFormGroup.patchValue(workersConfig, {emitEvent: false}); const { maxNumberOfWorkers, maxMessageNumberPerWorker } = workersConfig;
this.workersConfigFormGroup.reset({
maxNumberOfWorkers: maxNumberOfWorkers || 100,
maxMessageNumberPerWorker: maxMessageNumberPerWorker || 10,
}, {emitEvent: false});
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts

@ -33,7 +33,7 @@ import {
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from "rxjs/operators"; import { takeUntil, tap } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-add-connector-dialog', selector: 'tb-add-connector-dialog',

34
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html

@ -19,7 +19,7 @@
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2> <h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2>
<span fxFlex></span> <span fxFlex></span>
<div [tb-help]="helpLinkId()"></div> <div [tb-help]="HelpLinkByMappingTypeMap.get(this.data.mappingType)"></div>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
@ -57,8 +57,8 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate> <div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}">
gateway.mqtt-qos {{ 'gateway.mqtt-qos' | translate }}
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -140,8 +140,8 @@
<div class="tb-form-panel no-border no-padding" *ngIf="converterType === ConvertorTypeEnum.CUSTOM"> <div class="tb-form-panel no-border no-padding" *ngIf="converterType === ConvertorTypeEnum.CUSTOM">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" <div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}" translate> tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}">
gateway.extension {{ 'gateway.extension' | translate }}
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -369,8 +369,8 @@
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE"> <ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" <div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate> tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}">
gateway.device-name-filter {{ 'gateway.device-name-filter' | translate }}
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -388,8 +388,8 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}" translate> <div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}">
gateway.attribute-filter {{ 'gateway.attribute-filter' | translate }}
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -472,8 +472,8 @@
</tb-toggle-select> </tb-toggle-select>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}" translate> <div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}">
gateway.device-name-filter {{ 'gateway.device-name-filter' | translate }}
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -491,8 +491,8 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}" translate> <div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}">
gateway.method-filter {{ 'gateway.method-filter' | translate }}
</div> </div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -580,8 +580,8 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}" translate> <div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}">
gateway.response-topic-Qos {{ 'gateway.response-topic-Qos' | translate }}
</div> </div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="responseTopicQoS"> <mat-select formControlName="responseTopicQoS">
@ -618,8 +618,8 @@
<ng-template [ngSwitchCase]="MappingType.OPCUA"> <ng-template [ngSwitchCase]="MappingType.OPCUA">
<div class="tb-form-row column-xs" fxLayoutAlign="center"> <div class="tb-form-row column-xs" fxLayoutAlign="center">
<div class="tb-flex no-flex align-center" translate> <div class="tb-flex no-flex align-center" translate>
<div class="tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-node-hint' | translate }}" translate> <div class="tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-node-hint' | translate }}">
gateway.device-node {{ 'gateway.device-node' | translate }}
</div> </div>
</div> </div>
<div class="tb-flex device-config"> <div class="tb-flex device-config">

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss

@ -47,6 +47,7 @@
.mdc-evolution-chip-set__chips { .mdc-evolution-chip-set__chips {
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
flex-wrap: nowrap;
} }
} }
} }

37
ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts

@ -24,10 +24,15 @@ import { Router } from '@angular/router';
import { import {
Attribute, Attribute,
AttributesUpdate, AttributesUpdate,
ConnectorMapping,
ConnectorMappingFormValue,
ConverterMappingFormValue,
ConvertorType, ConvertorType,
ConvertorTypeTranslationsMap, ConvertorTypeTranslationsMap,
DataConversionTranslationsMap, DataConversionTranslationsMap,
DeviceConnectorMapping,
DeviceInfoType, DeviceInfoType,
HelpLinkByMappingTypeMap,
MappingHintTranslationsMap, MappingHintTranslationsMap,
MappingInfo, MappingInfo,
MappingKeysAddKeyTranslationsMap, MappingKeysAddKeyTranslationsMap,
@ -37,11 +42,11 @@ import {
MappingKeysType, MappingKeysType,
MappingType, MappingType,
MappingTypeTranslationsMap, MappingTypeTranslationsMap,
MappingValue,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
OPCUaSourceTypes, OPCUaSourceTypes,
QualityTypes, QualityTypes,
QualityTypeTranslationsMap, QualityTypeTranslationsMap,
RequestMappingFormValue,
RequestType, RequestType,
RequestTypesTranslationsMap, RequestTypesTranslationsMap,
RpcMethod, RpcMethod,
@ -55,14 +60,16 @@ import { startWith, takeUntil } from 'rxjs/operators';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MappingDataKeysPanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; import {
MappingDataKeysPanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component';
@Component({ @Component({
selector: 'tb-mapping-dialog', selector: 'tb-mapping-dialog',
templateUrl: './mapping-dialog.component.html', templateUrl: './mapping-dialog.component.html',
styleUrls: ['./mapping-dialog.component.scss'] styleUrls: ['./mapping-dialog.component.scss']
}) })
export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, MappingValue> implements OnDestroy { export class MappingDialogComponent extends DialogComponent<MappingDialogComponent, ConnectorMapping> implements OnDestroy {
mappingForm: UntypedFormGroup; mappingForm: UntypedFormGroup;
@ -97,6 +104,8 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
DataConversionTranslationsMap = DataConversionTranslationsMap; DataConversionTranslationsMap = DataConversionTranslationsMap;
HelpLinkByMappingTypeMap = HelpLinkByMappingTypeMap;
keysPopupClosed = true; keysPopupClosed = true;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -104,7 +113,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MappingInfo, @Inject(MAT_DIALOG_DATA) public data: MappingInfo,
public dialogRef: MatDialogRef<MappingDialogComponent, MappingValue>, public dialogRef: MatDialogRef<MappingDialogComponent, ConnectorMapping>,
private fb: FormBuilder, private fb: FormBuilder,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
@ -187,10 +196,6 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
} }
} }
helpLinkId(): string {
return 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping';
}
cancel(): void { cancel(): void {
if (this.keysPopupClosed) { if (this.keysPopupClosed) {
this.dialogRef.close(null); this.dialogRef.close(null);
@ -247,7 +252,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
} }
} }
private prepareMappingData(): { [key: string]: unknown } { private prepareMappingData(): ConnectorMapping {
const formValue = this.mappingForm.value; const formValue = this.mappingForm.value;
switch (this.data.mappingType) { switch (this.data.mappingType) {
case MappingType.DATA: case MappingType.DATA:
@ -270,7 +275,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
} }
} }
private prepareFormValueData(): { [key: string]: unknown } { private getFormValueData(): ConnectorMappingFormValue {
if (this.data.value && Object.keys(this.data.value).length) { if (this.data.value && Object.keys(this.data.value).length) {
switch (this.data.mappingType) { switch (this.data.mappingType) {
case MappingType.DATA: case MappingType.DATA:
@ -282,16 +287,16 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
type: converter.type, type: converter.type,
[converter.type]: {...converter} [converter.type]: {...converter}
} }
}; } as ConverterMappingFormValue;
case MappingType.REQUESTS: case MappingType.REQUESTS:
return { return {
requestType: this.data.value.requestType, requestType: this.data.value.requestType,
requestValue: { requestValue: {
[this.data.value.requestType]: this.data.value.requestValue [this.data.value.requestType]: this.data.value.requestValue
} }
}; } as RequestMappingFormValue;
default: default:
return this.data.value; return this.data.value as DeviceConnectorMapping;
} }
} }
} }
@ -317,7 +322,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
extensionConfig: [{}, []] extensionConfig: [{}, []]
}), }),
})); }));
this.mappingForm.patchValue(this.prepareFormValueData()); this.mappingForm.patchValue(this.getFormValueData());
this.mappingForm.get('converter.type').valueChanges.pipe( this.mappingForm.get('converter.type').valueChanges.pipe(
startWith(this.mappingForm.get('converter.type').value), startWith(this.mappingForm.get('converter.type').value),
takeUntil(this.destroy$) takeUntil(this.destroy$)
@ -397,7 +402,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
requestValueGroup.get('responseTimeout').enable({emitEvent: false}); requestValueGroup.get('responseTimeout').enable({emitEvent: false});
} }
}); });
this.mappingForm.patchValue(this.prepareFormValueData()); this.mappingForm.patchValue(this.getFormValueData());
} }
private createOPCUAMappingForm(): void { private createOPCUAMappingForm(): void {
@ -410,6 +415,6 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
rpc_methods: [[], []], rpc_methods: [[], []],
attributes_updates: [[], []] attributes_updates: [[], []]
}); });
this.mappingForm.patchValue(this.prepareFormValueData()); this.mappingForm.patchValue(this.getFormValueData());
} }
} }

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts

@ -619,6 +619,9 @@ export class GatewayConfigurationComponent implements OnInit {
updateCredentials = true; updateCredentials = true;
} else { } else {
updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken; updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken;
if (updateCredentials) {
this.initialCredentials.credentialsId = securityConfig.accessToken;
}
} }
if (updateCredentials) { if (updateCredentials) {
newCredentials = { newCredentials = {

7
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -20,7 +20,8 @@
<mat-toolbar class="mat-mdc-table-toolbar"> <mat-toolbar class="mat-mdc-table-toolbar">
<h2>{{ 'gateway.connectors' | translate }}</h2> <h2>{{ 'gateway.connectors' | translate }}</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button *ngIf="dataSource?.data?.length"
mat-icon-button
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
(click)="addConnector($event)" (click)="addConnector($event)"
matTooltip="{{ 'action.add' | translate }}" matTooltip="{{ 'action.add' | translate }}"
@ -182,6 +183,10 @@
formControlName="basicConfig" formControlName="basicConfig"
[generalTabContent]="generalTabContent"> [generalTabContent]="generalTabContent">
</tb-opc-ua-basic-config> </tb-opc-ua-basic-config>
<tb-modbus-basic-config *ngSwitchCase="connectorType.MODBUS"
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</tb-modbus-basic-config>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-template #defaultConfig> <ng-template #defaultConfig>

9
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -50,6 +50,7 @@ import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { import {
AddConnectorConfigData, AddConnectorConfigData,
ConnectorBaseConfig,
ConnectorConfigurationModes, ConnectorConfigurationModes,
ConnectorType, ConnectorType,
GatewayConnector, GatewayConnector,
@ -92,7 +93,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
allowBasicConfig = new Set<ConnectorType>([ allowBasicConfig = new Set<ConnectorType>([
ConnectorType.MQTT, ConnectorType.MQTT,
ConnectorType.OPCUA ConnectorType.OPCUA,
ConnectorType.MODBUS,
]); ]);
gatewayLogLevel = Object.values(GatewayLogLevel); gatewayLogLevel = Object.values(GatewayLogLevel);
@ -397,7 +399,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private clearOutConnectorForm(): void { private clearOutConnectorForm(): void {
this.initialConnector = null; this.initialConnector = null;
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
this.connectorForm.setValue({ this.connectorForm.setValue({
mode: ConnectorConfigurationModes.BASIC, mode: ConnectorConfigurationModes.BASIC,
name: '', name: '',
@ -719,7 +720,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
connector.key = 'auto'; connector.key = 'auto';
} }
if (!connector.configurationJson) { if (!connector.configurationJson) {
connector.configurationJson = {}; connector.configurationJson = {} as ConnectorBaseConfig;
} }
connector.basicConfig = connector.configurationJson; connector.basicConfig = connector.configurationJson;
@ -732,7 +733,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
switch (connector.type) { switch (connector.type) {
case ConnectorType.MQTT: case ConnectorType.MQTT:
case ConnectorType.OPCUA: case ConnectorType.OPCUA:
case ConnectorType.MODBUS:
this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true}); this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true});
this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false});
setTimeout(() => { setTimeout(() => {
this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC}); this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC});

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html

@ -31,15 +31,15 @@
<input matInput formControlName="requestTopicExpression" <input matInput formControlName="requestTopicExpression"
placeholder="sensor/${deviceName}/request/${methodName}/${requestId}"/> placeholder="sensor/${deviceName}/request/${methodName}/${requestId}"/>
</mat-form-field> </mat-form-field>
<mat-slide-toggle class="margin" (click)="$event.stopPropagation()" [formControl]="isMQTTWithResponse"> <mat-slide-toggle class="margin" (click)="$event.stopPropagation()" formControlName="withResponse">
{{ 'gateway.rpc.withResponse' | translate }} {{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
<mat-form-field *ngIf="isMQTTWithResponse.value"> <mat-form-field *ngIf="commandForm.get('withResponse')?.value">
<mat-label>{{ 'gateway.rpc.responseTopicExpression' | translate }}</mat-label> <mat-label>{{ 'gateway.rpc.responseTopicExpression' | translate }}</mat-label>
<input matInput formControlName="responseTopicExpression" <input matInput formControlName="responseTopicExpression"
placeholder="sensor/${deviceName}/response/${methodName}/${requestId}"/> placeholder="sensor/${deviceName}/response/${methodName}/${requestId}"/>
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="isMQTTWithResponse.value"> <mat-form-field *ngIf="commandForm.get('withResponse')?.value">
<mat-label>{{ 'gateway.rpc.responseTimeout' | translate }}</mat-label> <mat-label>{{ 'gateway.rpc.responseTimeout' | translate }}</mat-label>
<input matInput formControlName="responseTimeout" type="number" <input matInput formControlName="responseTimeout" type="number"
placeholder="10000" min="10" step="1"/> placeholder="10000" min="10" step="1"/>

32
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts

@ -53,7 +53,7 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component'; } from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component'; import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { takeUntil, tap } from "rxjs/operators"; import { filter, takeUntil, tap } from "rxjs/operators";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
@Component({ @Component({
@ -80,7 +80,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter(); saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter();
commandForm: FormGroup; commandForm: FormGroup;
isMQTTWithResponse: FormControl;
codesArray: Array<number> = [1, 2, 3, 4, 5, 6, 15, 16]; codesArray: Array<number> = [1, 2, 3, 4, 5, 6, 15, 16];
ConnectorType = ConnectorType; ConnectorType = ConnectorType;
modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[]; modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[];
@ -135,7 +134,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
this.propagateChange({...this.commandForm.value, ...value}); this.propagateChange({...this.commandForm.value, ...value});
} }
}); });
this.isMQTTWithResponse = this.fb.control(false);
this.observeMQTTWithResponse(); this.observeMQTTWithResponse();
} }
@ -153,9 +151,10 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], requestTopicExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicExpression: [{ value: null, disabled: true }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], responseTopicExpression: [{ value: null, disabled: true }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTimeout: [null, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]], responseTimeout: [{ value: null, disabled: true }, [Validators.min(10), Validators.pattern(this.numbersOnlyPattern)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
}) withResponse: [false, []],
});
break; break;
case ConnectorType.MODBUS: case ConnectorType.MODBUS:
formGroup = this.fb.group({ formGroup = this.fb.group({
@ -407,12 +406,21 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
} }
private observeMQTTWithResponse(): void { private observeMQTTWithResponse(): void {
this.isMQTTWithResponse.valueChanges.pipe( if (this.connectorType === ConnectorType.MQTT) {
tap((isActive: boolean) => { this.commandForm.get('withResponse').valueChanges.pipe(
const responseControl = this.commandForm.get('responseTopicExpression'); tap((isActive: boolean) => {
isActive ? responseControl.enable() : responseControl.disable(); const responseTopicControl = this.commandForm.get('responseTopicExpression');
}), const responseTimeoutControl = this.commandForm.get('responseTimeout');
takeUntil(this.destroy$), if (isActive) {
).subscribe(); responseTopicControl.enable();
responseTimeoutControl.enable();
} else {
responseTopicControl.disable();
responseTimeoutControl.disable();
}
}),
takeUntil(this.destroy$),
).subscribe();
}
} }
} }

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts

@ -206,9 +206,6 @@ export class GatewayServiceRPCComponent implements OnInit {
private updateTemplates() { private updateTemplates() {
this.templates = this.subscription.data[0].data[0][1].length ? this.templates = this.subscription.data[0].data[0][1].length ?
JSON.parse(this.subscription.data[0].data[0][1]) : []; JSON.parse(this.subscription.data[0].data[0][1]) : [];
if (this.templates.length && this.commandForm.get('params').value == "{}") {
this.commandForm.get('params').patchValue(this.templates[0].config);
}
this.cd.detectChanges(); this.cd.detectChanges();
} }

18
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html

@ -17,7 +17,7 @@
--> -->
<div class="statistics-container" fxLayout="row" fxLayout.lt-md="column"> <div class="statistics-container" fxLayout="row" fxLayout.lt-md="column">
<mat-card [formGroup]="statisticForm" *ngIf="!general"> <mat-card [formGroup]="statisticForm" *ngIf="!general">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label>{{ 'gateway.statistics.statistic' | translate }}</mat-label> <mat-label>{{ 'gateway.statistics.statistic' | translate }}</mat-label>
<mat-select formControlName="statisticKey"> <mat-select formControlName="statisticKey">
<mat-option *ngFor="let key of statisticsKeys" [value]="key"> <mat-option *ngFor="let key of statisticsKeys" [value]="key">
@ -30,8 +30,13 @@
</mat-form-field> </mat-form-field>
<mat-error <mat-error
*ngIf="!statisticsKeys.length && !commands.length"> *ngIf="!statisticsKeys.length && !commands.length">
{{'gateway.statistics.statistic-commands-empty' | translate }} {{ 'gateway.statistics.statistic-commands-empty' | translate }}
</mat-error> </mat-error>
<div>
<button mat-flat-button color="primary" (click)="navigateToStatistics()">
{{ 'gateway.statistics.statistics-button' | translate }}
</button>
</div>
<mat-form-field class="mat-block" *ngIf="commandObj"> <mat-form-field class="mat-block" *ngIf="commandObj">
<mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<input matInput [value]="commandObj.command" disabled> <input matInput [value]="commandObj.command" disabled>
@ -43,13 +48,16 @@
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="0"> <ng-container matColumnDef="0">
<mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.gateway.created-time' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.gateway.created-time' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index"> <mat-cell *matCellDef="let row; let rowIndex = index">
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }} {{ row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="1"> <ng-container matColumnDef="1">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header
style="width: 70%">{{ 'widgets.gateway.message' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let row"> <mat-cell *matCellDef="let row">
{{ row[1] }} {{ row[1] }}
</mat-cell> </mat-cell>

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

@ -28,6 +28,7 @@
height: 100%; height: 100%;
margin-right: 35px; margin-right: 35px;
padding: 15px; padding: 15px;
gap: 22px;
} }
@media only screen and (max-width: 750px) { @media only screen and (max-width: 750px) {

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts

@ -32,6 +32,7 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { deepClone } from '@core/utils';
@Component({ @Component({
selector: 'tb-gateway-statistics', selector: 'tb-gateway-statistics',
@ -135,6 +136,11 @@ export class GatewayStatisticsComponent implements AfterViewInit {
} }
} }
public navigateToStatistics() {
const params = deepClone(this.ctx.stateController.getStateParams());
this.ctx.stateController.openState('configuration', params);
}
public sortData() { public sortData() {
this.dataSource.sortData(this.dataSource.data, this.sort); this.dataSource.sortData(this.dataSource.data, this.sort);
} }

315
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -174,13 +174,25 @@ export interface ConnectorSecurity {
export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping; export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping;
export interface ConnectorBaseConfig { export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue;
mapping?: DeviceConnectorMapping[];
dataMapping?: ConverterConnectorMapping[]; export type ConnectorBaseConfig = MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig;
requestsMapping?: Record<RequestType, RequestMappingData> | RequestMappingData[];
server?: ServerConfig; export interface MQTTBasicConfig {
broker?: BrokerConfig; dataMapping: ConverterConnectorMapping[];
workers?: WorkersConfig; requestsMapping: Record<RequestType, RequestMappingData> | RequestMappingData[];
broker: BrokerConfig;
workers: WorkersConfig;
}
export interface OPCBasicConfig {
mapping: DeviceConnectorMapping[];
server: ServerConfig;
}
export interface ModbusBasicConfig {
master: ModbusMasterConfig;
slave: ModbusSlave;
} }
export interface WorkersConfig { export interface WorkersConfig {
@ -223,7 +235,7 @@ export interface AttributesUpdate {
value: string; value: string;
} }
interface Converter { export interface Converter {
type: ConvertorType; type: ConvertorType;
deviceNameJsonExpression: string; deviceNameJsonExpression: string;
deviceTypeJsonExpression: string; deviceTypeJsonExpression: string;
@ -239,14 +251,20 @@ export interface ConverterConnectorMapping {
converter: Converter; converter: Converter;
} }
export type ConverterMappingFormValue = Omit<ConverterConnectorMapping, 'converter'> & {
converter: {
type: ConvertorType;
} & Record<ConvertorType, Converter>;
};
export interface DeviceConnectorMapping { export interface DeviceConnectorMapping {
deviceNodePattern: string; deviceNodePattern: string;
deviceNodeSource: string; deviceNodeSource: string;
deviceInfo: DeviceInfo; deviceInfo: DeviceInfo;
attributes: Attribute[]; attributes?: Attribute[];
timeseries: Timeseries[]; timeseries?: Timeseries[];
rpc_methods: RpcMethod[]; rpc_methods?: RpcMethod[];
attributes_updates: AttributesUpdate[]; attributes_updates?: AttributesUpdate[];
} }
export enum ConnectorType { export enum ConnectorType {
@ -475,6 +493,11 @@ export interface MappingInfo {
buttonTitle: string; buttonTitle: string;
} }
export interface ModbusSlaveInfo {
value: SlaveConfig;
buttonTitle: string;
}
export enum ConnectorConfigurationModes { export enum ConnectorConfigurationModes {
BASIC = 'basic', BASIC = 'basic',
ADVANCED = 'advanced' ADVANCED = 'advanced'
@ -540,6 +563,14 @@ export const MappingHintTranslationsMap = new Map<MappingType, string>(
] ]
); );
export const HelpLinkByMappingTypeMap = new Map<MappingType, string>(
[
[MappingType.DATA, 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping'],
[MappingType.OPCUA, 'https://thingsboard.io/docs/iot-gateway/config/opc-ua/#section-mapping'],
[MappingType.REQUESTS, 'https://thingsboard.io/docs/iot-gateway/config/mqtt/#section-mapping']
]
);
export const QualityTypes = [0, 1 ,2]; export const QualityTypes = [0, 1 ,2];
export const QualityTypeTranslationsMap = new Map<number, string>( export const QualityTypeTranslationsMap = new Map<number, string>(
@ -597,6 +628,10 @@ export interface RequestMappingData {
requestValue: RequestDataItem; requestValue: RequestDataItem;
} }
export type RequestMappingFormValue = Omit<RequestMappingData, 'requestValue'> & {
requestValue: Record<RequestType, RequestDataItem>;
};
export interface RequestDataItem { export interface RequestDataItem {
type: string; type: string;
details: string; details: string;
@ -739,3 +774,259 @@ export const SecurityPolicyTypes = [
{ value: SecurityPolicy.BASIC256, name: 'Basic256' }, { value: SecurityPolicy.BASIC256, name: 'Basic256' },
{ value: SecurityPolicy.BASIC256SHA, name: 'Basic256SHA256' } { value: SecurityPolicy.BASIC256SHA, name: 'Basic256SHA256' }
]; ];
export enum ModbusProtocolType {
TCP = 'tcp',
UDP = 'udp',
Serial = 'serial',
}
export const ModbusProtocolLabelsMap = new Map<ModbusProtocolType, string>(
[
[ModbusProtocolType.TCP, 'TCP'],
[ModbusProtocolType.UDP, 'UDP'],
[ModbusProtocolType.Serial, 'Serial'],
]
);
export enum ModbusMethodType {
SOCKET = 'socket',
RTU = 'rtu',
}
export enum ModbusSerialMethodType {
RTU = 'rtu',
ASCII = 'ascii',
}
export const ModbusMethodLabelsMap = new Map<ModbusMethodType | ModbusSerialMethodType, string>(
[
[ModbusMethodType.SOCKET, 'Socket'],
[ModbusMethodType.RTU, 'RTU'],
[ModbusSerialMethodType.ASCII, 'ASCII'],
]
);
export const ModbusByteSizes = [5, 6, 7 ,8];
export enum ModbusParity {
Even = 'E',
Odd = 'O',
None = 'N'
}
export const ModbusParityLabelsMap = new Map<ModbusParity, string>(
[
[ModbusParity.Even, 'Even'],
[ModbusParity.Odd, 'Odd'],
[ModbusParity.None, 'None'],
]
);
export enum ModbusOrderType {
BIG = 'BIG',
LITTLE = 'LITTLE',
}
export enum ModbusRegisterType {
HoldingRegisters = 'holding_registers',
CoilsInitializer = 'coils_initializer',
InputRegisters = 'input_registers',
DiscreteInputs = 'discrete_inputs'
}
export const ModbusRegisterTranslationsMap = new Map<ModbusRegisterType, string>(
[
[ModbusRegisterType.HoldingRegisters, 'gateway.holding_registers'],
[ModbusRegisterType.CoilsInitializer, 'gateway.coils_initializer'],
[ModbusRegisterType.InputRegisters, 'gateway.input_registers'],
[ModbusRegisterType.DiscreteInputs, 'gateway.discrete_inputs']
]
);
export enum ModbusDataType {
STRING = 'string',
BYTES = 'bytes',
BITS = 'bits',
INT8 = '8int',
UINT8 = '8uint',
FLOAT8 = '8float',
INT16 = '16int',
UINT16 = '16uint',
FLOAT16 = '16float',
INT32 = '32int',
UINT32 = '32uint',
FLOAT32 = '32float',
INT64 = '64int',
UINT64 = '64uint',
FLOAT64 = '64float'
}
export enum ModbusObjectCountByDataType {
'8int' = 1,
'8uint' = 1,
'8float' = 1,
'16int' = 1,
'16uint' = 1,
'16float' = 1,
'32int' = 2,
'32uint' = 2,
'32float' = 2,
'64int' = 4,
'64uint' = 4,
'64float' = 4,
}
export enum ModbusValueKey {
ATTRIBUTES = 'attributes',
TIMESERIES = 'timeseries',
ATTRIBUTES_UPDATES = 'attributeUpdates',
RPC_REQUESTS = 'rpc',
}
export const ModbusKeysPanelTitleTranslationsMap = new Map<ModbusValueKey, string>(
[
[ModbusValueKey.ATTRIBUTES, 'gateway.attributes'],
[ModbusValueKey.TIMESERIES, 'gateway.timeseries'],
[ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.attribute-updates'],
[ModbusValueKey.RPC_REQUESTS, 'gateway.rpc-requests']
]
);
export const ModbusKeysAddKeyTranslationsMap = new Map<ModbusValueKey, string>(
[
[ModbusValueKey.ATTRIBUTES, 'gateway.add-attribute'],
[ModbusValueKey.TIMESERIES, 'gateway.add-timeseries'],
[ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.add-attribute-update'],
[ModbusValueKey.RPC_REQUESTS, 'gateway.add-rpc-request']
]
);
export const ModbusKeysDeleteKeyTranslationsMap = new Map<ModbusValueKey, string>(
[
[ModbusValueKey.ATTRIBUTES, 'gateway.delete-attribute'],
[ModbusValueKey.TIMESERIES, 'gateway.delete-timeseries'],
[ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.delete-attribute-update'],
[ModbusValueKey.RPC_REQUESTS, 'gateway.delete-rpc-request']
]
);
export const ModbusKeysNoKeysTextTranslationsMap = new Map<ModbusValueKey, string>(
[
[ModbusValueKey.ATTRIBUTES, 'gateway.no-attributes'],
[ModbusValueKey.TIMESERIES, 'gateway.no-timeseries'],
[ModbusValueKey.ATTRIBUTES_UPDATES, 'gateway.no-attribute-updates'],
[ModbusValueKey.RPC_REQUESTS, 'gateway.no-rpc-requests']
]
);
export const ModbusFunctionCodeTranslationsMap = new Map<number, string>(
[
[1, 'gateway.read-coils'],
[2, 'gateway.read-discrete-inputs'],
[3, 'gateway.read-multiple-holding-registers'],
[4, 'gateway.read-input-registers'],
[5, 'gateway.write-coil'],
[6, 'gateway.write-register'],
[15, 'gateway.write-coils'],
[16, 'gateway.write-registers'],
]
);
export interface ModbusMasterConfig {
slaves: SlaveConfig[];
}
export interface SlaveConfig {
name: string;
host?: string;
port: string | number;
serialPort?: string;
type: ModbusProtocolType;
method: ModbusMethodType;
timeout: number;
byteOrder: ModbusOrderType;
wordOrder: ModbusOrderType;
retries: boolean;
retryOnEmpty: boolean;
retryOnInvalid: boolean;
pollPeriod: number;
unitId: number;
deviceName: string;
deviceType: string;
sendDataOnlyOnChange: boolean;
connectAttemptTimeMs: number;
connectAttemptCount: number;
waitAfterFailedAttemptsMs: number;
attributes: ModbusValue[];
timeseries: ModbusValue[];
attributeUpdates: ModbusValue[];
rpc: ModbusValue[];
security?: ModbusSecurity;
baudrate?: number;
stopbits?: number;
bytesize?: number;
parity?: ModbusParity;
strict?: boolean;
}
export interface ModbusValue {
tag: string;
type: ModbusDataType;
functionCode?: number;
objectsCount: number;
address: number;
value?: string;
}
export interface ModbusSecurity {
certfile?: string;
keyfile?: string;
password?: string;
server_hostname?: string;
reqclicert?: boolean;
}
export interface ModbusSlave {
host?: string;
type: ModbusProtocolType;
method: ModbusMethodType;
unitId: number;
serialPort?: string;
baudrate?: number;
deviceName: string;
deviceType: string;
pollPeriod: number;
sendDataToThingsBoard: boolean;
byteOrder: ModbusOrderType;
identity: ModbusIdentity;
values: ModbusValuesState;
port: string | number;
security: ModbusSecurity;
}
export type ModbusValuesState = ModbusRegisterValues | ModbusValues;
export interface ModbusRegisterValues {
holding_registers: ModbusValues;
coils_initializer: ModbusValues;
input_registers: ModbusValues;
discrete_inputs: ModbusValues;
}
export interface ModbusValues {
attributes: ModbusValue[];
timeseries: ModbusValue[];
attributeUpdates: ModbusValue[];
rpc: ModbusValue[];
}
export interface ModbusIdentity {
vendorName?: string;
productCode?: string;
vendorUrl?: string;
productName?: string;
modelName?: string;
}
export const ModbusBaudrates = [4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600];

0
ui-ngx/src/app/modules/home/pipes/gateway-help-link/gateway-help-link.pipe.ts → ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-help-link.pipe.ts

42
ui-ngx/src/app/modules/home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe.ts

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2024 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 { Pipe, PipeTransform } from '@angular/core';
import { PortLimits } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { AbstractControl } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
@Pipe({
name: 'getGatewayPortTooltip',
standalone: true,
})
export class GatewayPortTooltipPipe implements PipeTransform {
constructor(private translate: TranslateService) {}
transform(portControl: AbstractControl): string {
if (portControl.hasError('required')) {
return this.translate.instant('gateway.port-required');
}
if (portControl.hasError('min') || portControl.hasError('max')) {
return this.translate.instant('gateway.port-limits-error', {
min: PortLimits.MIN,
max: PortLimits.MAX,
});
}
return '';
}
}

5
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.html

@ -48,7 +48,7 @@
{{ 'widgets.recent-dashboards.name' | translate }} {{ 'widgets.recent-dashboards.name' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell class="title" *matCellDef="let lastVisitedDashboard"> <mat-cell class="title" *matCellDef="let lastVisitedDashboard">
<a routerLink="/dashboards/{{lastVisitedDashboard.id}}">{{ lastVisitedDashboard.title }}</a> <a [routerLink]="createDashboardUrl(lastVisitedDashboard.id)">{{ lastVisitedDashboard.title }}</a>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="lastVisited"> <ng-container matColumnDef="lastVisited">
@ -78,7 +78,7 @@
class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon> class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</div> </div>
<div class="tb-cell title"> <div class="tb-cell title">
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a> <a [routerLink]="createDashboardUrl(dashboard.id)">{{ dashboard.title }}</a>
</div> </div>
</div> </div>
</div> </div>
@ -87,6 +87,7 @@
subscriptSizing="dynamic" subscriptSizing="dynamic"
appearance="outline" appearance="outline"
[useIdValue]="false" [useIdValue]="false"
[customerId]="customerId"
label="" label=""
placeholder="{{ 'dashboard.select-dashboard' | translate }}" placeholder="{{ 'dashboard.select-dashboard' | translate }}"
[(ngModel)]="starredDashboardValue" (ngModelChange)="onStarDashboard($event)"></tb-dashboard-autocomplete> [(ngModel)]="starredDashboardValue" (ngModelChange)="onStarDashboard($event)"></tb-dashboard-autocomplete>

18
ui-ngx/src/app/modules/home/components/widget/lib/home-page/recent-dashboards-widget.component.ts

@ -21,7 +21,8 @@ import {
Input, Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
QueryList, ViewChild, QueryList,
ViewChild,
ViewChildren ViewChildren
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -29,11 +30,12 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthState, getCurrentAuthUser } from '@core/auth/auth.selectors';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
import { import {
AbstractUserDashboardInfo, AbstractUserDashboardInfo,
LastVisitedDashboardInfo, StarredDashboardInfo, LastVisitedDashboardInfo,
StarredDashboardInfo,
UserDashboardAction, UserDashboardAction,
UserDashboardsInfo UserDashboardsInfo
} from '@shared/models/user-settings.models'; } from '@shared/models/user-settings.models';
@ -77,6 +79,8 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
hasDashboardsAccess = true; hasDashboardsAccess = true;
dirty = false; dirty = false;
public customerId: string;
private isFullscreenMode = getCurrentAuthState(this.store).forceFullscreen;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
@ -85,6 +89,9 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
} }
ngOnInit() { ngOnInit() {
if (this.authUser.authority === Authority.CUSTOMER_USER) {
this.customerId = this.authUser.customerId;
}
this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority); this.hasDashboardsAccess = [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER].includes(this.authUser.authority);
if (this.hasDashboardsAccess) { if (this.hasDashboardsAccess) {
this.reload(); this.reload();
@ -110,6 +117,11 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
); );
} }
public createDashboardUrl(id: string): string {
const baseUrl = this.isFullscreenMode ? '/dashboard/' : '/dashboards/';
return baseUrl + id;
}
toggleValueChange(value: 'last' | 'starred') { toggleValueChange(value: 'last' | 'starred') {
this.toggleValue = value; this.toggleValue = value;
if (this.dirty) { if (this.dirty) {

215
ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts

@ -24,7 +24,7 @@ import {
SetValueSettings, SetValueSettings,
ValueToDataType ValueToDataType
} from '@shared/models/action-widget-settings.models'; } from '@shared/models/action-widget-settings.models';
import { Circle, Effect, Element, G, Gradient, Runner, Svg, Text, Timeline } from '@svgdotjs/svg.js'; import { Circle, Effect, Element, G, Gradient, Path, Runner, Svg, Text, Timeline } from '@svgdotjs/svg.js';
import '@svgdotjs/svg.filter.js'; import '@svgdotjs/svg.filter.js';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { WidgetContext } from '@home/models/widget-component.models'; import { WidgetContext } from '@home/models/widget-component.models';
@ -35,7 +35,10 @@ export enum PowerButtonLayout {
outlined = 'outlined', outlined = 'outlined',
default_volume = 'default_volume', default_volume = 'default_volume',
simplified_volume = 'simplified_volume', simplified_volume = 'simplified_volume',
outlined_volume = 'outlined_volume' outlined_volume = 'outlined_volume',
default_icon = 'default_icon',
simplified_icon = 'simplified_icon',
outlined_icon = 'outlined_icon'
} }
export const powerButtonLayouts = Object.keys(PowerButtonLayout) as PowerButtonLayout[]; export const powerButtonLayouts = Object.keys(PowerButtonLayout) as PowerButtonLayout[];
@ -47,7 +50,10 @@ export const powerButtonLayoutTranslations = new Map<PowerButtonLayout, string>(
[PowerButtonLayout.outlined, 'widgets.power-button.layout-outlined'], [PowerButtonLayout.outlined, 'widgets.power-button.layout-outlined'],
[PowerButtonLayout.default_volume, 'widgets.power-button.layout-default-volume'], [PowerButtonLayout.default_volume, 'widgets.power-button.layout-default-volume'],
[PowerButtonLayout.simplified_volume, 'widgets.power-button.layout-simplified-volume'], [PowerButtonLayout.simplified_volume, 'widgets.power-button.layout-simplified-volume'],
[PowerButtonLayout.outlined_volume, 'widgets.power-button.layout-outlined-volume'] [PowerButtonLayout.outlined_volume, 'widgets.power-button.layout-outlined-volume'],
[PowerButtonLayout.default_icon, 'widgets.power-button.layout-default-icon'],
[PowerButtonLayout.simplified_icon, 'widgets.power-button.layout-simplified-icon'],
[PowerButtonLayout.outlined_icon, 'widgets.power-button.layout-outlined-icon']
] ]
); );
@ -58,7 +64,10 @@ export const powerButtonLayoutImages = new Map<PowerButtonLayout, string>(
[PowerButtonLayout.outlined, 'assets/widget/power-button/outlined-layout.svg'], [PowerButtonLayout.outlined, 'assets/widget/power-button/outlined-layout.svg'],
[PowerButtonLayout.default_volume, 'assets/widget/power-button/default-volume-layout.svg'], [PowerButtonLayout.default_volume, 'assets/widget/power-button/default-volume-layout.svg'],
[PowerButtonLayout.simplified_volume, 'assets/widget/power-button/simplified-volume-layout.svg'], [PowerButtonLayout.simplified_volume, 'assets/widget/power-button/simplified-volume-layout.svg'],
[PowerButtonLayout.outlined_volume, 'assets/widget/power-button/outlined-volume-layout.svg'] [PowerButtonLayout.outlined_volume, 'assets/widget/power-button/outlined-volume-layout.svg'],
[PowerButtonLayout.default_icon, 'assets/widget/power-button/default-icon-layout.svg'],
[PowerButtonLayout.simplified_icon, 'assets/widget/power-button/simplified-icon-layout.svg'],
[PowerButtonLayout.outlined_icon, 'assets/widget/power-button/outlined-icon-layout.svg']
] ]
); );
@ -219,6 +228,21 @@ export const powerButtonShapeSize = 110;
const cx = powerButtonShapeSize / 2; const cx = powerButtonShapeSize / 2;
const cy = powerButtonShapeSize / 2; const cy = powerButtonShapeSize / 2;
const powerCircle = 'M13 4.67063C13 3.65748 12.0377 2.91866 11.0946 3.28889C4.59815 5.83928 0 12.1545 0 19.5412C0 29.1835 ' +
'7.83502 37.0001 17.5 37.0001C27.165 37.0001 35 29.1835 35 19.5412C35 12.1545 30.4019 5.83928 23.9054 3.28889C22.9623 2.91866 22 ' +
'3.65748 22 4.67063C22 5.33931 22.434 5.92434 23.0519 6.17991C28.3077 8.35375 32 13.5209 32 19.5412C32 27.52 25.5148 34.0001 17.5 ' +
'34.0001C9.48521 34.0001 3 27.52 3 19.5412C3 13.5209 6.69234 8.35374 11.9481 6.17991C12.566 5.92434 13 5.33931 13 4.67063Z';
const powerLine = 'M16.5 1C16.5 0.447716 16.9477 0 17.5 0C18.0523 0 18.5 0.447715 18.5 1V17C18.5 ' +
'17.5523 18.0523 18 17.5 18C16.9477 18 16.5 17.5523 16.5 17V1Z';
const powerCircleStroke = 'M12 2.95698C12 1.45236 10.4775 0.438524 9.19424 1.22416C3.68417 4.59764 0 10.7283 0 17.7316C0 ' +
'28.3732 8.50659 37 19 37C29.4934 37 38 28.3732 38 17.7316C38 10.7283 34.3158 4.59764 28.8058 1.22416C27.5225 ' +
'0.438524 26 1.45236 26 2.95698C26 3.73878 26.4365 4.44718 27.0911 4.87461C31.2354 7.58066 34 12.3083 34 ' +
'17.7316C34 26.2172 27.2316 33 19 33C10.7684 33 4 26.2172 4 17.7316C4 12.3084 6.76462 7.58066 10.9089 ' +
'4.87461C11.5635 4.44718 12 3.73878 12 2.95698Z';
const powerLineStroke = 'M0 2.5C0 1.11929 1.11929 0 2.5 0C3.88071 0 5 1.11929 5 2.5V15.5C5 16.8807 3.88071 18 ' +
'2.5 18C1.11929 18 0 16.8807 0 15.5V2.5Z';
const powerButtonAnimation = (element: Element): Runner => element.animate(200, 0, 'now'); const powerButtonAnimation = (element: Element): Runner => element.animate(200, 0, 'now');
export abstract class PowerButtonShape { export abstract class PowerButtonShape {
@ -242,6 +266,12 @@ export abstract class PowerButtonShape {
return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.outlined_volume: case PowerButtonLayout.outlined_volume:
return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.default_icon:
return new DefaultIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.simplified_icon:
return new SimplifiedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.outlined_icon:
return new OutlinedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
} }
} }
@ -705,6 +735,31 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
private pressedTimeline: Timeline; private pressedTimeline: Timeline;
private centerGroup: G; private centerGroup: G;
protected drawOffCenter(centerGroup: G) {
this.offLabelShape = this.createOffLabel('400').addTo(centerGroup);
}
protected drawOnCenter() {
this.onLabelShape = this.createOnLabel('400');
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape,[this.onLabelShape]);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onLabelShape.timeline(pressedTimeline);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}});
}
protected drawShape() { protected drawShape() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0}); .fill({opacity: 0}).stroke({width: 0});
@ -723,15 +778,15 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
add.stop(1, '#FFFFFF', 1); add.stop(1, '#FFFFFF', 1);
}).from(0.832, 0.1188).to(0.268, 0.92); }).from(0.832, 0.1188).to(0.268, 0.92);
this.centerGroup = this.svgShape.group(); this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel('400').addTo(this.centerGroup); this.drawOffCenter(this.centerGroup);
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy);
this.onLabelShape = this.createOnLabel('400'); this.drawOnCenter();
this.createMask(this.onCircleShape, [this.onLabelShape]); this.addOnCenterToMask(this.onCircleShape);
this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3); this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3);
this.pressedTimeline = new Timeline(); this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline); this.centerGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline); this.addOnCenterTimeLine(this.pressedTimeline);
this.innerShadow.timeline(this.pressedTimeline); this.innerShadow.timeline(this.pressedTimeline);
} }
@ -751,7 +806,7 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
this.innerBorder.fill(this.innerBorderGradient); this.innerBorder.fill(this.innerBorderGradient);
this.innerBorder.attr({ 'fill-opacity': 1 }); this.innerBorder.attr({ 'fill-opacity': 1 });
} }
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); this.drawOffCenterColor(mainColor);
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} }
@ -776,14 +831,14 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
this.innerShadow.show(); this.innerShadow.show();
const pressedScale = 0.75; const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}}); this.onCenterAnimation(pressedScale);
this.innerShadow.animate(6, 0.6); this.innerShadow.animate(6, 0.6);
} }
protected onPressEnd() { protected onPressEnd() {
this.pressedTimeline.finish(); this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1}); powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); this.onCenterAnimation(1);
this.innerShadow.animateRestore().after(() => { this.innerShadow.animateRestore().after(() => {
if (this.disabled) { if (this.disabled) {
this.innerShadow.hide(); this.innerShadow.hide();
@ -793,6 +848,42 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
} }
class DefaultIconPowerButtonShape extends DefaultVolumePowerButtonShape {
private offPowerSymbolCircle: Path;
private offPowerSymbolLine: Path;
private onPowerSymbolCircle: Path;
private onPowerSymbolLine: Path;
protected drawOffCenter(centerGroup: G) {
this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup);
}
protected drawOnCenter() {
this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy);
this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12);
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onPowerSymbolCircle.timeline(pressedTimeline);
this.onPowerSymbolLine.timeline(pressedTimeline);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}});
powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}});
}
}
class SimplifiedVolumePowerButtonShape extends PowerButtonShape { class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
private outerBorder: Circle; private outerBorder: Circle;
@ -805,6 +896,10 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
private centerGroup: G; private centerGroup: G;
private onCenterGroup: G; private onCenterGroup: G;
protected drawCenterGroup(centerGroup: G, onCenterGroup: G) {
this.offLabelShape = this.createOffLabel().addTo(centerGroup);
this.onLabelShape = this.createOnLabel().addTo(onCenterGroup);
}
protected drawShape() { protected drawShape() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
@ -812,9 +907,8 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy); this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy);
this.createMask(this.outerBorder, [this.outerBorderMask]); this.createMask(this.outerBorder, [this.outerBorderMask]);
this.centerGroup = this.svgShape.group(); this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel().addTo(this.centerGroup);
this.onCenterGroup = this.svgShape.group(); this.onCenterGroup = this.svgShape.group();
this.onLabelShape = this.createOnLabel().addTo(this.onCenterGroup); this.drawCenterGroup(this.centerGroup, this.onCenterGroup);
this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 3, 0.3); this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 3, 0.3);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0); this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0);
this.pressedTimeline = new Timeline(); this.pressedTimeline = new Timeline();
@ -867,6 +961,27 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
} }
} }
class SimplifiedIconPowerButtonShape extends SimplifiedVolumePowerButtonShape {
private offPowerSymbolCircle: Path;
private offPowerSymbolLine: Path;
private onPowerSymbolCircle: Path;
private onPowerSymbolLine: Path;
protected drawCenterGroup(centerGroup: G, onCenterGroup: G) {
this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup);
this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(onCenterGroup);
this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(onCenterGroup);
}
protected drawColorState(mainColor: PowerButtonColor) {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
}
class OutlinedVolumePowerButtonShape extends PowerButtonShape { class OutlinedVolumePowerButtonShape extends PowerButtonShape {
private outerBorder: Circle; private outerBorder: Circle;
private outerBorderMask: Circle; private outerBorderMask: Circle;
@ -881,6 +996,30 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
private centerGroup: G; private centerGroup: G;
private onCenterGroup: G; private onCenterGroup: G;
protected drawOffCenter(centerGroup: G) {
this.offLabelShape = this.createOffLabel('800').addTo(centerGroup);
}
protected drawOnCenter() {
this.onLabelShape = this.createOnLabel('800');
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape,[this.onLabelShape]);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onLabelShape.timeline(pressedTimeline);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}});
}
protected drawShape() { protected drawShape() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0}); .fill({opacity: 0}).stroke({width: 0});
@ -895,19 +1034,19 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy); this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy);
this.createMask(this.innerBorder, [this.innerBorderMask]); this.createMask(this.innerBorder, [this.innerBorderMask]);
this.centerGroup = this.svgShape.group(); this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel('800').addTo(this.centerGroup); this.drawOffCenter(this.centerGroup);
this.onCenterGroup = this.svgShape.group(); this.onCenterGroup = this.svgShape.group();
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy) this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy)
.addTo(this.onCenterGroup); .addTo(this.onCenterGroup);
this.onLabelShape = this.createOnLabel('800'); this.drawOnCenter();
this.createMask(this.onCircleShape, [this.onLabelShape]); this.addOnCenterToMask(this.onCircleShape);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0); this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0);
this.backgroundShape.addClass('tb-small-shadow'); this.backgroundShape.addClass('tb-small-shadow');
this.pressedTimeline = new Timeline(); this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline); this.centerGroup.timeline(this.pressedTimeline);
this.onCenterGroup.timeline(this.pressedTimeline); this.onCenterGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline); this.addOnCenterTimeLine(this.pressedTimeline);
this.pressedShadow.timeline(this.pressedTimeline); this.pressedShadow.timeline(this.pressedTimeline);
} }
@ -919,7 +1058,7 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
this.outerBorder.attr({ 'fill-opacity': 1 }); this.outerBorder.attr({ 'fill-opacity': 1 });
} }
this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); this.innerBorder.attr({fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); this.drawOffCenterColor(mainColor);
this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
} }
@ -940,7 +1079,7 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
const pressedScale = 0.75; const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98}); powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98});
powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}}); this.onCenterAnimation(pressedScale / 0.98);
this.pressedShadow.animate(6, 0.6); this.pressedShadow.animate(6, 0.6);
} }
@ -948,8 +1087,44 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
this.pressedTimeline.finish(); this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1}); powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onCenterGroup).transform({scale: 1}); powerButtonAnimation(this.onCenterGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); this.onCenterAnimation(1);
this.pressedShadow.animateRestore(); this.pressedShadow.animateRestore();
} }
} }
class OutlinedIconPowerButtonShape extends OutlinedVolumePowerButtonShape {
private offPowerSymbolCircle: Path;
private offPowerSymbolLine: Path;
private onPowerSymbolCircle: Path;
private onPowerSymbolLine: Path;
protected drawOffCenter(centerGroup: G) {
this.offPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy).addTo(centerGroup);
this.offPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12).addTo(centerGroup);
}
protected drawOnCenter() {
this.onPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy);
this.onPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12);
}
protected addOnCenterToMask(onCircleShape: Circle) {
this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]);
}
protected addOnCenterTimeLine(pressedTimeline: Timeline) {
this.onPowerSymbolCircle.timeline(pressedTimeline);
this.onPowerSymbolLine.timeline(pressedTimeline);
}
protected drawOffCenterColor(mainColor: PowerButtonColor) {
this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity});
}
protected onCenterAnimation(scale: number) {
powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}});
powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}});
}
}

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.html

@ -41,7 +41,9 @@
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-row no-padding no-border"> <div class="tb-form-row no-padding no-border">
<mat-slide-toggle class="mat-slide" formControlName="showCounter"> <mat-slide-toggle class="mat-slide" formControlName="showCounter">
{{ 'widgets.notification.counter' | translate }} <div tb-hint-tooltip-icon="{{'widgets.notification.counter-hint' | translate}}">
{{ 'widgets.notification.counter' | translate }}
</div>
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
@ -49,7 +51,7 @@
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="counterValueFont" <tb-font-settings formControlName="counterValueFont"
clearButton clearButton
[previewText]="unreadNotificationWidgetSettingsForm.get('maxNotificationDisplay').value.toString()" [previewText]="countPreviewFn"
[initialPreviewStyle]="widgetConfig.config.titleStyle"> [initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings> </tb-font-settings>
<tb-color-input asBoxInput <tb-color-input asBoxInput

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/unread-notification-widget-settings.component.ts

@ -30,6 +30,8 @@ export class UnreadNotificationWidgetSettingsComponent extends WidgetSettingsCom
unreadNotificationWidgetSettingsForm: UntypedFormGroup; unreadNotificationWidgetSettingsForm: UntypedFormGroup;
countPreviewFn = this._countPreviewFn.bind(this);
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder) {
super(store); super(store);
@ -78,4 +80,8 @@ export class UnreadNotificationWidgetSettingsComponent extends WidgetSettingsCom
} }
} }
private _countPreviewFn(): string {
return this.unreadNotificationWidgetSettingsForm.get('maxNotificationDisplay').value?.toString() || '6';
}
} }

8
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.html

@ -65,6 +65,14 @@
<div class="tb-small-label" translate>widgets.gauge.max-value-short</div> <div class="tb-small-label" translate>widgets.gauge.max-value-short</div>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="maxValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput formControlName="maxValue" type="number" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'widgets.gauge.min-max-validation-text' | translate"
*ngIf="digitalGaugeWidgetSettingsForm.get('maxValue').hasError('maxValue')"
class="material-icons tb-suffix-show-always tb-error">
warning
</mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>

32
ui-ngx/src/app/modules/home/components/widget/lib/settings/gauge/digital-gauge-widget-settings.component.ts

@ -16,7 +16,14 @@
import { Datasource, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; import { Datasource, WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import {
AbstractControl,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup, ValidationErrors,
ValidatorFn,
Validators
} from '@angular/forms';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge'; import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge';
@ -167,7 +174,7 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent
donutStartAngle: [settings.donutStartAngle, []], donutStartAngle: [settings.donutStartAngle, []],
showMinMax: [settings.showMinMax, []], showMinMax: [settings.showMinMax, []],
minValue: [settings.minValue, []], minValue: [settings.minValue, []],
maxValue: [settings.maxValue, []], maxValue: [settings.maxValue, [this.maxValueValidation()]],
minMaxFont: [settings.minMaxFont, []], minMaxFont: [settings.minMaxFont, []],
minMaxColor: [settings.minMaxFont.color, []], minMaxColor: [settings.minMaxFont.color, []],
@ -206,6 +213,18 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent
}); });
} }
private maxValueValidation(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value: string = control.value;
if (value) {
if (value < control.parent?.get('minValue').value) {
return {maxValue: true};
}
}
return null;
};
}
protected prepareOutputSettings(settings) { protected prepareOutputSettings(settings) {
const barColor: ColorSettings = this.digitalGaugeWidgetSettingsForm.get('barColor').value; const barColor: ColorSettings = this.digitalGaugeWidgetSettingsForm.get('barColor').value;
@ -230,10 +249,15 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent
} }
protected validatorTriggers(): string[] { protected validatorTriggers(): string[] {
return ['gaugeType', 'showTitle', 'showUnitTitle', 'showValue', 'showMinMax', 'showTimestamp', 'showTicks', 'animation']; return ['gaugeType', 'showTitle', 'showUnitTitle', 'showValue', 'showMinMax', 'showTimestamp', 'showTicks', 'animation', 'minValue'];
} }
protected updateValidators(emitEvent: boolean) { protected updateValidators(emitEvent: boolean, trigger: string) {
if (trigger === 'minValue') {
this.digitalGaugeWidgetSettingsForm.get('maxValue').updateValueAndValidity({emitEvent: true});
this.digitalGaugeWidgetSettingsForm.get('maxValue').markAsTouched({onlySelf: true});
return;
}
const gaugeType: GaugeType = this.digitalGaugeWidgetSettingsForm.get('gaugeType').value; const gaugeType: GaugeType = this.digitalGaugeWidgetSettingsForm.get('gaugeType').value;
const showTitle: boolean = this.digitalGaugeWidgetSettingsForm.get('showTitle').value; const showTitle: boolean = this.digitalGaugeWidgetSettingsForm.get('showTitle').value;
const showUnitTitle: boolean = this.digitalGaugeWidgetSettingsForm.get('showUnitTitle').value; const showUnitTitle: boolean = this.digitalGaugeWidgetSettingsForm.get('showUnitTitle').value;

52
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -90,9 +90,6 @@ import { ToggleButtonWidgetComponent } from '@home/components/widget/lib/button/
import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component'; import { TimeSeriesChartWidgetComponent } from '@home/components/widget/lib/chart/time-series-chart-widget.component';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component'; import { MappingDialogComponent } from '@home/components/widget/lib/gateway/dialog/mapping-dialog.component';
import {
EllipsisChipListDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive';
import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component'; import { StatusWidgetComponent } from '@home/components/widget/lib/indicator/status-widget.component';
import { LatestChartComponent } from '@home/components/widget/lib/chart/latest-chart.component'; import { LatestChartComponent } from '@home/components/widget/lib/chart/latest-chart.component';
import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-chart-widget.component'; import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-chart-widget.component';
@ -112,18 +109,38 @@ import {
import { import {
NotificationTypeFilterPanelComponent NotificationTypeFilterPanelComponent
} from '@home/components/widget/lib/cards/notification-type-filter-panel.component'; } from '@home/components/widget/lib/cards/notification-type-filter-panel.component';
import { GatewayHelpLinkPipe } from '@home/pipes/public-api'; import { GatewayHelpLinkPipe } from '@home/components/widget/lib/gateway/pipes/gateway-help-link.pipe';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component';
import {
WorkersConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component';
import {
MqttBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component';
import {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import { import {
BrokerConfigControlComponent, OpcUaBasicConfigComponent
DeviceInfoTableComponent, } from '@home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component';
MappingDataKeysPanelComponent, import {
MappingTableComponent, ModbusBasicConfigComponent
MqttBasicConfigComponent, } from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component';
OpcUaBasicConfigComponent, import {
ServerConfigComponent, DeviceInfoTableComponent
TypeValuePanelComponent, } from '@home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component';
WorkersConfigControlComponent, import {
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api'; MappingDataKeysPanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component';
import {
TypeValuePanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component';
import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
@NgModule({ @NgModule({
@ -164,7 +181,6 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
GatewayConfigurationComponent, GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent, GatewayServiceRPCConnectorTemplateDialogComponent,
EllipsisChipListDirective,
ValueCardWidgetComponent, ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent, AggregatedValueCardWidgetComponent,
CountWidgetComponent, CountWidgetComponent,
@ -206,11 +222,13 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
GatewayHelpLinkPipe, GatewayHelpLinkPipe,
BrokerConfigControlComponent, BrokerConfigControlComponent,
WorkersConfigControlComponent, WorkersConfigControlComponent,
ServerConfigComponent, OpcServerConfigComponent,
MqttBasicConfigComponent, MqttBasicConfigComponent,
MappingTableComponent, MappingTableComponent,
OpcUaBasicConfigComponent, OpcUaBasicConfigComponent,
KeyValueIsNotEmptyPipe KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent,
EllipsisChipListDirective,
], ],
exports: [ exports: [
EntitiesTableWidgetComponent, EntitiesTableWidgetComponent,

4
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html

@ -58,7 +58,7 @@
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block" *ngIf="entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd"> <mat-form-field class="mat-block" *ngIf="entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd">
<mat-label translate>resource.title</mat-label> <mat-label translate>resource.title</mat-label>
<input matInput formControlName="title" required> <input matInput formControlName="title" required [readonly]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL">
<mat-error *ngIf="entityForm.get('title').hasError('required')"> <mat-error *ngIf="entityForm.get('title').hasError('required')">
{{ 'resource.title-required' | translate }} {{ 'resource.title-required' | translate }}
</mat-error> </mat-error>
@ -68,7 +68,7 @@
</mat-form-field> </mat-form-field>
<tb-file-input *ngIf="isAdd || (isEdit && entityForm.get('resourceType').value === resourceType.JS_MODULE)" <tb-file-input *ngIf="isAdd || (isEdit && entityForm.get('resourceType').value === resourceType.JS_MODULE)"
formControlName="data" formControlName="data"
required [required]="isAdd"
label="{{ (entityForm.get('resourceType').value === resourceType.LWM2M_MODEL ? 'resource.resource-files' : 'resource.resource-file') | translate }}" label="{{ (entityForm.get('resourceType').value === resourceType.LWM2M_MODEL ? 'resource.resource-files' : 'resource.resource-file') | translate }}"
[readAsBinary]="true" [readAsBinary]="true"
[maxSizeByte]="maxResourceSize" [maxSizeByte]="maxResourceSize"

44
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -29,7 +29,7 @@ import {
ResourceTypeMIMETypes, ResourceTypeMIMETypes,
ResourceTypeTranslationMap ResourceTypeTranslationMap
} from '@shared/models/resource.models'; } from '@shared/models/resource.models';
import { filter, startWith, takeUntil } from 'rxjs/operators'; import { startWith, takeUntil } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
@ -59,22 +59,9 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
this.entityForm.get('resourceType').valueChanges.pipe( if (this.isAdd) {
startWith(ResourceType.JS_MODULE), this.observeResourceTypeChange();
filter(() => this.isAdd), }
takeUntil(this.destroy$)
).subscribe((type) => {
if (type === this.resourceType.LWM2M_MODEL) {
this.entityForm.get('title').disable({emitEvent: false});
this.entityForm.patchValue({title: ''}, {emitEvent: false});
} else {
this.entityForm.get('title').enable({emitEvent: false});
}
this.entityForm.patchValue({
data: null,
fileName: null
}, {emitEvent: false});
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -96,7 +83,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, Validators.required], resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, Validators.required],
fileName: [entity ? entity.fileName : null, Validators.required], fileName: [entity ? entity.fileName : null, Validators.required],
data: [entity ? entity.data : null, Validators.required] data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []]
}); });
} }
@ -105,7 +92,6 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
this.entityForm.get('resourceType').disable({emitEvent: false}); this.entityForm.get('resourceType').disable({emitEvent: false});
if (entity.resourceType !== ResourceType.JS_MODULE) { if (entity.resourceType !== ResourceType.JS_MODULE) {
this.entityForm.get('fileName').disable({emitEvent: false}); this.entityForm.get('fileName').disable({emitEvent: false});
this.entityForm.get('data').disable({emitEvent: false});
} }
} }
this.entityForm.patchValue({ this.entityForm.patchValue({
@ -153,4 +139,24 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
horizontalPosition: 'right' horizontalPosition: 'right'
})); }));
} }
private observeResourceTypeChange(): void {
this.entityForm.get('resourceType').valueChanges.pipe(
startWith(ResourceType.JS_MODULE),
takeUntil(this.destroy$)
).subscribe((type: ResourceType) => this.onResourceTypeChange(type));
}
private onResourceTypeChange(type: ResourceType): void {
if (type === this.resourceType.LWM2M_MODEL) {
this.entityForm.get('title').disable({emitEvent: false});
this.entityForm.patchValue({title: ''}, {emitEvent: false});
} else {
this.entityForm.get('title').enable({emitEvent: false});
}
this.entityForm.patchValue({
data: null,
fileName: null
}, {emitEvent: false});
}
} }

6
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.html

@ -35,8 +35,7 @@
<div formGroupName="profileData"> <div formGroupName="profileData">
<tb-device-profile-transport-configuration <tb-device-profile-transport-configuration
formControlName="transportConfiguration" formControlName="transportConfiguration"
[isAdd] = "isTransportTypeChanged" [isAdd]="isTransportTypeChanged">
required>
</tb-device-profile-transport-configuration> </tb-device-profile-transport-configuration>
</div> </div>
</div> </div>
@ -65,8 +64,7 @@
<div class="mat-padding" [formGroup]="detailsForm"> <div class="mat-padding" [formGroup]="detailsForm">
<div formGroupName="profileData"> <div formGroupName="profileData">
<tb-device-profile-configuration <tb-device-profile-configuration
formControlName="configuration" formControlName="configuration">
required>
</tb-device-profile-configuration> </tb-device-profile-configuration>
</div> </div>
</div> </div>

17
ui-ngx/src/app/modules/home/pipes/public-api.ts

@ -1,17 +0,0 @@
///
/// Copyright © 2016-2024 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 './gateway-help-link/gateway-help-link.pipe';

2
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<ng-content></ng-content> <ng-content class="tb-hint-tooltip-content"></ng-content>
<tb-icon class="tb-hint-tooltip-icon tb-mat-18" <tb-icon class="tb-hint-tooltip-icon tb-mat-18"
*ngIf="tooltipText" *ngIf="tooltipText"
matTooltip="{{ tooltipText }}" matTooltip="{{ tooltipText }}"

8
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss

@ -21,11 +21,19 @@
} }
:host { :host {
.tb-hint-tooltip-content {
flex: 1 1 auto;
min-width: 0;
max-width: fit-content;
}
.tb-hint-tooltip-icon { .tb-hint-tooltip-icon {
color: #E0E0E0; color: #E0E0E0;
overflow: visible; overflow: visible;
order: 3; order: 3;
margin-left: 4px; margin-left: 4px;
flex-shrink: 0;
&:hover { &:hover {
color: #9E9E9E; color: #9E9E9E;
} }

48
ui-ngx/src/app/shared/components/table/table-datasource.abstract.ts

@ -0,0 +1,48 @@
///
/// Copyright © 2016-2024 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 { DataSource } from '@angular/cdk/collections';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export abstract class TbTableDatasource<T> implements DataSource<T> {
protected dataSubject = new BehaviorSubject<Array<T>>([]);
connect(): Observable<Array<T>> {
return this.dataSubject.asObservable();
}
disconnect(): void {
this.dataSubject.complete();
}
loadData(data: Array<T>): void {
this.dataSubject.next(data);
}
isEmpty(): Observable<boolean> {
return this.dataSubject.pipe(
map((data: T[]) => !data.length)
);
}
total(): Observable<number> {
return this.dataSubject.pipe(
map((data: T[]) => data.length)
);
}
}

20
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/ellipsis-chip-list.directive.ts → ui-ngx/src/app/shared/directives/ellipsis-chip-list.directive.ts

@ -20,7 +20,7 @@ import {
Inject, Inject,
Input, Input,
OnDestroy, OnDestroy,
Renderer2 Renderer2,
} from '@angular/core'; } from '@angular/core';
import { isEqual } from '@core/utils'; import { isEqual } from '@core/utils';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -30,13 +30,15 @@ import { takeUntil } from 'rxjs/operators';
@Directive({ @Directive({
// eslint-disable-next-line @angular-eslint/directive-selector // eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tb-ellipsis-chip-list]' selector: '[tb-ellipsis-chip-list]',
standalone: true,
}) })
export class EllipsisChipListDirective implements OnDestroy { export class EllipsisChipListDirective implements OnDestroy {
chipsValue: string[]; chipsValue: string[];
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private intersectionObserver: IntersectionObserver;
@Input('tb-ellipsis-chip-list') @Input('tb-ellipsis-chip-list')
set chips(value: string[]) { set chips(value: string[]) {
@ -59,6 +61,19 @@ export class EllipsisChipListDirective implements OnDestroy {
).subscribe(() => { ).subscribe(() => {
this.adjustChips(); this.adjustChips();
}); });
this.observeIntersection();
}
private observeIntersection(): void {
this.intersectionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.adjustChips();
}
});
});
this.intersectionObserver.observe(this.el.nativeElement);
} }
private adjustChips(): void { private adjustChips(): void {
@ -124,5 +139,6 @@ export class EllipsisChipListDirective implements OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
this.intersectionObserver.disconnect();
} }
} }

116
ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts

@ -0,0 +1,116 @@
///
/// Copyright © 2016-2024 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 {
AfterViewInit,
Directive,
ElementRef,
Input,
OnDestroy,
OnInit,
Renderer2,
} from '@angular/core';
import { fromEvent, Subject } from 'rxjs';
import { filter, takeUntil, tap } from 'rxjs/operators';
import { MatTooltip, TooltipPosition } from '@angular/material/tooltip';
import { coerceBoolean } from '@shared/decorators/coercion';
@Directive({
standalone: true,
selector: '[tbTruncateWithTooltip]',
providers: [MatTooltip],
})
export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDestroy {
@Input('tbTruncateWithTooltip')
text: string;
@Input()
@coerceBoolean()
tooltipEnabled = true;
@Input()
position: TooltipPosition = 'above';
private destroy$ = new Subject<void>();
constructor(
private elementRef: ElementRef,
private renderer: Renderer2,
private tooltip: MatTooltip
) {}
ngOnInit(): void {
this.observeMouseEvents();
this.applyTruncationStyles();
}
ngAfterViewInit(): void {
if (!this.text) {
this.text = this.elementRef.nativeElement.innerText;
}
this.tooltip.position = this.position;
}
ngOnDestroy(): void {
if (this.tooltip._isTooltipVisible()) {
this.hideTooltip();
}
this.destroy$.next();
this.destroy$.complete();
}
private observeMouseEvents(): void {
fromEvent(this.elementRef.nativeElement, 'mouseenter')
.pipe(
filter(() => this.tooltipEnabled),
filter(() => this.isOverflown(this.elementRef.nativeElement)),
tap(() => this.showTooltip()),
takeUntil(this.destroy$),
)
.subscribe();
fromEvent(this.elementRef.nativeElement, 'mouseleave')
.pipe(
filter(() => this.tooltipEnabled),
filter(() => this.tooltip._isTooltipVisible()),
tap(() => this.hideTooltip()),
takeUntil(this.destroy$),
)
.subscribe();
}
private applyTruncationStyles(): void {
this.renderer.setStyle(this.elementRef.nativeElement, 'white-space', 'nowrap');
this.renderer.setStyle(this.elementRef.nativeElement, 'overflow', 'hidden');
this.renderer.setStyle(this.elementRef.nativeElement, 'text-overflow', 'ellipsis');
}
private isOverflown(element: HTMLElement): boolean {
return element.clientWidth < element.scrollWidth;
}
private showTooltip(): void {
this.tooltip.message = this.text;
this.renderer.setAttribute(this.elementRef.nativeElement, 'matTooltip', this.text);
this.tooltip.show();
}
private hideTooltip(): void {
this.tooltip.hide();
}
}

2
ui-ngx/src/app/shared/pipe/key-value-not-empty.pipe.ts

@ -62,6 +62,6 @@ export class KeyValueIsNotEmptyPipe implements PipeTransform {
} }
private makeKeyValuePair(key: string, value: unknown): KeyValue<string, unknown> { private makeKeyValuePair(key: string, value: unknown): KeyValue<string, unknown> {
return {key: key, value: value}; return {key, value};
} }
} }

2
ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md

@ -1,4 +1,4 @@
Fields templatization feature allows you to process the incoming messages with dynamic configuration Fields templatization feature allows you to process the incoming messages with dynamic configuration
by substitution of templates specified in the configuration fields with values from message or message metadata. by substitution of templates specified in the configuration fields with values from message or message metadata.
For more detailed information, please refer to the ThingsBoard [documentation](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/templatization/) For more detailed information, please refer to the ThingsBoard [documentation{:target="_blank"}](${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/templatization/)

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

@ -2764,19 +2764,27 @@
"function": "Function" "function": "Function"
}, },
"gateway": { "gateway": {
"address": "Address",
"address-required": "Address required",
"add-entry": "Add configuration", "add-entry": "Add configuration",
"add-attribute": "Add attribute", "add-attribute": "Add attribute",
"add-attribute-update": "Add attribute update", "add-attribute-update": "Add attribute update",
"add-key": "Add key", "add-key": "Add key",
"add-timeseries": "Add time series", "add-timeseries": "Add time series",
"add-mapping": "Add mapping", "add-mapping": "Add mapping",
"add-slave": "Add Slave",
"arguments": "Arguments", "arguments": "Arguments",
"add-rpc-method": "Add method", "add-rpc-method": "Add method",
"add-rpc-request": "Add request",
"add-value": "Add argument", "add-value": "Add argument",
"baudrate": "Baudrate",
"bytesize": "Bytesize",
"delete-value": "Delete value", "delete-value": "Delete value",
"delete-rpc-method": "Delete method", "delete-rpc-method": "Delete method",
"delete-attribute-update": "Add attribute update", "delete-rpc-request": "Delete request",
"delete-attribute-update": "Delete attribute update",
"advanced": "Advanced", "advanced": "Advanced",
"advanced-connection-settings": "Advanced connection settings",
"attributes": "Attributes", "attributes": "Attributes",
"attribute-updates": "Attribute updates", "attribute-updates": "Attribute updates",
"attribute-filter": "Attribute filter", "attribute-filter": "Attribute filter",
@ -2786,6 +2794,8 @@
"attribute-name-expression-required": "Attribute name expression required.", "attribute-name-expression-required": "Attribute name expression required.",
"attribute-name-expression-hint": "Hint for Attribute name expression", "attribute-name-expression-hint": "Hint for Attribute name expression",
"basic": "Basic", "basic": "Basic",
"byte-order": "Byte order",
"word-order": "Word order",
"broker": { "broker": {
"connection": "Connection to broker", "connection": "Connection to broker",
"name": "Broker name", "name": "Broker name",
@ -2820,6 +2830,9 @@
"connectors-table-actions": "Actions", "connectors-table-actions": "Actions",
"connectors-table-key": "Key", "connectors-table-key": "Key",
"connectors-table-class": "Class", "connectors-table-class": "Class",
"connection-timeout": "Connection timeout (s)",
"connect-attempt-time": "Connect attempt time (ms)",
"connect-attempt-count": "Connect attempt count",
"copy-username": "Copy username", "copy-username": "Copy username",
"copy-password": "Copy password", "copy-password": "Copy password",
"copy-client-id": "Copy client ID", "copy-client-id": "Copy client ID",
@ -2848,7 +2861,8 @@
"device-name-filter-hint": "This field supports Regular expressions to filter incoming data by device name.", "device-name-filter-hint": "This field supports Regular expressions to filter incoming data by device name.",
"device-name-filter-required": "Device name filter is required.", "device-name-filter-required": "Device name filter is required.",
"details": "Details", "details": "Details",
"delete-mapping-title": "Delete mapping ?", "delete-mapping-title": "Delete mapping?",
"delete-slave-title": "Delete slave?",
"download-configuration-file": "Download configuration file", "download-configuration-file": "Download configuration file",
"download-docker-compose": "Download docker-compose.yml for your gateway", "download-docker-compose": "Download docker-compose.yml for your gateway",
"enable-remote-logging": "Enable remote logging", "enable-remote-logging": "Enable remote logging",
@ -2867,6 +2881,7 @@
"configuration-delete-dialog-confirm": "Turn Off", "configuration-delete-dialog-confirm": "Turn Off",
"connector-duplicate-name": "Connector with such name already exists.", "connector-duplicate-name": "Connector with such name already exists.",
"connector-side": "Connector side", "connector-side": "Connector side",
"client-communication-type": "Client communication type",
"payload-type": "Payload type", "payload-type": "Payload type",
"platform-side": "Platform side", "platform-side": "Platform side",
"JSON": "JSON", "JSON": "JSON",
@ -2892,8 +2907,11 @@
"device-node-hint": "Path or identifier for device node on OPC UA server. Relative paths from it for attributes and time series can be used.", "device-node-hint": "Path or identifier for device node on OPC UA server. Relative paths from it for attributes and time series can be used.",
"device-name": "Device name", "device-name": "Device name",
"device-profile": "Device profile", "device-profile": "Device profile",
"device-name-required": "Device name required",
"device-profile-required": "Device profile required",
"download-tip": "Download configuration file", "download-tip": "Download configuration file",
"drop-file": "Drop file here or", "drop-file": "Drop file here or",
"enable": "Enable",
"enable-subscription": "Enable subscription", "enable-subscription": "Enable subscription",
"extension": "Extension", "extension": "Extension",
"extension-hint": "Put your converter classname in the field. Custom converter with such class should be in extension/mqtt folder.", "extension-hint": "Put your converter classname in the field. Custom converter with such class should be in extension/mqtt folder.",
@ -2904,6 +2922,7 @@
"fill-connector-defaults-hint": "This property allows to fill connector configuration with default values on it's creation.", "fill-connector-defaults-hint": "This property allows to fill connector configuration with default values on it's creation.",
"from-device-request-settings": "Input request parsing", "from-device-request-settings": "Input request parsing",
"from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.", "from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.",
"function-code": "Function code",
"to-device-response-settings": "Output request processing", "to-device-response-settings": "Output request processing",
"to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}", "to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",
"gateway": "Gateway", "gateway": "Gateway",
@ -2942,8 +2961,13 @@
"inactivity-timeout-seconds-required": "Inactivity timeout is required", "inactivity-timeout-seconds-required": "Inactivity timeout is required",
"inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1", "inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"inactivity-timeout-seconds-pattern": "Inactivity timeout is not valid", "inactivity-timeout-seconds-pattern": "Inactivity timeout is not valid",
"unit-id": "Unit ID",
"host": "Host", "host": "Host",
"host-required": "Host is required.", "host-required": "Host is required.",
"holding_registers": "Holding registers",
"coils_initializer": "Coils initializer",
"input_registers": "Input registers",
"discrete_inputs": "Discrete inputs",
"json-parse": "Not valid JSON.", "json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.", "json-required": "Field cannot be empty.",
"JSONPath-hint": "This field supports constants and JSONPath expressions.", "JSONPath-hint": "This field supports constants and JSONPath expressions.",
@ -2977,12 +3001,14 @@
"max-messages-queue-for-worker": "Max messages queue per worker", "max-messages-queue-for-worker": "Max messages queue per worker",
"max-messages-queue-for-worker-hint": "Maximal messages count that will be in the queue \nfor each converter worker.", "max-messages-queue-for-worker-hint": "Maximal messages count that will be in the queue \nfor each converter worker.",
"max-messages-queue-for-worker-required": "Max messages queue per worker is required.", "max-messages-queue-for-worker-required": "Max messages queue per worker is required.",
"method": "Method",
"method-name": "Method name", "method-name": "Method name",
"method-required": "Method name is required.", "method-required": "Method name is required.",
"min-pack-send-delay": "Min pack send delay (in ms)", "min-pack-send-delay": "Min pack send delay (in ms)",
"min-pack-send-delay-required": "Min pack send delay is required", "min-pack-send-delay-required": "Min pack send delay is required",
"min-pack-send-delay-min": "Min pack send delay can not be less then 0", "min-pack-send-delay-min": "Min pack send delay can not be less then 0",
"mode": "Mode", "mode": "Mode",
"model-name": "Model name",
"mqtt-version": "MQTT version", "mqtt-version": "MQTT version",
"name": "Name", "name": "Name",
"name-required": "Name is required.", "name-required": "Name is required.",
@ -2996,17 +3022,22 @@
"no-keys": "No keys", "no-keys": "No keys",
"no-value": "No arguments", "no-value": "No arguments",
"no-rpc-methods": "No RPC methods", "no-rpc-methods": "No RPC methods",
"no-rpc-requests": "No RPC requests",
"path-hint": "The path is local to the gateway file system", "path-hint": "The path is local to the gateway file system",
"path-logs": "Path to log files", "path-logs": "Path to log files",
"path-logs-required": "Path is required.", "path-logs-required": "Path is required.",
"password": "Password", "password": "Password",
"password-required": "Password is required.", "password-required": "Password is required.",
"permit-without-calls": "Keep alive permit without calls", "permit-without-calls": "Keep alive permit without calls",
"poll-period": "Poll period (ms)",
"port": "Port", "port": "Port",
"port-required": "Port is required.", "port-required": "Port is required.",
"port-limits-error": "Port should be number from {{min}} to {{max}}.", "port-limits-error": "Port should be number from {{min}} to {{max}}.",
"private-key-path": "Path to private key file", "private-key-path": "Path to private key file",
"path-to-private-key-required": "Path to private key file is required.", "path-to-private-key-required": "Path to private key file is required.",
"parity": "Parity",
"product-code": "Product code",
"product-name": "Product name",
"raw": "Raw", "raw": "Raw",
"retain": "Retain", "retain": "Retain",
"retain-hint": "This flag tells the broker to store the message for a topic\nand ensures any new client subscribing to that topic\nwill receive the stored message.", "retain-hint": "This flag tells the broker to store the message for a topic\nand ensures any new client subscribing to that topic\nwill receive the stored message.",
@ -3015,6 +3046,9 @@
"remove-entry": "Remove configuration", "remove-entry": "Remove configuration",
"remote-shell": "Remote shell", "remote-shell": "Remote shell",
"remote-configuration": "Remote Configuration", "remote-configuration": "Remote Configuration",
"retries": "Retries",
"retries-on-empty": "Retries on empty",
"retries-on-invalid": "Retries on invalid",
"rpc": { "rpc": {
"title": "{{type}} Connector RPC parameters", "title": "{{type}} Connector RPC parameters",
"templates-title": "Connector RPC Templates", "templates-title": "Connector RPC Templates",
@ -3106,6 +3140,7 @@
"json-value-invalid": "JSON value has an invalid format" "json-value-invalid": "JSON value has an invalid format"
}, },
"rpc-methods": "RPC methods", "rpc-methods": "RPC methods",
"rpc-requests": "RPC requests",
"request" : { "request" : {
"connect-request": "Connect request", "connect-request": "Connect request",
"disconnect-request": "Disconnect request", "disconnect-request": "Disconnect request",
@ -3117,6 +3152,7 @@
"requests-mapping": "Requests mapping", "requests-mapping": "Requests mapping",
"requests-mapping-hint": "MQTT Connector requests allows you to connect, disconnect, process attribute requests from the device, handle attribute updates on the server and RPC processing configuration.", "requests-mapping-hint": "MQTT Connector requests allows you to connect, disconnect, process attribute requests from the device, handle attribute updates on the server and RPC processing configuration.",
"request-topic-expression": "Request topic expression", "request-topic-expression": "Request topic expression",
"request-client-certificate": "Request client certificate",
"request-topic-expression-required": "Request topic expression is required.", "request-topic-expression-required": "Request topic expression is required.",
"response-timeout": "Response timeout (ms)", "response-timeout": "Response timeout (ms)",
"response-timeout-required": "Response timeout is required.", "response-timeout-required": "Response timeout is required.",
@ -3127,7 +3163,10 @@
"response-topic-expression-required": "Response topic expression is required.", "response-topic-expression-required": "Response topic expression is required.",
"response-value-expression": "Response value expression", "response-value-expression": "Response value expression",
"response-value-expression-required": "Response value expression is required.", "response-value-expression-required": "Response value expression is required.",
"vendor-name": "Vendor name",
"vendor-url": "Vendor URL",
"value": "Value", "value": "Value",
"values": "Values",
"value-required": "Value is required.", "value-required": "Value is required.",
"value-expression": "Value expression", "value-expression": "Value expression",
"value-expression-required": "Value expression is required.", "value-expression-required": "Value expression is required.",
@ -3150,17 +3189,28 @@
}, },
"select-connector": "Select connector to display config", "select-connector": "Select connector to display config",
"send-change-data": "Send data only on change", "send-change-data": "Send data only on change",
"send-data-TB": "Send data to ThingsBoard",
"send-data-on-change": "Send data only on change",
"send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.", "send-change-data-hint": "The values will be saved to the database only if they are different from the corresponding values in the previous converted message. This functionality applies to both attributes and time series in the converter output.",
"server": "Server", "server": "Server",
"server-hostname": "Server hostname",
"server-slave": "Server (Slave)",
"servers-slaves": "Servers (Slaves)",
"server-port": "Server port", "server-port": "Server port",
"server-url": "Server endpoint url", "server-url": "Server endpoint url",
"server-connection": "Server Connection",
"server-config": "Server configuration",
"server-slave-config": "Server (Slave) configuration",
"server-url-required": "Server endpoint url is required.", "server-url-required": "Server endpoint url is required.",
"stopbits": "Stopbits",
"strict": "Strict",
"set": "Set", "set": "Set",
"show-map": "Show map", "show-map": "Show map",
"statistics": { "statistics": {
"statistic": "Statistic", "statistic": "Statistic",
"statistics": "Statistics", "statistics": "Statistics",
"statistic-commands-empty": "No configured statistic keys found. You can configure them in \"Statistics\" tab in general configuration.", "statistic-commands-empty": "No configured statistic keys found. You can configure them in \"Statistics\" tab in general configuration.",
"statistics-button": "Go to configuration",
"commands": "Commands", "commands": "Commands",
"send-period": "Statistic send period (in sec)", "send-period": "Statistic send period (in sec)",
"send-period-required": "Statistic send period is required", "send-period-required": "Statistic send period is required",
@ -3246,6 +3296,8 @@
"topic-required": "Topic filter is required.", "topic-required": "Topic filter is required.",
"tls-path-ca-certificate": "Path to CA certificate on gateway", "tls-path-ca-certificate": "Path to CA certificate on gateway",
"tls-path-client-certificate": "Path to client certificate on gateway", "tls-path-client-certificate": "Path to client certificate on gateway",
"tls-connection": "TLS Connection",
"master-connections": "Master Connections",
"method-filter": "Method filter", "method-filter": "Method filter",
"method-filter-hint": "Regular expression to filter incoming RPC method from platform.", "method-filter-hint": "Regular expression to filter incoming RPC method from platform.",
"method-filter-required": "Method filter is required.", "method-filter-required": "Method filter is required.",
@ -3265,13 +3317,25 @@
"at-least-once": "1 - At least once", "at-least-once": "1 - At least once",
"exactly-once": "2 - Exactly once" "exactly-once": "2 - Exactly once"
}, },
"objects-count": "Objects count",
"wait-after-failed-attempts": "Wait after failed attempts (ms)",
"tls-path-private-key": "Path to private key on gateway", "tls-path-private-key": "Path to private key on gateway",
"toggle-fullscreen": "Toggle fullscreen", "toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*", "transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON", "update-config": "Add/update configuration JSON",
"username": "Username", "username": "Username",
"username-required": "Username is required.", "username-required": "Username is required.",
"unit-id-required": "Unit ID is required.",
"read-coils": "Read Coils",
"read-discrete-inputs": "Read Discrete Inputs",
"read-multiple-holding-registers": "Read Multiple Holding Register",
"read-input-registers": "Read Input Registers",
"write-coil": "Write Coil",
"write-coils": "Write Coils",
"write-register": "Write Register",
"write-registers": "Write Registers",
"hints": { "hints": {
"modbus-server": "Starting with version 3.0, Gateway can run as a Modbus slave.",
"remote-configuration": "Enables remote configuration and management of the gateway", "remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget", "remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"host": "Hostname or IP address of platform server", "host": "Hostname or IP address of platform server",
@ -3310,7 +3374,9 @@
"grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.", "grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.",
"grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.", "grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.",
"permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls.", "permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls.",
"path-in-os": "Path in gateway os.",
"memory": "Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.", "memory": "Your data will be stored in the in-memory queue, it is a fastest but no persistence guarantee.",
"framer-type": "Type of framer.",
"file": "Your data will be stored in separated files and will be saved even after the gateway restart.", "file": "Your data will be stored in separated files and will be saved even after the gateway restart.",
"sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart.", "sqlite": "Your data will be stored in file based database. And will be saved even after the gateway restart.",
"opcua-timeout": "Timeout in seconds for connecting to OPC-UA server.", "opcua-timeout": "Timeout in seconds for connecting to OPC-UA server.",
@ -5864,6 +5930,9 @@
"layout-default-volume": "Default.Volume", "layout-default-volume": "Default.Volume",
"layout-simplified-volume": "Simplified.Volume", "layout-simplified-volume": "Simplified.Volume",
"layout-outlined-volume": "Outlined.Volume", "layout-outlined-volume": "Outlined.Volume",
"layout-default-icon": "Default.Icon",
"layout-simplified-icon": "Simplified.Icon",
"layout-outlined-icon": "Outlined.Icon",
"main": "Main", "main": "Main",
"background": "Background", "background": "Background",
"power-on-colors": "Power 'On' colors", "power-on-colors": "Power 'On' colors",
@ -6480,7 +6549,8 @@
"min-and-max-value": "Min and max value", "min-and-max-value": "Min and max value",
"min-and-max-label": "Min and max label", "min-and-max-label": "Min and max label",
"font": "Font", "font": "Font",
"tick-width-and-color": "Tick width and color" "tick-width-and-color": "Tick width and color",
"min-max-validation-text": "Max value must be bigger than min value"
}, },
"gpio": { "gpio": {
"pin": "Pin", "pin": "Pin",
@ -7247,6 +7317,7 @@
"notification": { "notification": {
"max-notification-display": "Maximum notifications to display", "max-notification-display": "Maximum notifications to display",
"counter": "Counter", "counter": "Counter",
"counter-hint": "Counter will be displayed if \"Widget title\" is enabled",
"icon": "Icon", "icon": "Icon",
"counter-value": "Value", "counter-value": "Value",
"counter-color": "Color", "counter-color": "Color",

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

Loading…
Cancel
Save