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",
"hasBasicMode": true,
"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
}

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) {
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);
for (int i = 0; i < hex.length(); i += 2) {
// 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);
}
@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
public void floatToHex_Test() {
Float value = 123456789.00f;

2
pom.xml

@ -83,7 +83,7 @@
<zookeeper.version>3.9.2</zookeeper.version>
<protobuf.version>3.25.3</protobuf.version> <!-- A Major v4 does not support by the pubsub yet-->
<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>
<paho.client.version>1.2.5</paho.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 {
return (authPayload.authUser && authPayload.authUser.isPublic) ||
(authPayload.userDetails && authPayload.userDetails.additionalInfo &&
authPayload.userDetails.additionalInfo.defaultDashboardId &&
authPayload.userDetails.additionalInfo.defaultDashboardFullscreen &&
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>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
isAdd="true"
required>
isAdd="true">
</tb-device-profile-transport-configuration>
</form>
</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) {
$event.preventDefault();
$event.stopPropagation();
const assetProfile: AssetProfile = {
name: profileName
} as AssetProfile;
@ -337,7 +337,7 @@ export class AssetProfileAutocompleteComponent implements ControlValueAccessor,
}
editAssetProfile($event: Event) {
$event.preventDefault();
$event.stopPropagation();
this.assetProfileService.getAssetProfile(this.modelValue.id).subscribe(
(assetProfile) => {
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) {
$event.preventDefault();
$event.stopPropagation();
const deviceProfile: DeviceProfile = {
name: profileName,
transportType: this.transportType
@ -360,7 +360,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
}
editDeviceProfile($event: Event) {
$event.preventDefault();
$event.stopPropagation();
this.deviceProfileService.getDeviceProfile(this.modelValue.id).subscribe(
(deviceProfile) => {
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>
<ng-template matExpansionPanelContent>
<tb-device-profile-configuration
formControlName="configuration"
required>
formControlName="configuration">
</tb-device-profile-configuration>
</ng-template>
</mat-expansion-panel>
@ -151,8 +150,7 @@
<ng-template matExpansionPanelContent>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
[isAdd] = "isTransportTypeChanged"
required>
[isAdd] = "isTransportTypeChanged">
</tb-device-profile-transport-configuration>
</ng-template>
</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';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
CoapDeviceProfileTransportConfiguration,
coapDeviceTypeTranslationMap,
@ -74,7 +73,6 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control
coapTransportConfigurationFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private requiredValue: boolean;
private transportPayloadTypeConfiguration = this.fb.group({
transportPayloadType: [TransportPayloadType.JSON, Validators.required],
@ -84,15 +82,6 @@ export class CoapDeviceProfileTransportConfigurationComponent implements Control
deviceRpcResponseProtoSchema: [defaultRpcResponseSchema, Validators.required]
});
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
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 { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DefaultDeviceProfileConfiguration,
DeviceProfileConfiguration,
@ -39,15 +38,6 @@ export class DefaultDeviceProfileConfigurationComponent implements ControlValueA
defaultDeviceProfileConfigurationFormGroup: UntypedFormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
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 { 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 { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DefaultDeviceProfileTransportConfiguration,
DeviceProfileTransportConfiguration,
DeviceTransportType
} from '@shared/models/device.models';
import { DefaultDeviceProfileTransportConfiguration, DeviceTransportType } from '@shared/models/device.models';
@Component({
selector: 'tb-default-device-profile-transport-configuration',
templateUrl: './default-device-profile-transport-configuration.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DefaultDeviceProfileTransportConfigurationComponent),
multi: true
}]
providers: [
{
provide: NG_VALUE_ACCESSOR,
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;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@ -86,12 +89,17 @@ export class DefaultDeviceProfileTransportConfigurationComponent implements Cont
this.defaultDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false});
}
private updateModel() {
let configuration: DeviceProfileTransportConfiguration = null;
if (this.defaultDeviceProfileTransportConfigurationFormGroup.valid) {
configuration = this.defaultDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
configuration.type = DeviceTransportType.DEFAULT;
validate(c: UntypedFormControl): ValidationErrors | null {
return (this.defaultDeviceProfileTransportConfigurationFormGroup.valid) ? null : {
configuration: {
valid: false
}
}
}
private updateModel() {
const configuration = this.defaultDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
configuration.type = DeviceTransportType.DEFAULT;
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">
<ng-template [ngSwitchCase]="deviceProfileType.DEFAULT">
<tb-default-device-profile-configuration
[required]="required"
formControlName="configuration">
</tb-default-device-profile-configuration>
</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 { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileConfiguration, DeviceProfileType } from '@shared/models/device.models';
import { deepClone } from '@core/utils';
import { Subject } from 'rxjs';
@ -42,15 +41,6 @@ export class DeviceProfileConfigurationComponent implements ControlValueAccessor
private destroy$ = new Subject<void>();
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
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">
<ng-template [ngSwitchCase]="deviceTransportType.DEFAULT">
<tb-default-device-profile-transport-configuration
[required]="required"
formControlName="configuration">
</tb-default-device-profile-transport-configuration>
</ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.MQTT">
<tb-mqtt-device-profile-transport-configuration
[required]="required"
formControlName="configuration">
</tb-mqtt-device-profile-transport-configuration>
</ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.COAP">
<tb-coap-device-profile-transport-configuration
[required]="required"
formControlName="configuration">
</tb-coap-device-profile-transport-configuration>
</ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.LWM2M">
<tb-profile-lwm2m-device-transport-configuration
[isAdd]="isAdd"
[required]="required"
formControlName="configuration">
</tb-profile-lwm2m-device-transport-configuration>
</ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.SNMP">
<tb-snmp-device-profile-transport-configuration
[required]="required"
formControlName="configuration">
</tb-snmp-device-profile-transport-configuration>
</ng-template>

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

@ -23,12 +23,10 @@ import {
UntypedFormControl,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
Validator
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DeviceProfileTransportConfiguration, DeviceTransportType } from '@shared/models/device.models';
import { deepClone } from '@core/utils';
@ -55,15 +53,6 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
deviceProfileTransportConfigurationFormGroup: UntypedFormGroup;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
disabled: boolean;
@ -87,7 +76,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
ngOnInit() {
this.deviceProfileTransportConfigurationFormGroup = this.fb.group({
configuration: [null, Validators.required]
configuration: [null]
});
this.deviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => {
this.updateModel();
@ -110,7 +99,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
delete configuration.type;
}
setTimeout(() => {
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration});
}, 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 style="min-width:150px">{{ (serverFormGroup.get('bootstrapServerIs').value ?
'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) + ': ' }}
<span style="font-style: italic">{{ serverFormGroup.get('shortServerId').value }}</span>
</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,
Validators
} from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
ATTRIBUTE,
DEFAULT_EDRX_CYCLE,
@ -47,7 +46,7 @@ import {
ObjectIDVerTranslationMap
} from './lwm2m-profile-config.models';
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 _ from 'lodash';
import { Subject } from 'rxjs';
@ -77,7 +76,6 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
public disabled = false;
public isTransportWasRunWithBootstrap = true;
public isBootstrapServerUpdateEnable: boolean;
private requiredValue: boolean;
private destroy$ = new Subject<void>();
lwm2mDeviceProfileFormGroup: UntypedFormGroup;
@ -88,15 +86,6 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro
sortFunction: (key: string, value: object) => object;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
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';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
defaultAttributesSchema,
defaultRpcRequestSchema,
@ -70,16 +69,6 @@ export class MqttDeviceProfileTransportConfigurationComponent implements Control
mqttDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
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,
Validators
} from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
DeviceTransportType,
SnmpDeviceProfileTransportConfiguration
@ -63,16 +62,6 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements OnInit,
snmpDeviceProfileTransportConfigurationFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
}
@Input()
set required(value: boolean) {
this.requiredValue = coerceBooleanProperty(value);
}
@Input()
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">
<tb-font-settings formControlName="counterValueFont"
clearButton
[previewText]="unreadNotificationWidgetConfigForm.get('maxNotificationDisplay').value.toString()"
[previewText]="countPreviewFn"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<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;
countPreviewFn = this._countPreviewFn.bind(this);
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
@ -87,16 +89,6 @@ export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigCom
const showTitle: boolean = this.unreadNotificationWidgetConfigForm.get('showTitle').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) {
this.unreadNotificationWidgetConfigForm.get('iconSize').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('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 {
@ -174,4 +190,8 @@ export class UnreadNotificationBasicConfigComponent extends BasicWidgetConfigCom
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>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<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>
</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.
///
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, ValidatorFn } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
@ -109,7 +109,7 @@ export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigCom
showMinMax: [settings.showMinMax, []],
minValue: [settings.minValue, []],
maxValue: [settings.maxValue, []],
maxValue: [settings.maxValue, [this.maxValueValidation()]],
minMaxFont: [settings.minMaxFont, []],
minMaxColor: [settings.minMaxFont?.color, []],
@ -182,11 +182,29 @@ export class DigitalSimpleGaugeBasicConfigComponent extends BasicWidgetConfigCom
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[] {
return ['gaugeType', 'showValue', 'showTitle', 'showMinMax'];
return ['gaugeType', 'showValue', 'showTitle', 'showMinMax', 'minValue'];
}
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;
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"
(markAsRead)="markAsRead($event)">
</tb-notification>
<mat-divider *ngIf="!last" style="margin: 4px"></mat-divider>
</div>
</section>
</div>

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

@ -60,6 +60,6 @@
width: 24px;
height: 22px;
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.viewAllAction.show = isDefined(this.settings.enableViewAll) ? this.settings.enableViewAll : true;
this.store.pipe(select(selectUserDetails), take(1)).subscribe(
user => this.viewAllAction.show = !user.additionalInfo?.defaultDashboardFullscreen
);
if (this.viewAllAction.show) {
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.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,
counterValueFont: {
family: 'Roboto',
size: 14,
size: 12,
sizeUnit: 'px',
style: 'normal',
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
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="portErrorTooltip"
[matTooltip]="brokerConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(brokerConfigFormGroup.get('port').hasError('required') ||
brokerConfigFormGroup.get('port').hasError('min') ||
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.
///
import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
@ -35,8 +35,9 @@ import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { generateSecret } from '@core/utils';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
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({
selector: 'tb-broker-config-control',
@ -47,6 +48,7 @@ import { Subject } from 'rxjs';
CommonModule,
SharedModule,
SecurityConfigComponent,
GatewayPortTooltipPipe,
],
providers: [
{
@ -72,13 +74,14 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder,
private cdr: ChangeDetectorRef,
private translate: TranslateService) {
this.brokerConfigFormGroup = this.fb.group({
name: ['', []],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
version: [5, []],
clientId: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
clientId: ['tb_gw_' + generateSecret(5), [Validators.pattern(noLeadTrailSpacesRegex)]],
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 {
this.destroy$.next();
this.destroy$.complete();
@ -119,7 +109,13 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid
}
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 {

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-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.key
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}">
{{ 'gateway.key' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -97,8 +97,8 @@
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}" translate>
gateway.value
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}">
{{ 'gateway.value' | translate }}
</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap">
<input matInput required formControlName="value"
@ -158,8 +158,8 @@
</div>
<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="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.method-name' | translate }}" translate>
gateway.method-name
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.method-name' | translate }}">
{{ 'gateway.method-name' | translate }}
</div>
<div class="tb-flex no-gap">
<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">
<ng-container [matColumnDef]="column.def" *ngFor="let column of mappingColumns; let i = index">
<mat-header-cell *matHeaderCellDef class="table-value-column"
[class.request-column]="mappingType !== mappingTypeEnum.DATA">
[class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ column.title | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column"
[class.request-column]="mappingType !== mappingTypeEnum.DATA">
<mat-cell tbTruncateWithTooltip *matCellDef="let mapping" class="table-value-column"
[class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ mapping[column.def] }}
</mat-cell>
</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 { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, take, takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, take, takeUntil } from 'rxjs/operators';
import {
ControlValueAccessor,
FormBuilder,
@ -52,12 +52,13 @@ import {
RequestType,
RequestTypesTranslationsMap
} 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 { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({
selector: 'tb-mapping-table',
@ -77,7 +78,7 @@ import { CommonModule } from '@angular/common';
}
],
standalone: true,
imports: [CommonModule, SharedModule]
imports: [CommonModule, SharedModule, TruncateWithTooltipDirective]
})
export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
@ -191,7 +192,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
$event.stopPropagation();
}
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,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
@ -206,7 +207,7 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
if (isDefinedAndNotNull(index)) {
this.mappingFormGroup.at(index).patchValue(res);
} else {
this.mappingFormGroup.push(this.fb.group(res));
this.pushDataAsFormArrays([res]);
}
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 {
@ -310,33 +311,8 @@ export class MappingTableComponent implements ControlValueAccessor, Validator, A
}
}
export class MappingDatasource implements DataSource<{[key: string]: any}> {
private mappingSubject = new BehaviorSubject<Array<{[key: string]: any}>>([]);
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)
);
export class MappingDatasource extends TbTableDatasource<MappingValue> {
constructor() {
super();
}
}

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,
} from '@angular/forms';
import {
ConnectorBaseConfig,
MappingType,
MQTTBasicConfig,
RequestMappingData,
RequestType,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
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 { Subject } from 'rxjs';
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({
selector: 'tb-mqtt-basic-config',
@ -112,17 +118,18 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.onTouched = fn;
}
writeValue(basicConfig: ConnectorBaseConfig): void {
writeValue(basicConfig: MQTTBasicConfig): void {
const { broker, dataMapping = [], requestsMapping } = basicConfig;
const editedBase = {
workers: {
maxNumberOfWorkers: basicConfig.broker?.maxNumberOfWorkers,
maxMessageNumberPerWorker: basicConfig.broker?.maxMessageNumberPerWorker,
},
dataMapping: basicConfig.dataMapping || [],
broker: basicConfig.broker || {},
requestsMapping: Array.isArray(basicConfig.requestsMapping)
? basicConfig.requestsMapping
: this.getRequestDataArray(basicConfig.requestsMapping),
workers: broker && (broker.maxNumberOfWorkers || broker.maxMessageNumberPerWorker) ? {
maxNumberOfWorkers: broker.maxNumberOfWorkers,
maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker,
} : {},
dataMapping: dataMapping || [],
broker: broker || {},
requestsMapping: Array.isArray(requestsMapping)
? requestsMapping
: this.getRequestDataArray(requestsMapping),
};
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%;
height: 100%;
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';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { SecurityConfigComponent } from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
import { Subject } from 'rxjs';
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({
selector: 'tb-server-config',
templateUrl: './server-config.component.html',
styleUrls: ['./server-config.component.scss'],
selector: 'tb-opc-server-config',
templateUrl: './opc-server-config.component.html',
styleUrls: ['./opc-server-config.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ServerConfigComponent),
useExisting: forwardRef(() => OpcServerConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ServerConfigComponent),
useExisting: forwardRef(() => OpcServerConfigComponent),
multi: true
}
],
@ -59,9 +62,10 @@ import { takeUntil } from 'rxjs/operators';
CommonModule,
SharedModule,
SecurityConfigComponent,
TruncateWithTooltipDirective,
]
})
export class ServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
securityPolicyTypes = SecurityPolicyTypes;
serverConfigFormGroup: UntypedFormGroup;
@ -112,6 +116,16 @@ export class ServerConfigComponent implements ControlValueAccessor, Validator, O
}
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>
</mat-tab>
<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 label="{{ 'gateway.data-mapping' | translate }}*">
<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,
} from '@angular/forms';
import {
ConnectorBaseConfig,
ConnectorType,
MappingType,
OPCBasicConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
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 { 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({
selector: 'tb-opc-ua-basic-config',
@ -65,7 +73,7 @@ import { Subject } from 'rxjs';
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
ServerConfigComponent,
OpcServerConfigComponent,
],
styleUrls: ['./opc-ua-basic-config.component.scss']
})
@ -109,7 +117,7 @@ export class OpcUaBasicConfigComponent implements ControlValueAccessor, Validato
this.onTouched = fn;
}
writeValue(basicConfig: ConnectorBaseConfig): void {
writeValue(basicConfig: OPCBasicConfig): void {
const editedBase = {
server: basicConfig.server || {},
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 {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
@ -87,7 +88,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {}
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
this.securityFormGroup = this.fb.group({
@ -127,6 +128,7 @@ export class SecurityConfigComponent implements ControlValueAccessor, OnInit, On
}
this.securityFormGroup.reset(securityInfo, {emitEvent: false});
}
this.cdr.markForCheck();
}
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-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}" translate>
gateway.max-number-of-workers
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.max-number-of-workers' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -40,8 +40,8 @@
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" [style.width.%]="50"
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}" translate>
gateway.max-messages-queue-for-worker
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.max-messages-queue-for-worker' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<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,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup, ValidationErrors, Validator,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-workers-config-control',
@ -42,6 +45,7 @@ import { takeUntil } from 'rxjs/operators';
imports: [
CommonModule,
SharedModule,
TruncateWithTooltipDirective,
],
providers: [
{
@ -91,7 +95,11 @@ export class WorkersConfigControlComponent implements OnDestroy, ControlValueAcc
}
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 {

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';
import { Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from "rxjs/operators";
import { takeUntil, tap } from 'rxjs/operators';
@Component({
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">
<h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2>
<span fxFlex></span>
<div [tb-help]="helpLinkId()"></div>
<div [tb-help]="HelpLinkByMappingTypeMap.get(this.data.mappingType)"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
@ -57,8 +57,8 @@
</div>
</div>
<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>
gateway.mqtt-qos
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}">
{{ 'gateway.mqtt-qos' | translate }}
</div>
<div class="tb-flex no-gap">
<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-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}" translate>
gateway.extension
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}">
{{ 'gateway.extension' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -369,8 +369,8 @@
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE">
<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>
gateway.device-name-filter
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}">
{{ 'gateway.device-name-filter' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -388,8 +388,8 @@
</div>
</div>
<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>
gateway.attribute-filter
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}">
{{ 'gateway.attribute-filter' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -472,8 +472,8 @@
</tb-toggle-select>
</div>
<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>
gateway.device-name-filter
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}">
{{ 'gateway.device-name-filter' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -491,8 +491,8 @@
</div>
</div>
<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>
gateway.method-filter
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}">
{{ 'gateway.method-filter' | translate }}
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
@ -580,8 +580,8 @@
</div>
</div>
<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>
gateway.response-topic-Qos
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}">
{{ 'gateway.response-topic-Qos' | translate }}
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="responseTopicQoS">
@ -618,8 +618,8 @@
<ng-template [ngSwitchCase]="MappingType.OPCUA">
<div class="tb-form-row column-xs" fxLayoutAlign="center">
<div class="tb-flex no-flex align-center" translate>
<div class="tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-node-hint' | translate }}" translate>
gateway.device-node
<div class="tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-node-hint' | translate }}">
{{ 'gateway.device-node' | translate }}
</div>
</div>
<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 {
justify-content: flex-end;
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 {
Attribute,
AttributesUpdate,
ConnectorMapping,
ConnectorMappingFormValue,
ConverterMappingFormValue,
ConvertorType,
ConvertorTypeTranslationsMap,
DataConversionTranslationsMap,
DeviceConnectorMapping,
DeviceInfoType,
HelpLinkByMappingTypeMap,
MappingHintTranslationsMap,
MappingInfo,
MappingKeysAddKeyTranslationsMap,
@ -37,11 +42,11 @@ import {
MappingKeysType,
MappingType,
MappingTypeTranslationsMap,
MappingValue,
noLeadTrailSpacesRegex,
OPCUaSourceTypes,
QualityTypes,
QualityTypeTranslationsMap,
RequestMappingFormValue,
RequestType,
RequestTypesTranslationsMap,
RpcMethod,
@ -55,14 +60,16 @@ import { startWith, takeUntil } from 'rxjs/operators';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
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({
selector: 'tb-mapping-dialog',
templateUrl: './mapping-dialog.component.html',
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;
@ -97,6 +104,8 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
DataConversionTranslationsMap = DataConversionTranslationsMap;
HelpLinkByMappingTypeMap = HelpLinkByMappingTypeMap;
keysPopupClosed = true;
private destroy$ = new Subject<void>();
@ -104,7 +113,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MappingInfo,
public dialogRef: MatDialogRef<MappingDialogComponent, MappingValue>,
public dialogRef: MatDialogRef<MappingDialogComponent, ConnectorMapping>,
private fb: FormBuilder,
private popoverService: TbPopoverService,
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 {
if (this.keysPopupClosed) {
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;
switch (this.data.mappingType) {
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) {
switch (this.data.mappingType) {
case MappingType.DATA:
@ -282,16 +287,16 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
type: converter.type,
[converter.type]: {...converter}
}
};
} as ConverterMappingFormValue;
case MappingType.REQUESTS:
return {
requestType: this.data.value.requestType,
requestValue: {
[this.data.value.requestType]: this.data.value.requestValue
}
};
} as RequestMappingFormValue;
default:
return this.data.value;
return this.data.value as DeviceConnectorMapping;
}
}
}
@ -317,7 +322,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
extensionConfig: [{}, []]
}),
}));
this.mappingForm.patchValue(this.prepareFormValueData());
this.mappingForm.patchValue(this.getFormValueData());
this.mappingForm.get('converter.type').valueChanges.pipe(
startWith(this.mappingForm.get('converter.type').value),
takeUntil(this.destroy$)
@ -397,7 +402,7 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
requestValueGroup.get('responseTimeout').enable({emitEvent: false});
}
});
this.mappingForm.patchValue(this.prepareFormValueData());
this.mappingForm.patchValue(this.getFormValueData());
}
private createOPCUAMappingForm(): void {
@ -410,6 +415,6 @@ export class MappingDialogComponent extends DialogComponent<MappingDialogCompone
rpc_methods: [[], []],
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;
} else {
updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken;
if (updateCredentials) {
this.initialCredentials.credentialsId = securityConfig.accessToken;
}
}
if (updateCredentials) {
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">
<h2>{{ 'gateway.connectors' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
<button *ngIf="dataSource?.data?.length"
mat-icon-button
[disabled]="isLoading$ | async"
(click)="addConnector($event)"
matTooltip="{{ 'action.add' | translate }}"
@ -182,6 +183,10 @@
formControlName="basicConfig"
[generalTabContent]="generalTabContent">
</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-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 {
AddConnectorConfigData,
ConnectorBaseConfig,
ConnectorConfigurationModes,
ConnectorType,
GatewayConnector,
@ -92,7 +93,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
allowBasicConfig = new Set<ConnectorType>([
ConnectorType.MQTT,
ConnectorType.OPCUA
ConnectorType.OPCUA,
ConnectorType.MODBUS,
]);
gatewayLogLevel = Object.values(GatewayLogLevel);
@ -397,7 +399,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private clearOutConnectorForm(): void {
this.initialConnector = null;
this.connectorForm.setControl('basicConfig', this.fb.group({}), {emitEvent: false});
this.connectorForm.setValue({
mode: ConnectorConfigurationModes.BASIC,
name: '',
@ -719,7 +720,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
connector.key = 'auto';
}
if (!connector.configurationJson) {
connector.configurationJson = {};
connector.configurationJson = {} as ConnectorBaseConfig;
}
connector.basicConfig = connector.configurationJson;
@ -732,7 +733,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
switch (connector.type) {
case ConnectorType.MQTT:
case ConnectorType.OPCUA:
case ConnectorType.MODBUS:
this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true});
this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false});
setTimeout(() => {
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"
placeholder="sensor/${deviceName}/request/${methodName}/${requestId}"/>
</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 }}
</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>
<input matInput formControlName="responseTopicExpression"
placeholder="sensor/${deviceName}/response/${methodName}/${requestId}"/>
</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>
<input matInput formControlName="responseTimeout" type="number"
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';
import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
import { takeUntil, tap } from "rxjs/operators";
import { filter, takeUntil, tap } from "rxjs/operators";
import { Subject } from "rxjs";
@Component({
@ -80,7 +80,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter();
commandForm: FormGroup;
isMQTTWithResponse: FormControl;
codesArray: Array<number> = [1, 2, 3, 4, 5, 6, 15, 16];
ConnectorType = ConnectorType;
modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[];
@ -135,7 +134,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
this.propagateChange({...this.commandForm.value, ...value});
}
});
this.isMQTTWithResponse = this.fb.control(false);
this.observeMQTTWithResponse();
}
@ -153,9 +151,10 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
methodFilter: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: [null, [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)]],
})
withResponse: [false, []],
});
break;
case ConnectorType.MODBUS:
formGroup = this.fb.group({
@ -407,12 +406,21 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
}
private observeMQTTWithResponse(): void {
this.isMQTTWithResponse.valueChanges.pipe(
tap((isActive: boolean) => {
const responseControl = this.commandForm.get('responseTopicExpression');
isActive ? responseControl.enable() : responseControl.disable();
}),
takeUntil(this.destroy$),
).subscribe();
if (this.connectorType === ConnectorType.MQTT) {
this.commandForm.get('withResponse').valueChanges.pipe(
tap((isActive: boolean) => {
const responseTopicControl = this.commandForm.get('responseTopicExpression');
const responseTimeoutControl = this.commandForm.get('responseTimeout');
if (isActive) {
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() {
this.templates = this.subscription.data[0].data[0][1].length ?
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();
}

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">
<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-select formControlName="statisticKey">
<mat-option *ngFor="let key of statisticsKeys" [value]="key">
@ -30,8 +30,13 @@
</mat-form-field>
<mat-error
*ngIf="!statisticsKeys.length && !commands.length">
{{'gateway.statistics.statistic-commands-empty' | translate }}
{{ 'gateway.statistics.statistic-commands-empty' | translate }}
</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-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<input matInput [value]="commandObj.command" disabled>
@ -43,13 +48,16 @@
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<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">
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
{{ row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<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">
{{ row[1] }}
</mat-cell>

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

@ -28,6 +28,7 @@
height: 100%;
margin-right: 35px;
padding: 15px;
gap: 22px;
}
@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 { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { deepClone } from '@core/utils';
@Component({
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() {
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 interface ConnectorBaseConfig {
mapping?: DeviceConnectorMapping[];
dataMapping?: ConverterConnectorMapping[];
requestsMapping?: Record<RequestType, RequestMappingData> | RequestMappingData[];
server?: ServerConfig;
broker?: BrokerConfig;
workers?: WorkersConfig;
export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue;
export type ConnectorBaseConfig = MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig;
export interface MQTTBasicConfig {
dataMapping: ConverterConnectorMapping[];
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 {
@ -223,7 +235,7 @@ export interface AttributesUpdate {
value: string;
}
interface Converter {
export interface Converter {
type: ConvertorType;
deviceNameJsonExpression: string;
deviceTypeJsonExpression: string;
@ -239,14 +251,20 @@ export interface ConverterConnectorMapping {
converter: Converter;
}
export type ConverterMappingFormValue = Omit<ConverterConnectorMapping, 'converter'> & {
converter: {
type: ConvertorType;
} & Record<ConvertorType, Converter>;
};
export interface DeviceConnectorMapping {
deviceNodePattern: string;
deviceNodeSource: string;
deviceInfo: DeviceInfo;
attributes: Attribute[];
timeseries: Timeseries[];
rpc_methods: RpcMethod[];
attributes_updates: AttributesUpdate[];
attributes?: Attribute[];
timeseries?: Timeseries[];
rpc_methods?: RpcMethod[];
attributes_updates?: AttributesUpdate[];
}
export enum ConnectorType {
@ -475,6 +493,11 @@ export interface MappingInfo {
buttonTitle: string;
}
export interface ModbusSlaveInfo {
value: SlaveConfig;
buttonTitle: string;
}
export enum ConnectorConfigurationModes {
BASIC = 'basic',
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 QualityTypeTranslationsMap = new Map<number, string>(
@ -597,6 +628,10 @@ export interface RequestMappingData {
requestValue: RequestDataItem;
}
export type RequestMappingFormValue = Omit<RequestMappingData, 'requestValue'> & {
requestValue: Record<RequestType, RequestDataItem>;
};
export interface RequestDataItem {
type: string;
details: string;
@ -739,3 +774,259 @@ export const SecurityPolicyTypes = [
{ value: SecurityPolicy.BASIC256, name: 'Basic256' },
{ 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 }}
</mat-header-cell>
<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>
</ng-container>
<ng-container matColumnDef="lastVisited">
@ -78,7 +78,7 @@
class="star" [ngClass]="{'starred': dashboard.starred}">{{ dashboard.starred ? 'star' : 'star_border' }}</mat-icon>
</div>
<div class="tb-cell title">
<a routerLink="/dashboards/{{dashboard.id}}">{{ dashboard.title }}</a>
<a [routerLink]="createDashboardUrl(dashboard.id)">{{ dashboard.title }}</a>
</div>
</div>
</div>
@ -87,6 +87,7 @@
subscriptSizing="dynamic"
appearance="outline"
[useIdValue]="false"
[customerId]="customerId"
label=""
placeholder="{{ 'dashboard.select-dashboard' | translate }}"
[(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,
OnDestroy,
OnInit,
QueryList, ViewChild,
QueryList,
ViewChild,
ViewChildren
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
@ -29,11 +30,12 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum';
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 {
AbstractUserDashboardInfo,
LastVisitedDashboardInfo, StarredDashboardInfo,
LastVisitedDashboardInfo,
StarredDashboardInfo,
UserDashboardAction,
UserDashboardsInfo
} from '@shared/models/user-settings.models';
@ -77,6 +79,8 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
hasDashboardsAccess = true;
dirty = false;
public customerId: string;
private isFullscreenMode = getCurrentAuthState(this.store).forceFullscreen;
constructor(protected store: Store<AppState>,
private cd: ChangeDetectorRef,
@ -85,6 +89,9 @@ export class RecentDashboardsWidgetComponent extends PageComponent implements On
}
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);
if (this.hasDashboardsAccess) {
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') {
this.toggleValue = value;
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,
ValueToDataType
} 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 tinycolor from 'tinycolor2';
import { WidgetContext } from '@home/models/widget-component.models';
@ -35,7 +35,10 @@ export enum PowerButtonLayout {
outlined = 'outlined',
default_volume = 'default_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[];
@ -47,7 +50,10 @@ export const powerButtonLayoutTranslations = new Map<PowerButtonLayout, string>(
[PowerButtonLayout.outlined, 'widgets.power-button.layout-outlined'],
[PowerButtonLayout.default_volume, 'widgets.power-button.layout-default-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.default_volume, 'assets/widget/power-button/default-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 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');
export abstract class PowerButtonShape {
@ -242,6 +266,12 @@ export abstract class PowerButtonShape {
return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick);
case PowerButtonLayout.outlined_volume:
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 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() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.fill({opacity: 0}).stroke({width: 0});
@ -723,15 +778,15 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
add.stop(1, '#FFFFFF', 1);
}).from(0.832, 0.1188).to(0.268, 0.92);
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.onLabelShape = this.createOnLabel('400');
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.drawOnCenter();
this.addOnCenterToMask(this.onCircleShape);
this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3);
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.addOnCenterTimeLine(this.pressedTimeline);
this.innerShadow.timeline(this.pressedTimeline);
}
@ -751,7 +806,7 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
this.innerBorder.fill(this.innerBorderGradient);
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});
}
@ -776,14 +831,14 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape {
this.innerShadow.show();
const pressedScale = 0.75;
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);
}
protected onPressEnd() {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).transform({scale: 1});
powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}});
this.onCenterAnimation(1);
this.innerShadow.animateRestore().after(() => {
if (this.disabled) {
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 {
private outerBorder: Circle;
@ -805,6 +896,10 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape {
private centerGroup: G;
private onCenterGroup: G;
protected drawCenterGroup(centerGroup: G, onCenterGroup: G) {
this.offLabelShape = this.createOffLabel().addTo(centerGroup);
this.onLabelShape = this.createOnLabel().addTo(onCenterGroup);
}
protected drawShape() {
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.createMask(this.outerBorder, [this.outerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel().addTo(this.centerGroup);
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.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0);
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 {
private outerBorder: Circle;
private outerBorderMask: Circle;
@ -881,6 +996,30 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
private centerGroup: 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() {
this.outerBorder = this.svgShape.circle(powerButtonShapeSize).center(cx, cy)
.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.createMask(this.innerBorder, [this.innerBorderMask]);
this.centerGroup = this.svgShape.group();
this.offLabelShape = this.createOffLabel('800').addTo(this.centerGroup);
this.drawOffCenter(this.centerGroup);
this.onCenterGroup = this.svgShape.group();
this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy)
.addTo(this.onCenterGroup);
this.onLabelShape = this.createOnLabel('800');
this.createMask(this.onCircleShape, [this.onLabelShape]);
this.drawOnCenter();
this.addOnCenterToMask(this.onCircleShape);
this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0);
this.backgroundShape.addClass('tb-small-shadow');
this.pressedTimeline = new Timeline();
this.centerGroup.timeline(this.pressedTimeline);
this.onCenterGroup.timeline(this.pressedTimeline);
this.onLabelShape.timeline(this.pressedTimeline);
this.addOnCenterTimeLine(this.pressedTimeline);
this.pressedShadow.timeline(this.pressedTimeline);
}
@ -919,7 +1058,7 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
this.outerBorder.attr({ 'fill-opacity': 1 });
}
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});
}
@ -940,7 +1079,7 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
const pressedScale = 0.75;
powerButtonAnimation(this.centerGroup).transform({scale: pressedScale});
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);
}
@ -948,8 +1087,44 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape {
this.pressedTimeline.finish();
powerButtonAnimation(this.centerGroup).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();
}
}
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-row no-padding no-border">
<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>
</div>
<div class="tb-form-row space-between">
@ -49,7 +51,7 @@
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-font-settings formControlName="counterValueFont"
clearButton
[previewText]="unreadNotificationWidgetSettingsForm.get('maxNotificationDisplay').value.toString()"
[previewText]="countPreviewFn"
[initialPreviewStyle]="widgetConfig.config.titleStyle">
</tb-font-settings>
<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;
countPreviewFn = this._countPreviewFn.bind(this);
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
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>
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<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>
</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 { 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 { AppState } from '@core/core.state';
import { GaugeType } from '@home/components/widget/lib/canvas-digital-gauge';
@ -167,7 +174,7 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent
donutStartAngle: [settings.donutStartAngle, []],
showMinMax: [settings.showMinMax, []],
minValue: [settings.minValue, []],
maxValue: [settings.maxValue, []],
maxValue: [settings.maxValue, [this.maxValueValidation()]],
minMaxFont: [settings.minMaxFont, []],
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) {
const barColor: ColorSettings = this.digitalGaugeWidgetSettingsForm.get('barColor').value;
@ -230,10 +249,15 @@ export class DigitalGaugeWidgetSettingsComponent extends WidgetSettingsComponent
}
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 showTitle: boolean = this.digitalGaugeWidgetSettingsForm.get('showTitle').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 { 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 {
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 { LatestChartComponent } from '@home/components/widget/lib/chart/latest-chart.component';
import { PieChartWidgetComponent } from '@home/components/widget/lib/chart/pie-chart-widget.component';
@ -112,18 +109,38 @@ import {
import {
NotificationTypeFilterPanelComponent
} 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 {
BrokerConfigControlComponent,
DeviceInfoTableComponent,
MappingDataKeysPanelComponent,
MappingTableComponent,
MqttBasicConfigComponent,
OpcUaBasicConfigComponent,
ServerConfigComponent,
TypeValuePanelComponent,
WorkersConfigControlComponent,
} from '@home/components/widget/lib/gateway/connectors-configuration/public-api';
OpcUaBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc-ua-basic-config/opc-ua-basic-config.component';
import {
ModbusBasicConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component';
import {
DeviceInfoTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component';
import {
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';
@NgModule({
@ -164,7 +181,6 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
GatewayServiceRPCConnectorTemplateDialogComponent,
EllipsisChipListDirective,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent,
CountWidgetComponent,
@ -206,11 +222,13 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
GatewayHelpLinkPipe,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
ServerConfigComponent,
OpcServerConfigComponent,
MqttBasicConfigComponent,
MappingTableComponent,
OpcUaBasicConfigComponent,
KeyValueIsNotEmptyPipe
KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent,
EllipsisChipListDirective,
],
exports: [
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 class="mat-block" *ngIf="entityForm.get('resourceType').value !== resourceType.LWM2M_MODEL || !isAdd">
<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')">
{{ 'resource.title-required' | translate }}
</mat-error>
@ -68,7 +68,7 @@
</mat-form-field>
<tb-file-input *ngIf="isAdd || (isEdit && entityForm.get('resourceType').value === resourceType.JS_MODULE)"
formControlName="data"
required
[required]="isAdd"
label="{{ (entityForm.get('resourceType').value === resourceType.LWM2M_MODEL ? 'resource.resource-files' : 'resource.resource-file') | translate }}"
[readAsBinary]="true"
[maxSizeByte]="maxResourceSize"

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

@ -29,7 +29,7 @@ import {
ResourceTypeMIMETypes,
ResourceTypeTranslationMap
} 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 { isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthState } from '@core/auth/auth.selectors';
@ -59,22 +59,9 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
ngOnInit() {
super.ngOnInit();
this.entityForm.get('resourceType').valueChanges.pipe(
startWith(ResourceType.JS_MODULE),
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});
});
if (this.isAdd) {
this.observeResourceTypeChange();
}
}
ngOnDestroy() {
@ -96,7 +83,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, 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});
if (entity.resourceType !== ResourceType.JS_MODULE) {
this.entityForm.get('fileName').disable({emitEvent: false});
this.entityForm.get('data').disable({emitEvent: false});
}
}
this.entityForm.patchValue({
@ -153,4 +139,24 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
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">
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
[isAdd] = "isTransportTypeChanged"
required>
[isAdd]="isTransportTypeChanged">
</tb-device-profile-transport-configuration>
</div>
</div>
@ -65,8 +64,7 @@
<div class="mat-padding" [formGroup]="detailsForm">
<div formGroupName="profileData">
<tb-device-profile-configuration
formControlName="configuration"
required>
formControlName="configuration">
</tb-device-profile-configuration>
</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.
-->
<ng-content></ng-content>
<ng-content class="tb-hint-tooltip-content"></ng-content>
<tb-icon class="tb-hint-tooltip-icon tb-mat-18"
*ngIf="tooltipText"
matTooltip="{{ tooltipText }}"

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

@ -21,11 +21,19 @@
}
:host {
.tb-hint-tooltip-content {
flex: 1 1 auto;
min-width: 0;
max-width: fit-content;
}
.tb-hint-tooltip-icon {
color: #E0E0E0;
overflow: visible;
order: 3;
margin-left: 4px;
flex-shrink: 0;
&:hover {
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,
Input,
OnDestroy,
Renderer2
Renderer2,
} from '@angular/core';
import { isEqual } from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
@ -30,13 +30,15 @@ import { takeUntil } from 'rxjs/operators';
@Directive({
// 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 {
chipsValue: string[];
private destroy$ = new Subject<void>();
private intersectionObserver: IntersectionObserver;
@Input('tb-ellipsis-chip-list')
set chips(value: string[]) {
@ -59,6 +61,19 @@ export class EllipsisChipListDirective implements OnDestroy {
).subscribe(() => {
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 {
@ -124,5 +139,6 @@ export class EllipsisChipListDirective implements OnDestroy {
ngOnDestroy(): void {
this.destroy$.next();
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> {
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
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"
},
"gateway": {
"address": "Address",
"address-required": "Address required",
"add-entry": "Add configuration",
"add-attribute": "Add attribute",
"add-attribute-update": "Add attribute update",
"add-key": "Add key",
"add-timeseries": "Add time series",
"add-mapping": "Add mapping",
"add-slave": "Add Slave",
"arguments": "Arguments",
"add-rpc-method": "Add method",
"add-rpc-request": "Add request",
"add-value": "Add argument",
"baudrate": "Baudrate",
"bytesize": "Bytesize",
"delete-value": "Delete value",
"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-connection-settings": "Advanced connection settings",
"attributes": "Attributes",
"attribute-updates": "Attribute updates",
"attribute-filter": "Attribute filter",
@ -2786,6 +2794,8 @@
"attribute-name-expression-required": "Attribute name expression required.",
"attribute-name-expression-hint": "Hint for Attribute name expression",
"basic": "Basic",
"byte-order": "Byte order",
"word-order": "Word order",
"broker": {
"connection": "Connection to broker",
"name": "Broker name",
@ -2820,6 +2830,9 @@
"connectors-table-actions": "Actions",
"connectors-table-key": "Key",
"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-password": "Copy password",
"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-required": "Device name filter is required.",
"details": "Details",
"delete-mapping-title": "Delete mapping ?",
"delete-mapping-title": "Delete mapping?",
"delete-slave-title": "Delete slave?",
"download-configuration-file": "Download configuration file",
"download-docker-compose": "Download docker-compose.yml for your gateway",
"enable-remote-logging": "Enable remote logging",
@ -2867,6 +2881,7 @@
"configuration-delete-dialog-confirm": "Turn Off",
"connector-duplicate-name": "Connector with such name already exists.",
"connector-side": "Connector side",
"client-communication-type": "Client communication type",
"payload-type": "Payload type",
"platform-side": "Platform side",
"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-name": "Device name",
"device-profile": "Device profile",
"device-name-required": "Device name required",
"device-profile-required": "Device profile required",
"download-tip": "Download configuration file",
"drop-file": "Drop file here or",
"enable": "Enable",
"enable-subscription": "Enable subscription",
"extension": "Extension",
"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.",
"from-device-request-settings": "Input request parsing",
"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-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",
"gateway": "Gateway",
@ -2942,8 +2961,13 @@
"inactivity-timeout-seconds-required": "Inactivity timeout is required",
"inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"inactivity-timeout-seconds-pattern": "Inactivity timeout is not valid",
"unit-id": "Unit ID",
"host": "Host",
"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-required": "Field cannot be empty.",
"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-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.",
"method": "Method",
"method-name": "Method name",
"method-required": "Method name is required.",
"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-min": "Min pack send delay can not be less then 0",
"mode": "Mode",
"model-name": "Model name",
"mqtt-version": "MQTT version",
"name": "Name",
"name-required": "Name is required.",
@ -2996,17 +3022,22 @@
"no-keys": "No keys",
"no-value": "No arguments",
"no-rpc-methods": "No RPC methods",
"no-rpc-requests": "No RPC requests",
"path-hint": "The path is local to the gateway file system",
"path-logs": "Path to log files",
"path-logs-required": "Path is required.",
"password": "Password",
"password-required": "Password is required.",
"permit-without-calls": "Keep alive permit without calls",
"poll-period": "Poll period (ms)",
"port": "Port",
"port-required": "Port is required.",
"port-limits-error": "Port should be number from {{min}} to {{max}}.",
"private-key-path": "Path to private key file",
"path-to-private-key-required": "Path to private key file is required.",
"parity": "Parity",
"product-code": "Product code",
"product-name": "Product name",
"raw": "Raw",
"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.",
@ -3015,6 +3046,9 @@
"remove-entry": "Remove configuration",
"remote-shell": "Remote shell",
"remote-configuration": "Remote Configuration",
"retries": "Retries",
"retries-on-empty": "Retries on empty",
"retries-on-invalid": "Retries on invalid",
"rpc": {
"title": "{{type}} Connector RPC parameters",
"templates-title": "Connector RPC Templates",
@ -3106,6 +3140,7 @@
"json-value-invalid": "JSON value has an invalid format"
},
"rpc-methods": "RPC methods",
"rpc-requests": "RPC requests",
"request" : {
"connect-request": "Connect request",
"disconnect-request": "Disconnect request",
@ -3117,6 +3152,7 @@
"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.",
"request-topic-expression": "Request topic expression",
"request-client-certificate": "Request client certificate",
"request-topic-expression-required": "Request topic expression is required.",
"response-timeout": "Response timeout (ms)",
"response-timeout-required": "Response timeout is required.",
@ -3127,7 +3163,10 @@
"response-topic-expression-required": "Response topic expression is required.",
"response-value-expression": "Response value expression",
"response-value-expression-required": "Response value expression is required.",
"vendor-name": "Vendor name",
"vendor-url": "Vendor URL",
"value": "Value",
"values": "Values",
"value-required": "Value is required.",
"value-expression": "Value expression",
"value-expression-required": "Value expression is required.",
@ -3150,17 +3189,28 @@
},
"select-connector": "Select connector to display config",
"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.",
"server": "Server",
"server-hostname": "Server hostname",
"server-slave": "Server (Slave)",
"servers-slaves": "Servers (Slaves)",
"server-port": "Server port",
"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.",
"stopbits": "Stopbits",
"strict": "Strict",
"set": "Set",
"show-map": "Show map",
"statistics": {
"statistic": "Statistic",
"statistics": "Statistics",
"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",
"send-period": "Statistic send period (in sec)",
"send-period-required": "Statistic send period is required",
@ -3246,6 +3296,8 @@
"topic-required": "Topic filter is required.",
"tls-path-ca-certificate": "Path to CA 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-hint": "Regular expression to filter incoming RPC method from platform.",
"method-filter-required": "Method filter is required.",
@ -3265,13 +3317,25 @@
"at-least-once": "1 - At least 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",
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON",
"username": "Username",
"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": {
"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-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"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-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.",
"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.",
"framer-type": "Type of framer.",
"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.",
"opcua-timeout": "Timeout in seconds for connecting to OPC-UA server.",
@ -5864,6 +5930,9 @@
"layout-default-volume": "Default.Volume",
"layout-simplified-volume": "Simplified.Volume",
"layout-outlined-volume": "Outlined.Volume",
"layout-default-icon": "Default.Icon",
"layout-simplified-icon": "Simplified.Icon",
"layout-outlined-icon": "Outlined.Icon",
"main": "Main",
"background": "Background",
"power-on-colors": "Power 'On' colors",
@ -6480,7 +6549,8 @@
"min-and-max-value": "Min and max value",
"min-and-max-label": "Min and max label",
"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": {
"pin": "Pin",
@ -7247,6 +7317,7 @@
"notification": {
"max-notification-display": "Maximum notifications to display",
"counter": "Counter",
"counter-hint": "Counter will be displayed if \"Widget title\" is enabled",
"icon": "Icon",
"counter-value": "Value",
"counter-color": "Color",

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

Loading…
Cancel
Save