Browse Source

Merge pull request #11814 from maxunbearable/improvement/4601-gateway-extension-deletion

Deleted gateway components after migration to Gateway extension
pull/11872/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
cd142d820e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 77
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract.ts
  2. 69
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts
  3. 71
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/modbus-version-processor.abstract.ts
  4. 101
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract.ts
  5. 56
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/opc-version-processor.abstract.ts
  6. 24
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html
  7. 23
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss
  8. 96
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts
  9. 813
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html
  10. 103
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss
  11. 568
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts
  12. 64
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html
  13. 64
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss
  14. 396
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts
  15. 170
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts
  16. 99
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.html
  17. 57
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss
  18. 164
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts
  19. 229
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html
  20. 60
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.scss
  21. 197
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.ts
  22. 125
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html
  23. 101
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.scss
  24. 323
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  25. 76
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.abstract.ts
  26. 38
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html
  27. 18
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss
  28. 76
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts
  29. 80
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-legacy-basic-config.component.ts
  30. 255
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  31. 45
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss
  32. 306
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  33. 150
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html
  34. 90
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.scss
  35. 245
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts
  36. 66
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.html
  37. 163
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts
  38. 238
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html
  39. 283
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  40. 84
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-legacy-slave-dialog.component.ts
  41. 208
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract.ts
  42. 312
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  43. 36
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss
  44. 87
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  45. 129
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.html
  46. 23
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss
  47. 240
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.ts
  48. 93
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract.ts
  49. 41
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.html
  50. 23
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.scss
  51. 97
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.ts
  52. 117
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-legacy-basic-config.component.ts
  53. 78
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/broker-config-control/broker-config-control.component.html
  54. 124
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/broker-config-control/broker-config-control.component.ts
  55. 59
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/workers-config-control/workers-config-control.component.html
  56. 108
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/workers-config-control/workers-config-control.component.ts
  57. 136
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.html
  58. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.scss
  59. 152
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component.ts
  60. 30
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component.html
  61. 23
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component.scss
  62. 88
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-basic-config.component.ts
  63. 88
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc/opc-ua-basic-config/opc-ua-legacy-basic-config.component.ts
  64. 55
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.html
  65. 174
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.ts
  66. 65
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.html
  67. 29
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.scss
  68. 132
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.ts
  69. 81
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component.html
  70. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component.scss
  71. 166
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component.ts
  72. 48
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/mqtt-rpc-parameters/mqtt-rpc-parameters.component.html
  73. 24
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/mqtt-rpc-parameters/mqtt-rpc-parameters.component.scss
  74. 139
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/mqtt-rpc-parameters/mqtt-rpc-parameters.component.ts
  75. 93
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/opc-rpc-parameters/opc-rpc-parameters.component.html
  76. 32
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/opc-rpc-parameters/opc-rpc-parameters.component.scss
  77. 169
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/opc-rpc-parameters/opc-rpc-parameters.component.ts
  78. 128
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.html
  79. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.scss
  80. 177
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component.ts
  81. 101
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html
  82. 49
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.scss
  83. 160
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.ts
  84. 53
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html
  85. 75
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.scss
  86. 42
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts
  87. 107
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.html
  88. 22
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.scss
  89. 149
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/add-connector-dialog.component.ts
  90. 732
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.html
  91. 86
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.scss
  92. 421
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/dialog/mapping-dialog.component.ts
  93. 318
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  94. 156
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  95. 897
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  96. 286
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html
  97. 36
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss
  98. 426
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts
  99. 380
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.models.ts
  100. 56
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html

77
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract.ts

@ -1,77 +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.
///
import { AfterViewInit, Directive, EventEmitter, inject, Input, OnDestroy, Output, TemplateRef } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, ValidationErrors, Validator } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Directive()
export abstract class GatewayConnectorBasicConfigDirective<InputBasicConfig, OutputBasicConfig>
implements AfterViewInit, ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<any>;
@Output() initialized = new EventEmitter<void>();
basicFormGroup: FormGroup;
protected fb = inject(FormBuilder);
protected onChange!: (value: OutputBasicConfig) => void;
protected onTouched!: () => void;
protected destroy$ = new Subject<void>();
constructor() {
this.basicFormGroup = this.initBasicFormGroup();
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((value) => this.onBasicFormGroupChange(value));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewInit(): void {
this.initialized.emit();
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : { basicFormGroup: { valid: false } };
}
registerOnChange(fn: (value: OutputBasicConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(config: OutputBasicConfig): void {
this.basicFormGroup.setValue(this.mapConfigToFormValue(config), { emitEvent: false });
}
protected onBasicFormGroupChange(value: InputBasicConfig): void {
this.onChange(this.getMappedValue(value));
this.onTouched();
}
protected abstract mapConfigToFormValue(config: OutputBasicConfig): InputBasicConfig;
protected abstract getMappedValue(config: InputBasicConfig): OutputBasicConfig;
protected abstract initBasicFormGroup(): FormGroup;
}

69
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts

@ -1,69 +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.
///
import { GatewayConnector, GatewayVersion } from '@home/components/widget/lib/gateway/gateway-widget.models';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
gatewayVersion: number;
configVersion: number;
protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector<BasicConfig>) {
this.gatewayVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.gatewayVersionIn);
this.configVersion = GatewayConnectorVersionMappingUtil.parseVersion(this.connector.configVersion);
}
getProcessedByVersion(): GatewayConnector<BasicConfig> {
if (!this.isVersionUpdateNeeded()) {
return this.connector;
}
return this.processVersionUpdate();
}
private processVersionUpdate(): GatewayConnector<BasicConfig> {
if (this.isVersionUpgradeNeeded()) {
return this.getUpgradedVersion();
} else if (this.isVersionDowngradeNeeded()) {
return this.getDowngradedVersion();
}
return this.connector;
}
private isVersionUpdateNeeded(): boolean {
if (!this.gatewayVersion) {
return false;
}
return this.configVersion !== this.gatewayVersion;
}
private isVersionUpgradeNeeded(): boolean {
return this.gatewayVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current)
&& (!this.configVersion || this.configVersion < this.gatewayVersion);
}
private isVersionDowngradeNeeded(): boolean {
return this.configVersion && this.configVersion >= GatewayConnectorVersionMappingUtil.parseVersion(GatewayVersion.Current)
&& (this.configVersion > this.gatewayVersion);
}
protected abstract getDowngradedVersion(): GatewayConnector<BasicConfig>;
protected abstract getUpgradedVersion(): GatewayConnector<BasicConfig>;
}

71
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/modbus-version-processor.abstract.ts

@ -1,71 +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.
///
import {
GatewayConnector,
LegacySlaveConfig,
ModbusBasicConfig,
ModbusBasicConfig_v3_5_2,
ModbusLegacyBasicConfig,
ModbusLegacySlave,
ModbusMasterConfig,
ModbusSlave,
} from '../gateway-widget.models';
import { GatewayConnectorVersionProcessor } from './gateway-connector-version-processor.abstract';
import { ModbusVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/modbus-version-mapping.util';
export class ModbusVersionProcessor extends GatewayConnectorVersionProcessor<any> {
constructor(
protected gatewayVersionIn: string,
protected connector: GatewayConnector<ModbusBasicConfig>
) {
super(gatewayVersionIn, connector);
}
getUpgradedVersion(): GatewayConnector<ModbusBasicConfig_v3_5_2> {
const configurationJson = this.connector.configurationJson;
return {
...this.connector,
configurationJson: {
master: configurationJson.master?.slaves
? ModbusVersionMappingUtil.mapMasterToUpgradedVersion(configurationJson.master as ModbusMasterConfig<LegacySlaveConfig>)
: { slaves: [] },
slave: configurationJson.slave
? ModbusVersionMappingUtil.mapSlaveToUpgradedVersion(configurationJson.slave as ModbusLegacySlave)
: {} as ModbusSlave,
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<ModbusBasicConfig_v3_5_2>;
}
getDowngradedVersion(): GatewayConnector<ModbusLegacyBasicConfig> {
const configurationJson = this.connector.configurationJson;
return {
...this.connector,
configurationJson: {
...configurationJson,
slave: configurationJson.slave
? ModbusVersionMappingUtil.mapSlaveToDowngradedVersion(configurationJson.slave as ModbusSlave)
: {} as ModbusLegacySlave,
master: configurationJson.master?.slaves
? ModbusVersionMappingUtil.mapMasterToDowngradedVersion(configurationJson.master as ModbusMasterConfig)
: { slaves: [] },
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<ModbusLegacyBasicConfig>;
}
}

101
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/mqtt-version-processor.abstract.ts

@ -1,101 +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.
///
import { isEqual } from '@core/utils';
import {
GatewayConnector,
MQTTBasicConfig,
MQTTBasicConfig_v3_5_2,
MQTTLegacyBasicConfig,
RequestMappingData,
RequestType,
} from '../gateway-widget.models';
import { MqttVersionMappingUtil } from '../utils/mqtt-version-mapping.util';
import { GatewayConnectorVersionProcessor } from './gateway-connector-version-processor.abstract';
export class MqttVersionProcessor extends GatewayConnectorVersionProcessor<MQTTBasicConfig> {
private readonly mqttRequestTypeKeys = Object.values(RequestType);
constructor(
protected gatewayVersionIn: string,
protected connector: GatewayConnector<MQTTBasicConfig>
) {
super(gatewayVersionIn, connector);
}
getUpgradedVersion(): GatewayConnector<MQTTBasicConfig_v3_5_2> {
const {
connectRequests,
disconnectRequests,
attributeRequests,
attributeUpdates,
serverSideRpc
} = this.connector.configurationJson as MQTTLegacyBasicConfig;
let configurationJson = {
...this.connector.configurationJson,
requestsMapping: MqttVersionMappingUtil.mapRequestsToUpgradedVersion({
connectRequests,
disconnectRequests,
attributeRequests,
attributeUpdates,
serverSideRpc
}),
mapping: MqttVersionMappingUtil.mapMappingToUpgradedVersion((this.connector.configurationJson as MQTTLegacyBasicConfig).mapping),
};
this.mqttRequestTypeKeys.forEach((key: RequestType) => {
const { [key]: removedValue, ...rest } = configurationJson as MQTTLegacyBasicConfig;
configurationJson = { ...rest } as any;
});
this.cleanUpConfigJson(configurationJson as MQTTBasicConfig_v3_5_2);
return {
...this.connector,
configurationJson,
configVersion: this.gatewayVersionIn
} as GatewayConnector<MQTTBasicConfig_v3_5_2>;
}
getDowngradedVersion(): GatewayConnector<MQTTLegacyBasicConfig> {
const { requestsMapping, mapping, ...restConfig } = this.connector.configurationJson as MQTTBasicConfig_v3_5_2;
const updatedRequestsMapping = requestsMapping
? MqttVersionMappingUtil.mapRequestsToDowngradedVersion(requestsMapping as Record<RequestType, RequestMappingData[]>) : {};
const updatedMapping = MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping);
return {
...this.connector,
configurationJson: {
...restConfig,
...updatedRequestsMapping,
mapping: updatedMapping,
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<MQTTLegacyBasicConfig>;
}
private cleanUpConfigJson(configurationJson: MQTTBasicConfig_v3_5_2): void {
if (isEqual(configurationJson.requestsMapping, {})) {
delete configurationJson.requestsMapping;
}
if (isEqual(configurationJson.mapping, [])) {
delete configurationJson.mapping;
}
}
}

56
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/opc-version-processor.abstract.ts

@ -1,56 +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.
///
import {
GatewayConnector, LegacyServerConfig,
OPCBasicConfig,
OPCBasicConfig_v3_5_2,
OPCLegacyBasicConfig,
} from '../gateway-widget.models';
import { GatewayConnectorVersionProcessor } from './gateway-connector-version-processor.abstract';
import { OpcVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/opc-version-mapping.util';
export class OpcVersionProcessor extends GatewayConnectorVersionProcessor<OPCBasicConfig> {
constructor(
protected gatewayVersionIn: string,
protected connector: GatewayConnector<OPCBasicConfig>
) {
super(gatewayVersionIn, connector);
}
getUpgradedVersion(): GatewayConnector<OPCBasicConfig_v3_5_2> {
const server = this.connector.configurationJson.server as LegacyServerConfig;
return {
...this.connector,
configurationJson: {
server: server ? OpcVersionMappingUtil.mapServerToUpgradedVersion(server) : {},
mapping: server?.mapping ? OpcVersionMappingUtil.mapMappingToUpgradedVersion(server.mapping) : [],
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<OPCBasicConfig_v3_5_2>;
}
getDowngradedVersion(): GatewayConnector<OPCLegacyBasicConfig> {
return {
...this.connector,
configurationJson: {
server: OpcVersionMappingUtil.mapServerToDowngradedVersion(this.connector.configurationJson as OPCBasicConfig_v3_5_2)
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<OPCLegacyBasicConfig>;
}
}

24
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html

@ -1,24 +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.
-->
<tb-json-object-edit
fillHeight="true"
class="config-container flex flex-col"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
[formControl]="advancedFormControl"
/>

23
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss

@ -1,23 +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.
*/
:host {
.config-container {
height: calc(100% - 60px);
padding: 8px;
}
}

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

@ -1,96 +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.
///
import { Component, forwardRef, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validators
} from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { GatewayConfigValue } from '@home/components/widget/lib/gateway/configuration/models/gateway-configuration.models';
@Component({
selector: 'tb-gateway-advanced-configuration',
templateUrl: './gateway-advanced-configuration.component.html',
styleUrls: ['./gateway-advanced-configuration.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GatewayAdvancedConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => GatewayAdvancedConfigurationComponent),
multi: true
}
],
})
export class GatewayAdvancedConfigurationComponent implements OnDestroy, ControlValueAccessor, Validators {
advancedFormControl: FormControl;
private onChange: (value: unknown) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.advancedFormControl = this.fb.control('');
this.advancedFormControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: unknown) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(advancedConfig: GatewayConfigValue): void {
this.advancedFormControl.reset(advancedConfig, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.advancedFormControl.valid ? null : {
advancedFormControl: {valid: false}
};
}
}

813
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html

@ -1,813 +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.
-->
<mat-tab-group class="tab-group-block" [formGroup]="basicFormGroup" [class.dialog-mode]="dialogMode">
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-template matTabContent>
<div formGroupName="thingsboard" class="mat-content mat-padding configuration-block">
<div class="tb-form-panel no-padding-bottom">
<div tb-hint-tooltip-icon="{{ 'gateway.hints.remote-configuration' | translate }}"
class="tb-form-row no-border no-padding">
<mat-slide-toggle class="mat-slide" color="primary" formControlName="remoteConfiguration">
{{ 'gateway.remote-configuration' | translate }}
</mat-slide-toggle>
</div>
<div tb-hint-tooltip-icon="{{ 'gateway.hints.remote-shell' | translate }}"
class="tb-form-row no-border no-padding">
<mat-slide-toggle class="mat-slide" color="primary" formControlName="remoteShell">
{{ 'gateway.remote-shell' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-row no-border no-padding tb-standard-fields">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.thingsboard-host</mat-label>
<input matInput formControlName="host"/>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.host' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="basicFormGroup.get('thingsboard.host').hasError('required')">
{{ 'gateway.thingsboard-host-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.thingsboard-port</mat-label>
<input matInput formControlName="port" type="number" min="0"/>
<mat-error *ngIf="basicFormGroup.get('thingsboard.port').hasError('required')">
{{ 'gateway.thingsboard-port-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('thingsboard.port').hasError('min')">
{{ 'gateway.thingsboard-port-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('thingsboard.port').hasError('max')">
{{ 'gateway.thingsboard-port-max' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('thingsboard.port').hasError('pattern')">
{{ 'gateway.thingsboard-port-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.port' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel">
<div translate class="tb-form-panel-title">security.security</div>
<ng-container formGroupName="security">
<tb-toggle-select class="toggle-group" formControlName="type">
<tb-toggle-option *ngFor="let securityType of securityTypes | keyvalue"
[value]="securityType.key">{{ securityType.value | translate }}
</tb-toggle-option>
</tb-toggle-select>
<mat-form-field appearance="outline"
*ngIf="basicFormGroup.get('thingsboard.security.type').value.toLowerCase().includes('accesstoken')">
<mat-label translate>security.access-token</mat-label>
<input matInput formControlName="accessToken"/>
<mat-error *ngIf="basicFormGroup.get('thingsboard.security.accessToken').hasError('required')">
{{ 'security.access-token-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="basicFormGroup.get('thingsboard.security.accessToken').value"
[copyText]="basicFormGroup.get('thingsboard.security.accessToken').value"
tooltipText="{{ 'device.copy-access-token' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.token' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<section>
<div class="tb-form-row no-border no-padding tb-standard-fields"
*ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>security.clientId</mat-label>
<input matInput formControlName="clientId"/>
<mat-error *ngIf="basicFormGroup.get('thingsboard.security.clientId').hasError('required')">
{{ 'security.clientId-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="basicFormGroup.get('thingsboard.security.clientId').value"
[copyText]="basicFormGroup.get('thingsboard.security.clientId').value"
tooltipText="{{ 'gateway.copy-client-id' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.client-id' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>security.username</mat-label>
<input matInput formControlName="username"/>
<mat-error *ngIf="basicFormGroup.get('thingsboard.security.username').hasError('required')">
{{ 'security.username-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="basicFormGroup.get('thingsboard.security.username').value"
[copyText]="basicFormGroup.get('thingsboard.security.username').value"
tooltipText="{{ 'gateway.copy-username' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.username' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 100%"
*ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'">
<mat-label translate>gateway.password</mat-label>
<input matInput formControlName="password"/>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="basicFormGroup.get('thingsboard.security.password').value"
[copyText]="basicFormGroup.get('thingsboard.security.password').value"
tooltipText="{{ 'gateway.copy-password' | translate }}"
tooltipPosition="above"
icon="content_copy">
</tb-copy-button>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.password' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
<tb-error style="margin-top: -12px; display: block;"
*ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'"
[error]="basicFormGroup.get('thingsboard.security').hasError('atLeastOne') ?
('device.client-id-or-user-name-necessary' | translate) : ''"></tb-error>
<tb-file-input
hint="{{ 'gateway.hints.ca-cert' | translate }}"
*ngIf="basicFormGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')"
formControlName="caCert"
label="{{ 'security.ca-cert' | translate }}"
[allowedExtensions]="'pem, cert, key'"
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'"
dropLabel="{{ 'gateway.drop-file' | translate }}">
</tb-file-input>
</ng-container>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab label="{{ 'gateway.logs.logs' | translate }}">
<ng-template matTabContent>
<div formGroupName="logs" class="mat-content mat-padding configuration-block">
<div class="tb-form-panel no-padding-bottom">
<div class="flex flex-col">
<mat-form-field appearance="outline">
<mat-label translate>gateway.logs.date-format</mat-label>
<input matInput formControlName="dateFormat"/>
<mat-error *ngIf="basicFormGroup.get('logs.dateFormat').hasError('required')">
{{ 'gateway.logs.date-format-required' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.date-form' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label translate>gateway.logs.log-format</mat-label>
<textarea matInput formControlName="logFormat" rows="2"></textarea>
<mat-error *ngIf="basicFormGroup.get('logs.logFormat').hasError('required')">
{{ 'gateway.logs.log-format-required' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.log-format' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel no-padding-bottom" formGroupName="remote">
<div translate class="tb-form-panel-title">gateway.logs.remote</div>
<div tb-hint-tooltip-icon="{{ 'gateway.hints.remote-log' | translate }}"
class="tb-form-row no-border no-padding">
<mat-slide-toggle class="mat-slide" color="primary" formControlName="enabled">
{{ 'gateway.logs.remote-logs' | translate }}
</mat-slide-toggle>
</div>
<mat-form-field appearance="outline">
<mat-label translate>gateway.logs.level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-panel no-padding-bottom" formGroupName="local">
<div translate class="tb-form-panel-title">gateway.logs.local</div>
<tb-toggle-select class="toggle-group" [formControl]="logSelector">
<tb-toggle-option *ngFor="let logConfig of localLogsConfigs" [value]="logConfig"
class="first-capital">{{ localLogsConfigTranslateMap.get(logConfig) }}</tb-toggle-option>
</tb-toggle-select>
<ng-container [formGroup]="getLogFormGroup(logSelector.value)">
<div class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.logs.level</mat-label>
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.logs.file-path</mat-label>
<input matInput formControlName="filePath"/>
<mat-error *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.filePath').hasError('required')">
{{ 'gateway.logs.file-path-required' | translate }}
</mat-error>
</mat-form-field>
</div>
<div class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<div class="tb-form-row no-border no-padding tb-standard-fields saving-period">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.logs.saving-period</mat-label>
<input matInput formControlName="savingTime" type="number" min="0"/>
<mat-error
*ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('required')">
{{ 'gateway.logs.saving-period-required' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('min')">
{{ 'gateway.logs.saving-period-min' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" hideRequiredMarker style="min-width: 110px; width: 30%">
<mat-select formControlName="savingPeriod">
<mat-option *ngFor="let period of logSavingPeriods | keyvalue" [value]="period.key">
{{ period.value | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.logs.backup-count</mat-label>
<input matInput formControlName="backupCount" type="number" min="0"/>
<mat-error
*ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('required')">
{{ 'gateway.logs.backup-count-required' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')">
{{ 'gateway.logs.backup-count-min' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.backup-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</ng-container>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab label="{{ 'gateway.storage' | translate }}">
<ng-template matTabContent>
<div formGroupName="storage" class="mat-content mat-padding configuration-block">
<div class="tb-form-panel no-padding-bottom">
<div translate class="tb-form-panel-title">gateway.storage</div>
<div translate class="tb-form-panel-hint">gateway.hints.storage</div>
<tb-toggle-select class="toggle-group" formControlName="type">
<tb-toggle-option *ngFor="let storageType of storageTypes" [value]="storageType">
{{ storageTypesTranslationMap.get(storageType) | translate }}
</tb-toggle-option>
</tb-toggle-select>
<div class="tb-form-panel-hint">{{ 'gateway.hints.' + basicFormGroup.get('storage.type').value | translate }}</div>
<ng-container [ngSwitch]="basicFormGroup.get('storage.type').value">
<section *ngSwitchCase="StorageTypes.MEMORY" class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-read-record-count</mat-label>
<input type="number" matInput formControlName="read_records_count"/>
<mat-error *ngIf="basicFormGroup.get('storage.read_records_count').hasError('required')">
{{ 'gateway.storage-read-record-count-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.read_records_count').hasError('min')">
{{ 'gateway.storage-read-record-count-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.read_records_count').hasError('pattern')">
{{ 'gateway.storage-read-record-count-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.read-record-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-max-records</mat-label>
<input type="number" matInput formControlName="max_records_count"/>
<mat-error *ngIf="basicFormGroup.get('storage.max_records_count').hasError('required')">
{{ 'gateway.storage-max-records-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.max_records_count').hasError('min')">
{{ 'gateway.storage-max-records-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.max_records_count').hasError('pattern')">
{{ 'gateway.storage-max-records-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.max-records-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
<section *ngSwitchCase="StorageTypes.FILE">
<div class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-data-folder-path</mat-label>
<input matInput formControlName="data_folder_path"/>
<mat-error *ngIf="basicFormGroup.get('storage.data_folder_path').hasError('required')">
{{ 'gateway.storage-data-folder-path-required' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
aria-label="help-icon"
matSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.data-folder' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-max-files</mat-label>
<input matInput type="number" formControlName="max_file_count"/>
<mat-error *ngIf="basicFormGroup.get('storage.max_file_count').hasError('required')">
{{ 'gateway.storage-max-files-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.max_file_count').hasError('min')">
{{ 'gateway.storage-max-files-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.max_file_count').hasError('pattern')">
{{ 'gateway.storage-max-files-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.max-file-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-max-read-record-count</mat-label>
<input matInput type="number" formControlName="max_read_records_count"/>
<mat-error *ngIf="basicFormGroup.get('storage.max_read_records_count').hasError('required')">
{{ 'gateway.storage-max-read-record-count-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.max_read_records_count').hasError('min')">
{{ 'gateway.storage-max-read-record-count-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.max_read_records_count').hasError('pattern')">
{{ 'gateway.storage-max-read-record-count-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.max-read-count' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-max-file-records</mat-label>
<input matInput type="number" formControlName="max_records_per_file"/>
<mat-error *ngIf="basicFormGroup.get('storage.max_records_per_file').hasError('required')">
{{ 'gateway.storage-max-records-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.max_records_per_file').hasError('min')">
{{ 'gateway.storage-max-records-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.max_records_per_file').hasError('pattern')">
{{ 'gateway.storage-max-records-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.max-records' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
</section>
<section *ngSwitchCase="StorageTypes.SQLITE">
<div class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-path</mat-label>
<input matInput formControlName="data_file_path"/>
<mat-error *ngIf="basicFormGroup.get('storage.data_file_path').hasError('required')">
{{ 'gateway.storage-path-required' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.data-folder' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.messages-ttl-check-in-hours</mat-label>
<input matInput type="number" formControlName="messages_ttl_check_in_hours"/>
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_check_in_hours').hasError('required')">
{{ 'gateway.messages-ttl-check-in-hours-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_check_in_hours').hasError('min')">
{{ 'gateway.messages-ttl-check-in-hours-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_check_in_hours').hasError('pattern')">
{{ 'gateway.messages-ttl-check-in-hours-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.ttl-check-hour' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="mat-block">
<mat-label translate>gateway.messages-ttl-in-days</mat-label>
<input matInput type="number" formControlName="messages_ttl_in_days"/>
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_in_days').hasError('required')">
{{ 'gateway.messages-ttl-in-days-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_in_days').hasError('min')">
{{ 'gateway.messages-ttl-in-days-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_in_days').hasError('pattern')">
{{ 'gateway.messages-ttl-in-days-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.ttl-messages-day' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
</ng-container>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab label="{{ 'gateway.grpc' | translate }}">
<ng-template matTabContent>
<div formGroupName="grpc" class="mat-content mat-padding configuration-block">
<div class="tb-form-panel no-padding-bottom">
<mat-slide-toggle class="mat-slide" color="primary" formControlName="enabled">
{{ 'gateway.grpc' | translate }}
</mat-slide-toggle>
<div tb-hint-tooltip-icon="{{ 'gateway.hints.permit-without-calls' | translate }}"
class="tb-form-row no-border no-padding">
<mat-slide-toggle class="mat-slide" color="primary" formControlName="keepalivePermitWithoutCalls">
{{ 'gateway.permit-without-calls' | translate }}
</mat-slide-toggle>
</div>
<section>
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.server-port</mat-label>
<input matInput formControlName="serverPort" type="number" min="0"/>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.server-port' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="basicFormGroup.get('grpc.serverPort').hasError('required')">
{{ 'gateway.thingsboard-port-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.serverPort').hasError('min')">
{{ 'gateway.thingsboard-port-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.serverPort').hasError('max')">
{{ 'gateway.thingsboard-port-max' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.serverPort').hasError('pattern')">
{{ 'gateway.thingsboard-port-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.grpc-keep-alive-timeout</mat-label>
<input matInput formControlName="keepAliveTimeoutMs" type="number" min="0"/>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-keep-alive-timeout' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeoutMs').hasError('required')">
{{ 'gateway.grpc-keep-alive-timeout-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeoutMs').hasError('min')">
{{ 'gateway.grpc-keep-alive-timeout-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeoutMs').hasError('pattern')">
{{ 'gateway.grpc-keep-alive-timeout-pattern' | translate }}
</mat-error>
</mat-form-field>
</section>
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.grpc-keep-alive</mat-label>
<input matInput formControlName="keepAliveTimeMs" type="number" min="0"/>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-keep-alive' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeMs').hasError('required')">
{{ 'gateway.grpc-keep-alive-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeMs').hasError('min')">
{{ 'gateway.grpc-keep-alive-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeMs').hasError('pattern')">
{{ 'gateway.grpc-keep-alive-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.grpc-min-time-between-pings</mat-label>
<input matInput formControlName="minTimeBetweenPingsMs" type="number" min="0"/>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-min-time-between-pings' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="basicFormGroup.get('grpc.minTimeBetweenPingsMs').hasError('required')">
{{ 'gateway.grpc-min-time-between-pings-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.minTimeBetweenPingsMs').hasError('min')">
{{ 'gateway.grpc-min-time-between-pings-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.minTimeBetweenPingsMs').hasError('pattern')">
{{ 'gateway.grpc-min-time-between-pings-pattern' | translate }}
</mat-error>
</mat-form-field>
</section>
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.grpc-max-pings-without-data</mat-label>
<input matInput formControlName="maxPingsWithoutData" type="number" min="0"/>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-max-pings-without-data' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="basicFormGroup.get('grpc.maxPingsWithoutData').hasError('required')">
{{ 'gateway.grpc-max-pings-without-data-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.maxPingsWithoutData').hasError('min')">
{{ 'gateway.grpc-max-pings-without-data-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.maxPingsWithoutData').hasError('pattern')">
{{ 'gateway.grpc-max-pings-without-data-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.grpc-min-ping-interval-without-data</mat-label>
<input matInput formControlName="minPingIntervalWithoutDataMs" type="number" min="0"/>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-min-ping-interval-without-data' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="basicFormGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('required')">
{{ 'gateway.grpc-min-ping-interval-without-data-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('min')">
{{ 'gateway.grpc-min-ping-interval-without-data-min' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('pattern')">
{{ 'gateway.grpc-min-ping-interval-without-data-pattern' | translate }}
</mat-error>
</mat-form-field>
</section>
</section>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab label="{{ 'gateway.statistics.statistics' | translate }}">
<ng-template matTabContent>
<div formGroupName="thingsboard" class="mat-content mat-padding configuration-block">
<div class="tb-form-panel no-padding-bottom" formGroupName="statistics">
<mat-slide-toggle color="primary" class="mat-slide" formControlName="enable">
{{ 'gateway.statistics.statistics' | translate }}
</mat-slide-toggle>
<mat-form-field appearance="outline">
<mat-label translate>gateway.statistics.send-period</mat-label>
<input matInput formControlName="statsSendPeriodInSeconds" type="number" min="60"/>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('required')">
{{ 'gateway.statistics.send-period-required' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('min')">
{{ 'gateway.statistics.send-period-min' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('pattern')">
{{ 'gateway.statistics.send-period-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>gateway.statistics.commands</div>
<div class="tb-form-panel-hint" translate>gateway.hints.commands</div>
<ng-container formGroupName="statistics">
<div formArrayName="commands" class="statistics-container flex flex-row"
*ngFor="let commandControl of commandFormArray().controls; let $index = index">
<section [formGroupName]="$index" class="tb-form-panel stroked no-padding-bottom no-gap command-container">
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.statistics.attribute-name</mat-label>
<input matInput formControlName="attributeOnGateway"/>
<mat-error *ngIf="commandControl.get('attributeOnGateway').hasError('required')">
{{ 'gateway.statistics.attribute-name-required' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.attribute' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.statistics.timeout</mat-label>
<input matInput formControlName="timeout" type="number" min="0"/>
<mat-error *ngIf="commandControl.get('timeout').hasError('required')">
{{ 'gateway.statistics.timeout-required' | translate }}
</mat-error>
<mat-error *ngIf="commandControl.get('timeout').hasError('min')">
{{ 'gateway.statistics.timeout-min' | translate }}
</mat-error>
<mat-error *ngIf="commandControl.get('timeout').hasError('pattern')">
{{ 'gateway.statistics.timeout-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.timeout' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
<mat-form-field appearance="outline" class="mat-block">
<mat-label translate>gateway.statistics.command</mat-label>
<input matInput formControlName="command"/>
<mat-error *ngIf="commandControl.get('command').hasError('required')">
{{ 'gateway.statistics.command-required' | translate }}
</mat-error>
<mat-error *ngIf="commandControl.get('command').hasError('pattern')">
{{ 'gateway.statistics.command-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.command' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
<button mat-icon-button (click)="removeCommandControl($index, $event)"
class="tb-box-button"
[disabled]="!basicFormGroup.get('thingsboard.remoteConfiguration').value"
matTooltip="{{ 'gateway.statistics.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
<button mat-stroked-button color="primary"
style="width: fit-content;"
type="button"
[disabled]="!basicFormGroup.get('thingsboard.remoteConfiguration').value"
(click)="addCommand()">
{{ 'gateway.statistics.add' | translate }}
</button>
</ng-container>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab label="{{ 'gateway.other' | translate }}">
<ng-template matTabContent>
<div formGroupName="thingsboard" class="mat-content mat-padding configuration-block">
<div class="tb-form-panel" formGroupName="checkingDeviceActivity"
[class.no-padding-bottom]="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value">
<div tb-hint-tooltip-icon="{{ 'gateway.hints.check-device-activity' | translate }}"
class="tb-form-row no-border no-padding">
<mat-slide-toggle class="mat-slide" color="primary" formControlName="checkDeviceInactivity">
{{ 'gateway.checking-device-activity' | translate }}
</mat-slide-toggle>
</div>
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs"
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.inactivity-timeout-seconds</mat-label>
<input matInput formControlName="inactivityTimeoutSeconds" type="number" min="0"/>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('required')">
{{ 'gateway.inactivity-timeout-seconds-required' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')">
{{ 'gateway.inactivity-timeout-seconds-min' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('pattern')">
{{ 'gateway.inactivity-timeout-seconds-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.inactivity-timeout' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.inactivity-check-period-seconds</mat-label>
<input matInput type="number" min="0" formControlName="inactivityCheckPeriodSeconds"/>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')">
{{ 'gateway.inactivity-check-period-seconds-required' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')">
{{ 'gateway.inactivity-check-period-seconds-min' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('pattern')">
{{ 'gateway.inactivity-check-period-seconds-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.inactivity-period' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
</div>
<div class="tb-form-panel no-padding-bottom">
<div class="tb-form-panel-title" translate>gateway.advanced</div>
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.min-pack-send-delay</mat-label>
<input matInput formControlName="minPackSendDelayMS" type="number" min="0"/>
<mat-error *ngIf="basicFormGroup.get('thingsboard.minPackSendDelayMS').hasError('required')">
{{ 'gateway.min-pack-send-delay-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('thingsboard.minPackSendDelayMS').hasError('min')">
{{ 'gateway.min-pack-send-delay-min' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.minPackSendDelayMS').hasError('pattern')">
{{ 'gateway.min-pack-send-delay-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.minimal-pack-delay' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.mqtt-qos</mat-label>
<input matInput formControlName="qos" type="number" min="0" max="1"/>
<mat-error *ngIf="basicFormGroup.get('thingsboard.qos').hasError('required')">
{{ 'gateway.mqtt-qos-required' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('thingsboard.qos').hasError('min')">
{{ 'gateway.mqtt-qos-range' | translate }}
</mat-error>
<mat-error *ngIf="basicFormGroup.get('thingsboard.qos').hasError('max')">
{{ 'gateway.mqtt-qos-range' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.qos' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.statistics.check-connectors-configuration</mat-label>
<input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number" min="0"/>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')">
{{ 'gateway.statistics.check-connectors-configuration-required' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')">
{{ 'gateway.statistics.check-connectors-configuration-min' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')">
{{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.statistics.max-payload-size-bytes</mat-label>
<input matInput formControlName="maxPayloadSizeBytes" type="number" min="0"/>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('required')">
{{ 'gateway.statistics.max-payload-size-bytes-required' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('min')">
{{ 'gateway.statistics.max-payload-size-bytes-min' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('pattern')">
{{ 'gateway.statistics.max-payload-size-bytes-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.max-payload-size-bytes' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.statistics.min-pack-size-to-send</mat-label>
<input matInput formControlName="minPackSizeToSend" type="number" min="0"/>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('required')">
{{ 'gateway.statistics.min-pack-size-to-send-required' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('min')">
{{ 'gateway.statistics.min-pack-size-to-send-min' | translate }}
</mat-error>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('pattern')">
{{ 'gateway.statistics.min-pack-size-to-send-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.min-pack-size-to-send' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
</div>
</div>
</ng-template>
</mat-tab>
</mat-tab-group>

103
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss

@ -1,103 +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.
*/
:host {
width: 100%;
height: 100%;
display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
.configuration-block {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 70vh;
}
.dialog-mode {
.configuration-block {
max-height: 60vh;
}
}
.mat-toolbar {
grid-row: 1;
background: transparent;
color: rgba(0, 0, 0, .87) !important;
}
.tab-group-block {
min-width: 0;
height: 100%;
min-height: 0;
grid-row: 2;
}
.toggle-group {
margin-right: auto;
}
.first-capital {
text-transform: capitalize;
}
textarea {
resize: none;
}
.saving-period {
flex: 1;
}
.statistics-container {
width: 100%;
.command-container {
width: 100%;
}
}
mat-form-field {
mat-error {
display: none !important;
}
mat-error:first-child {
display: block !important;
}
}
}
:host ::ng-deep {
.pointer-event {
pointer-events: all;
}
.toggle-group span {
padding: 0 25px;
}
.mat-mdc-form-field-icon-suffix {
color: #E0E0E0;
&:hover {
color: #9E9E9E;
}
}
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

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

@ -1,568 +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.
///
import {
ChangeDetectorRef,
Component,
EventEmitter,
forwardRef,
Input,
OnDestroy,
Output
} from '@angular/core';
import {
ControlValueAccessor,
FormArray,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
ValidatorFn,
Validators
} from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog } from '@angular/material/dialog';
import {
GatewayRemoteConfigurationDialogComponent,
GatewayRemoteConfigurationDialogData
} from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog';
import { DeviceService } from '@core/http/device.service';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import {
GatewayLogLevel,
GecurityTypesTranslationsMap,
LocalLogsConfigTranslateMap,
LocalLogsConfigs,
LogSavingPeriod,
LogSavingPeriodTranslations,
SecurityTypes,
StorageTypes,
StorageTypesTranslationMap,
} from '../../gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { coerceBoolean } from '@shared/decorators/coercion';
import {
GatewayConfigCommand,
GatewayConfigSecurity,
GatewayConfigValue,
LogConfig
} from '@home/components/widget/lib/gateway/configuration/models/gateway-configuration.models';
@Component({
selector: 'tb-gateway-basic-configuration',
templateUrl: './gateway-basic-configuration.component.html',
styleUrls: ['./gateway-basic-configuration.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GatewayBasicConfigurationComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => GatewayBasicConfigurationComponent),
multi: true
}
],
})
export class GatewayBasicConfigurationComponent implements OnDestroy, ControlValueAccessor, Validators {
@Input()
device: EntityId;
@coerceBoolean()
@Input()
dialogMode = false;
@Output()
initialCredentialsUpdated = new EventEmitter<DeviceCredentials>();
StorageTypes = StorageTypes;
storageTypes = Object.values(StorageTypes);
storageTypesTranslationMap = StorageTypesTranslationMap;
logSavingPeriods = LogSavingPeriodTranslations;
localLogsConfigs = Object.keys(LocalLogsConfigs) as LocalLogsConfigs[];
localLogsConfigTranslateMap = LocalLogsConfigTranslateMap;
securityTypes = GecurityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
logSelector: FormControl;
basicFormGroup: FormGroup;
private onChange: (value: GatewayConfigValue) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder,
private deviceService: DeviceService,
private cd: ChangeDetectorRef,
private dialog: MatDialog) {
this.initBasicFormGroup();
this.observeFormChanges();
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: GatewayConfigValue) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(basicConfig: GatewayConfigValue): void {
this.basicFormGroup.patchValue(basicConfig, {emitEvent: false});
this.checkAndFetchCredentials(basicConfig?.thingsboard?.security ?? {} as GatewayConfigSecurity);
if (basicConfig?.grpc) {
this.toggleRpcFields(basicConfig.grpc.enabled);
}
const commands = basicConfig?.thingsboard?.statistics?.commands ?? [];
commands.forEach((command: GatewayConfigCommand) => this.addCommand(command, false));
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false}
};
}
private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) {
return (group: FormGroup): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
return hasAtLeastOne ? null : {atLeastOne: true};
};
}
private toggleRpcFields(enable: boolean): void {
const grpcGroup = this.basicFormGroup.get('grpc') as FormGroup;
if (enable) {
grpcGroup.get('serverPort').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false});
} else {
grpcGroup.get('serverPort').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false});
}
}
private addLocalLogConfig(name: string, config: LogConfig): void {
const localLogsFormGroup = this.basicFormGroup.get('logs.local') as FormGroup;
const configGroup = this.fb.group({
logLevel: [config.logLevel || GatewayLogLevel.INFO, [Validators.required]],
filePath: [config.filePath || './logs', [Validators.required]],
backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]],
savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]],
savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]]
});
localLogsFormGroup.addControl(name, configGroup);
}
getLogFormGroup(value: string): FormGroup {
return this.basicFormGroup.get(`logs.local.${value}`) as FormGroup;
}
commandFormArray(): FormArray {
return this.basicFormGroup.get('thingsboard.statistics.commands') as FormArray;
}
removeCommandControl(index: number, event: PointerEvent): void {
if (event.pointerType === '') {
return;
}
this.commandFormArray().removeAt(index);
this.basicFormGroup.markAsDirty();
}
private removeAllSecurityValidators(): void {
const securityGroup = this.basicFormGroup.get('thingsboard.security') as FormGroup;
securityGroup.clearValidators();
for (const controlsKey in securityGroup.controls) {
if (controlsKey !== 'type') {
securityGroup.controls[controlsKey].clearValidators();
securityGroup.controls[controlsKey].setErrors(null);
securityGroup.controls[controlsKey].updateValueAndValidity();
}
}
}
private removeAllStorageValidators(): void {
const storageGroup = this.basicFormGroup.get('storage') as FormGroup;
for (const storageKey in storageGroup.controls) {
if (storageKey !== 'type') {
storageGroup.controls[storageKey].clearValidators();
storageGroup.controls[storageKey].setErrors(null);
storageGroup.controls[storageKey].updateValueAndValidity();
}
}
}
private openConfigurationConfirmDialog(): void {
this.deviceService.getDevice(this.device.id).pipe(takeUntil(this.destroy$)).subscribe(gateway => {
this.dialog.open<GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogData>
(GatewayRemoteConfigurationDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
gatewayName: gateway.name
}
}).afterClosed().pipe(take(1)).subscribe(
(res) => {
if (!res) {
this.basicFormGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false});
}
}
);
});
}
addCommand(command?: GatewayConfigCommand, emitEvent: boolean = true): void {
const { attributeOnGateway = null, command: cmd = null, timeout = null } = command || {};
const commandFormGroup = this.fb.group({
attributeOnGateway: [attributeOnGateway, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
command: [cmd, [Validators.required, Validators.pattern(/^(?=\S).*\S$/)]],
timeout: [timeout, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]]
});
this.commandFormArray().push(commandFormGroup, { emitEvent });
}
private initBasicFormGroup(): void {
this.basicFormGroup = this.fb.group({
thingsboard: this.initThingsboardFormGroup(),
storage: this.initStorageFormGroup(),
grpc: this.initGrpcFormGroup(),
connectors: this.fb.array([]),
logs: this.initLogsFormGroup(),
});
}
private initThingsboardFormGroup(): FormGroup {
return this.fb.group({
host: [window.location.hostname, [Validators.required, Validators.pattern(/^[^\s]+$/)]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false],
remoteConfiguration: [true],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
statistics: this.fb.group({
enable: [true],
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(60), Validators.pattern(/^-?[0-9]+$/)]],
commands: this.fb.array([])
}),
maxPayloadSizeBytes: [8196, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [50, [Validators.required, Validators.min(10), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [true],
checkingDeviceActivity: this.initCheckingDeviceActivityFormGroup(),
security: this.initSecurityFormGroup(),
qos: [1, [Validators.required, Validators.min(0), Validators.max(1), Validators.pattern(/^[^.\s]+$/)]]
});
}
private initStorageFormGroup(): FormGroup {
return this.fb.group({
type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [100, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_count: [100000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_folder_path: ['./data/', [Validators.required]],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_file_path: ['./data/data.db', [Validators.required]],
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
});
}
private initGrpcFormGroup(): FormGroup {
return this.fb.group({
enabled: [false],
serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepalivePermitWithoutCalls: [true],
maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
});
}
private initLogsFormGroup(): FormGroup {
return this.fb.group({
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
logFormat: [
'%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
[Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]
],
type: ['remote', [Validators.required]],
remote: this.fb.group({
enabled: [false],
logLevel: [GatewayLogLevel.INFO, [Validators.required]]
}),
local: this.fb.group({})
});
}
private initCheckingDeviceActivityFormGroup(): FormGroup {
return this.fb.group({
checkDeviceInactivity: [false],
inactivityTimeoutSeconds: [200, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
});
}
private initSecurityFormGroup(): FormGroup {
return this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
clientId: [null, [Validators.pattern(/^[^.\s]+$/)]],
username: [null, [Validators.pattern(/^[^.\s]+$/)]],
password: [null, [Validators.pattern(/^[^.\s]+$/)]],
caCert: [null],
cert: [null],
privateKey: [null]
});
}
private observeFormChanges(): void {
this.observeSecurityPasswordChanges();
this.observeRemoteConfigurationChanges();
this.observeDeviceActivityChanges();
this.observeSecurityTypeChanges();
this.observeStorageTypeChanges();
}
private observeSecurityPasswordChanges(): void {
const securityUsername = this.basicFormGroup.get('thingsboard.security.username');
this.basicFormGroup.get('thingsboard.security.password').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(password => {
if (password && password !== '') {
securityUsername.setValidators([Validators.required]);
} else {
securityUsername.clearValidators();
}
securityUsername.updateValueAndValidity({ emitEvent: false });
});
}
private observeRemoteConfigurationChanges(): void {
this.basicFormGroup.get('thingsboard.remoteConfiguration').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(enabled => {
if (!enabled) {
this.openConfigurationConfirmDialog();
}
});
this.logSelector = this.fb.control(LocalLogsConfigs.service);
for (const key of Object.keys(LocalLogsConfigs)) {
this.addLocalLogConfig(key, {} as LogConfig);
}
}
private observeDeviceActivityChanges(): void {
const checkingDeviceActivityGroup = this.basicFormGroup.get('thingsboard.checkingDeviceActivity') as FormGroup;
checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(enabled => {
checkingDeviceActivityGroup.updateValueAndValidity();
const validators = [Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)];
if (enabled) {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators(validators);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators(validators);
} else {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators();
}
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').updateValueAndValidity({ emitEvent: false });
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({ emitEvent: false });
});
this.basicFormGroup.get('grpc.enabled').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
this.toggleRpcFields(value);
});
}
private observeSecurityTypeChanges(): void {
const securityGroup = this.basicFormGroup.get('thingsboard.security') as FormGroup;
securityGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.removeAllSecurityValidators();
switch (type) {
case SecurityTypes.ACCESS_TOKEN:
this.addAccessTokenValidators(securityGroup);
break;
case SecurityTypes.TLS_PRIVATE_KEY:
this.addTlsPrivateKeyValidators(securityGroup);
break;
case SecurityTypes.TLS_ACCESS_TOKEN:
this.addTlsAccessTokenValidators(securityGroup);
break;
case SecurityTypes.USERNAME_PASSWORD:
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
break;
}
securityGroup.updateValueAndValidity();
});
['caCert', 'privateKey', 'cert'].forEach(field => {
securityGroup.get(field).valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => this.cd.detectChanges());
});
}
private observeStorageTypeChanges(): void {
const storageGroup = this.basicFormGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.removeAllStorageValidators();
switch (type) {
case StorageTypes.MEMORY:
this.addMemoryStorageValidators(storageGroup);
break;
case StorageTypes.FILE:
this.addFileStorageValidators(storageGroup);
break;
case StorageTypes.SQLITE:
this.addSqliteStorageValidators(storageGroup);
break;
}
});
}
private addAccessTokenValidators(group: FormGroup): void {
group.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
group.get('accessToken').updateValueAndValidity();
}
private addTlsPrivateKeyValidators(group: FormGroup): void {
['caCert', 'privateKey', 'cert'].forEach(field => {
group.get(field).addValidators([Validators.required]);
group.get(field).updateValueAndValidity();
});
}
private addTlsAccessTokenValidators(group: FormGroup): void {
this.addAccessTokenValidators(group);
group.get('caCert').addValidators([Validators.required]);
group.get('caCert').updateValueAndValidity();
}
private addMemoryStorageValidators(group: FormGroup): void {
group.get('read_records_count').addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get('max_records_count').addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get('read_records_count').updateValueAndValidity({ emitEvent: false });
group.get('max_records_count').updateValueAndValidity({ emitEvent: false });
}
private addFileStorageValidators(group: FormGroup): void {
['max_file_count', 'max_read_records_count', 'max_records_per_file'].forEach(field => {
group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get(field).updateValueAndValidity({ emitEvent: false });
});
}
private addSqliteStorageValidators(group: FormGroup): void {
['messages_ttl_check_in_hours', 'messages_ttl_in_days'].forEach(field => {
group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get(field).updateValueAndValidity({ emitEvent: false });
});
}
private checkAndFetchCredentials(security: GatewayConfigSecurity): void {
if (security.type === SecurityTypes.TLS_PRIVATE_KEY) {
return;
}
this.deviceService.getDeviceCredentials(this.device.id).pipe(takeUntil(this.destroy$)).subscribe(credentials => {
this.initialCredentialsUpdated.emit(credentials);
this.updateSecurityType(security, credentials);
this.updateCredentials(credentials, security);
});
}
private updateSecurityType(security, credentials: DeviceCredentials): void {
const isAccessToken = credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN
|| security.type === SecurityTypes.TLS_ACCESS_TOKEN;
const securityType = isAccessToken
? (security.type === SecurityTypes.TLS_ACCESS_TOKEN ? SecurityTypes.TLS_ACCESS_TOKEN : SecurityTypes.ACCESS_TOKEN)
: (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC ? SecurityTypes.USERNAME_PASSWORD : null);
if (securityType) {
this.basicFormGroup.get('thingsboard.security.type').setValue(securityType, { emitEvent: false });
}
}
private updateCredentials(credentials: DeviceCredentials, security: GatewayConfigSecurity): void {
switch (credentials.credentialsType) {
case DeviceCredentialsType.ACCESS_TOKEN:
this.updateAccessTokenCredentials(credentials, security);
break;
case DeviceCredentialsType.MQTT_BASIC:
this.updateMqttBasicCredentials(credentials);
break;
case DeviceCredentialsType.X509_CERTIFICATE:
break;
}
}
private updateAccessTokenCredentials(credentials: DeviceCredentials, security: GatewayConfigSecurity): void {
this.basicFormGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId, { emitEvent: false });
if (security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.basicFormGroup.get('thingsboard.security.caCert').setValue(security.caCert, { emitEvent: false });
}
}
private updateMqttBasicCredentials(credentials: DeviceCredentials): void {
const parsedValue = JSON.parse(credentials.credentialsValue);
this.basicFormGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId, { emitEvent: false });
this.basicFormGroup.get('thingsboard.security.username').setValue(parsedValue.userName, { emitEvent: false });
this.basicFormGroup.get('thingsboard.security.password').setValue(parsedValue.password, { emitEvent: false });
}
}

64
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html

@ -1,64 +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 [formGroup]="gatewayConfigGroup" class="gateway-config-container">
<div class="content-wrapper">
<mat-toolbar color="primary" [class.page-header]="!dialogRef">
<div class="tb-flex space-between align-center">
<h2 translate>gateway.gateway-configuration</h2>
<div class="toolbar-actions">
<tb-toggle-select [class.dialog-toggle]="!!dialogRef" formControlName="mode" appearance="{{dialogRef ? 'stroked' : 'fill'}}">
<tb-toggle-option [value]="ConfigurationModes.BASIC">
{{ 'gateway.basic' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="ConfigurationModes.ADVANCED">
{{ 'gateway.advanced' | translate }}
</tb-toggle-option>
</tb-toggle-select>
<button *ngIf="dialogRef" mat-icon-button (click)="cancel()" type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</div>
</div>
</mat-toolbar>
<tb-gateway-basic-configuration
*ngIf="gatewayConfigGroup.get('mode').value === ConfigurationModes.BASIC"
formControlName="basicConfig"
[device]="device"
[dialogMode]="!!dialogRef"
(initialCredentialsUpdated)="initialCredentials = $event"
/>
<tb-gateway-advanced-configuration
*ngIf="gatewayConfigGroup.get('mode').value === ConfigurationModes.ADVANCED"
formControlName="advancedConfig"
/>
</div>
<div class="actions">
<button mat-button color="primary"
type="button"
*ngIf="dialogRef"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
[disabled]="gatewayConfigGroup.invalid || !gatewayConfigGroup.dirty"
(click)="saveConfig()">
{{ 'action.save' | translate }}
</button>
</div>
</div>

64
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss

@ -1,64 +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.
*/
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.page-header.mat-toolbar {
background: transparent;
color: rgba(0, 0, 0, .87) !important;
}
.actions {
grid-row: 3;
padding: 8px 16px 8px 8px;
display: flex;
gap: 8px;
justify-content: flex-end;
position: absolute;
bottom: 0;
right: 0;
z-index: 1;
background: white;
width: 100%;
}
.gateway-config-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.content-wrapper {
flex: 1;
}
.toolbar-actions {
display: flex;
align-items: center;
}
}
.dialog-toggle {
::ng-deep.mat-button-toggle-button {
color: rgba(255, 255, 255, .75);
}
}

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

@ -1,396 +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.
///
import { ChangeDetectorRef, Component, Input, AfterViewInit, OnDestroy } from '@angular/core';
import {
FormBuilder,
FormGroup,
} from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { DeviceService } from '@core/http/device.service';
import { Observable, of, Subject } from 'rxjs';
import { mergeMap, switchMap, takeUntil } from 'rxjs/operators';
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import {
GatewayLogLevel,
SecurityTypes,
ConfigurationModes,
LocalLogsConfigs,
LogSavingPeriod, Attribute
} from '../gateway-widget.models';
import { deepTrim, isEqual } from '@core/utils';
import {
GatewayConfigSecurity,
GatewayConfigValue,
GatewayGeneralConfig,
GatewayGRPCConfig,
GatewayLogsConfig,
GatewayStorageConfig,
LocalLogs,
LogAttribute,
LogConfig,
} from './models/gateway-configuration.models';
import { DeviceId } from '@shared/models/id/device-id';
@Component({
selector: 'tb-gateway-configuration',
templateUrl: './gateway-configuration.component.html',
styleUrls: ['./gateway-configuration.component.scss']
})
export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
@Input() device: EntityId;
@Input() dialogRef: MatDialogRef<GatewayConfigurationComponent>;
initialCredentials: DeviceCredentials;
gatewayConfigGroup: FormGroup;
ConfigurationModes = ConfigurationModes;
private destroy$ = new Subject<void>();
private readonly gatewayConfigAttributeKeys =
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel', 'mode'];
constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private deviceService: DeviceService,
private cd: ChangeDetectorRef
) {
this.gatewayConfigGroup = this.fb.group({
basicConfig: [],
advancedConfig: [],
mode: [ConfigurationModes.BASIC],
});
this.observeAlignConfigs();
}
ngAfterViewInit(): void {
this.fetchConfigAttribute(this.device);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
saveConfig(): void {
const { mode, advancedConfig } = deepTrim(this.removeEmpty(this.gatewayConfigGroup.value));
const value = { mode, ...advancedConfig as GatewayConfigValue };
value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands ?? []);
const attributes = this.generateAttributes(value);
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).pipe(
switchMap(_ => this.updateCredentials(value.thingsboard.security)),
takeUntil(this.destroy$),
).subscribe(() => {
if (this.dialogRef) {
this.dialogRef.close();
} else {
this.gatewayConfigGroup.markAsPristine();
this.cd.detectChanges();
}
});
}
private observeAlignConfigs(): void {
this.gatewayConfigGroup.get('basicConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
const advancedControl = this.gatewayConfigGroup.get('advancedConfig');
if (!isEqual(advancedControl.value, value) && this.gatewayConfigGroup.get('mode').value === ConfigurationModes.BASIC) {
advancedControl.patchValue(value, {emitEvent: false});
}
});
this.gatewayConfigGroup.get('advancedConfig').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
const basicControl = this.gatewayConfigGroup.get('basicConfig');
if (!isEqual(basicControl.value, value) && this.gatewayConfigGroup.get('mode').value === ConfigurationModes.ADVANCED) {
basicControl.patchValue(value, {emitEvent: false});
}
});
}
private generateAttributes(value: GatewayConfigValue): Attribute[] {
const attributes = [];
const addAttribute = (key: string, val: unknown) => {
attributes.push({ key, value: val });
};
const addTimestampedAttribute = (key: string, val: unknown) => {
val = {...val as Record<string, unknown>, ts: new Date().getTime()};
addAttribute(key, val);
};
addAttribute('RemoteLoggingLevel', value.logs?.remote?.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE);
delete value.connectors;
addAttribute('logs_configuration', this.generateLogsFile(value.logs));
addTimestampedAttribute('grpc_configuration', value.grpc);
addTimestampedAttribute('storage_configuration', value.storage);
addTimestampedAttribute('general_configuration', value.thingsboard);
addAttribute('mode', value.mode);
return attributes;
}
private updateCredentials(securityConfig: GatewayConfigSecurity): Observable<DeviceCredentials> {
let newCredentials: Partial<DeviceCredentials> = {};
switch (securityConfig.type) {
case SecurityTypes.USERNAME_PASSWORD:
if (this.shouldUpdateCredentials(securityConfig)) {
newCredentials = this.generateMqttCredentials(securityConfig);
}
break;
case SecurityTypes.ACCESS_TOKEN:
case SecurityTypes.TLS_ACCESS_TOKEN:
if (this.shouldUpdateAccessToken(securityConfig)) {
newCredentials = {
credentialsType: DeviceCredentialsType.ACCESS_TOKEN,
credentialsId: securityConfig.accessToken
};
}
break;
}
return Object.keys(newCredentials).length
? this.deviceService.saveDeviceCredentials({ ...this.initialCredentials, ...newCredentials })
: of(null);
}
private shouldUpdateCredentials(securityConfig: GatewayConfigSecurity): boolean {
if (this.initialCredentials.credentialsType !== DeviceCredentialsType.MQTT_BASIC) {
return true;
}
const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue);
return !(
parsedCredentials.clientId === securityConfig.clientId &&
parsedCredentials.userName === securityConfig.username &&
parsedCredentials.password === securityConfig.password
);
}
private generateMqttCredentials(securityConfig: GatewayConfigSecurity): Partial<DeviceCredentials> {
const { clientId, username, password } = securityConfig;
const credentialsValue = {
...(clientId && { clientId }),
...(username && { userName: username }),
...(password && { password }),
};
return {
credentialsType: DeviceCredentialsType.MQTT_BASIC,
credentialsValue: JSON.stringify(credentialsValue)
};
}
private shouldUpdateAccessToken(securityConfig: GatewayConfigSecurity): boolean {
return this.initialCredentials.credentialsType !== DeviceCredentialsType.ACCESS_TOKEN ||
this.initialCredentials.credentialsId !== securityConfig.accessToken;
}
cancel(): void {
if (this.dialogRef) {
this.dialogRef.close();
}
}
private removeEmpty(obj: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v != null)
.map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v as Record<string, unknown>) : v])
);
}
private generateLogsFile(logsObj: GatewayLogsConfig): LogAttribute {
const logAttrObj = {
version: 1,
disable_existing_loggers: false,
formatters: {
LogFormatter: {
class: 'logging.Formatter',
format: logsObj.logFormat,
datefmt: logsObj.dateFormat,
}
},
handlers: {
consoleHandler: {
class: 'logging.StreamHandler',
formatter: 'LogFormatter',
level: 0,
stream: 'ext://sys.stdout'
},
databaseHandler: {
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler',
formatter: 'LogFormatter',
filename: './logs/database.log',
backupCount: 1,
encoding: 'utf-8'
}
},
loggers: {
database: {
handlers: ['databaseHandler', 'consoleHandler'],
level: 'DEBUG',
propagate: false
}
},
root: {
level: 'ERROR',
handlers: [
'consoleHandler'
]
},
ts: new Date().getTime()
};
this.addLocalLoggers(logAttrObj, logsObj.local);
return logAttrObj;
}
private addLocalLoggers(logAttrObj: LogAttribute, localLogs: LocalLogs): void {
for (const key of Object.keys(localLogs)) {
logAttrObj.handlers[key + 'Handler'] = this.createHandlerObj(localLogs[key], key);
logAttrObj.loggers[key] = this.createLoggerObj(localLogs[key], key);
}
}
private createHandlerObj(logObj: LogConfig, key: string) {
return {
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler',
formatter: 'LogFormatter',
filename: `${logObj.filePath}/${key}.log`,
backupCount: logObj.backupCount,
interval: logObj.savingTime,
when: logObj.savingPeriod,
encoding: 'utf-8'
};
}
private createLoggerObj(logObj: LogConfig, key: string) {
return {
handlers: [`${key}Handler`, 'consoleHandler'],
level: logObj.logLevel,
propagate: false
};
}
private fetchConfigAttribute(entityId: EntityId): void {
if (entityId.id === NULL_UUID) {
return;
}
this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE,
)
.pipe(
mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes(
entityId, AttributeScope.SHARED_SCOPE, this.gatewayConfigAttributeKeys)
),
takeUntil(this.destroy$)
)
.subscribe(attributes => {
this.updateConfigs(attributes);
this.cd.detectChanges();
});
}
private updateConfigs(attributes: AttributeData[]): void {
const formValue: GatewayConfigValue = {
thingsboard: {} as GatewayGeneralConfig,
grpc: {} as GatewayGRPCConfig,
logs: {} as GatewayLogsConfig,
storage: {} as GatewayStorageConfig,
mode: ConfigurationModes.BASIC
};
attributes.forEach(attr => {
switch (attr.key) {
case 'general_configuration':
formValue.thingsboard = attr.value;
this.updateFormControls(attr.value);
break;
case 'grpc_configuration':
formValue.grpc = attr.value;
break;
case 'logs_configuration':
formValue.logs = this.logsToObj(attr.value);
break;
case 'storage_configuration':
formValue.storage = attr.value;
break;
case 'mode':
formValue.mode = attr.value;
break;
case 'RemoteLoggingLevel':
formValue.logs = {
...formValue.logs,
remote: {
enabled: attr.value !== GatewayLogLevel.NONE,
logLevel: attr.value
}
};
}
});
this.gatewayConfigGroup.get('basicConfig').setValue(formValue, { emitEvent: false });
this.gatewayConfigGroup.get('advancedConfig').setValue(formValue, { emitEvent: false });
}
private updateFormControls(thingsboard: GatewayGeneralConfig): void {
const { type, accessToken, ...securityConfig } = thingsboard.security ?? {};
this.initialCredentials = {
deviceId: this.device as DeviceId,
credentialsType: type as unknown as DeviceCredentialsType,
credentialsId: accessToken,
credentialsValue: JSON.stringify(securityConfig)
};
}
private logsToObj(logsConfig: LogAttribute): GatewayLogsConfig {
const { format: logFormat, datefmt: dateFormat } = logsConfig.formatters.LogFormatter;
const localLogs = Object.keys(LocalLogsConfigs).reduce((acc, key) => {
const handler = logsConfig.handlers[`${key}Handler`] || {};
const logger = logsConfig.loggers[key] || {};
acc[key] = {
logLevel: logger.level || GatewayLogLevel.INFO,
filePath: handler.filename?.split(`/${key}`)[0] || './logs',
backupCount: handler.backupCount || 7,
savingTime: handler.interval || 3,
savingPeriod: handler.when || LogSavingPeriod.days
};
return acc;
}, {}) as LocalLogs;
return { local: localLogs, logFormat, dateFormat };
}
}

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

@ -1,170 +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.
///
import {
ConfigurationModes,
GatewayConnector,
LocalLogsConfigs,
LogSavingPeriod,
SecurityTypes,
StorageTypes
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-form.models';
export interface GatewayConfigValue {
mode: ConfigurationModes;
thingsboard: GatewayGeneralConfig;
storage: GatewayStorageConfig;
grpc: GatewayGRPCConfig;
connectors?: GatewayConnector[];
logs: GatewayLogsConfig;
}
export interface GatewayGRPCConfig {
enabled: boolean;
serverPort: number;
keepAliveTimeMs: number;
keepAliveTimeoutMs: number;
keepalivePermitWithoutCalls: boolean;
maxPingsWithoutData: number;
minTimeBetweenPingsMs: number;
minPingIntervalWithoutDataMs: number;
}
export interface GatewayStorageConfig {
type: StorageTypes;
read_records_count?: number;
max_records_count?: number;
data_folder_path?: string;
max_file_count?: number;
max_read_records_count?: number;
max_records_per_file?: number;
data_file_path?: string;
messages_ttl_check_in_hours?: number;
messages_ttl_in_days?: number;
}
export interface GatewayGeneralConfig {
host: string;
port: number;
remoteShell: boolean;
remoteConfiguration: boolean;
checkConnectorsConfigurationInSeconds: number;
statistics: {
enable: boolean;
statsSendPeriodInSeconds: number;
commands: GatewayConfigCommand[];
};
maxPayloadSizeBytes: number;
minPackSendDelayMS: number;
minPackSizeToSend: number;
handleDeviceRenaming: boolean;
checkingDeviceActivity: {
checkDeviceInactivity: boolean;
inactivityTimeoutSeconds?: number;
inactivityCheckPeriodSeconds?: number;
};
security: GatewayConfigSecurity;
qos: number;
}
export interface GatewayLogsConfig {
dateFormat: string;
logFormat: string;
type?: string;
remote?: {
enabled: boolean;
logLevel: GatewayLogLevel;
};
local: LocalLogs;
}
export interface GatewayConfigSecurity {
type: SecurityTypes;
accessToken?: string;
clientId?: string;
username?: string;
password?: string;
caCert?: string;
cert?: string;
privateKey?: string;
}
export interface GatewayConfigCommand {
attributeOnGateway: string;
command: string;
timeout: number;
}
export interface LogConfig {
logLevel: GatewayLogLevel;
filePath: string;
backupCount: number;
savingTime: number;
savingPeriod: LogSavingPeriod;
}
export type LocalLogs = Record<LocalLogsConfigs, LogConfig>;
interface LogFormatterConfig {
class: string;
format: string;
datefmt: string;
}
interface StreamHandlerConfig {
class: string;
formatter: string;
level: string | number;
stream: string;
}
interface FileHandlerConfig {
class: string;
formatter: string;
filename: string;
backupCount: number;
encoding: string;
}
interface LoggerConfig {
handlers: string[];
level: string;
propagate: boolean;
}
interface RootConfig {
level: string;
handlers: string[];
}
export interface LogAttribute {
version: number;
disable_existing_loggers: boolean;
formatters: {
LogFormatter: LogFormatterConfig;
};
handlers: {
consoleHandler: StreamHandlerConfig;
databaseHandler: FileHandlerConfig;
};
loggers: {
database: LoggerConfig;
};
root: RootConfig;
ts: number;
}

99
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.html

@ -1,99 +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 stroked" [formGroup]="mappingFormGroup">
<div class="tb-form-panel-title" [class.tb-required]="required" translate>device.device</div>
<div class="tb-form-table no-padding no-gap">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell table-name-column" translate>gateway.device-info.entity-field</div>
<div *ngIf="useSource" class="tb-form-table-header-cell table-column" translate>gateway.device-info.source</div>
<div class="tb-form-table-header-cell table-column" translate>
gateway.device-info.expression
</div>
</div>
<div class="tb-form-table-body no-gap">
<div class="tb-form-table-row tb-form-row no-border same-padding top-same-padding"
[class.bottom-same-padding]="deviceInfoType !== DeviceInfoType.FULL">
<div class="fixed-title-width tb-required" translate>gateway.device-info.name</div>
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNameExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-table-row-cell tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate"
*ngIf="mappingFormGroup.get('deviceNameExpression').hasError('required') &&
mappingFormGroup.get('deviceNameExpression').touched"
class="tb-error">
warning
</mat-icon>
<div *ngIf="'name-field' | getGatewayHelpLink : mappingFormGroup.get('deviceNameExpressionSource').value : sourceTypes"
matSuffix
class="see-example"
[tb-help-popup]="'name-field' | getGatewayHelpLink : mappingFormGroup.get('deviceNameExpressionSource').value : sourceTypes"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-table-row tb-form-row no-border same-padding bottom-same-padding"
*ngIf="deviceInfoType === DeviceInfoType.FULL">
<div class="fixed-title-width tb-required" translate>gateway.device-info.profile-name</div>
<div class="tb-flex no-gap raw-value-option" *ngIf="useSource">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceProfileExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-table-row-cell tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceProfileExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-info.device-profile-expression-required') | translate"
*ngIf="mappingFormGroup.get('deviceProfileExpression').hasError('required') &&
mappingFormGroup.get('deviceProfileExpression').touched"
class="tb-error">
warning
</mat-icon>
<div *ngIf="'profile-name' | getGatewayHelpLink: mappingFormGroup.get('deviceProfileExpressionSource').value : sourceTypes"
matSuffix
class="see-example"
[tb-help-popup]="'profile-name' | getGatewayHelpLink: mappingFormGroup.get('deviceProfileExpressionSource').value : sourceTypes"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
</div>
</div>
</div>

57
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.scss

@ -1,57 +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.
*/
:host {
width: 100%;
height: 100%;
display: block;
.tb-form-row {
&.bottom-same-padding {
padding-bottom: 16px;
}
&.top-same-padding {
padding-top: 16px;
}
.fixed-title-width {
width: 19%;
}
}
.table-column {
width: 40%;
}
.table-name-column {
width: 20%;
}
.raw-name {
width: 19%;
}
.raw-value-option {
max-width: 40%;
}
}
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

164
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/device-info-table/device-info-table.component.ts

@ -1,164 +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.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog } from '@angular/material/dialog';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
DeviceInfoType,
noLeadTrailSpacesRegex,
OPCUaSourceType,
SourceType,
SourceTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-device-info-table',
templateUrl: './device-info-table.component.html',
styleUrls: ['./device-info-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceInfoTableComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DeviceInfoTableComponent),
multi: true
}
]
})
export class DeviceInfoTableComponent extends PageComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
DeviceInfoType = DeviceInfoType;
@coerceBoolean()
@Input()
useSource = true;
@coerceBoolean()
@Input()
required = false;
@Input()
sourceTypes: Array<SourceType | OPCUaSourceType> = Object.values(SourceType);
deviceInfoTypeValue: any;
get deviceInfoType(): any {
return this.deviceInfoTypeValue;
}
@Input()
set deviceInfoType(value: any) {
if (this.deviceInfoTypeValue !== value) {
this.deviceInfoTypeValue = value;
}
}
mappingFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(protected store: Store<AppState>,
public translate: TranslateService,
public dialog: MatDialog,
private fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.mappingFormGroup = this.fb.group({
deviceNameExpression: ['', this.required ?
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)]]
});
if (this.useSource) {
this.mappingFormGroup.addControl('deviceNameExpressionSource',
this.fb.control(this.sourceTypes[0], []));
}
if (this.deviceInfoType === DeviceInfoType.FULL) {
if (this.useSource) {
this.mappingFormGroup.addControl('deviceProfileExpressionSource',
this.fb.control(this.sourceTypes[0], []));
}
this.mappingFormGroup.addControl('deviceProfileExpression',
this.fb.control('', this.required ?
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)] : [Validators.pattern(noLeadTrailSpacesRegex)]));
}
this.mappingFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfo: any) {
this.mappingFormGroup.patchValue(deviceInfo, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.mappingFormGroup.valid ? null : {
mappingForm: { valid: false }
};
}
updateView(value: any) {
this.propagateChange(value);
}
}

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

@ -1,229 +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-mapping-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: trackByKey; 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 class="flex flex-row flex-wrap">
<mat-panel-title>
<ng-container *ngIf="keysType !== MappingKeysType.RPC_METHODS">
<div tbTruncateWithTooltip class="title-container">
{{ keyControl.get('key').value }}
</div>
{{ '-' }}
</ng-container>
<div tbTruncateWithTooltip class="title-container">{{ valueTitle(keyControl) }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-panel no-border no-padding"
*ngIf="keysType !== MappingKeysType.CUSTOM && keysType !== MappingKeysType.RPC_METHODS">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.platform-side</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}">
{{ 'gateway.key' | translate }}
</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="keyControl.get('key').hasError('required') &&
keyControl.get('key').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.connector-side</div>
<div class="tb-form-row">
<div class="fixed-title-width tb-required" translate>gateway.type</div>
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<mat-select name="valueType" formControlName="type">
<mat-select-trigger *ngIf="!rawData">
<div class="tb-flex align-center">
<mat-icon *ngIf="valueTypes.get(keyControl.get('type').value)?.icon" class="tb-mat-18"
[svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon">
</mat-icon>
<span *ngIf="!rawData; else rawText">
{{ (valueTypes.get(keyControl.get('type').value)?.name || valueTypes.get(keyControl.get('type').value)) | translate }}
</span>
<ng-template #rawText>
<span>{{ 'gateway.raw' | translate }}</span>
</ng-template>
</div>
</mat-select-trigger>
<ng-container *ngIf="!rawData; else rawOption">
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
<mat-icon *ngIf="valueTypes.get(valueType).icon" class="tb-mat-20"
svgIcon="{{ valueTypes.get(valueType).icon }}">
</mat-icon>
<span>
{{ valueTypes.get(valueType).name || valueTypes.get(valueType) | translate }}
</span>
</mat-option>
</ng-container>
<ng-template #rawOption>
<mat-option [value]="'raw'">
<span>{{ 'gateway.raw' | translate }}</span>
</mat-option>
</ng-template>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.JSONPath-hint' | translate }}">
{{ 'gateway.value' | translate }}
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-flex no-gap">
<input matInput required 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>
<div matSuffix
class="see-example"
*ngIf="this.keysType | getGatewayHelpLink : keyControl.get('type').value : valueTypeKeys"
[tb-help-popup]="this.keysType | getGatewayHelpLink : keyControl.get('type').value : valueTypeKeys"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-panel no-border no-padding" *ngIf="keysType === MappingKeysType.CUSTOM">
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.key</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="keyControl.get('key').hasError('required') && keyControl.get('key').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.value</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex">
<input matInput required 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 class="tb-form-panel no-border no-padding" *ngIf="keysType === MappingKeysType.RPC_METHODS">
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.method-name' | translate }}">
{{ 'gateway.method-name' | translate }}
</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="method" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.method-required') | translate"
*ngIf="keyControl.get('method').hasError('required') && keyControl.get('method').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-panel stroked tb-flex">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header class="flex flex-wrap">
<mat-panel-title>
<div class="title-container" tb-hint-tooltip-icon="{{ 'gateway.hints.arguments' | translate }}">
{{ 'gateway.arguments' | translate }}{{' (' + keyControl.get('arguments').value?.length + ')'}}
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-type-value-panel formControlName="arguments"></tb-type-value-panel>
</ng-template>
</mat-expansion-panel>
</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>

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

@ -1,60 +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.
*/
:host {
.tb-mapping-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-value-input {
width: 100%;
}
.tb-form-panel {
.mat-mdc-icon-button {
width: 56px;
height: 56px;
padding: 16px;
color: rgba(0, 0, 0, 0.54);
}
}
.see-example {
width: 32px;
height: 32px;
margin: 4px;
}
}
}
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

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

@ -1,197 +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.
///
import {
Component,
EventEmitter,
Input,
OnInit,
Output
} from '@angular/core';
import {
AbstractControl,
FormControl,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
Validators
} from '@angular/forms';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { isDefinedAndNotNull } from '@core/utils';
import {
MappingDataKey,
MappingKeysType,
MappingValueType,
mappingValueTypesMap,
noLeadTrailSpacesRegex,
OPCUaSourceType,
RpcMethodsMapping,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
selector: 'tb-mapping-data-keys-panel',
templateUrl: './mapping-data-keys-panel.component.html',
styleUrls: ['./mapping-data-keys-panel.component.scss'],
providers: []
})
export class MappingDataKeysPanelComponent extends PageComponent implements OnInit {
@Input()
panelTitle: string;
@Input()
addKeyTitle: string;
@Input()
deleteKeyTitle: string;
@Input()
noKeysText: string;
@Input()
keys: Array<MappingDataKey> | {[key: string]: any};
@Input()
keysType: MappingKeysType;
@Input()
valueTypeKeys: Array<MappingValueType | OPCUaSourceType> = Object.values(MappingValueType);
@Input()
valueTypeEnum = MappingValueType;
@Input()
valueTypes: Map<string, any> = mappingValueTypesMap;
@Input()
@coerceBoolean()
rawData = false;
@Input()
popover: TbPopoverComponent<MappingDataKeysPanelComponent>;
@Output()
keysDataApplied = new EventEmitter<Array<MappingDataKey> | {[key: string]: unknown}>();
MappingKeysType = MappingKeysType;
dataKeyType: DataKeyType;
keysListFormArray: UntypedFormArray;
errorText = '';
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.keysListFormArray = this.prepareKeysFormArray(this.keys);
}
trackByKey(index: number, keyControl: AbstractControl): any {
return keyControl;
}
addKey(): void {
let dataKeyFormGroup: FormGroup;
if (this.keysType === MappingKeysType.RPC_METHODS) {
dataKeyFormGroup = this.fb.group({
method: ['', [Validators.required]],
arguments: [[], []]
});
} else {
dataKeyFormGroup = this.fb.group({
key: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]]
});
}
if (this.keysType !== MappingKeysType.CUSTOM && this.keysType !== MappingKeysType.RPC_METHODS) {
const controlValue = this.rawData ? 'raw' : this.valueTypeKeys[0];
dataKeyFormGroup.addControl('type', this.fb.control(controlValue));
}
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 {
let keys = this.keysListFormArray.value;
if (this.keysType === MappingKeysType.CUSTOM) {
keys = {};
for (let key of this.keysListFormArray.value) {
keys[key.key] = key.value;
}
}
this.keysDataApplied.emit(keys);
}
private prepareKeysFormArray(keys: Array<MappingDataKey | RpcMethodsMapping> | {[key: string]: any}): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
if (keys) {
if (this.keysType === MappingKeysType.CUSTOM) {
keys = Object.keys(keys).map(key => {
return {key, value: keys[key], type: ''};
});
}
keys.forEach((keyData) => {
let dataKeyFormGroup: FormGroup;
if (this.keysType === MappingKeysType.RPC_METHODS) {
dataKeyFormGroup = this.fb.group({
method: [keyData.method, [Validators.required]],
arguments: [[...keyData.arguments], []]
});
} else {
const { key, value, type } = keyData;
dataKeyFormGroup = this.fb.group({
key: [key, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [value, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, []]
});
}
keysControlGroups.push(dataKeyFormGroup);
});
}
return this.fb.array(keysControlGroups);
}
valueTitle(keyControl: FormControl): string {
const value = keyControl.get(this.keysType === MappingKeysType.RPC_METHODS ? 'method' : 'value').value;
if (isDefinedAndNotNull(value)) {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
return '';
}
}

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

@ -1,125 +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-mapping-table tb-absolute-fill">
<div class="tb-mapping-table-content flex flex-col">
<mat-toolbar class="mat-mdc-table-toolbar" [class.!hidden]="textSearchMode">
<div class="mat-toolbar-tools" *ngIf="(dataSource.isEmpty() | async) === false">
<div class="title-container">
<span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span>
</div>
<span class="flex-1"></span>
<button mat-icon-button
(click)="manageMapping($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" [class.!hidden]="!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 class="flex-1">
<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]="column.def" *ngFor="let column of mappingColumns; let i = index">
<mat-header-cell *matHeaderCellDef class="table-value-column"
[class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ column.title | translate }}
</mat-header-cell>
<mat-cell tbTruncateWithTooltip *matCellDef="let mapping" class="table-value-column"
[class.request-column]="mappingType === mappingTypeEnum.REQUESTS">
{{ mapping[column.def] }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef>
<div class="gt-md:!hidden" style="width: 48px; min-width: 48px; max-width: 48px;"></div>
<div class="lt-lg:!hidden" [style]="{ minWidth: '96px', textAlign: 'center'}"></div>
</mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index">
<ng-template #rowActions>
<button mat-icon-button
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</ng-template>
<div class="flex flex-1 flex-row items-stretch justify-end lt-lg:!hidden"
[style]="{ minWidth: '96px', textAlign: 'center'}">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</div>
<div class="gt-md:!hidden">
<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">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row *matRowDef="let mapping; columns: displayedColumns;"></mat-row>
</table>
<section [class.!hidden]="textSearchMode || (dataSource.isEmpty() | async) === false"
class="mat-headline-5 tb-absolute-fill tb-add-new items-center justify-center">
<button mat-button class="connector"
(click)="manageMapping($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'gateway.add-mapping' | translate }}</span>
</button>
</section>
</div>
<span [class.!hidden]="!textSearchMode || (dataSource.isEmpty() | async) === false"
class="no-data-found items-center justify-center" translate>
widget.no-data-found
</span>
</div>
</div>

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

@ -1,101 +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.
*/
@import '../scss/constants';
:host {
width: 100%;
height: 100%;
display: block;
.tb-mapping-table {
.tb-mapping-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
&.tb-outlined-border {
box-shadow: 0 0 0 0 rgb(0 0 0 / 20%), 0 0 0 0 rgb(0 0 0 / 14%), 0 0 0 0 rgb(0 0 0 / 12%);
border: solid 1px #e0e0e0;
border-radius: 4px;
}
.mat-toolbar-tools{
min-height: auto;
}
.title-container{
overflow: hidden;
}
.tb-mapping-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: 23%;
&.request-column {
width: 38%;
}
}
}
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.no-data-found {
height: calc(100% - 120px);
}
@media #{$mat-xs} {
.mat-toolbar {
height: auto;
min-height: 100px;
.tb-mapping-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
}
}
}

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

@ -1,323 +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.
///
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
forwardRef,
Input,
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,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
AttributeUpdate,
ConnectorMapping,
ConnectRequest,
ConverterConnectorMapping,
ConvertorTypeTranslationsMap,
DeviceConnectorMapping,
DisconnectRequest,
MappingInfo,
MappingType,
MappingTypeTranslationsMap,
MappingValue,
RequestMappingValue,
RequestType,
RequestTypesTranslationsMap,
ServerSideRpc
} from '@home/components/widget/lib/gateway/gateway-widget.models';
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 { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({
selector: 'tb-mapping-table',
templateUrl: './mapping-table.component.html',
styleUrls: ['./mapping-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MappingTableComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MappingTableComponent),
multi: true
}
],
standalone: true,
imports: [CommonModule, SharedModule]
})
export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
@Input()
@coerceBoolean()
required = false;
@Input()
set mappingType(value: MappingType) {
if (this.mappingTypeValue !== value) {
this.mappingTypeValue = value;
}
}
get mappingType(): MappingType {
return this.mappingTypeValue;
}
@ViewChild('searchInput') searchInputField: ElementRef;
mappingTypeTranslationsMap = MappingTypeTranslationsMap;
mappingTypeEnum = MappingType;
displayedColumns = [];
mappingColumns = [];
textSearchMode = false;
dataSource: MappingDatasource;
hidePageSize = false;
activeValue = false;
dirtyValue = false;
mappingTypeValue: MappingType;
mappingFormGroup: UntypedFormArray;
textSearch = this.fb.control('', {nonNullable: true});
private onChange: (value: string) => void = () => {};
private onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
constructor(public translate: TranslateService,
public dialog: MatDialog,
private dialogService: DialogService,
private fb: FormBuilder) {
this.mappingFormGroup = this.fb.array([]);
this.dirtyValue = !this.activeValue;
this.dataSource = new MappingDatasource();
}
ngOnInit(): void {
this.setMappingColumns();
this.displayedColumns.push(...this.mappingColumns.map(column => column.def), 'actions');
this.mappingFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateTableData(value);
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) => {
const searchText = text.trim();
this.updateTableData(this.mappingFormGroup.value, searchText.trim());
});
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(connectorMappings: ConnectorMapping[]): void {
this.mappingFormGroup.clear();
this.pushDataAsFormArrays(connectorMappings);
}
validate(): ValidationErrors | null {
return !this.required || this.mappingFormGroup.controls.length ? null : {
mappingFormGroup: {valid: false}
};
}
enterFilterMode(): void {
this.textSearchMode = true;
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
}
exitFilterMode(): void {
this.updateTableData(this.mappingFormGroup.value);
this.textSearchMode = false;
this.textSearch.reset();
}
manageMapping($event: Event, index?: number): void {
if ($event) {
$event.stopPropagation();
}
const value = isDefinedAndNotNull(index) ? this.mappingFormGroup.at(index).value : {};
this.dialog.open<MappingDialogComponent, MappingInfo, ConnectorMapping>(MappingDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
mappingType: this.mappingType,
value,
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply'
}
}).afterClosed()
.pipe(take(1), takeUntil(this.destroy$))
.subscribe(res => {
if (res) {
if (isDefinedAndNotNull(index)) {
this.mappingFormGroup.at(index).patchValue(res);
} else {
this.pushDataAsFormArrays([res]);
}
this.mappingFormGroup.markAsDirty();
}
});
}
private updateTableData(value: ConnectorMapping[], textSearch?: string): void {
let tableValue = value.map(mappingValue => this.getMappingValue(mappingValue));
if (textSearch) {
tableValue = tableValue.filter(mappingValue =>
Object.values(mappingValue).some(val =>
val.toString().toLowerCase().includes(textSearch.toLowerCase())
)
);
}
this.dataSource.loadData(tableValue);
}
deleteMapping($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('gateway.delete-mapping-title'),
'',
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
if (result) {
this.mappingFormGroup.removeAt(index);
this.mappingFormGroup.markAsDirty();
}
});
}
private pushDataAsFormArrays(data: ConnectorMapping[]): void {
if (data?.length) {
data.forEach((mapping: ConnectorMapping) => this.mappingFormGroup.push(this.fb.control(mapping)));
}
}
private getMappingValue(value: ConnectorMapping): MappingValue {
switch (this.mappingType) {
case MappingType.DATA:
const converterType = ConvertorTypeTranslationsMap.get((value as ConverterConnectorMapping).converter?.type);
return {
topicFilter: (value as ConverterConnectorMapping).topicFilter,
QoS: (value as ConverterConnectorMapping).subscriptionQos,
converter: converterType ? this.translate.instant(converterType) : ''
};
case MappingType.REQUESTS:
let details: string;
const requestValue = value as RequestMappingValue;
if (requestValue.requestType === RequestType.ATTRIBUTE_UPDATE) {
details = (requestValue.requestValue as AttributeUpdate).attributeFilter;
} else if (requestValue.requestType === RequestType.SERVER_SIDE_RPC) {
details = (requestValue.requestValue as ServerSideRpc).methodFilter;
} else {
details = (requestValue.requestValue as ConnectRequest | DisconnectRequest).topicFilter;
}
return {
requestType: (value as RequestMappingValue).requestType,
type: this.translate.instant(RequestTypesTranslationsMap.get((value as RequestMappingValue).requestType)),
details
};
case MappingType.OPCUA:
const deviceNamePattern = (value as DeviceConnectorMapping).deviceInfo?.deviceNameExpression;
const deviceProfileExpression = (value as DeviceConnectorMapping).deviceInfo?.deviceProfileExpression;
const { deviceNodePattern } = value as DeviceConnectorMapping;
return {
deviceNodePattern,
deviceNamePattern,
deviceProfileExpression
};
default:
return {} as MappingValue;
}
}
private setMappingColumns(): void {
switch (this.mappingType) {
case MappingType.DATA:
this.mappingColumns.push(
{ def: 'topicFilter', title: 'gateway.topic-filter' },
{ def: 'QoS', title: 'gateway.mqtt-qos' },
{ def: 'converter', title: 'gateway.payload-type' }
);
break;
case MappingType.REQUESTS:
this.mappingColumns.push(
{ def: 'type', title: 'gateway.type' },
{ def: 'details', title: 'gateway.details' }
);
break;
case MappingType.OPCUA:
this.mappingColumns.push(
{ def: 'deviceNodePattern', title: 'gateway.device-node' },
{ def: 'deviceNamePattern', title: 'gateway.device-name' },
{ def: 'deviceProfileExpression', title: 'gateway.device-profile' }
);
}
}
}
export class MappingDatasource extends TbTableDatasource<MappingValue> {
constructor() {
super();
}
}

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

@ -1,76 +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.
///
import { Directive } from '@angular/core';
import { FormControl, FormGroup, ValidationErrors } from '@angular/forms';
import { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils';
import { GatewayConnectorBasicConfigDirective } from '@home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract';
import {
ModbusBasicConfig,
ModbusBasicConfig_v3_5_2,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Directive()
export abstract class ModbusBasicConfigDirective<InputBasicConfig, OutputBasicConfig>
extends GatewayConnectorBasicConfigDirective<InputBasicConfig, OutputBasicConfig> {
enableSlaveControl: FormControl<boolean> = new FormControl(false);
constructor() {
super();
this.enableSlaveControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(enable => {
this.updateSlaveEnabling(enable);
this.basicFormGroup.get('slave').updateValueAndValidity({ emitEvent: !!this.onChange });
});
}
override writeValue(basicConfig: OutputBasicConfig & ModbusBasicConfig): void {
super.writeValue(basicConfig);
this.onEnableSlaveControl(basicConfig);
}
override validate(): ValidationErrors | null {
const { master, slave } = this.basicFormGroup.value;
const isEmpty = !master?.slaves?.length && (isEqual(slave, {}) || !slave);
if (!this.basicFormGroup.valid || isEmpty) {
return { basicFormGroup: { valid: false } };
}
return null;
}
protected override initBasicFormGroup(): FormGroup {
return this.fb.group({
master: [],
slave: [],
});
}
private updateSlaveEnabling(isEnabled: boolean): void {
if (isEnabled) {
this.basicFormGroup.get('slave').enable({ emitEvent: false });
} else {
this.basicFormGroup.get('slave').disable({ emitEvent: false });
}
}
private onEnableSlaveControl(basicConfig: ModbusBasicConfig): void {
this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {}));
}
}

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

@ -1,38 +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.
-->
<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 [isLegacy]="isLegacy" formControlName="master"></tb-modbus-master-table>
</mat-tab>
<mat-tab label="{{ 'gateway.server-config' | translate }}">
<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">
<mat-slide-toggle class="mat-slide" [formControl]="enableSlaveControl">
<mat-label>
{{ 'gateway.enable' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<tb-modbus-slave-config formControlName="slave"></tb-modbus-slave-config>
</mat-tab>
</mat-tab-group>

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

@ -1,18 +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.
*/
:host {
height: 100%;
}

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

@ -1,76 +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.
///
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
ModbusBasicConfig_v3_5_2,
ModbusMasterConfig,
ModbusSlave
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import {
ModbusBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.abstract';
@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 extends ModbusBasicConfigDirective<ModbusBasicConfig_v3_5_2, ModbusBasicConfig_v3_5_2> {
isLegacy = false;
protected override mapConfigToFormValue({ master, slave }: ModbusBasicConfig_v3_5_2): ModbusBasicConfig_v3_5_2 {
return {
master: master?.slaves ? master : { slaves: [] } as ModbusMasterConfig,
slave: slave ?? {} as ModbusSlave,
};
}
protected override getMappedValue(value: ModbusBasicConfig_v3_5_2): ModbusBasicConfig_v3_5_2 {
return {
master: value.master,
slave: value.slave,
};
}
}

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

@ -1,80 +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.
///
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
LegacySlaveConfig,
ModbusBasicConfig,
ModbusLegacyBasicConfig,
ModbusLegacySlave,
ModbusMasterConfig,
ModbusSlave
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ModbusVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/modbus-version-mapping.util';
import {
ModbusBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.abstract';
@Component({
selector: 'tb-modbus-legacy-basic-config',
templateUrl: './modbus-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusLegacyBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusLegacyBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusSlaveConfigComponent,
ModbusMasterTableComponent,
EllipsisChipListDirective,
],
styleUrls: ['./modbus-basic-config.component.scss'],
})
export class ModbusLegacyBasicConfigComponent extends ModbusBasicConfigDirective<ModbusBasicConfig, ModbusLegacyBasicConfig> {
isLegacy = true;
protected override mapConfigToFormValue(config: ModbusLegacyBasicConfig): ModbusBasicConfig {
return {
master: config.master?.slaves ? config.master : { slaves: [] } as ModbusMasterConfig<LegacySlaveConfig>,
slave: config.slave ? ModbusVersionMappingUtil.mapSlaveToUpgradedVersion(config.slave as ModbusLegacySlave) : {},
} as ModbusBasicConfig;
}
protected override getMappedValue(value: ModbusBasicConfig): ModbusLegacyBasicConfig {
return {
master: value.master as ModbusMasterConfig<LegacySlaveConfig>,
slave: value.slave ? ModbusVersionMappingUtil.mapSlaveToDowngradedVersion(value.slave as ModbusSlave) : {} as ModbusLegacySlave,
};
}
}

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

@ -1,255 +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-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 class="flex-wrap">
<mat-panel-title>
<div *ngIf="isMaster else tagName" class="title-container" tbTruncateWithTooltip>
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
</div>
<ng-template #tagName>
<div class="tb-flex">
<div class="title-container tb-flex">{{ 'gateway.key' | translate }}:
<span class="key-label" tbTruncateWithTooltip>{{ keyControl.get('tag').value }}</span>
</div>
<div class="title-container">{{ 'gateway.address' | translate }}:
<span class="key-label">{{ keyControl.get('address').value }}</span>
</div>
<div class="title-container">{{ 'gateway.type' | translate }}:
<span class="key-label">{{ keyControl.get('type').value }}</span>
</div>
</div>
</ng-template>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-hint tb-primary-fill tb-flex center align-center">
{{ 'gateway.hints.modbus.data-keys' | translate }}
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/modbus-functions-data-types_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.platform-side</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.key' | translate }}" translate>
gateway.key
</div>
<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 class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.connector-side</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>
gateway.type
</div>
<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 *ngIf="withFunctionCode" class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.function-code</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.objects-count' | translate }}" translate>gateway.objects-count</div>
<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]="!ModbusEditableDataTypes.includes(keyControl.get('type').value)"
/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.objects-count-required') | translate"
*ngIf="keyControl.get('objectsCount').hasError('required') &&
keyControl.get('objectsCount').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.address' | translate }}" translate>gateway.address</div>
<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 *ngIf="showModifiersMap.get(keyControl.get('id').value)" class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [expanded]="enableModifiersControlMap.get(keyControl.get('id').value).value">
<mat-expansion-panel-header class="flex-wrap">
<mat-panel-title>
<mat-slide-toggle
[formControl]="enableModifiersControlMap.get(keyControl.get('id').value)"
class="mat-slide"
(click)="$event.stopPropagation()"
>
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.modifier' | translate }}">
{{ 'gateway.modifier' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="tb-flex no-gap">
<div class="tb-form-row column-xs tb-flex full-width">
<div class="fixed-title-width" translate>gateway.type</div>
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="modifierType">
<mat-select-trigger>
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="ModifierTypesMap.get(keyControl.get('modifierType').value)?.icon"></mat-icon>
<span>{{ ModifierTypesMap.get(keyControl.get('modifierType').value)?.name | translate}}</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let modifierType of modifierTypes" [value]="modifierType">
<mat-icon class="tb-mat-20" svgIcon="{{ ModifierTypesMap.get(modifierType).icon }}">
</mat-icon>
<span>{{ ModifierTypesMap.get(modifierType).name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.value</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex">
<input matInput required formControlName="modifierValue" step="0.1" type="number"
placeholder="{{ 'gateway.set' | translate }}" />
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.modifier-invalid') | translate"
*ngIf="keyControl.get('modifierValue').hasError('pattern') &&
keyControl.get('modifierValue').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</mat-expansion-panel>
</div>
<div *ngIf="isMaster" class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.value</div>
<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>
<tb-report-strategy
*ngIf="withReportStrategy"
[defaultValue]="ReportStrategyDefaultValue.Key"
formControlName="reportStrategy"
[isExpansionMode]="true"
/>
</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>

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

@ -1,45 +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.
*/
:host {
.tb-modbus-keys-panel {
width: 77vw;
max-width: 700px;
.title-container {
width: 180px;
}
.key-label {
font-weight: 400;
}
.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);
}
}
}
}

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

@ -1,306 +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.
///
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import {
AbstractControl,
FormArray,
FormControl,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
ModbusDataType,
ModbusEditableDataTypes,
ModbusFormValue,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusValue,
ModbusValueKey,
ModifierType,
ModifierTypesMap,
noLeadTrailSpacesRegex,
nonZeroFloat,
ReportStrategyDefaultValue,
} 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';
import {
ReportStrategyComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component';
@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,
ReportStrategyComponent,
]
})
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
@coerceBoolean()
@Input() isMaster = false;
@coerceBoolean()
@Input() hideNewFields = 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);
modifierTypes: ModifierType[] = Object.values(ModifierType);
withFunctionCode = true;
withReportStrategy = true;
enableModifiersControlMap = new Map<string, FormControl<boolean>>();
showModifiersMap = new Map<string, boolean>();
functionCodesMap = new Map();
defaultFunctionCodes = [];
readonly ModbusEditableDataTypes = ModbusEditableDataTypes;
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
readonly ModifierTypesMap = ModifierTypesMap;
readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue;
private destroy$ = new Subject<void>();
private readonly defaultReadFunctionCodes = [3, 4];
private readonly bitsReadFunctionCodes = [1, 2];
private readonly defaultWriteFunctionCodes = [6, 16];
private readonly bitsWriteFunctionCodes = [5, 15];
constructor(private fb: UntypedFormBuilder) {}
ngOnInit(): void {
this.withFunctionCode = !this.isMaster || (this.keysType !== ModbusValueKey.ATTRIBUTES && this.keysType !== ModbusValueKey.TIMESERIES);
this.withReportStrategy = !this.isMaster
&& (this.keysType === ModbusValueKey.ATTRIBUTES || this.keysType === ModbusValueKey.TIMESERIES)
&& !this.hideNewFields;
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 id = generateSecret(5);
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]],
reportStrategy: [{ value: null, disabled: !this.withReportStrategy }],
modifierType: [{ value: ModifierType.MULTIPLIER, disabled: true }],
modifierValue: [{ value: 1, disabled: true }, [Validators.pattern(nonZeroFloat)]],
id: [{value: id, disabled: true}],
});
this.showModifiersMap.set(id, false);
this.enableModifiersControlMap.set(id, this.fb.control(false));
this.observeKeyDataType(dataKeyFormGroup);
this.observeEnableModifier(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.getFormValue());
}
private getFormValue(): ModbusValue[] {
return this.mapKeysWithModifier(
this.withReportStrategy
? this.cleanUpEmptyStrategies(this.keysListFormArray.value)
: this.keysListFormArray.value
);
}
private cleanUpEmptyStrategies(values: ModbusValue[]): ModbusValue[] {
return values.map((key) => {
const { reportStrategy, ...updatedKey } = key;
return !reportStrategy ? updatedKey : key;
});
}
private mapKeysWithModifier(values: Array<ModbusFormValue>): Array<ModbusValue> {
return values.map((keyData, i) => {
if (this.showModifiersMap.get(this.keysListFormArray.controls[i].get('id').value)) {
const { modifierType, modifierValue, ...value } = keyData;
return modifierType ? { ...value, [modifierType]: modifierValue } : value;
}
return keyData;
});
}
private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
if (values) {
values.forEach(value => {
const dataKeyFormGroup = this.createDataKeyFormGroup(value);
this.observeKeyDataType(dataKeyFormGroup);
this.observeEnableModifier(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, multiplier, divider, reportStrategy } = modbusValue;
const id = generateSecret(5);
const showModifier = this.shouldShowModifier(type);
this.showModifiersMap.set(id, showModifier);
this.enableModifiersControlMap.set(id, this.fb.control((multiplier || divider) && showModifier));
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]],
modifierType: [{
value: divider ? ModifierType.DIVIDER : ModifierType.MULTIPLIER,
disabled: !this.enableModifiersControlMap.get(id).value
}],
modifierValue: [
{ value: multiplier ?? divider ?? 1, disabled: !this.enableModifiersControlMap.get(id).value },
[Validators.pattern(nonZeroFloat)]
],
id: [{ value: id, disabled: true }],
reportStrategy: [{ value: reportStrategy, disabled: !this.withReportStrategy }],
});
}
private shouldShowModifier(type: ModbusDataType): boolean {
return !this.isMaster
&& (this.keysType === ModbusValueKey.ATTRIBUTES || this.keysType === ModbusValueKey.TIMESERIES)
&& (!this.ModbusEditableDataTypes.includes(type));
}
private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.ModbusEditableDataTypes.includes(dataType)) {
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
}
const withModifier = this.shouldShowModifier(dataType);
this.showModifiersMap.set(keyFormGroup.get('id').value, withModifier);
this.updateFunctionCodes(keyFormGroup, dataType);
});
}
private observeEnableModifier(keyFormGroup: FormGroup): void {
this.enableModifiersControlMap.get(keyFormGroup.get('id').value).valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(showModifier => this.toggleModifierControls(keyFormGroup, showModifier));
}
private toggleModifierControls(keyFormGroup: FormGroup, enable: boolean): void {
const modifierTypeControl = keyFormGroup.get('modifierType');
const modifierValueControl = keyFormGroup.get('modifierValue');
if (enable) {
modifierTypeControl.enable();
modifierValueControl.enable();
} else {
modifierTypeControl.disable();
modifierValueControl.disable();
}
}
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[] {
const writeFunctionCodes = [
...(dataType === ModbusDataType.BITS ? this.bitsWriteFunctionCodes : []), ...this.defaultWriteFunctionCodes
];
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return writeFunctionCodes.sort((a, b) => a - b);
}
const functionCodes = [...this.defaultReadFunctionCodes];
if (dataType === ModbusDataType.BITS) {
functionCodes.push(...this.bitsReadFunctionCodes);
}
if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
functionCodes.push(...writeFunctionCodes);
}
return functionCodes.sort((a, b) => a - b);
}
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;
}
}

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

@ -1,150 +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-master-table tb-absolute-fill">
<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-master' | translate }}</div>
</div>
<div class="tb-master-table-content flex flex-col">
<mat-toolbar class="mat-mdc-table-toolbar" [class.!hidden]="textSearchMode">
<div class="mat-toolbar-tools" *ngIf="(dataSource.isEmpty() | async) === false">
<div class="title-container">
<span class="tb-master-table-title">{{ 'gateway.servers-slaves' | translate}}</span>
</div>
<span class="flex-1"></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" [class.!hidden]="!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 class="flex-1">
<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]="'deviceName'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
<div tbTruncateWithTooltip>{{ 'gateway.device-name' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
<div tbTruncateWithTooltip>{{ slave['deviceName'] }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'info'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.info' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
<div tbTruncateWithTooltip>{{ slave['host'] ?? slave['port'] }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'unitId'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.unit-id' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
<div tbTruncateWithTooltip>{{ slave['unitId'] }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
<div>{{ 'gateway.type' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(slave['type']) }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef>
<div class="gt-md:!hidden" style="width: 48px; min-width: 48px; max-width: 48px;"></div>
<div class="lt-lg:!hidden" [style]="{ minWidth: '96px', textAlign: 'center'}"></div>
</mat-header-cell>
<mat-cell *matCellDef="let slave; let i = index">
<ng-template #rowActions>
<button mat-icon-button
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteSlave($event, i)">
<tb-icon>delete</tb-icon>
</button>
</ng-template>
<div class="flex flex-1 flex-row items-stretch justify-end lt-lg:!hidden"
[style]="{ minWidth: '96px', textAlign: 'center'}">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</div>
<div class="gt-md:!hidden">
<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">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row class="mat-row-select" *matHeaderRowDef="['deviceName', 'info', 'unitId', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let slave; columns: ['deviceName', 'info', 'unitId', 'type', 'actions']"></mat-row>
</table>
<section [class.!hidden]="textSearchMode || (dataSource.isEmpty() | async) === false"
class="mat-headline-5 tb-absolute-fill tb-add-new items-center justify-center">
<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 [class.!hidden]="!textSearchMode || (dataSource.isEmpty() | async) === false"
class="no-data-found items-center justify-center" 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

@ -1,90 +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.
*/
@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
}
}
}

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

@ -1,245 +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.
///
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { MatDialog, MatDialogRef } 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_VALUE_ACCESSOR,
UntypedFormGroup,
} from '@angular/forms';
import {
LegacySlaveConfig,
ModbusMasterConfig,
ModbusProtocolLabelsMap,
ModbusSlaveInfo,
ModbusValues,
SlaveConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isDefinedAndNotNull } 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';
import { coerceBoolean } from '@shared/decorators/coercion';
import {
ModbusLegacySlaveDialogComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-legacy-slave-dialog.component';
@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
},
],
standalone: true,
imports: [CommonModule, SharedModule]
})
export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
@ViewChild('searchInput') searchInputField: ElementRef;
@coerceBoolean()
@Input() isLegacy = false;
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);
}
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.getSlaveDialog(value, withIndex ? 'action.apply' : 'action.add').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();
}
});
}
private getSlaveDialog(
value: LegacySlaveConfig | SlaveConfig,
buttonTitle: string
): MatDialogRef<ModbusLegacySlaveDialogComponent | ModbusSlaveDialogComponent> {
if (this.isLegacy) {
return this.dialog.open<ModbusLegacySlaveDialogComponent, ModbusSlaveInfo<LegacySlaveConfig>, ModbusValues>
(ModbusLegacySlaveDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value: value as LegacySlaveConfig,
hideNewFields: true,
buttonTitle
}
});
}
return this.dialog.open<ModbusSlaveDialogComponent, ModbusSlaveInfo, ModbusValues>(ModbusSlaveDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value: value as SlaveConfig,
buttonTitle,
hideNewFields: false,
}
});
}
deleteSlave($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();
}
}

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

@ -1,66 +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" [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" tbTruncateWithTooltip tb-hint-tooltip-icon="{{ 'gateway.hints.ca-cert' | translate }}">
<span tbTruncateWithTooltip translate>gateway.client-cert-path</span>
</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" tb-hint-tooltip-icon="{{ 'gateway.private-key-path' | translate }}">
<span tbTruncateWithTooltip translate>gateway.private-key-path</span>
</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">
<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

@ -1,163 +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.
///
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();
});
}
}

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

@ -1,238 +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 [formGroup]="slaveConfigFormGroup" class="slave-container">
<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">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.host' | translate }}" translate>gateway.host</div>
<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 *ngIf="protocolType !== ModbusProtocolType.Serial else serialPort"
class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.port' | translate }}" translate>gateway.port</div>
<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>
<ng-template #serialPort>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.serial-port' | translate }}" translate>gateway.port</div>
<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>
</ng-template>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.framer-type' | translate }}" translate>
gateway.method
</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.unit-id' | translate }}" translate>gateway.unit-id</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.device-name</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.poll-period' | translate }}">
<span tbTruncateWithTooltip translate>
gateway.poll-period
</span>
</div>
<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 *ngIf="protocolType === ModbusProtocolType.Serial" class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.baudrate' | translate }}" translate>gateway.baudrate</div>
<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 class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.send-data-to-platform' | 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">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.byte-order' | translate }}" translate>gateway.byte-order</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.word-order' | translate }}" translate>gateway.word-order</div>
<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 *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 class="flex-wrap">
<mat-panel-title>
<mat-slide-toggle [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">
<div class="fixed-title-width" translate>gateway.vendor-name</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.product-code</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.vendor-url</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.product-name</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.model-name</div>
<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>
</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>

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

@ -1,283 +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.
///
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 { 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,
],
})
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 isSlaveEnabled = false;
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: [null, [Validators.required]],
baudrate: [this.modbusBaudrates[0]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
pollPeriod: [5000, [Validators.required]],
sendDataToThingsBoard: [false],
byteOrder:[ModbusOrderType.BIG],
wordOrder: [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.observeShowSecurity();
}
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);
}
setDisabledState(isDisabled: boolean): void {
this.isSlaveEnabled = !isDisabled;
this.updateFormEnableState();
}
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.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 updateFormEnableState(): void {
if (this.isSlaveEnabled) {
this.slaveConfigFormGroup.enable({emitEvent: false});
this.showSecurityControl.enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.disable({emitEvent: false});
this.showSecurityControl.disable({emitEvent: false});
}
this.updateEnablingByProtocol();
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(): void {
const isSerial = this.protocolType === ModbusProtocolType.Serial;
const enableKeys = isSerial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys;
const disableKeys = isSerial ? 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,
wordOrder = 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,
wordOrder,
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 });
}
}

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

@ -1,84 +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.
///
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import {
FormBuilder,
} from '@angular/forms';
import {
LegacySlaveConfig,
ModbusProtocolType,
ModbusSlaveInfo,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { ModbusValuesComponent } from '../modbus-values/modbus-values.component';
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.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 {
ReportStrategyComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component';
import {
ModbusSlaveDialogAbstract
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract';
@Component({
selector: 'tb-modbus-legacy-slave-dialog',
templateUrl: './modbus-slave-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
ReportStrategyComponent,
],
styleUrls: ['./modbus-slave-dialog.component.scss'],
})
export class ModbusLegacySlaveDialogComponent extends ModbusSlaveDialogAbstract<ModbusLegacySlaveDialogComponent, LegacySlaveConfig> {
constructor(
protected fb: FormBuilder,
protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ModbusSlaveInfo,
public dialogRef: MatDialogRef<ModbusLegacySlaveDialogComponent, LegacySlaveConfig>,
) {
super(fb, store, router, data, dialogRef);
}
protected override getSlaveResultData(): LegacySlaveConfig {
const { values, type, serialPort, ...rest } = this.slaveConfigFormGroup.value;
const slaveResult = { ...rest, type, ...values };
if (type === ModbusProtocolType.Serial) {
slaveResult.port = serialPort;
}
return slaveResult;
}
protected override addFieldsToFormGroup(): void {
this.slaveConfigFormGroup.addControl('sendDataOnlyOnChange', this.fb.control(false));
}
}

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

@ -1,208 +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.
///
import { Directive, Inject, OnDestroy } from '@angular/core';
import {
FormBuilder,
FormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import {
ModbusBaudrates,
ModbusByteSizes,
ModbusMethodLabelsMap,
ModbusMethodType,
ModbusOrderType,
ModbusParity,
ModbusParityLabelsMap,
ModbusProtocolLabelsMap,
ModbusProtocolType,
ModbusSerialMethodType,
ModbusSlaveInfo,
noLeadTrailSpacesRegex,
PortLimits,
ReportStrategyDefaultValue,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
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 { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils';
import { helpBaseUrl } from '@shared/models/constants';
@Directive()
export abstract class ModbusSlaveDialogAbstract<Component, Config> extends DialogComponent<Component, Config> 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 ReportStrategyDefaultValue = ReportStrategyDefaultValue;
readonly modbusHelpLink =
helpBaseUrl + '/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'];
private destroy$ = new Subject<void>();
constructor(
protected fb: FormBuilder,
protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ModbusSlaveInfo,
public dialogRef: MatDialogRef<Component, Config>,
) {
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;
}
this.dialogRef.close(this.getSlaveResultData());
}
private initializeSlaveFormGroup(): void {
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, [Validators.required]],
baudrate: [this.modbusBaudrates[0]],
stopbits: [1],
bytesize: [ModbusByteSizes[0]],
parity: [ModbusParity.None],
strict: [true],
unitId: [null, [Validators.required]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
timeout: [35],
byteOrder: [ModbusOrderType.BIG],
wordOrder: [ModbusOrderType.BIG],
retries: [true],
retryOnEmpty: [true],
retryOnInvalid: [true],
pollPeriod: [5000, [Validators.required]],
connectAttemptTimeMs: [5000, [Validators.required]],
connectAttemptCount: [5, [Validators.required]],
waitAfterFailedAttemptsMs: [300000, [Validators.required]],
values: [{}],
security: [{}],
});
this.addFieldsToFormGroup();
}
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});
}
}
protected abstract addFieldsToFormGroup(): void;
protected abstract getSlaveResultData(): Config;
}

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

@ -1,312 +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="slaves-config-container">
<mat-toolbar color="primary">
<h2>{{ 'gateway.server-slave' | translate }}</h2>
<span class="flex-1"></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="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">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.host' | translate }}" translate>gateway.host</div>
<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 *ngIf="protocolType !== ModbusProtocolType.Serial else serialPort"
class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.port' | translate }}" translate>gateway.port</div>
<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>
<ng-template #serialPort>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.serial-port' | translate }}" translate>gateway.port</div>
<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>
</ng-template>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.framer-type' | translate }}" translate>
gateway.method
</div>
<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>
<ng-container *ngIf="protocolType === ModbusProtocolType.Serial">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.baudrate' | translate }}" translate>gateway.baudrate</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.bytesize' | translate }}" translate>gateway.bytesize</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.stopbits' | translate }}" translate>gateway.stopbits</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.parity' | translate }}" translate>gateway.parity</div>
<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 class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="strict">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.strict' | translate }}">
{{ 'gateway.strict' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</ng-container>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.unit-id' | translate }}" translate>gateway.unit-id</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.device-name</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.device-profile</div>
<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 *ngIf="data.hideNewFields else reportStrategy" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange">
<mat-label>
{{ 'gateway.send-data-on-change' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<ng-template #reportStrategy>
<tb-report-strategy [defaultValue]="ReportStrategyDefaultValue.Device" formControlName="reportStrategy" [isExpansionMode]="true"/>
</ng-template>
<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">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.connection-timeout' | translate }}" translate>gateway.connection-timeout</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.byte-order' | translate }}" translate>gateway.byte-order</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.word-order' | translate }}" translate>gateway.word-order</div>
<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 *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 class="flex-wrap">
<mat-panel-title>
<mat-slide-toggle [formControl]="showSecurityControl" class="mat-slide justify-start" (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 class="security-config" formControlName="security"></tb-modbus-security-config>
</mat-expansion-panel>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="retries">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.retries' | translate }}">
{{ 'gateway.retries' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="retryOnEmpty">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.retries-on-empty' | translate }}">
{{ 'gateway.retries-on-empty' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="retryOnInvalid">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.retries-on-invalid' | translate }}">
{{ 'gateway.retries-on-invalid' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width-260 tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.poll-period' | translate }}">
<span tbTruncateWithTooltip translate>
gateway.poll-period
</span>
</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width-260 tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.connect-attempt-time' | translate }}" translate>gateway.connect-attempt-time</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width-260 tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.connect-attempt-count' | translate }}" translate>gateway.connect-attempt-count</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width-260 tb-required" tb-hint-tooltip-icon="{{ 'gateway.hints.modbus.wait-after-failed-attempts' | translate }}" translate>gateway.wait-after-failed-attempts</div>
<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>
</mat-expansion-panel>
</div>
<div class="tb-form-panel stroked">
<tb-modbus-values [singleMode]="true" [hideNewFields]="data.hideNewFields" formControlName="values"></tb-modbus-values>
</div>
</div>
</div>
</div>
<div mat-dialog-actions class="justify-end">
<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>

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

@ -1,36 +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.
*/
:host {
.slaves-config-container {
width: 80vw;
max-width: 900px;
}
.slave-name-label {
margin-right: 16px;
color: rgba(0, 0, 0, 0.87);
}
.fixed-title-width-260 {
min-width: 260px;
}
::ng-deep.security-config {
.fixed-title-width {
min-width: 230px;
}
}
}

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

@ -1,87 +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.
///
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import {
FormBuilder,
} from '@angular/forms';
import {
ModbusProtocolType,
ModbusSlaveInfo,
SlaveConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { ModbusValuesComponent } from '../modbus-values/modbus-values.component';
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.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 {
ReportStrategyComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component';
import {
ModbusSlaveDialogAbstract
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract';
@Component({
selector: 'tb-modbus-slave-dialog',
templateUrl: './modbus-slave-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
ReportStrategyComponent,
],
styleUrls: ['./modbus-slave-dialog.component.scss'],
})
export class ModbusSlaveDialogComponent extends ModbusSlaveDialogAbstract<ModbusSlaveDialogComponent, SlaveConfig> {
constructor(
protected fb: FormBuilder,
protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ModbusSlaveInfo,
public dialogRef: MatDialogRef<ModbusSlaveDialogComponent, SlaveConfig>,
) {
super(fb, store, router, data, dialogRef);
}
protected override getSlaveResultData(): SlaveConfig {
const { values, type, serialPort, ...rest } = this.slaveConfigFormGroup.value;
const slaveResult = { ...rest, type, ...values };
if (type === ModbusProtocolType.Serial) {
slaveResult.port = serialPort;
}
if (!slaveResult.reportStrategy) {
delete slaveResult.reportStrategy;
}
return slaveResult;
}
protected override addFieldsToFormGroup(): void {
this.slaveConfigFormGroup.addControl('reportStrategy', this.fb.control(null));
}
}

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

@ -1,129 +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.
-->
<ng-container *ngIf="singleMode else multipleView">
<div [formGroup]="valuesFormGroup" class="tb-form-panel no-border no-padding padding-top">
<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">
<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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#rpcRequestsButton
(click)="manageKeys($event, rpcRequestsButton, ModbusValueKey.RPC_REQUESTS, register)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</ng-template>

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

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

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

@ -1,240 +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.
///
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;
@coerceBoolean()
@Input() hideNewFields = 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),
hideNewFields: this.hideNewFields,
};
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 ?? [],
};
}
}

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

@ -1,93 +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.
///
import { Directive } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
BrokerConfig,
MappingType,
MQTTBasicConfig,
MQTTBasicConfig_v3_5_2,
RequestMappingData,
RequestMappingValue,
RequestType,
WorkersConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isObject } from '@core/utils';
import {
GatewayConnectorBasicConfigDirective
} from '@home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract';
@Directive()
export abstract class MqttBasicConfigDirective<BasicConfig>
extends GatewayConnectorBasicConfigDirective<MQTTBasicConfig_v3_5_2, BasicConfig> {
MappingType = MappingType;
protected override initBasicFormGroup(): FormGroup {
return this.fb.group({
mapping: [],
requestsMapping: [],
broker: [],
workers: [],
});
}
protected getRequestDataArray(value: Record<RequestType, RequestMappingData[]>): RequestMappingData[] {
const mappingConfigs = [];
if (isObject(value)) {
Object.keys(value).forEach((configKey: string) => {
for (const mapping of value[configKey]) {
mappingConfigs.push({
requestType: configKey,
requestValue: mapping
});
}
});
}
return mappingConfigs;
}
protected getRequestDataObject(array: RequestMappingValue[]): Record<RequestType, RequestMappingValue[]> {
return array.reduce((result, { requestType, requestValue }) => {
result[requestType].push(requestValue);
return result;
}, {
connectRequests: [],
disconnectRequests: [],
attributeRequests: [],
attributeUpdates: [],
serverSideRpc: [],
});
}
protected getBrokerMappedValue(broker: BrokerConfig, workers: WorkersConfig): BrokerConfig {
return {
...broker,
maxNumberOfWorkers: workers.maxNumberOfWorkers ?? 100,
maxMessageNumberPerWorker: workers.maxMessageNumberPerWorker ?? 10,
};
}
writeValue(basicConfig: BasicConfig): void {
this.basicFormGroup.setValue(this.mapConfigToFormValue(basicConfig), { emitEvent: false });
}
protected abstract override mapConfigToFormValue(config: BasicConfig): MQTTBasicConfig_v3_5_2;
protected abstract override getMappedValue(config: MQTTBasicConfig): BasicConfig;
}

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

@ -1,41 +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.
-->
<mat-tab-group [formGroup]="basicFormGroup">
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.broker.connection' | translate }}*">
<tb-broker-config-control formControlName="broker"></tb-broker-config-control>
</mat-tab>
<mat-tab label="{{ 'gateway.data-mapping' | translate }}*">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="mapping" [required]="true" [mappingType]="MappingType.DATA"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.requests-mapping' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top tb-flex fill-height">
<tb-mapping-table formControlName="requestsMapping" [mappingType]="MappingType.REQUESTS"></tb-mapping-table>
</div>
</mat-tab>
<mat-tab label="{{ 'gateway.workers-settings' | translate }}">
<div class="tb-form-panel no-border no-padding">
<tb-workers-config-control formControlName="workers"></tb-workers-config-control>
</div>
</mat-tab>
</mat-tab-group>

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

@ -1,23 +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.
*/
:host {
height: 100%;
}
:host ::ng-deep {
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;
}
}

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

@ -1,97 +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.
///
import { Component, forwardRef, ChangeDetectionStrategy } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
import {
BrokerConfig,
MQTTBasicConfig_v3_5_2,
RequestMappingData,
RequestMappingValue,
RequestType, WorkersConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import {
MqttBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
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/mqtt/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/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',
templateUrl: './mqtt-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MqttBasicConfigComponent),
multi: true
}
],
styleUrls: ['./mqtt-basic-config.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
],
})
export class MqttBasicConfigComponent extends MqttBasicConfigDirective<MQTTBasicConfig_v3_5_2> {
protected override mapConfigToFormValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTBasicConfig_v3_5_2 {
const { broker, mapping = [], requestsMapping } = basicConfig;
return{
workers: broker && (broker.maxNumberOfWorkers || broker.maxMessageNumberPerWorker) ? {
maxNumberOfWorkers: broker.maxNumberOfWorkers,
maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker,
} : {} as WorkersConfig,
mapping: mapping ?? [],
broker: broker ?? {} as BrokerConfig,
requestsMapping: this.getRequestDataArray(requestsMapping as Record<RequestType, RequestMappingData[]>),
};
}
protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTBasicConfig_v3_5_2 {
const { broker, workers, mapping, requestsMapping } = basicConfig || {};
return {
broker: this.getBrokerMappedValue(broker, workers),
mapping,
requestsMapping: (requestsMapping as RequestMappingData[])?.length
? this.getRequestDataObject(requestsMapping as RequestMappingValue[])
: {} as Record<RequestType, RequestMappingValue[]>
};
}
}

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

@ -1,117 +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.
///
import { Component, forwardRef, ChangeDetectionStrategy } from '@angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
import {
BrokerConfig,
MQTTBasicConfig_v3_5_2,
MQTTLegacyBasicConfig,
RequestMappingData,
RequestMappingValue,
RequestType, WorkersConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { MqttVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/mqtt-version-mapping.util';
import {
MqttBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract';
import { isDefinedAndNotNull } from '@core/utils';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
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/mqtt/workers-config-control/workers-config-control.component';
import {
BrokerConfigControlComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/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-legacy-basic-config',
templateUrl: './mqtt-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttLegacyBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MqttLegacyBasicConfigComponent),
multi: true
}
],
styleUrls: ['./mqtt-basic-config.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
WorkersConfigControlComponent,
BrokerConfigControlComponent,
MappingTableComponent,
],
})
export class MqttLegacyBasicConfigComponent extends MqttBasicConfigDirective<MQTTLegacyBasicConfig> {
protected override mapConfigToFormValue(config: MQTTLegacyBasicConfig): MQTTBasicConfig_v3_5_2 {
const {
broker,
mapping = [],
connectRequests = [],
disconnectRequests = [],
attributeRequests = [],
attributeUpdates = [],
serverSideRpc = []
} = config as MQTTLegacyBasicConfig;
const updatedRequestMapping = MqttVersionMappingUtil.mapRequestsToUpgradedVersion({
connectRequests,
disconnectRequests,
attributeRequests,
attributeUpdates,
serverSideRpc
});
return {
workers: broker && (broker.maxNumberOfWorkers || broker.maxMessageNumberPerWorker) ? {
maxNumberOfWorkers: broker.maxNumberOfWorkers,
maxMessageNumberPerWorker: broker.maxMessageNumberPerWorker,
} : {} as WorkersConfig,
mapping: MqttVersionMappingUtil.mapMappingToUpgradedVersion(mapping) || [],
broker: broker || {} as BrokerConfig,
requestsMapping: this.getRequestDataArray(updatedRequestMapping),
};
}
protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTLegacyBasicConfig {
const { broker, workers, mapping, requestsMapping } = basicConfig || {};
const updatedRequestMapping = (requestsMapping as RequestMappingData[])?.length
? this.getRequestDataObject(requestsMapping as RequestMappingValue[])
: {} as Record<RequestType, RequestMappingData[]>;
return {
broker: this.getBrokerMappedValue(broker, workers),
mapping: MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping),
...(MqttVersionMappingUtil.mapRequestsToDowngradedVersion(updatedRequestMapping as Record<RequestType, RequestMappingData[]>))
};
}
}

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

@ -1,78 +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]="brokerConfigFormGroup">
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.host</div>
<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="brokerConfigFormGroup.get('host').hasError('required')
&& brokerConfigFormGroup.get('host').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.port</div>
<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]="brokerConfigFormGroup.get('port') | getGatewayPortTooltip"
*ngIf="(brokerConfigFormGroup.get('port').hasError('required') ||
brokerConfigFormGroup.get('port').hasError('min') ||
brokerConfigFormGroup.get('port').hasError('max')) &&
brokerConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.mqtt-version</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="version">
<mat-option *ngFor="let version of mqttVersions" [value]="version.value">{{ version.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.client-id</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="clientId" placeholder="{{ 'gateway.set' | translate }}"/>
<button type="button"
matSuffix
mat-icon-button
aria-label="Generate"
matTooltip="{{ 'gateway.generate-client-id' | translate }}"
matTooltipPosition="above"
(click)="generate('clientId')"
*ngIf="!brokerConfigFormGroup.get('clientId').value">
<mat-icon>autorenew</mat-icon>
</button>
</mat-form-field>
</div>
<tb-security-config formControlName="security">
</tb-security-config>
</div>

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

@ -1,124 +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.
///
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
BrokerConfig,
MqttVersions,
noLeadTrailSpacesRegex,
PortLimits,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { generateSecret } from '@core/utils';
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',
templateUrl: './broker-config-control.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
GatewayPortTooltipPipe,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BrokerConfigControlComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => BrokerConfigControlComponent),
multi: true
}
]
})
export class BrokerConfigControlComponent implements ControlValueAccessor, Validator, OnDestroy {
brokerConfigFormGroup: UntypedFormGroup;
mqttVersions = MqttVersions;
portLimits = PortLimits;
private onChange: (value: string) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder,
private cdr: ChangeDetectorRef) {
this.brokerConfigFormGroup = this.fb.group({
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
version: [5, []],
clientId: ['tb_gw_' + generateSecret(5), [Validators.pattern(noLeadTrailSpacesRegex)]],
security: []
});
this.brokerConfigFormGroup.valueChanges.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
generate(formControlName: string): void {
this.brokerConfigFormGroup.get(formControlName)?.patchValue('tb_gw_' + generateSecret(5));
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(brokerConfig: BrokerConfig): void {
const {
version = 5,
clientId = `tb_gw_${generateSecret(5)}`,
security = {},
} = brokerConfig;
this.brokerConfigFormGroup.reset({ ...brokerConfig, version, clientId, security }, { emitEvent: false });
this.cdr.markForCheck();
}
validate(): ValidationErrors | null {
return this.brokerConfigFormGroup.valid ? null : {
brokerConfigFormGroup: {valid: false}
};
}
}

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

@ -1,59 +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]="workersConfigFormGroup">
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" style="width: 50%"
tb-hint-tooltip-icon="{{ 'gateway.max-number-of-workers-hint' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.max-number-of-workers' | translate }}</div>
</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxNumberOfWorkers"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-number-of-workers-required') | translate"
*ngIf="workersConfigFormGroup.get('maxNumberOfWorkers').hasError('min') ||
(workersConfigFormGroup.get('maxNumberOfWorkers').hasError('required') &&
workersConfigFormGroup.get('maxNumberOfWorkers').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" style="width: 50%"
tb-hint-tooltip-icon="{{ 'gateway.max-messages-queue-for-worker-hint' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.max-messages-queue-for-worker' | translate }}</div>
</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="maxMessageNumberPerWorker"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.max-messages-queue-for-worker-required') | translate"
*ngIf="workersConfigFormGroup.get('maxMessageNumberPerWorker').hasError('min') ||
(workersConfigFormGroup.get('maxMessageNumberPerWorker').hasError('required') &&
workersConfigFormGroup.get('maxMessageNumberPerWorker').touched)"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>

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

@ -1,108 +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.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
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';
@Component({
selector: 'tb-workers-config-control',
templateUrl: './workers-config-control.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
SharedModule,
],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WorkersConfigControlComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => WorkersConfigControlComponent),
multi: true
}
]
})
export class WorkersConfigControlComponent implements OnDestroy, ControlValueAccessor, Validator {
workersConfigFormGroup: UntypedFormGroup;
private onChange: (value: string) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.workersConfigFormGroup = this.fb.group({
maxNumberOfWorkers: [100, [Validators.required, Validators.min(1)]],
maxMessageNumberPerWorker: [10, [Validators.required, Validators.min(1)]],
});
this.workersConfigFormGroup.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(workersConfig: WorkersConfig): void {
const { maxNumberOfWorkers, maxMessageNumberPerWorker } = workersConfig;
this.workersConfigFormGroup.reset({
maxNumberOfWorkers: maxNumberOfWorkers || 100,
maxMessageNumberPerWorker: maxMessageNumberPerWorker || 10,
}, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.workersConfigFormGroup.valid ? null : {
workersConfigFormGroup: {valid: false}
};
}
}

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

@ -1,136 +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="tb-form-row column-xs">
<div class="fixed-title-width" tbTruncateWithTooltip translate>gateway.server-url</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.opc-timeout' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.timeout' | translate }}</div>
</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="1000" 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 class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.security-policy' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.security-policy' | translate }}</div>
</div>
<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 class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.scan-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.scan-period' | translate }}</div>
</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="1000" 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 *ngIf="!hideNewFields" class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.poll-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.poll-period' | translate }}</div>
</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="50" name="value"
formControlName="pollPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.poll-period-error' | translate: {min: 50}"
*ngIf="(serverConfigFormGroup.get('pollPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('pollPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('pollPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.sub-check-period' | translate }}</div>
</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="100" 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: 100}"
*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 class="tb-form-row">
<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">
<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>

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

@ -1,20 +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.
*/
:host {
width: 100%;
height: 100%;
display: block;
}

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

@ -1,152 +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.
///
import { AfterViewInit, ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import {
noLeadTrailSpacesRegex,
SecurityPolicy,
SecurityPolicyTypes,
ServerConfig
} 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 {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import { HOUR } from '@shared/models/time/time.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
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(() => OpcServerConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OpcServerConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
]
})
export class OpcServerConfigComponent implements ControlValueAccessor, Validator, AfterViewInit, OnDestroy {
@Input()
@coerceBoolean()
hideNewFields: boolean = false;
securityPolicyTypes = SecurityPolicyTypes;
serverConfigFormGroup: UntypedFormGroup;
onChange!: (value: string) => void;
onTouched!: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.serverConfigFormGroup = this.fb.group({
url: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
timeoutInMillis: [1000, [Validators.required, Validators.min(1000)]],
scanPeriodInMillis: [HOUR, [Validators.required, Validators.min(1000)]],
pollPeriodInMillis: [5000, [Validators.required, Validators.min(50)]],
enableSubscriptions: [true, []],
subCheckPeriodInMillis: [100, [Validators.required, Validators.min(100)]],
showMap: [false, []],
security: [SecurityPolicy.BASIC128, []],
identity: []
});
this.serverConfigFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.onChange(value);
this.onTouched();
});
}
ngAfterViewInit(): void {
if (this.hideNewFields) {
this.serverConfigFormGroup.get('pollPeriodInMillis').disable({emitEvent: false});
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.serverConfigFormGroup.valid ? null : {
serverConfigFormGroup: { valid: false }
};
}
writeValue(serverConfig: ServerConfig): void {
const {
timeoutInMillis = 1000,
scanPeriodInMillis = HOUR,
pollPeriodInMillis = 5000,
enableSubscriptions = true,
subCheckPeriodInMillis = 100,
showMap = false,
security = SecurityPolicy.BASIC128,
identity = {},
} = serverConfig;
this.serverConfigFormGroup.reset({
...serverConfig,
timeoutInMillis,
scanPeriodInMillis,
pollPeriodInMillis,
enableSubscriptions,
subCheckPeriodInMillis,
showMap,
security,
identity,
}, { emitEvent: false });
}
}

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

@ -1,30 +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.
-->
<mat-tab-group [formGroup]="basicFormGroup">
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.server' | translate }}*">
<tb-opc-server-config formControlName="server" [hideNewFields]="isLegacy"></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">
<tb-mapping-table formControlName="mapping" [required]="true" [mappingType]="mappingTypes.OPCUA"></tb-mapping-table>
</div>
</mat-tab>
</mat-tab-group>

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

@ -1,23 +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.
*/
:host {
height: 100%;
}
:host ::ng-deep {
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;
}
}

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

@ -1,88 +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.
///
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
MappingType,
OPCBasicConfig_v3_5_2,
ServerConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { MappingTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component';
import {
GatewayConnectorBasicConfigDirective
} from '@home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract';
@Component({
selector: 'tb-opc-ua-basic-config',
templateUrl: './opc-ua-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpcUaBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OpcUaBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
MappingTableComponent,
OpcServerConfigComponent,
],
styleUrls: ['./opc-ua-basic-config.component.scss']
})
export class OpcUaBasicConfigComponent extends GatewayConnectorBasicConfigDirective<OPCBasicConfig_v3_5_2, OPCBasicConfig_v3_5_2> {
mappingTypes = MappingType;
isLegacy = false;
protected override initBasicFormGroup(): FormGroup {
return this.fb.group({
mapping: [],
server: [],
});
}
protected override mapConfigToFormValue(config: OPCBasicConfig_v3_5_2): OPCBasicConfig_v3_5_2 {
return {
server: config.server ?? {} as ServerConfig,
mapping: config.mapping ?? [],
};
}
protected override getMappedValue(value: OPCBasicConfig_v3_5_2): OPCBasicConfig_v3_5_2 {
return {
server: value.server,
mapping: value.mapping,
};
}
}

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

@ -1,88 +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.
///
import { ChangeDetectionStrategy, Component, forwardRef } from '@angular/core';
import { FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
MappingType,
OPCBasicConfig_v3_5_2,
OPCLegacyBasicConfig, ServerConfig,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import { MappingTableComponent } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import {
OpcServerConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/opc/opc-server-config/opc-server-config.component';
import {
GatewayConnectorBasicConfigDirective
} from '@home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract';
import { OpcVersionMappingUtil } from '@home/components/widget/lib/gateway/utils/opc-version-mapping.util';
@Component({
selector: 'tb-opc-ua-legacy-basic-config',
templateUrl: './opc-ua-basic-config.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpcUaLegacyBasicConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OpcUaLegacyBasicConfigComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
SecurityConfigComponent,
MappingTableComponent,
OpcServerConfigComponent,
],
styleUrls: ['./opc-ua-basic-config.component.scss']
})
export class OpcUaLegacyBasicConfigComponent extends GatewayConnectorBasicConfigDirective<OPCBasicConfig_v3_5_2, OPCLegacyBasicConfig> {
mappingTypes = MappingType;
isLegacy = true;
protected override initBasicFormGroup(): FormGroup {
return this.fb.group({
mapping: [],
server: [],
});
}
protected override mapConfigToFormValue(config: OPCLegacyBasicConfig): OPCBasicConfig_v3_5_2 {
return {
server: config.server ? OpcVersionMappingUtil.mapServerToUpgradedVersion(config.server) : {} as ServerConfig,
mapping: config.server?.mapping ? OpcVersionMappingUtil.mapMappingToUpgradedVersion(config.server.mapping) : [],
};
}
protected override getMappedValue(value: OPCBasicConfig_v3_5_2): OPCLegacyBasicConfig {
return {
server: OpcVersionMappingUtil.mapServerToDowngradedVersion(value),
};
}
}

55
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.html

@ -1,55 +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 [formGroup]="reportStrategyFormGroup" class="tb-form-panel stroked">
<mat-expansion-panel *ngIf="isExpansionMode else defaultMode" class="tb-settings" [expanded]="showStrategyControl.value">
<mat-expansion-panel-header class="flex-wrap">
<mat-panel-title>
<mat-slide-toggle [formControl]="showStrategyControl" class="mat-slide" (click)="$event.stopPropagation()">
<mat-label>
{{ 'gateway.report-strategy.label' | translate }}
</mat-label>
</mat-slide-toggle>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-container [ngTemplateOutlet]="strategyFields"></ng-container>
</mat-expansion-panel>
<ng-template #defaultMode>
<div class="tb-form-panel-title" translate>gateway.report-strategy.label</div>
<ng-container [ngTemplateOutlet]="strategyFields"></ng-container>
</ng-template>
<ng-template #strategyFields>
<div class="tb-form-row column-xs">
<div class="fixed-title-width">{{ 'gateway.type' | translate }}</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of reportStrategyTypes" [value]="type">{{ ReportTypeTranslateMap.get(type) | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="reportStrategyFormGroup.get('type').value !== ReportStrategyType.OnChange" class="tb-form-row column-xs">
<div class="fixed-title-width tb-required">
<span tbTruncateWithTooltip translate>
gateway.report-strategy.report-period
</span>
</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="reportPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</ng-template>
</div>

174
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/report-strategy/report-strategy.component.ts

@ -1,174 +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.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
Input,
OnDestroy,
} from '@angular/core';
import { Subject } from 'rxjs';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validators
} from '@angular/forms';
import {
ReportStrategyConfig,
ReportStrategyDefaultValue,
ReportStrategyType,
ReportStrategyTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { filter, takeUntil } from 'rxjs/operators';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import {
ModbusSecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component';
import { coerceBoolean, coerceNumber } from '@shared/decorators/coercion';
@Component({
selector: 'tb-report-strategy',
templateUrl: './report-strategy.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ReportStrategyComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ReportStrategyComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
ModbusSecurityConfigComponent,
]
})
export class ReportStrategyComponent implements ControlValueAccessor, OnDestroy {
@coerceBoolean()
@Input() isExpansionMode = false;
@coerceNumber()
@Input() defaultValue = ReportStrategyDefaultValue.Key;
reportStrategyFormGroup: UntypedFormGroup;
showStrategyControl: FormControl<boolean>;
readonly reportStrategyTypes = Object.values(ReportStrategyType);
readonly ReportTypeTranslateMap = ReportStrategyTypeTranslationsMap;
readonly ReportStrategyType = ReportStrategyType;
private onChange: (value: ReportStrategyConfig) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.showStrategyControl = this.fb.control(false);
this.reportStrategyFormGroup = this.fb.group({
type: [{ value: ReportStrategyType.OnReportPeriod, disabled: true }, []],
reportPeriod: [{ value: this.defaultValue, disabled: true }, [Validators.required]],
});
this.observeStrategyFormChange();
this.observeStrategyToggle();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
writeValue(reportStrategyConfig: ReportStrategyConfig): void {
if (this.isExpansionMode) {
this.showStrategyControl.setValue(!!reportStrategyConfig, {emitEvent: false});
}
if (reportStrategyConfig) {
this.reportStrategyFormGroup.enable({emitEvent: false});
}
const { type = ReportStrategyType.OnReportPeriod, reportPeriod = this.defaultValue } = reportStrategyConfig ?? {};
this.reportStrategyFormGroup.setValue({ type, reportPeriod }, {emitEvent: false});
this.onTypeChange(type);
}
validate(): ValidationErrors | null {
return this.reportStrategyFormGroup.valid || this.reportStrategyFormGroup.disabled ? null : {
reportStrategyForm: { valid: false }
};
}
registerOnChange(fn: (value: ReportStrategyConfig) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
private observeStrategyFormChange(): void {
this.reportStrategyFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.onChange(value);
this.onTouched();
});
this.reportStrategyFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => this.onTypeChange(type));
}
private observeStrategyToggle(): void {
this.showStrategyControl.valueChanges
.pipe(takeUntil(this.destroy$), filter(() => this.isExpansionMode))
.subscribe(enable => {
if (enable) {
this.reportStrategyFormGroup.enable({emitEvent: false});
this.reportStrategyFormGroup.get('reportPeriod').addValidators(Validators.required);
this.onChange(this.reportStrategyFormGroup.value);
} else {
this.reportStrategyFormGroup.disable({emitEvent: false});
this.reportStrategyFormGroup.get('reportPeriod').removeValidators(Validators.required);
this.onChange(null);
}
this.reportStrategyFormGroup.updateValueAndValidity({emitEvent: false});
});
}
private onTypeChange(type: ReportStrategyType): void {
const reportPeriodControl = this.reportStrategyFormGroup.get('reportPeriod');
if (type === ReportStrategyType.OnChange) {
reportPeriodControl.disable({emitEvent: false});
} else if (!this.isExpansionMode || this.showStrategyControl.value) {
reportPeriodControl.enable({emitEvent: false});
}
}
}

65
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.html

@ -1,65 +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-row space-between same-padding tb-flex column" [formGroup]="securityFormGroup">
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fields-label" translate>gateway.security</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of securityTypes" [value]="type">
{{ SecurityTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<ng-container *ngIf="securityFormGroup.get('type').value === BrokerSecurityType.BASIC">
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.username</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="username" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.username-required') | translate"
*ngIf="securityFormGroup.get('username').hasError('required') && securityFormGroup.get('username').touched"
class="tb-error">
warning
</mat-icon>
</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 }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.password-required') | translate"
*ngIf="securityFormGroup.get('password').hasError('required')
&& securityFormGroup.get('password').touched"
class="tb-error">
warning
</mat-icon>
<div [class.hide-toggle]="securityFormGroup.get('password').hasError('required')" 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>
</ng-container>
</div>

29
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.scss

@ -1,29 +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.
*/
:host {
width: 100%;
height: 100%;
display: block;
margin-bottom: 10px;
.fields-label {
font-weight: 500;
}
.hide-toggle {
display: none;
}
}

132
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rest-connector-secuirity/rest-connector-security.component.ts

@ -1,132 +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.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import { Subject } from 'rxjs';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { takeUntil } from 'rxjs/operators';
import {
noLeadTrailSpacesRegex,
RestSecurityType,
RestSecurityTypeTranslationsMap
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'tb-rest-connector-security',
templateUrl: './rest-connector-security.component.html',
styleUrls: ['./rest-connector-security.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RestConnectorSecurityComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => RestConnectorSecurityComponent),
multi: true
}
],
standalone: true,
imports: [
SharedModule,
CommonModule,
]
})
export class RestConnectorSecurityComponent implements ControlValueAccessor, Validator, OnDestroy {
BrokerSecurityType = RestSecurityType;
securityTypes: RestSecurityType[] = Object.values(RestSecurityType);
SecurityTypeTranslationsMap = RestSecurityTypeTranslationsMap;
securityFormGroup: UntypedFormGroup;
private destroy$ = new Subject<void>();
private propagateChange = (_: any) => {};
constructor(private fb: FormBuilder) {
this.securityFormGroup = this.fb.group({
type: [RestSecurityType.ANONYMOUS, []],
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
});
this.observeSecurityForm();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfo: any): void {
if (!deviceInfo.type) {
deviceInfo.type = RestSecurityType.ANONYMOUS;
}
this.securityFormGroup.reset(deviceInfo);
this.updateView(deviceInfo);
}
validate(): ValidationErrors | null {
return this.securityFormGroup.valid ? null : {
securityForm: { valid: false }
};
}
private updateView(value: any): void {
this.propagateChange(value);
}
private updateValidators(type: RestSecurityType): void {
if (type === RestSecurityType.BASIC) {
this.securityFormGroup.get('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
} else {
this.securityFormGroup.get('username').disable({emitEvent: false});
this.securityFormGroup.get('password').disable({emitEvent: false});
}
}
private observeSecurityForm(): void {
this.securityFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateView(value));
this.securityFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => this.updateValidators(type));
}
}

81
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component.html

@ -1,81 +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.
-->
<ng-container [formGroup]="rpcParametersFormGroup">
<div class="tb-form-hint tb-primary-fill no-padding-top hint-container">
{{ 'gateway.rpc.hint.modbus-response-reading' | translate }}<br>
{{ 'gateway.rpc.hint.modbus-writing-functions' | translate }}
</div>
<div class="flex flex-1 flex-row gap-2.5">
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.rpc.type' | translate }}</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of modbusDataTypes" [value]="type">{{ type }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.rpc.functionCode' | translate }}</mat-label>
<mat-select formControlName="functionCode">
<mat-option *ngFor="let code of functionCodes" [value]="code">{{ ModbusFunctionCodeTranslationsMap.get(code) | translate}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="flex flex-1 flex-row gap-2.5">
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.rpc.address' | translate }}</mat-label>
<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="rpcParametersFormGroup.get('address').hasError('required') &&
rpcParametersFormGroup.get('address').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.rpc.objectsCount' | translate }}</mat-label>
<input
matInput
type="number"
min="1"
max="50000"
name="value"
formControlName="objectsCount"
placeholder="{{ 'gateway.set' | translate }}"
[readonly]="!ModbusEditableDataTypes.includes(rpcParametersFormGroup.get('type').value)"
/>
</mat-form-field>
</div>
<div *ngIf="writeFunctionCodes.includes(rpcParametersFormGroup.get('functionCode').value)" class="flex">
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.rpc.value' | translate }}</mat-label>
<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="rpcParametersFormGroup.get('value').hasError('required') && rpcParametersFormGroup.get('value').touched"
class="tb-error"
>
warning
</mat-icon>
</mat-form-field>
</div>
</ng-container>

20
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component.scss

@ -1,20 +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.
*/
:host {
.hint-container {
margin-bottom: 12px;
}
}

166
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/modbus-rpc-parameters/modbus-rpc-parameters.component.ts

@ -1,166 +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.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {
ModbusDataType,
ModbusEditableDataTypes,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
noLeadTrailSpacesRegex,
RPCTemplateConfigModbus,
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
selector: 'tb-modbus-rpc-parameters',
templateUrl: './modbus-rpc-parameters.component.html',
styleUrls: ['./modbus-rpc-parameters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ModbusRpcParametersComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusRpcParametersComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
})
export class ModbusRpcParametersComponent implements ControlValueAccessor, Validator, OnDestroy {
rpcParametersFormGroup: UntypedFormGroup;
functionCodes: Array<number>;
readonly ModbusEditableDataTypes = ModbusEditableDataTypes;
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
readonly modbusDataTypes = Object.values(ModbusDataType) as ModbusDataType[];
readonly writeFunctionCodes = [5, 6, 15, 16];
private readonly defaultFunctionCodes = [3, 4, 6, 16];
private readonly readFunctionCodes = [1, 2, 3, 4];
private readonly bitsFunctionCodes = [...this.readFunctionCodes, ...this.writeFunctionCodes];
private onChange: (value: RPCTemplateConfigModbus) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.rpcParametersFormGroup = this.fb.group({
type: [ModbusDataType.BYTES, [Validators.required]],
functionCode: [this.defaultFunctionCodes[0], [Validators.required]],
value: [{value: '', disabled: true}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
address: [null, [Validators.required]],
objectsCount: [1, [Validators.required]],
});
this.updateFunctionCodes(this.rpcParametersFormGroup.get('type').value);
this.observeValueChanges();
this.observeKeyDataType();
this.observeFunctionCode();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: RPCTemplateConfigModbus) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.rpcParametersFormGroup.valid ? null : {
rpcParametersFormGroup: { valid: false }
};
}
writeValue(value: RPCTemplateConfigModbus): void {
this.rpcParametersFormGroup.patchValue(value, {emitEvent: false});
}
private observeValueChanges(): void {
this.rpcParametersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.onChange(value);
this.onTouched();
});
}
private observeKeyDataType(): void {
this.rpcParametersFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.ModbusEditableDataTypes.includes(dataType)) {
this.rpcParametersFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
}
this.updateFunctionCodes(dataType);
});
}
private observeFunctionCode(): void {
this.rpcParametersFormGroup.get('functionCode').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(code => this.updateValueEnabling(code));
}
private updateValueEnabling(code: number): void {
if (this.writeFunctionCodes.includes(code)) {
this.rpcParametersFormGroup.get('value').enable({emitEvent: false});
} else {
this.rpcParametersFormGroup.get('value').setValue(null);
this.rpcParametersFormGroup.get('value').disable({emitEvent: false});
}
}
private updateFunctionCodes(dataType: ModbusDataType): void {
this.functionCodes = dataType === ModbusDataType.BITS ? this.bitsFunctionCodes : this.defaultFunctionCodes;
if (!this.functionCodes.includes(this.rpcParametersFormGroup.get('functionCode').value)) {
this.rpcParametersFormGroup.get('functionCode').patchValue(this.functionCodes[0], {emitEvent: false});
}
}
}

48
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/mqtt-rpc-parameters/mqtt-rpc-parameters.component.html

@ -1,48 +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.
-->
<ng-container [formGroup]="rpcParametersFormGroup">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.method-name' | translate }}</mat-label>
<input matInput formControlName="methodFilter"
placeholder="echo"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.requestTopicExpression' | translate }}</mat-label>
<input matInput formControlName="requestTopicExpression"
placeholder="sensor/${deviceName}/request/${methodName}/${requestId}"/>
</mat-form-field>
<mat-slide-toggle class="margin" (click)="$event.stopPropagation()" formControlName="withResponse">
{{ 'gateway.rpc.withResponse' | translate }}
</mat-slide-toggle>
<mat-form-field *ngIf="rpcParametersFormGroup.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="rpcParametersFormGroup.get('withResponse')?.value">
<mat-label>{{ 'gateway.rpc.responseTimeout' | translate }}</mat-label>
<input matInput formControlName="responseTimeout" type="number"
placeholder="10000" min="10" step="1"/>
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.rpc.valueExpression' | translate }}</mat-label>
<input matInput formControlName="valueExpression"
placeholder="${params}"/>
</mat-form-field>
</ng-container>

24
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/mqtt-rpc-parameters/mqtt-rpc-parameters.component.scss

@ -1,24 +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.
*/
:host {
display: flex;
flex-direction: column;
.mat-mdc-slide-toggle.margin {
margin-bottom: 10px;
margin-left: 10px;
}
}

139
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/mqtt-rpc-parameters/mqtt-rpc-parameters.component.ts

@ -1,139 +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.
///
import {
ChangeDetectionStrategy,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator, Validators,
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil, tap } from 'rxjs/operators';
import {
integerRegex,
noLeadTrailSpacesRegex,
RPCTemplateConfigMQTT
} from '@home/components/widget/lib/gateway/gateway-widget.models';
@Component({
selector: 'tb-mqtt-rpc-parameters',
templateUrl: './mqtt-rpc-parameters.component.html',
styleUrls: ['./mqtt-rpc-parameters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MqttRpcParametersComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MqttRpcParametersComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
})
export class MqttRpcParametersComponent implements ControlValueAccessor, Validator, OnDestroy {
rpcParametersFormGroup: UntypedFormGroup;
private onChange: (value: RPCTemplateConfigMQTT) => void = (_) => {};
private onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
this.rpcParametersFormGroup = this.fb.group({
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: [{ value: null, disabled: true }, [Validators.min(10), Validators.pattern(integerRegex)]],
valueExpression: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
withResponse: [false, []],
});
this.observeValueChanges();
this.observeWithResponse();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: RPCTemplateConfigMQTT) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.rpcParametersFormGroup.valid ? null : {
rpcParametersFormGroup: { valid: false }
};
}
writeValue(value: RPCTemplateConfigMQTT): void {
this.rpcParametersFormGroup.patchValue(value, {emitEvent: false});
this.toggleResponseFields(value.withResponse);
}
private observeValueChanges(): void {
this.rpcParametersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.onChange(value);
this.onTouched();
});
}
private observeWithResponse(): void {
this.rpcParametersFormGroup.get('withResponse').valueChanges.pipe(
tap((isActive: boolean) => this.toggleResponseFields(isActive)),
takeUntil(this.destroy$),
).subscribe();
}
private toggleResponseFields(enabled: boolean): void {
const responseTopicControl = this.rpcParametersFormGroup.get('responseTopicExpression');
const responseTimeoutControl = this.rpcParametersFormGroup.get('responseTimeout');
if (enabled) {
responseTopicControl.enable();
responseTimeoutControl.enable();
} else {
responseTopicControl.disable();
responseTimeoutControl.disable();
}
}
}

93
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/opc-rpc-parameters/opc-rpc-parameters.component.html

@ -1,93 +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.
-->
<ng-container [formGroup]="rpcParametersFormGroup">
<div class="tb-form-hint tb-primary-fill tb-flex no-padding-top hint-container">
{{ 'gateway.rpc.hint.opc-method' | translate }}
</div>
<mat-form-field class="tb-flex">
<mat-label>{{ 'gateway.rpc.method' | translate }}</mat-label>
<input matInput formControlName="method" placeholder="multiply"/>
</mat-form-field>
<fieldset class="tb-form-panel stroked arguments-container" formArrayName="arguments">
<strong>
<span class="fields-label">{{ 'gateway.rpc.arguments' | translate }}</span>
</strong>
<div class="flex flex-1 items-center justify-center gap-2.5"
*ngFor="let argumentFormGroup of rpcParametersFormGroup.get('arguments')['controls']; let i = index" [formGroup]="argumentFormGroup">
<div class="tb-form-row column-xs type-container items-center justify-between">
<div class="tb-required" translate>gateway.type</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-select-trigger>
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(argumentFormGroup.get('type').value)?.icon">
</mat-icon>
<span>{{ valueTypes.get(argumentFormGroup.get('type').value)?.name | translate }}</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypes.get(valueType).icon }}">
</mat-icon>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs value-container item-center justify-between">
<div class="tb-required" translate>gateway.value</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex">
<ng-container [ngSwitch]="argumentFormGroup.get('type').value">
<input *ngSwitchCase="MappingValueType.STRING" matInput required formControlName="string"
placeholder="{{ 'gateway.set' | translate }}" />
<input *ngSwitchCase="MappingValueType.INTEGER" matInput required formControlName="integer" type="number"
placeholder="{{ 'gateway.set' | translate }}" />
<input *ngSwitchCase="MappingValueType.DOUBLE" matInput required formControlName="double" type="number"
placeholder="{{ 'gateway.set' | translate }}" />
<mat-select *ngSwitchCase="MappingValueType.BOOLEAN" formControlName="boolean">
<mat-option [value]="true">true</mat-option>
<mat-option [value]="false">false</mat-option>
</mat-select>
</ng-container>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="argumentFormGroup.get(argumentFormGroup.get('type').value).hasError('required')
&& argumentFormGroup.get(argumentFormGroup.get('type').value).touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<button mat-icon-button (click)="removeArgument(i)"
class="tb-box-button"
matTooltip="{{ 'gateway.rpc.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
<button mat-raised-button
class="self-start"
(click)="addArgument()">
{{ 'gateway.rpc.add-argument' | translate }}
</button>
</fieldset>
</ng-container>

32
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/opc-rpc-parameters/opc-rpc-parameters.component.scss

@ -1,32 +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.
*/
:host {
.arguments-container {
margin-bottom: 10px;
}
.type-container {
width: 40%;
}
.value-container {
width: 50%;
}
.hint-container {
margin-bottom: 12px;
}
}

169
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/rpc-parameters/opc-rpc-parameters/opc-rpc-parameters.component.ts

@ -1,169 +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.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
OnDestroy,
} from '@angular/core';
import {
ControlValueAccessor,
FormArray,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator, Validators,
} from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import {
integerRegex,
MappingValueType,
mappingValueTypesMap,
noLeadTrailSpacesRegex,
OPCTypeValue,
RPCTemplateConfigOPC
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isDefinedAndNotNull, isEqual } from '@core/utils';
@Component({
selector: 'tb-opc-rpc-parameters',
templateUrl: './opc-rpc-parameters.component.html',
styleUrls: ['./opc-rpc-parameters.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OpcRpcParametersComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => OpcRpcParametersComponent),
multi: true
}
],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
})
export class OpcRpcParametersComponent implements ControlValueAccessor, Validator, OnDestroy {
rpcParametersFormGroup: UntypedFormGroup;
readonly valueTypeKeys: MappingValueType[] = Object.values(MappingValueType);
readonly MappingValueType = MappingValueType;
readonly valueTypes = mappingValueTypesMap;
private onChange: (value: RPCTemplateConfigOPC) => void = (_) => {} ;
private onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {
this.rpcParametersFormGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
arguments: this.fb.array([]),
});
this.observeValueChanges();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: RPCTemplateConfigOPC) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
validate(): ValidationErrors | null {
return this.rpcParametersFormGroup.valid ? null : {
rpcParametersFormGroup: { valid: false }
};
}
writeValue(params: RPCTemplateConfigOPC): void {
this.clearArguments();
params.arguments?.map(({type, value}) => ({type, [type]: value }))
.forEach(argument => this.addArgument(argument as OPCTypeValue));
this.cdr.markForCheck();
this.rpcParametersFormGroup.get('method').patchValue(params.method);
}
private observeValueChanges(): void {
this.rpcParametersFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(params => {
const updatedArguments = params.arguments.map(({type, ...config}) => ({type, value: config[type]}));
this.onChange({method: params.method, arguments: updatedArguments});
this.onTouched();
});
}
removeArgument(index: number): void {
(this.rpcParametersFormGroup.get('arguments') as FormArray).removeAt(index);
}
addArgument(value: OPCTypeValue = {} as OPCTypeValue): void {
const fromGroup = this.fb.group({
type: [value.type ?? MappingValueType.STRING],
string: [
value.string ?? { value: '', disabled: !(isEqual(value, {}) || value.string)},
[Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]
],
integer: [
{value: value.integer ?? 0, disabled: !isDefinedAndNotNull(value.integer)},
[Validators.required, Validators.pattern(integerRegex)]
],
double: [{value: value.double ?? 0, disabled: !isDefinedAndNotNull(value.double)}, [Validators.required]],
boolean: [{value: value.boolean ?? false, disabled: !isDefinedAndNotNull(value.boolean)}, [Validators.required]],
});
this.observeTypeChange(fromGroup);
(this.rpcParametersFormGroup.get('arguments') as FormArray).push(fromGroup, {emitEvent: false});
}
clearArguments(): void {
const formArray = this.rpcParametersFormGroup.get('arguments') as FormArray;
while (formArray.length !== 0) {
formArray.removeAt(0);
}
}
private observeTypeChange(dataKeyFormGroup: FormGroup): void {
dataKeyFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => {
dataKeyFormGroup.disable({emitEvent: false});
dataKeyFormGroup.get('type').enable({emitEvent: false});
dataKeyFormGroup.get(type).enable({emitEvent: false});
});
}
}

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

@ -1,128 +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-row space-between same-padding tb-flex column" [formGroup]="securityFormGroup">
<div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width tb-required">{{ title | translate }}</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of securityTypes" [value]="type">
{{ SecurityTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<ng-container [ngSwitch]="securityFormGroup.get('type').value">
<ng-template [ngSwitchCase]="BrokerSecurityType.BASIC">
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.username</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="username" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.username-required') | translate"
*ngIf="securityFormGroup.get('username').hasError('required')
&& securityFormGroup.get('username').touched"
class="tb-error">
warning
</mat-icon>
</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>
</ng-template>
<ng-template [ngSwitchCase]="BrokerSecurityType.CERTIFICATES">
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.path-hint' | translate }}</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.CA-certificate-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="pathToCACert" 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="pathToPrivateKey" 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.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="pathToClientCert" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<ng-container *ngIf="extendCertificatesModel">
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.mode</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="mode">
<mat-option *ngFor="let type of modeTypes" [value]="type">
{{ type }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="tb-form-row space-between tb-flex fill-width">
<div class="fixed-title-width" translate>gateway.username</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="username" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.username-required') | translate"
*ngIf="securityFormGroup.get('username').hasError('required')
&& securityFormGroup.get('username').touched"
class="tb-error">
warning
</mat-icon>
</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>
</ng-container>
</ng-template>
</ng-container>
</div>

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

@ -1,20 +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.
*/
:host {
width: 100%;
height: 100%;
display: block;
}

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

@ -1,177 +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.
///
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
forwardRef,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import { Subject } from 'rxjs';
import {
ControlValueAccessor,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validators
} from '@angular/forms';
import {
SecurityType,
SecurityTypeTranslationsMap,
ModeType,
noLeadTrailSpacesRegex,
ConnectorSecurity
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coercion';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
@Component({
selector: 'tb-security-config',
templateUrl: './security-config.component.html',
styleUrls: ['./security-config.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SecurityConfigComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => SecurityConfigComponent),
multi: true
}
],
standalone: true,
imports:[
CommonModule,
SharedModule,
]
})
export class SecurityConfigComponent implements ControlValueAccessor, OnInit, OnDestroy {
@Input()
title = 'gateway.security';
@Input()
@coerceBoolean()
extendCertificatesModel = false;
BrokerSecurityType = SecurityType;
securityTypes = Object.values(SecurityType) as SecurityType[];
modeTypes = Object.values(ModeType);
SecurityTypeTranslationsMap = SecurityTypeTranslationsMap;
securityFormGroup: UntypedFormGroup;
private onChange: (value: string) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
this.securityFormGroup = this.fb.group({
type: [SecurityType.ANONYMOUS, []],
username: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToCACert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToPrivateKey: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
pathToClientCert: ['', [Validators.pattern(noLeadTrailSpacesRegex)]]
});
if (this.extendCertificatesModel) {
this.securityFormGroup.addControl('mode', this.fb.control(ModeType.NONE, []));
}
this.securityFormGroup.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.onChange(value);
this.onTouched();
});
this.securityFormGroup.get('type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((type) => this.updateValidators(type));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
writeValue(securityInfo: ConnectorSecurity): void {
if (!securityInfo) {
const defaultSecurity = {type: SecurityType.ANONYMOUS};
this.securityFormGroup.reset(defaultSecurity, {emitEvent: false});
} else {
if (!securityInfo.type) {
securityInfo.type = SecurityType.ANONYMOUS;
}
this.updateValidators(securityInfo.type);
this.securityFormGroup.reset(securityInfo, {emitEvent: false});
}
this.cdr.markForCheck();
}
validate(): ValidationErrors | null {
return this.securityFormGroup.get('type').value !== SecurityType.BASIC || this.securityFormGroup.valid ? null : {
securityForm: { valid: false }
};
}
registerOnChange(fn: (value: string) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
private updateValidators(type: SecurityType): void {
if (type) {
this.securityFormGroup.get('username').disable({emitEvent: false});
this.securityFormGroup.get('password').disable({emitEvent: false});
this.securityFormGroup.get('pathToCACert').disable({emitEvent: false});
this.securityFormGroup.get('pathToPrivateKey').disable({emitEvent: false});
this.securityFormGroup.get('pathToClientCert').disable({emitEvent: false});
this.securityFormGroup.get('mode')?.disable({emitEvent: false});
if (type === SecurityType.BASIC) {
this.securityFormGroup.get('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
} else if (type === SecurityType.CERTIFICATES) {
this.securityFormGroup.get('pathToCACert').enable({emitEvent: false});
this.securityFormGroup.get('pathToPrivateKey').enable({emitEvent: false});
this.securityFormGroup.get('pathToClientCert').enable({emitEvent: false});
if (this.extendCertificatesModel) {
const modeControl = this.securityFormGroup.get('mode');
if (modeControl && !modeControl.value) {
modeControl.setValue(ModeType.NONE, {emitEvent: false});
}
modeControl?.enable({emitEvent: false});
this.securityFormGroup.get('username').enable({emitEvent: false});
this.securityFormGroup.get('password').enable({emitEvent: false});
}
}
}
}
}

101
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html

@ -1,101 +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">
<div class="tb-form-panel no-border no-padding key-panel" *ngIf="valueListFormArray.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 valueListFormArray.controls; trackBy: trackByKey; 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 class="flex-wrap">
<mat-panel-title>
<div class="title-container" tbTruncateWithTooltip>{{ valueTitle(keyControl.get(keyControl.get('type').value).value) }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.type</div>
<mat-form-field class="tb-flex no-gap fill-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-select-trigger>
<div class="tb-flex align-center">
<mat-icon class="tb-mat-18" [svgIcon]="valueTypes.get(keyControl.get('type').value)?.icon">
</mat-icon>
<span>
{{ valueTypes.get(keyControl.get('type').value)?.name | translate}}
</span>
</div>
</mat-select-trigger>
<mat-option *ngFor="let valueType of valueTypeKeys" [value]="valueType">
<mat-icon class="tb-mat-20" svgIcon="{{ valueTypes.get(valueType).icon }}">
</mat-icon>
<span>{{ valueTypes.get(valueType).name | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.value</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" class="tb-inline-field tb-suffix-absolute flex flex-1">
<ng-container [ngSwitch]="keyControl.get('type').value">
<input *ngSwitchCase="MappingValueType.STRING" matInput required formControlName="string"
placeholder="{{ 'gateway.set' | translate }}" />
<input *ngSwitchCase="MappingValueType.INTEGER" matInput required formControlName="integer" type="number"
placeholder="{{ 'gateway.set' | translate }}" />
<input *ngSwitchCase="MappingValueType.DOUBLE" matInput required formControlName="double" type="number"
placeholder="{{ 'gateway.set' | translate }}" />
<mat-select *ngSwitchCase="MappingValueType.BOOLEAN" formControlName="boolean">
<mat-option [value]="true">true</mat-option>
<mat-option [value]="false">false</mat-option>
</mat-select>
</ng-container>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="keyControl.get(keyControl.get('type').value).hasError('required')
&& keyControl.get(keyControl.get('type').value).touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</ng-container>
</div>
<button type="button"
mat-icon-button
(click)="deleteKey($event, $index)"
[matTooltip]="'gateway.delete-value' | translate"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ 'gateway.add-value' | translate }}
</button>
</div>
</div>
<ng-template #noKeys>
<div class="tb-flex no-flex center align-center key-panel">
<span class="tb-prompt" translate>{{ 'gateway.no-value' }}</span>
</div>
</ng-template>

49
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.scss

@ -1,49 +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.
*/
:host {
.title-container {
max-width: 11vw;
}
.key-panel {
height: 250px;
overflow: auto;
}
.tb-form-panel {
.mat-mdc-icon-button {
width: 56px;
height: 56px;
padding: 16px;
color: rgba(0, 0, 0, 0.54);
}
}
.see-example {
width: 32px;
height: 32px;
margin: 4px;
}
}
:host ::ng-deep {
.mat-mdc-form-field-icon-suffix {
display: flex;
}
}

160
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.ts

@ -1,160 +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.
///
import { Component, forwardRef, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { isDefinedAndNotNull } from '@core/utils';
import {
integerRegex,
MappingDataKey,
MappingValueType,
mappingValueTypesMap,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-type-value-panel',
templateUrl: './type-value-panel.component.html',
styleUrls: ['./type-value-panel.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TypeValuePanelComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => TypeValuePanelComponent),
multi: true
}
]
})
export class TypeValuePanelComponent implements ControlValueAccessor, Validator, OnInit, OnDestroy {
valueTypeKeys: MappingValueType[] = Object.values(MappingValueType);
valueTypes = mappingValueTypesMap;
valueListFormArray: UntypedFormArray;
readonly MappingValueType = MappingValueType;
private destroy$ = new Subject<void>();
private propagateChange = (v: any) => {};
constructor(private fb: UntypedFormBuilder) {}
ngOnInit(): void {
this.valueListFormArray = this.fb.array([]);
this.valueListFormArray.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
this.updateView(value);
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
trackByKey(_: number, keyControl: AbstractControl): any {
return keyControl;
}
addKey(): void {
const dataKeyFormGroup = this.fb.group({
type: [MappingValueType.STRING],
string: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
integer: [{value: 0, disabled: true}, [Validators.required, Validators.pattern(integerRegex)]],
double: [{value: 0, disabled: true}, [Validators.required]],
boolean: [{value: false, disabled: true}, [Validators.required]],
});
this.observeTypeChange(dataKeyFormGroup);
this.valueListFormArray.push(dataKeyFormGroup);
}
private observeTypeChange(dataKeyFormGroup: FormGroup): void {
dataKeyFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => {
dataKeyFormGroup.disable({emitEvent: false});
dataKeyFormGroup.get('type').enable({emitEvent: false});
dataKeyFormGroup.get(type).enable({emitEvent: false});
});
}
deleteKey($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}
this.valueListFormArray.removeAt(index);
this.valueListFormArray.markAsDirty();
}
valueTitle(value: any): string {
if (isDefinedAndNotNull(value)) {
if (typeof value === 'object') {
return JSON.stringify(value);
}
return value;
}
return '';
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
writeValue(deviceInfoArray: Array<MappingDataKey>): void {
for (const deviceInfo of deviceInfoArray) {
const config = {
type: [deviceInfo.type],
string: [{value: '', disabled: true}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
integer: [{value: 0, disabled: true}, [Validators.required, Validators.pattern(integerRegex)]],
double: [{value: 0, disabled: true}, [Validators.required]],
boolean: [{value: false, disabled: true}, [Validators.required]],
};
config[deviceInfo.type][0] = {value: deviceInfo.value, disabled: false};
const dataKeyFormGroup = this.fb.group(config);
this.observeTypeChange(dataKeyFormGroup);
this.valueListFormArray.push(dataKeyFormGroup);
}
}
validate(): ValidationErrors | null {
return this.valueListFormArray.valid ? null : {
valueListForm: { valid: false }
};
}
private updateView(value: any): void {
this.propagateChange(value.map(({type, ...config}) => ({type, value: config[type]})));
}
}

53
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html

@ -1,53 +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 mat-dialog-content style="padding: 16px 16px 8px" class="tb-form-panel no-border">
<div class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>device.connectivity.install-necessary-client-tools</div>
<div class="tb-form-row no-border no-padding space-between">
<div class="tb-no-data-text tb-commands-hint" translate>gateway.install-docker-compose</div>
<a mat-stroked-button color="primary" href="https://docs.docker.com/compose/install/" target="_blank">
<mat-icon>description</mat-icon>
{{ 'common.documentation' | translate }}
</a>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.download-configuration-file</div>
<div class="tb-form-row no-border no-padding space-between">
<div class="tb-no-data-text tb-commands-hint" translate>gateway.download-docker-compose</div>
<button mat-stroked-button color="primary" (click)="download($event)">
<mat-icon>download</mat-icon>
{{ 'action.download' | translate }}
</button>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.launch-gateway</div>
<div class="tb-no-data-text tb-commands-hint" translate>gateway.launch-docker-compose</div>
<tb-markdown usePlainMarkdown containerClass="start-code"
data="
```bash
docker compose up
{:copy-code}
```
"></tb-markdown>
</div>
</div>

75
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.scss

@ -1,75 +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.
*/
:host {
.tb-commands-hint {
color: inherit;
font-weight: normal;
flex: 1;
}
}
:host ::ng-deep {
.tb-markdown-view {
.start-code {
.code-wrapper {
padding: 0;
pre[class*=language-] {
margin: 0;
background: #F3F6FA;
border-color: #305680;
padding-right: 38px;
overflow: scroll;
padding-bottom: 4px;
min-height: 42px;
scrollbar-width: thin;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
}
}
button.clipboard-btn {
right: -2px;
p {
color: #305680;
}
p, div {
background-color: #F3F6FA;
}
div {
img {
display: none;
}
&:after {
content: "";
position: initial;
display: block;
width: 18px;
height: 18px;
background: #305680;
mask-image: url(/assets/copy-code-icon.svg);
-webkit-mask-image: url(/assets/copy-code-icon.svg);
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
}
}
}
}
}
}

42
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts

@ -1,42 +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.
///
import { Component, Input } from '@angular/core';
import { DeviceService } from '@core/http/device.service';
@Component({
selector: 'tb-gateway-command',
templateUrl: './device-gateway-command.component.html',
styleUrls: ['./device-gateway-command.component.scss']
})
export class DeviceGatewayCommandComponent {
@Input()
deviceId: string;
constructor(private deviceService: DeviceService) {
}
download($event: Event) {
if ($event) {
$event.stopPropagation();
}
if (this.deviceId) {
this.deviceService.downloadGatewayDockerComposeFile(this.deviceId).subscribe(() => {});
}
}
}

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

@ -1,107 +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 [formGroup]="connectorForm" class="add-connector">
<mat-toolbar color="primary">
<h2>{{ "gateway.add-connector" | translate}}</h2>
<span class="flex-1"></span>
<div [tb-help]="helpLinkId()"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.type</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="type">
<mat-option *ngFor="let type of gatewayConnectorDefaultTypesTranslatesMap | keyvalue" [value]="type.key">
{{ type.value }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.name</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput autocomplete="off" name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(connectorForm.get('name').hasError('duplicateName') ?
'gateway.connector-duplicate-name' :'gateway.name-required') | translate"
*ngIf="(connectorForm.get('name').hasError('required') && connectorForm.get('name').touched)
|| connectorForm.get('name').hasError('duplicateName')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.CUSTOM" class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.connectors-table-class</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="class" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.GRPC" class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.connectors-table-key</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="key" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.remote-logging-level</div>
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="connectorForm.get('type').value !== connectorType.GRPC && connectorForm.get('type').value !== connectorType.CUSTOM"
class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide" formControlName="useDefaults">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.fill-connector-defaults-hint' | translate }}">
{{ 'gateway.fill-connector-defaults' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div *ngIf="connectorForm.get('type').value === connectorType.MQTT" class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.send-change-data-hint' | translate }}">
{{ 'gateway.send-change-data' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
</div>
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="connectorForm.invalid || !connectorForm.dirty">
{{ 'action.add' | translate }}
</button>
</div>
</div>

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

@ -1,22 +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.
*/
:host {
.add-connector {
min-width: 400px;
width: 500px;
}
}

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

@ -1,149 +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.
///
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, UntypedFormControl, UntypedFormGroup, ValidatorFn, Validators } from '@angular/forms';
import { BaseData, HasId } from '@shared/models/base-data';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import {
AddConnectorConfigData,
ConnectorType,
CreatedConnectorConfigData,
GatewayConnector,
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
GatewayVersion,
GatewayVersionedDefaultConfig,
noLeadTrailSpacesRegex
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Observable, Subject } from 'rxjs';
import { ResourcesService } from '@core/services/resources.service';
import { takeUntil, tap } from 'rxjs/operators';
import { helpBaseUrl } from '@shared/models/constants';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
@Component({
selector: 'tb-add-connector-dialog',
templateUrl: './add-connector-dialog.component.html',
styleUrls: ['./add-connector-dialog.component.scss'],
providers: [],
})
export class AddConnectorDialogComponent
extends DialogComponent<AddConnectorDialogComponent, BaseData<HasId>> implements OnInit, OnDestroy {
connectorForm: UntypedFormGroup;
connectorType = ConnectorType;
gatewayConnectorDefaultTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
submitted = false;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddConnectorConfigData,
public dialogRef: MatDialogRef<AddConnectorDialogComponent, CreatedConnectorConfigData>,
private fb: FormBuilder,
private isLatestVersionConfig: LatestVersionConfigPipe,
private resourcesService: ResourcesService) {
super(store, router, dialogRef);
this.connectorForm = this.fb.group({
type: [ConnectorType.MQTT, []],
name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]],
logLevel: [GatewayLogLevel.INFO, []],
useDefaults: [true, []],
sendDataOnlyOnChange: [false, []],
class: ['', []],
key: ['auto', []],
});
}
ngOnInit(): void {
this.observeTypeChange();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
helpLinkId(): string {
return helpBaseUrl + '/docs/iot-gateway/configuration/';
}
cancel(): void {
this.dialogRef.close(null);
}
add(): void {
this.submitted = true;
const value = this.connectorForm.getRawValue();
if (value.useDefaults) {
this.getDefaultConfig(value.type).subscribe((defaultConfig: GatewayVersionedDefaultConfig) => {
const gatewayVersion = this.data.gatewayVersion;
if (gatewayVersion) {
value.configVersion = gatewayVersion;
}
value.configurationJson = (this.isLatestVersionConfig.transform(gatewayVersion)
? defaultConfig[GatewayVersion.Current]
: defaultConfig[GatewayVersion.Legacy])
?? defaultConfig;
if (this.connectorForm.valid) {
this.dialogRef.close(value);
}
});
} else if (this.connectorForm.valid) {
this.dialogRef.close(value);
}
}
private uniqNameRequired(): ValidatorFn {
return (control: UntypedFormControl) => {
const newName = control.value.trim().toLowerCase();
const isDuplicate = this.data.dataSourceData.some(({ value: { name } }) =>
name.toLowerCase() === newName
);
return isDuplicate ? { duplicateName: { valid: false } } : null;
};
}
private observeTypeChange(): void {
this.connectorForm.get('type').valueChanges.pipe(
tap((type: ConnectorType) => {
const useDefaultControl = this.connectorForm.get('useDefaults');
if (type === ConnectorType.GRPC || type === ConnectorType.CUSTOM) {
useDefaultControl.setValue(false);
} else if (!useDefaultControl.value) {
useDefaultControl.setValue(true);
}
}),
takeUntil(this.destroy$),
).subscribe();
}
private getDefaultConfig(type: string): Observable<GatewayVersionedDefaultConfig | GatewayConnector> {
return this.resourcesService.loadJsonResource(`/assets/metadata/connector-default-configs/${type}.json`);
};
}

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

@ -1,732 +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 [formGroup]="mappingForm" class="key-mapping">
<mat-toolbar color="primary">
<h2>{{ MappingTypeTranslationsMap.get(this.data?.mappingType) | translate}}</h2>
<span class="flex-1"></span>
<div [tb-help]="HelpLinkByMappingTypeMap.get(this.data.mappingType)"></div>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="tb-form-panel no-border no-padding">
<div class="tb-form-hint tb-primary-fill">
{{ MappingHintTranslationsMap.get(this.data?.mappingType) | translate }}
</div>
<ng-container [ngSwitch]="data.mappingType">
<ng-template [ngSwitchCase]="MappingType.DATA">
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="topicFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.topic-required') | translate"
*ngIf="mappingForm.get('topicFilter').hasError('required') &&
mappingForm.get('topicFilter').touched;"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.response-topic-Qos-hint' | translate }}">
{{ 'gateway.mqtt-qos' | translate }}
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="subscriptionQos">
<mat-option *ngFor="let type of qualityTypes" [value]="type">
{{ QualityTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ng-container formGroupName="converter">
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.payload-type</div>
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option *ngFor="let type of convertorTypes" [value]="type">
{{ ConvertorTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.data-conversion</div>
<div class="tb-form-hint tb-primary-fill">
{{ DataConversionTranslationsMap.get(converterType) | translate }}
</div>
<ng-container [formGroupName]="converterType" [ngSwitch]="converterType">
<ng-template [ngSwitchCase]="ConvertorTypeEnum.JSON">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true">
</tb-device-info-table>
</ng-template>
<ng-template [ngSwitchCase]="ConvertorTypeEnum.BYTES">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL"
[sourceTypes]="[sourceTypesEnum.MSG, sourceTypesEnum.CONST]" required="true">
</tb-device-info-table>
</ng-template>
<div class="tb-form-panel no-border no-padding"
*ngIf="converterType === ConvertorTypeEnum.BYTES || converterType === ConvertorTypeEnum.JSON">
<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]="converterAttributes" class="tb-flex">
<mat-chip *ngFor="let attribute of converterAttributes">
{{ attribute }}
</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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesButton
(click)="manageKeys($event, attributesButton, MappingKeysType.ATTRIBUTES)">
<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]="converterTelemetry">
<mat-chip *ngFor="let telemetry of converterTelemetry">
{{ telemetry }}
</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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#telemetryButton
(click)="manageKeys($event, telemetryButton, MappingKeysType.TIMESERIES)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</div>
<div class="tb-form-panel no-border no-padding" *ngIf="converterType === ConvertorTypeEnum.CUSTOM">
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.extension-hint' | translate }}">
{{ 'gateway.extension' | translate }}
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="extension" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.extension-required') | translate"
*ngIf="mappingForm.get('converter.custom.extension').hasError('required') &&
mappingForm.get('converter.custom.extension').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row space-between same-padding tb-flex column">
<div class="tb-form-panel-title" translate>gateway.extension-configuration</div>
<div class="tb-form-hint tb-primary-fill">{{ 'gateway.extension-configuration-hint' | translate }}</div>
<div class="tb-form-row space-between tb-flex">
<div class="fixed-title-width" translate>gateway.keys</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="customKeys" class="tb-flex">
<mat-chip *ngFor="let telemetry of customKeys">
{{ telemetry }}
</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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#keysButton
(click)="manageKeys($event, keysButton, MappingKeysType.CUSTOM)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>
</ng-container>
</ng-template>
<ng-template [ngSwitchCase]="MappingType.REQUESTS">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.request-type</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="requestType">
<mat-option *ngFor="let type of requestTypes" [value]="type">
{{ RequestTypesTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ng-container formGroupName="requestValue">
<ng-container [formGroup]="mappingForm.get('requestValue').get(requestMappingType)" [ngSwitch]="requestMappingType">
<div class="tb-form-row column-xs"
*ngIf="requestMappingType === RequestTypeEnum.ATTRIBUTE_REQUEST ||
requestMappingType === RequestTypeEnum.CONNECT_REQUEST ||
requestMappingType === RequestTypeEnum.DISCONNECT_REQUEST">
<div class="fixed-title-width tb-required" translate>gateway.topic-filter</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" [formControl]="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter')"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.topic-required') | translate"
*ngIf="mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').hasError('required') &&
mappingForm.get('requestValue').get(requestMappingType).get('topicFilter').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/topic-filter_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
<ng-template [ngSwitchCase]="RequestTypeEnum.CONNECT_REQUEST">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.FULL" required="true">
</tb-device-info-table>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.DISCONNECT_REQUEST">
<tb-device-info-table formControlName="deviceInfo" [deviceInfoType]="DeviceInfoType.PARTIAL" required="true">
</tb-device-info-table>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_REQUEST">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-required" translate>gateway.from-device-request-settings</div>
<div class="tb-form-hint tb-primary-fill" translate>
gateway.from-device-request-settings-hint
</div>
<div class="tb-form-row column-xs" formGroupName="deviceInfo">
<div class="fixed-title-width tb-flex no-flex align-center" translate>
<div class="tb-required" translate>gateway.device-info.device-name-expression</div>
</div>
<div class="flex flex-1">
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNameExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-info.device-name-expression-required') | translate"
*ngIf="(mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.deviceInfo.deviceNameExpression').touched)"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.attribute-name-expression</div>
<div class="flex flex-1">
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="attributeNameExpressionSource">
<mat-option *ngFor="let type of sourceTypes" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="attributeNameExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.attribute-name-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeRequests.attributeNameExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.attributeNameExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title tb-required" translate>gateway.to-device-response-settings</div>
<div class="tb-form-hint tb-primary-fill" translate>
gateway.to-device-response-settings-hint
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-value-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeRequests.valueExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.valueExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeRequests.topicExpression').hasError('required') &&
mappingForm.get('requestValue.attributeRequests.topicExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="retain">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}">
{{ 'gateway.retain' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.ATTRIBUTE_UPDATE">
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required"
tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}">
{{ 'gateway.device-name-filter' | translate }}
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.deviceNameFilter').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.attribute-filter-hint' | translate }}">
{{ 'gateway.attribute-filter' | translate }}
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="attributeFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.attribute-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.attributeFilter').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.attributeFilter').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.response-value-expression</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-value-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.valueExpression').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.valueExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="topicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.attributeUpdates.topicExpression').hasError('required') &&
mappingForm.get('requestValue.attributeUpdates.topicExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="retain">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.retain-hint' | translate }}">
{{ 'gateway.retain' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</ng-template>
<ng-template [ngSwitchCase]="RequestTypeEnum.SERVER_SIDE_RPC">
<div class="tb-flex row center align-center no-gap fill-width">
<tb-toggle-select formControlName="type" appearance="fill">
<tb-toggle-option [value]="ServerSideRPCType.TWO_WAY">
{{ 'gateway.with-response' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="ServerSideRPCType.ONE_WAY">
{{ 'gateway.without-response' | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.device-name-filter-hint' | translate }}">
{{ 'gateway.device-name-filter' | translate }}
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNameFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-name-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.deviceNameFilter').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" tb-hint-tooltip-icon="{{ 'gateway.method-filter-hint' | translate }}">
{{ 'gateway.method-filter' | translate }}
</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="methodFilter" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.method-filter-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.methodFilter').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.methodFilter').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.request-topic-expression</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="requestTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.request-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.requestTopicExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.value-expression</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="valueExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.valueExpression').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.valueExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
<ng-container *ngIf="mappingForm.get('requestValue.serverSideRpc.type').value === ServerSideRPCType.TWO_WAY">
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.response-topic-expression</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="responseTopicExpression" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.response-topic-expression-required') | translate"
*ngIf="mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').hasError('required') &&
mappingForm.get('requestValue.serverSideRpc.responseTopicExpression').touched"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'widget/lib/gateway/expressions_fn'"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<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">
<mat-option *ngFor="let type of qualityTypes" [value]="type">
{{ QualityTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width tb-required" translate>gateway.response-timeout</div>
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" type="number" min="1" formControlName="responseTimeout"
placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="responseTimeoutErrorTooltip"
*ngIf="(mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('required') ||
mappingForm.get('requestValue.serverSideRpc.responseTimeout').hasError('min')) &&
mappingForm.get('requestValue.serverSideRpc.responseTimeout').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</ng-container>
</ng-template>
</ng-container>
</ng-container>
</ng-template>
<ng-template [ngSwitchCase]="MappingType.OPCUA">
<div class="tb-form-row column-xs">
<div class="tb-flex no-flex align-center" translate>
<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">
<mat-form-field class="tb-flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="deviceNodeSource">
<mat-option *ngFor="let type of [OPCUaSourceTypesEnum.PATH, OPCUaSourceTypesEnum.IDENTIFIER]" [value]="type">
{{ SourceTypeTranslationsMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="tb-flex device-node-pattern-field" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="deviceNodePattern" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.device-node-required') | translate"
*ngIf="(mappingForm.get('deviceNodePattern').hasError('required') &&
mappingForm.get('deviceNodePattern').touched)"
class="tb-error">
warning
</mat-icon>
<div matSuffix
class="see-example"
[tb-help-popup]="'device-node' | getGatewayHelpLink: mappingForm.get('deviceNodeSource').value"
tb-help-popup-placement="left"
[tb-help-popup-style]="{maxWidth: '970px'}">
</div>
</mat-form-field>
</div>
</div>
<tb-device-info-table formControlName="deviceInfo" [sourceTypes]="OPCUaSourceTypes" [deviceInfoType]="DeviceInfoType.FULL" required="true">
</tb-device-info-table>
<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]="opcAttributes" class="tb-flex">
<mat-chip *ngFor="let attribute of opcAttributes">
{{ attribute }}
</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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#opcAttributesButton
(click)="manageKeys($event, opcAttributesButton, MappingKeysType.ATTRIBUTES)">
<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]="opcTelemetry">
<mat-chip *ngFor="let telemetry of opcTelemetry">
{{ telemetry }}
</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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#opcTelemetryButton
(click)="manageKeys($event, opcTelemetryButton, MappingKeysType.TIMESERIES)">
<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]="opcAttributesUpdates" class="tb-flex">
<mat-chip *ngFor="let attribute of opcAttributesUpdates">
{{ attribute }}
</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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#attributesUpdatesButton
(click)="manageKeys($event, attributesUpdatesButton, MappingKeysType.ATTRIBUTES_UPDATES)">
<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-methods</div>
<div class="tb-flex ellipsis-chips-container">
<mat-chip-listbox [tb-ellipsis-chip-list]="opcRpcMethods" class="tb-flex">
<mat-chip *ngFor="let attribute of opcRpcMethods">
{{ attribute }}
</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"
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
#rpcMethodsButton
(click)="manageKeys($event, rpcMethodsButton, MappingKeysType.RPC_METHODS)">
<tb-icon matButtonIcon>edit</tb-icon>
</button>
</div>
</div>
</ng-template>
</ng-container>
</div>
</div>
<div mat-dialog-actions class="justify-end">
<button mat-button color="primary"
type="button"
cdkFocusInitial
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="mappingForm.invalid || !mappingForm.dirty || !keysPopupClosed">
{{ this.data.buttonTitle | translate }}
</button>
</div>
</div>

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

@ -1,86 +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.
*/
:host {
display: grid;
height: 100%;
.key-mapping {
max-width: 900px;
display: flex;
flex-direction: column;
.mat-toolbar {
min-height: 64px;
}
tb-toggle-select {
padding: 4px 0;
}
}
.mat-mdc-dialog-content {
height: 670px;
}
.ellipsis-chips-container {
max-width: 70%;
}
}
:host ::ng-deep {
.key-mapping {
.mat-mdc-chip-listbox {
.mdc-evolution-chip-set__chips {
justify-content: flex-end;
align-items: center;
flex-wrap: nowrap;
}
}
}
.tb-form-row {
.fixed-title-width {
min-width: 40px;
width: 35%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mat-mdc-form-field {
width: 0;
}
}
.see-example {
width: 32px;
height: 32px;
margin: 4px;
}
.mat-mdc-form-field-icon-suffix {
display: flex;
}
.device-config {
gap: 12px;
padding-left: 10px;
padding-right: 10px;
}
.device-node-pattern-field {
flex-basis: 3%;
}
}

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

@ -1,421 +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.
///
import { Component, Inject, OnDestroy, Renderer2, ViewContainerRef } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import {
Attribute,
AttributesUpdate,
ConnectorMapping,
ConnectorMappingFormValue,
ConverterMappingFormValue,
ConvertorType,
ConvertorTypeTranslationsMap,
DataConversionTranslationsMap,
DeviceConnectorMapping,
DeviceInfoType,
HelpLinkByMappingTypeMap,
MappingHintTranslationsMap,
MappingInfo,
MappingKeysAddKeyTranslationsMap,
MappingKeysDeleteKeyTranslationsMap,
MappingKeysNoKeysTextTranslationsMap,
MappingKeysPanelTitleTranslationsMap,
MappingKeysType,
MappingType,
MappingTypeTranslationsMap,
noLeadTrailSpacesRegex,
OPCUaSourceType,
QualityTypes,
QualityTypeTranslationsMap,
RequestMappingData,
RequestMappingFormValue,
RequestType,
RequestTypesTranslationsMap,
RpcMethod,
ServerSideRPCType,
SourceType,
SourceTypeTranslationsMap,
Timeseries
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs';
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/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, ConnectorMapping> implements OnDestroy {
mappingForm: UntypedFormGroup;
MappingType = MappingType;
qualityTypes = QualityTypes;
QualityTranslationsMap = QualityTypeTranslationsMap;
convertorTypes: ConvertorType[] = Object.values(ConvertorType);
ConvertorTypeEnum = ConvertorType;
ConvertorTypeTranslationsMap = ConvertorTypeTranslationsMap;
sourceTypes: SourceType[] = Object.values(SourceType);
OPCUaSourceTypes = Object.values(OPCUaSourceType) as Array<OPCUaSourceType>;
OPCUaSourceTypesEnum = OPCUaSourceType;
sourceTypesEnum = SourceType;
SourceTypeTranslationsMap = SourceTypeTranslationsMap;
requestTypes: RequestType[] = Object.values(RequestType);
RequestTypeEnum = RequestType;
RequestTypesTranslationsMap = RequestTypesTranslationsMap;
DeviceInfoType = DeviceInfoType;
ServerSideRPCType = ServerSideRPCType;
MappingKeysType = MappingKeysType;
MappingHintTranslationsMap = MappingHintTranslationsMap;
MappingTypeTranslationsMap = MappingTypeTranslationsMap;
DataConversionTranslationsMap = DataConversionTranslationsMap;
HelpLinkByMappingTypeMap = HelpLinkByMappingTypeMap;
keysPopupClosed = true;
private destroy$ = new Subject<void>();
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MappingInfo,
public dialogRef: MatDialogRef<MappingDialogComponent, ConnectorMapping>,
private fb: FormBuilder,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private translate: TranslateService) {
super(store, router, dialogRef);
this.createMappingForm();
}
get converterAttributes(): Array<string> {
if (this.converterType) {
return this.mappingForm.get('converter').get(this.converterType).value.attributes.map((value: Attribute) => value.key);
}
}
get converterTelemetry(): Array<string> {
if (this.converterType) {
return this.mappingForm.get('converter').get(this.converterType).value.timeseries.map((value: Timeseries) => value.key);
}
}
get opcAttributes(): Array<string> {
return this.mappingForm.get('attributes').value?.map((value: Attribute) => value.key) || [];
}
get opcTelemetry(): Array<string> {
return this.mappingForm.get('timeseries').value?.map((value: Timeseries) => value.key) || [];
}
get opcRpcMethods(): Array<string> {
return this.mappingForm.get('rpc_methods').value?.map((value: RpcMethod) => value.method) || [];
}
get opcAttributesUpdates(): Array<string> {
return this.mappingForm.get('attributes_updates')?.value?.map((value: AttributesUpdate) => value.key) || [];
}
get converterType(): ConvertorType {
return this.mappingForm.get('converter').get('type').value;
}
get customKeys(): Array<string> {
return Object.keys(this.mappingForm.get('converter').get('custom').value.extensionConfig);
}
get requestMappingType(): RequestType {
return this.mappingForm.get('requestType').value;
}
get responseTimeoutErrorTooltip(): string {
const control = this.mappingForm.get('requestValue.serverSideRpc.responseTimeout');
if (control.hasError('required')) {
return this.translate.instant('gateway.response-timeout-required');
} else if (control.hasError('min')) {
return this.translate.instant('gateway.response-timeout-limits-error', {min: 1});
}
return '';
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
private createMappingForm(): void {
switch (this.data.mappingType) {
case MappingType.DATA:
this.mappingForm = this.fb.group({});
this.createDataMappingForm();
break;
case MappingType.REQUESTS:
this.mappingForm = this.fb.group({});
this.createRequestMappingForm();
break;
case MappingType.OPCUA:
this.createOPCUAMappingForm();
}
}
cancel(): void {
if (this.keysPopupClosed) {
this.dialogRef.close(null);
}
}
add(): void {
if (this.mappingForm.valid) {
this.dialogRef.close(this.prepareMappingData());
}
}
manageKeys($event: Event, matButton: MatButton, keysType: MappingKeysType): void {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const group = this.data.mappingType !== MappingType.OPCUA ? this.mappingForm.get('converter').get(this.converterType)
: this.mappingForm;
const keysControl = group.get(keysType);
const ctx: { [key: string]: any } = {
keys: keysControl.value,
keysType,
rawData: this.mappingForm.get('converter.type')?.value === ConvertorType.BYTES,
panelTitle: MappingKeysPanelTitleTranslationsMap.get(keysType),
addKeyTitle: MappingKeysAddKeyTranslationsMap.get(keysType),
deleteKeyTitle: MappingKeysDeleteKeyTranslationsMap.get(keysType),
noKeysText: MappingKeysNoKeysTextTranslationsMap.get(keysType)
};
if (this.data.mappingType === MappingType.OPCUA) {
ctx.valueTypeKeys = Object.values(OPCUaSourceType);
ctx.valueTypeEnum = OPCUaSourceType;
ctx.valueTypes = SourceTypeTranslationsMap;
}
this.keysPopupClosed = false;
const dataKeysPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, MappingDataKeysPanelComponent, 'leftBottom', false, null,
ctx,
{},
{}, {}, true);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
});
dataKeysPanelPopover.tbHideStart.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.keysPopupClosed = true;
});
}
}
private prepareMappingData(): ConnectorMapping {
const formValue = this.mappingForm.value;
switch (this.data.mappingType) {
case MappingType.DATA:
const {converter, topicFilter, subscriptionQos} = formValue;
return {
topicFilter,
subscriptionQos,
converter: {
type: converter.type,
...converter[converter.type]
}
};
case MappingType.REQUESTS:
return {
requestType: formValue.requestType,
requestValue: formValue.requestValue[formValue.requestType]
};
default:
return formValue;
}
}
private getFormValueData(): ConnectorMappingFormValue {
if (this.data.value && Object.keys(this.data.value).length) {
switch (this.data.mappingType) {
case MappingType.DATA:
const {converter, topicFilter, subscriptionQos} = this.data.value;
return {
topicFilter,
subscriptionQos,
converter: {
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 Record<RequestType, RequestMappingData>
};
default:
return this.data.value as DeviceConnectorMapping;
}
}
}
private createDataMappingForm(): void {
this.mappingForm.addControl('topicFilter',
this.fb.control('', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]));
this.mappingForm.addControl('subscriptionQos', this.fb.control(0));
this.mappingForm.addControl('converter', this.fb.group({
type: [ConvertorType.JSON, []],
json: this.fb.group({
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []]
}),
bytes: this.fb.group({
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []]
}),
custom: this.fb.group({
extension: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
extensionConfig: [{}, []]
}),
}));
this.mappingForm.patchValue(this.getFormValueData());
this.mappingForm.get('converter.type').valueChanges.pipe(
startWith(this.mappingForm.get('converter.type').value),
takeUntil(this.destroy$)
).subscribe((value) => {
const converterGroup = this.mappingForm.get('converter');
converterGroup.get('json').disable({emitEvent: false});
converterGroup.get('bytes').disable({emitEvent: false});
converterGroup.get('custom').disable({emitEvent: false});
converterGroup.get(value).enable({emitEvent: false});
});
}
private createRequestMappingForm(): void {
this.mappingForm.addControl('requestType', this.fb.control(RequestType.CONNECT_REQUEST, []));
this.mappingForm.addControl('requestValue', this.fb.group({
connectRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
disconnectRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: [{}, []]
}),
attributeRequests: this.fb.group({
topicFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceInfo: this.fb.group({
deviceNameExpressionSource: [SourceType.MSG, []],
deviceNameExpression: ['', [Validators.required]],
}),
attributeNameExpressionSource: [SourceType.MSG, []],
attributeNameExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
retain: [false, []]
}),
attributeUpdates: this.fb.group({
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
attributeFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
topicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
retain: [true, []]
}),
serverSideRpc: this.fb.group({
type: [ServerSideRPCType.TWO_WAY, []],
deviceNameFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
methodFilter: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
requestTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
valueExpression: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
responseTopicQoS: [0, []],
responseTimeout: [10000, [Validators.required, Validators.min(1)]],
})
}));
this.mappingForm.get('requestType').valueChanges.pipe(
startWith(this.mappingForm.get('requestType').value),
takeUntil(this.destroy$)
).subscribe((value) => {
const requestValueGroup = this.mappingForm.get('requestValue');
requestValueGroup.get('connectRequests').disable({emitEvent: false});
requestValueGroup.get('disconnectRequests').disable({emitEvent: false});
requestValueGroup.get('attributeRequests').disable({emitEvent: false});
requestValueGroup.get('attributeUpdates').disable({emitEvent: false});
requestValueGroup.get('serverSideRpc').disable({emitEvent: false});
requestValueGroup.get(value).enable();
});
this.mappingForm.get('requestValue.serverSideRpc.type').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((value) => {
const requestValueGroup = this.mappingForm.get('requestValue.serverSideRpc');
if (value === ServerSideRPCType.ONE_WAY) {
requestValueGroup.get('responseTopicExpression').disable({emitEvent: false});
requestValueGroup.get('responseTopicQoS').disable({emitEvent: false});
requestValueGroup.get('responseTimeout').disable({emitEvent: false});
} else {
requestValueGroup.get('responseTopicExpression').enable({emitEvent: false});
requestValueGroup.get('responseTopicQoS').enable({emitEvent: false});
requestValueGroup.get('responseTimeout').enable({emitEvent: false});
}
});
this.mappingForm.patchValue(this.getFormValueData());
}
private createOPCUAMappingForm(): void {
this.mappingForm = this.fb.group({
deviceNodeSource: [OPCUaSourceType.PATH, []],
deviceNodePattern: ['', [Validators.required]],
deviceInfo: [{}, []],
attributes: [[], []],
timeseries: [[], []],
rpc_methods: [[], []],
attributes_updates: [[], []]
});
this.mappingForm.patchValue(this.getFormValueData());
}
}

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

@ -1,318 +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="connector-container tb-form-panel no-border">
<section class="table-section tb-form-panel no-padding section-container flex">
<mat-toolbar class="mat-mdc-table-toolbar justify-between">
<h2>{{ 'gateway.connectors' | translate }}</h2>
<button *ngIf="dataSource?.data?.length"
mat-icon-button
[disabled]="isLoading$ | async"
(click)="onAddConnector($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</mat-toolbar>
<div class="table-container">
<section *ngIf="!dataSource?.data?.length"
class="mat-headline-5 tb-absolute-fill tb-add-new items-center justify-center">
<button mat-button class="connector"
(click)="onAddConnector($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'gateway.add-connector' | translate }}</span>
</button>
</section>
<table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<ng-container matColumnDef="enabled" sticky>
<mat-header-cell *matHeaderCellDef style="width: 60px;min-width: 60px;">
{{ 'gateway.connectors-table-enabled' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute">
<mat-slide-toggle [checked]="activeConnectors.includes(attribute.key)"
(click)="$event.stopPropagation(); onEnableConnector(attribute)"></mat-slide-toggle>
</mat-cell>
</ng-container>
<ng-container matColumnDef="key">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%">
{{ 'gateway.connectors-table-name' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.key }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%">
{{ 'gateway.connectors-table-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
{{ returnType(attribute) }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="syncStatus">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%">
{{ 'gateway.configuration' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<div class="status" [class]="isConnectorSynced(attribute) ? 'status-sync' : 'status-unsync'">
{{ isConnectorSynced(attribute) ? 'sync' : 'out of sync' }}
</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="errors">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%">
{{ 'gateway.connectors-table-status' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)"
[class]="{'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef>
<div class="gt-md:!hidden" style="width: 48px; min-width: 48px; max-width: 48px;"></div>
<div class="lt-lg:!hidden" [style]="{ minWidth: '144px', maxWidth: '144px', textAlign: 'center'}">{{ 'gateway.connectors-table-actions' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let attribute">
<div class="flex-row justify-end lt-md:!hidden" [style]="{ minWidth: '144px', maxWidth: '144px', width: '144px', textAlign: 'center'}">
<button mat-icon-button
matTooltip="RPC"
matTooltipPosition="above"
(click)="connectorRpc(attribute, $event)">
<mat-icon>private_connectivity</mat-icon>
</button>
<button mat-icon-button
matTooltip="Logs"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltipPosition="above"
(click)="deleteConnector(attribute, $event)">
<mat-icon>delete</mat-icon>
</button>
</div>
<div class="gt-sm:!hidden">
<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
matTooltip="RPC"
matTooltipPosition="above"
(click)="connectorRpc(attribute, $event)">
<mat-icon>private_connectivity</mat-icon>
</button>
<button mat-icon-button
matTooltip="Logs"
matTooltipPosition="above"
(click)="connectorLogs(attribute, $event)">
<mat-icon>list</mat-icon>
</button>
<button mat-icon-button
matTooltip="Delete connector"
matTooltipPosition="above"
(click)="deleteConnector(attribute, $event)">
<mat-icon>delete</mat-icon>
</button>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row class="mat-row-select" [class]="{'tb-current-entity': isSameConnector(attribute)}"
*matRowDef="let attribute; let i = index; columns: displayedColumns;" (click)="selectConnector($event, attribute)"></mat-row>
</table>
</div>
</section>
<section [formGroup]="connectorForm" class="tb-form-panel section-container flex">
<div class="tb-form-panel-title tb-flex no-flex space-between align-center">
<div class="tb-form-panel-title">
{{ initialConnector?.type ? GatewayConnectorTypesTranslatesMap.get(initialConnector.type) : '' }}
{{ 'gateway.configuration' | translate }}
<span class="version-placeholder" *ngIf="connectorForm.get('configVersion').value">v{{connectorForm.get('configVersion').value}}</span>
</div>
<tb-toggle-select *ngIf="initialConnector && allowBasicConfig.has(initialConnector.type)"
formControlName="mode" appearance="fill">
<tb-toggle-option [value]="ConnectorConfigurationModes.BASIC">
{{ 'gateway.basic' | translate }}
</tb-toggle-option>
<tb-toggle-option [value]="ConnectorConfigurationModes.ADVANCED">
{{ 'gateway.advanced' | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<span [class.!hidden]="initialConnector"
class="no-data-found items-center justify-center" translate>
gateway.select-connector
</span>
<section class="tb-form-panel section-container no-border no-padding tb-flex space-between" *ngIf="initialConnector">
<ng-container *ngIf="connectorForm.get('mode')?.value === ConnectorConfigurationModes.BASIC else defaultConfig">
<ng-container [ngSwitch]="initialConnector.type">
<ng-container *ngSwitchCase="ConnectorType.MQTT">
<tb-mqtt-basic-config
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
/>
<ng-template #legacy>
<tb-mqtt-legacy-basic-config
(initialized)="basicConfigInitSubject.next()"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
/>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.OPCUA">
<tb-opc-ua-basic-config
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
/>
<ng-template #legacy>
<tb-opc-ua-legacy-basic-config
(initialized)="basicConfigInitSubject.next()"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
/>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.MODBUS">
<tb-modbus-basic-config
*ngIf="connectorForm.get('configVersion').value | isLatestVersionConfig else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
/>
<ng-template #legacy>
<tb-modbus-legacy-basic-config
formControlName="basicConfig"
(initialized)="basicConfigInitSubject.next()"
[generalTabContent]="generalTabContent"
/>
</ng-template>
</ng-container>
</ng-container>
</ng-container>
<ng-template #defaultConfig>
<mat-tab-group>
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-container [ngTemplateOutlet]="generalTabContent"></ng-container>
</mat-tab>
<mat-tab label="{{ 'gateway.configuration' | translate }}*">
<tb-json-object-edit
[fillHeight]="true"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
</mat-tab>
</mat-tab-group>
</ng-template>
<div class="flex justify-end">
<button mat-raised-button color="primary"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="onSaveConnector()">
{{ 'action.save' | translate }}
</button>
</div>
</section>
</section>
</div>
<ng-template #generalTabContent>
<section [formGroup]="connectorForm" class="tb-form-panel no-border no-padding padding-top section-container flex">
<div class="tb-form-row column-xs">
<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 autocomplete="off" name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(connectorForm.get('name').hasError('duplicateName') ?
'gateway.connector-duplicate-name' : 'gateway.name-required') | translate"
*ngIf="(connectorForm.get('name').hasError('required') && connectorForm.get('name').touched) ||
connectorForm.get('name').hasError('duplicateName')"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === ConnectorType.CUSTOM" class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.connectors-table-class</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="class" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === ConnectorType.GRPC" class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.connectors-table-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="key" placeholder="{{ 'gateway.set' | translate }}"/>
</mat-form-field>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>gateway.logs-configuration</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="enableRemoteLogging">
<mat-label>
{{ 'gateway.enable-remote-logging' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>gateway.remote-logging-level</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="logLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<div *ngIf="connectorForm.get('type').value === ConnectorType.MQTT" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="sendDataOnlyOnChange">
<mat-label tb-hint-tooltip-icon="{{ 'gateway.send-change-data-hint' | translate }}">
{{ 'gateway.send-change-data' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<tb-report-strategy
[defaultValue]="ReportStrategyDefaultValue.Connector"
*ngIf="connectorForm.get('type').value === ConnectorType.MODBUS && (connectorForm.get('configVersion').value | isLatestVersionConfig)"
formControlName="reportStrategy"
/>
</section>
</ng-template>

156
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss

@ -1,156 +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.
*/
@import '../../../../../../../scss/constants';
:host {
width: 100%;
height: 100%;
display: block;
overflow-x: auto;
padding: 0;
.version-placeholder {
color: gray;
font-size: 12px
}
.connector-container {
height: 100%;
width: 100%;
flex-direction: row;
@media #{$mat-lt-lg} {
flex-direction: column;
}
& > section:not(.table-section) {
max-width: unset;
@media #{$mat-gt-md} {
max-width: 50%;
}
}
.table-section {
min-height: 35vh;
overflow: hidden;
.table-container {
overflow: auto;
}
}
.flex {
flex: 1;
}
.input-container {
height: auto;
}
.section-container {
background-color: #fff;
}
}
.mat-toolbar {
background: transparent;
color: rgba(0, 0, 0, .87) !important;
}
.mat-mdc-slide-toggle {
margin: 0 8px;
}
.status {
text-align: center;
border-radius: 16px;
font-weight: 500;
width: fit-content;
padding: 5px 15px;
&-sync {
background: rgba(25, 128, 56, .06);
color: rgb(25, 128, 56);
}
&-unsync {
background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
}
}
mat-row {
cursor: pointer;
}
.dot {
height: 12px;
width: 12px;
background-color: #bbb;
border-radius: 50%;
display: inline-block;
}
.hasErrors {
background-color: rgb(203, 37, 48);
}
.noErrors {
background-color: rgb(25, 128, 56);
}
}
:host ::ng-deep {
.connector-container {
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;
}
.mat-mdc-tab-body.mat-mdc-tab-body-active {
position: absolute;
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
width: 30%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.tb-add-new {
display: flex;
z-index: 999;
pointer-events: none;
background-color: #fff;
button.connector {
height: auto;
padding-right: 12px;
font-size: 20px;
border-style: dashed;
border-width: 2px;
border-radius: 8px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
color: rgba(0, 0, 0, 0.38);
}
}
}
}

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

@ -1,897 +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.
///
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Input,
NgZone,
OnDestroy,
ViewChild
} from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { AttributeService } from '@core/http/attribute.service';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin, Observable, of, Subject, Subscription } from 'rxjs';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
import { Direction, SortOrder } from '@shared/models/page/sort-order';
import { MatSort } from '@angular/material/sort';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { MatTableDataSource } from '@angular/material/table';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { camelCase, deepClone, isEqual, isString } from '@core/utils';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models';
import {
AddConnectorConfigData,
ConnectorBaseConfig,
ConnectorBaseInfo,
ConfigurationModes,
ConnectorType,
GatewayAttributeData,
GatewayConnector,
GatewayConnectorDefaultTypesTranslatesMap,
GatewayLogLevel,
noLeadTrailSpacesRegex,
ReportStrategyDefaultValue,
ReportStrategyType,
} from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { debounceTime, filter, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { ErrorStateMatcher } from '@angular/material/core';
import { PageData } from '@shared/models/page/page-data';
import {
GatewayConnectorVersionMappingUtil
} from '@home/components/widget/lib/gateway/utils/gateway-connector-version-mapping.util';
import { LatestVersionConfigPipe } from '@home/components/widget/lib/gateway/pipes/latest-version-config.pipe';
export class ForceErrorStateMatcher implements ErrorStateMatcher {
isErrorState(control: FormControl | null): boolean {
return (control && control.invalid);
}
}
@Component({
selector: 'tb-gateway-connector',
templateUrl: './gateway-connectors.component.html',
providers: [{ provide: ErrorStateMatcher, useClass: ForceErrorStateMatcher }],
styleUrls: ['./gateway-connectors.component.scss']
})
export class GatewayConnectorComponent extends PageComponent implements AfterViewInit, OnDestroy {
@Input()
ctx: WidgetContext;
@Input()
device: EntityId;
@ViewChild('nameInput') nameInput: ElementRef;
@ViewChild(MatSort, {static: false}) sort: MatSort;
readonly ConnectorType = ConnectorType;
readonly allowBasicConfig = new Set<ConnectorType>([
ConnectorType.MQTT,
ConnectorType.OPCUA,
ConnectorType.MODBUS,
]);
readonly gatewayLogLevel = Object.values(GatewayLogLevel);
readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap;
readonly ConnectorConfigurationModes = ConfigurationModes;
readonly ReportStrategyDefaultValue = ReportStrategyDefaultValue;
pageLink: PageLink;
dataSource: MatTableDataSource<GatewayAttributeData>;
connectorForm: FormGroup;
activeConnectors: Array<string>;
mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
basicConfigInitSubject = new Subject<void>();
private gatewayVersion: string;
private isGatewayActive: boolean;
private inactiveConnectors: Array<string>;
private attributeDataSource: AttributeDatasource;
private inactiveConnectorsDataSource: AttributeDatasource;
private serverDataSource: AttributeDatasource;
private activeData: Array<any> = [];
private inactiveData: Array<any> = [];
private sharedAttributeData: Array<GatewayAttributeData> = [];
private basicConfigSub: Subscription;
private jsonConfigSub: Subscription;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onErrorsUpdated();
}),
onDataUpdateError: (_, e) => this.ctx.ngZone.run(() => {
this.onDataUpdateError(e);
})
}
};
private destroy$ = new Subject<void>();
private subscription: IWidgetSubscription;
private attributeUpdateSubject = new Subject<GatewayAttributeData>();
constructor(protected store: Store<AppState>,
private fb: FormBuilder,
private translate: TranslateService,
private attributeService: AttributeService,
private dialogService: DialogService,
private dialog: MatDialog,
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private utils: UtilsService,
private isLatestVersionConfig: LatestVersionConfigPipe,
private cd: ChangeDetectorRef) {
super(store);
this.initDataSources();
this.initConnectorForm();
this.observeAttributeChange();
}
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = this.getSortingDataAccessor();
this.ctx.$scope.gatewayConnectors = this;
this.loadConnectors();
this.loadGatewayState();
this.observeModeChange();
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
}
onSaveConnector(): void {
this.saveConnector(this.getUpdatedConnectorData(this.connectorForm.value), false);
}
private saveConnector(connector: GatewayConnector, isNew = true): void {
const scope = (isNew || this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE;
forkJoin(this.getEntityAttributeTasks(connector, scope)).pipe(take(1)).subscribe(_ => {
this.showToast(isNew
? this.translate.instant('gateway.connector-created')
: this.translate.instant('gateway.connector-updated')
);
this.initialConnector = connector;
this.updateData(true);
this.connectorForm.markAsPristine();
});
}
private getEntityAttributeTasks(value: GatewayConnector, scope: AttributeScope): Observable<any>[] {
const tasks = [];
const attributesToSave = [{ key: value.name, value }];
const attributesToDelete = [];
const shouldAddToConnectorsList = !this.activeConnectors.includes(value.name) && scope === AttributeScope.SHARED_SCOPE
|| !this.inactiveConnectors.includes(value.name) && scope === AttributeScope.SERVER_SCOPE;
const isNewConnector = this.initialConnector && this.initialConnector.name !== value.name;
if (isNewConnector) {
attributesToDelete.push({ key: this.initialConnector.name });
this.removeConnectorFromList(this.initialConnector.name, true);
this.removeConnectorFromList(this.initialConnector.name, false);
}
if (shouldAddToConnectorsList) {
if (scope === AttributeScope.SHARED_SCOPE) {
this.activeConnectors.push(value.name);
} else {
this.inactiveConnectors.push(value.name);
}
}
if (isNewConnector || shouldAddToConnectorsList) {
tasks.push(this.getSaveEntityAttributesTask(scope));
}
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave));
if (attributesToDelete.length) {
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, attributesToDelete));
}
return tasks;
}
private getSaveEntityAttributesTask(scope: AttributeScope): Observable<any> {
const key = scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors';
const value = scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors;
return this.attributeService.saveEntityAttributes(this.device, scope, [{ key, value }]);
}
private removeConnectorFromList(connectorName: string, isActive: boolean): void {
const list = isActive? this.activeConnectors : this.inactiveConnectors;
const index = list.indexOf(connectorName);
if (index !== -1) {
list.splice(index, 1);
}
}
private getUpdatedConnectorData(connector: GatewayConnector): GatewayConnector {
const value = {...connector };
value.configuration = `${camelCase(value.name)}.json`;
delete value.basicConfig;
if (value.type !== ConnectorType.GRPC) {
delete value.key;
}
if (value.type !== ConnectorType.CUSTOM) {
delete value.class;
}
if (value.type === ConnectorType.MODBUS && this.isLatestVersionConfig.transform(value.configVersion)) {
if (!value.reportStrategy) {
value.reportStrategy = {
type: ReportStrategyType.OnReportPeriod,
reportPeriod: ReportStrategyDefaultValue.Connector
};
delete value.sendDataOnlyOnChange;
}
}
if (this.gatewayVersion && !value.configVersion) {
value.configVersion = this.gatewayVersion;
}
value.ts = Date.now();
return value;
}
private updateData(reload: boolean = false): void {
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => {
this.activeData = data.data.filter(value => this.activeConnectors.includes(value.key));
this.combineData();
this.generateSubscription();
this.setClientData(data);
});
this.inactiveConnectorsDataSource.loadAttributes(this.device, AttributeScope.SHARED_SCOPE, this.pageLink, reload).subscribe(data => {
this.sharedAttributeData = data.data.filter(value => this.activeConnectors.includes(value.key));
this.combineData();
});
this.serverDataSource.loadAttributes(this.device, AttributeScope.SERVER_SCOPE, this.pageLink, reload).subscribe(data => {
this.inactiveData = data.data.filter(value => this.inactiveConnectors.includes(value.key));
this.combineData();
});
}
isConnectorSynced(attribute: GatewayAttributeData): boolean {
const connectorData = attribute.value;
if (!connectorData.ts || attribute.skipSync || !this.isGatewayActive) {
return false;
}
const clientIndex = this.activeData.findIndex(data => {
const sharedData = typeof data.value === 'string' ? JSON.parse(data.value) : data.value;
return sharedData.name === connectorData.name;
});
if (clientIndex === -1) {
return false;
}
const sharedIndex = this.sharedAttributeData.findIndex(data => {
const sharedData = data.value;
const hasSameName = sharedData.name === connectorData.name;
const hasEmptyConfig = isEqual(sharedData.configurationJson, {}) && hasSameName;
const hasSameConfig = this.hasSameConfig(sharedData.configurationJson, connectorData.configurationJson);
const isRecentlyCreated = sharedData.ts && sharedData.ts <= connectorData.ts;
return hasSameName && isRecentlyCreated && (hasSameConfig || hasEmptyConfig);
});
return sharedIndex !== -1;
}
private hasSameConfig(sharedDataConfigJson: ConnectorBaseInfo, connectorDataConfigJson: ConnectorBaseInfo): boolean {
const { name, id, enableRemoteLogging, logLevel, reportStrategy, configVersion, ...sharedDataConfig } = sharedDataConfigJson;
const {
name: connectorName,
id: connectorId,
enableRemoteLogging: connectorEnableRemoteLogging,
logLevel: connectorLogLevel,
reportStrategy: connectorReportStrategy,
configVersion: connectorConfigVersion,
...connectorConfig
} = connectorDataConfigJson;
return isEqual(sharedDataConfig, connectorConfig);
}
private combineData(): void {
const combinedData = [
...this.activeData,
...this.inactiveData,
...this.sharedAttributeData
];
const latestData = combinedData.reduce((acc, attribute) => {
const existingItemIndex = acc.findIndex(item => item.key === attribute.key);
if (existingItemIndex === -1) {
acc.push(attribute);
} else if (
attribute.lastUpdateTs > acc[existingItemIndex].lastUpdateTs &&
!this.isConnectorSynced(acc[existingItemIndex])
) {
acc[existingItemIndex] = { ...attribute, skipSync: true };
}
return acc;
}, []);
this.dataSource.data = latestData.map(attribute => ({
...attribute,
value: typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value
}));
}
private clearOutConnectorForm(): void {
this.initialConnector = null;
this.connectorForm.setValue({
mode: ConfigurationModes.BASIC,
name: '',
type: ConnectorType.MQTT,
sendDataOnlyOnChange: false,
enableRemoteLogging: false,
logLevel: GatewayLogLevel.INFO,
key: 'auto',
class: '',
configuration: '',
configurationJson: {},
basicConfig: {},
configVersion: '',
reportStrategy: [{ value: {}, disabled: true }],
}, {emitEvent: false});
this.connectorForm.markAsPristine();
}
selectConnector($event: Event, attribute: GatewayAttributeData): void {
if ($event) {
$event.stopPropagation();
}
const connector = attribute.value;
if (connector?.name !== this.initialConnector?.name) {
this.confirmConnectorChange().subscribe((result) => {
if (result) {
this.setFormValue(connector);
}
});
}
}
isSameConnector(attribute: GatewayAttributeData): boolean {
if (!this.initialConnector) {
return false;
}
const connector = attribute.value;
return this.initialConnector.name === connector.name;
}
showToast(message: string): void {
this.store.dispatch(new ActionNotificationShow(
{
message,
type: 'success',
duration: 1000,
verticalPosition: 'top',
horizontalPosition: 'left',
target: 'dashboardRoot',
forceDismiss: true
}));
}
returnType(attribute: GatewayAttributeData): string {
const value = attribute.value;
return this.GatewayConnectorTypesTranslatesMap.get(value.type);
}
deleteConnector(attribute: GatewayAttributeData, $event: Event): void {
$event?.stopPropagation();
const title = `Delete connector \"${attribute.key}\"?`;
const content = `All connector data will be deleted.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').pipe(
take(1),
switchMap((result) => {
if (!result) {
return;
}
const tasks: Array<Observable<any>> = [];
const scope = this.activeConnectors.includes(attribute.value?.name) ?
AttributeScope.SHARED_SCOPE :
AttributeScope.SERVER_SCOPE;
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute]));
this.removeConnectorFromList(attribute.key, true);
this.removeConnectorFromList(attribute.key, false);
tasks.push(this.getSaveEntityAttributesTask(scope));
return forkJoin(tasks);
})
).subscribe(() => {
if (this.initialConnector ? this.initialConnector.name === attribute.key : true) {
this.clearOutConnectorForm();
this.cd.detectChanges();
this.connectorForm.disable();
}
this.updateData(true);
});
}
connectorLogs(attribute: GatewayAttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
const params = deepClone(this.ctx.stateController.getStateParams());
params.connector_logs = attribute;
params.targetEntityParamName = 'connector_logs';
this.ctx.stateController.openState('connector_logs', params);
}
connectorRpc(attribute: GatewayAttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
}
const params = deepClone(this.ctx.stateController.getStateParams());
params.connector_rpc = attribute;
params.targetEntityParamName = 'connector_rpc';
this.ctx.stateController.openState('connector_rpc', params);
}
onEnableConnector(attribute: GatewayAttributeData): void {
attribute.value.ts = new Date().getTime();
this.updateActiveConnectorKeys(attribute.key);
this.attributeUpdateSubject.next(attribute);
}
getErrorsCount(attribute: GatewayAttributeData): string {
const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data
.find(data => data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`);
return (connector && this.activeConnectors.includes(connectorName)) ? (connector.data[0][1] || 0) : 'Inactive';
}
onAddConnector(event?: Event): void {
event?.stopPropagation();
this.confirmConnectorChange()
.pipe(
take(1),
filter(Boolean),
switchMap(() => this.openAddConnectorDialog()),
filter(Boolean),
)
.subscribe(connector => this.addConnector(connector));
}
private addConnector(connector: GatewayConnector): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
if (!connector.configurationJson) {
connector.configurationJson = {} as ConnectorBaseConfig;
}
if (this.gatewayVersion && !connector.configVersion) {
connector.configVersion = this.gatewayVersion;
}
connector.basicConfig = connector.configurationJson;
this.initialConnector = connector;
const previousType = this.connectorForm.get('type').value;
this.setInitialConnectorValues(connector);
this.saveConnector(this.getUpdatedConnectorData(connector));
if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
this.patchBasicConfigConnector(connector);
} else {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector(connector);
});
}
}
private setInitialConnectorValues(connector: GatewayConnector): void {
const {basicConfig, mode, ...initialConnector} = connector;
this.toggleReportStrategy(connector.type);
this.connectorForm.get('mode').setValue(this.allowBasicConfig.has(connector.type)
? connector.mode ?? ConfigurationModes.BASIC
: null, {emitEvent: false}
);
this.connectorForm.patchValue(initialConnector, {emitEvent: false});
}
private openAddConnectorDialog(): Observable<GatewayConnector> {
return this.ctx.ngZone.run(() =>
this.dialog.open<AddConnectorDialogComponent, AddConnectorConfigData>(AddConnectorDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataSourceData: this.dataSource.data,
gatewayVersion: this.gatewayVersion,
}
}).afterClosed()
);
}
uniqNameRequired(): ValidatorFn {
return (control: UntypedFormControl) => {
const newName = control.value?.trim().toLowerCase();
const isDuplicate = this.dataSource.data.some(connectorAttr => connectorAttr.value.name.toLowerCase() === newName);
const isSameAsInitial = this.initialConnector?.name.toLowerCase() === newName;
if (isDuplicate && !isSameAsInitial) {
return { duplicateName: { valid: false } };
}
return null;
};
}
private initDataSources(): void {
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC};
this.pageLink = new PageLink(1000, 0, null, sortOrder);
this.attributeDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.inactiveConnectorsDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.serverDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate);
this.dataSource = new MatTableDataSource<GatewayAttributeData>([]);
}
private initConnectorForm(): void {
this.connectorForm = this.fb.group({
mode: [ConfigurationModes.BASIC],
name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]],
type: ['', [Validators.required]],
enableRemoteLogging: [false],
logLevel: ['', [Validators.required]],
sendDataOnlyOnChange: [false],
key: ['auto'],
class: [''],
configuration: [''],
configurationJson: [{}, [Validators.required]],
basicConfig: [{}],
configVersion: [''],
reportStrategy: [{ value: {}, disabled: true }],
});
this.connectorForm.disable();
}
private getSortingDataAccessor(): (data: GatewayAttributeData, sortHeaderId: string) => string | number {
return (data: GatewayAttributeData, sortHeaderId: string) => {
switch (sortHeaderId) {
case 'syncStatus':
return this.isConnectorSynced(data) ? 1 : 0;
case 'enabled':
return this.activeConnectors.includes(data.key) ? 1 : 0;
case 'errors':
const errors = this.getErrorsCount(data);
if (typeof errors === 'string') {
return this.sort.direction.toUpperCase() === Direction.DESC ? -1 : Infinity;
}
return errors;
default:
return data[sortHeaderId] || data.value[sortHeaderId];
}
};
}
private loadConnectors(): void {
if (!this.device || this.device.id === NULL_UUID) {
return;
}
forkJoin([
this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors']),
this.attributeService.getEntityAttributes(this.device, AttributeScope.CLIENT_SCOPE, ['Version'])
]).pipe(takeUntil(this.destroy$)).subscribe(attributes => {
this.activeConnectors = this.parseConnectors(attributes[0]);
this.inactiveConnectors = this.parseConnectors(attributes[1]);
this.gatewayVersion = attributes[2][0]?.value;
this.updateData(true);
});
}
private loadGatewayState(): void {
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE)
.pipe(takeUntil(this.destroy$))
.subscribe((attributes: AttributeData[]) => {
const active = attributes.find(data => data.key === 'active').value;
const lastDisconnectedTime = attributes.find(data => data.key === 'lastDisconnectTime')?.value;
const lastConnectedTime = attributes.find(data => data.key === 'lastConnectTime')?.value;
this.isGatewayActive = this.getGatewayStatus(active, lastConnectedTime, lastDisconnectedTime);
});
}
private parseConnectors(attribute: GatewayAttributeData[]): string[] {
const connectors = attribute?.[0]?.value || [];
return isString(connectors) ? JSON.parse(connectors) : connectors;
}
private observeModeChange(): void {
this.connectorForm.get('mode').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.connectorForm.get('mode').markAsPristine();
});
}
private observeAttributeChange(): void {
this.attributeUpdateSubject.pipe(
debounceTime(300),
tap((attribute: GatewayAttributeData) => this.executeAttributeUpdates(attribute)),
takeUntil(this.destroy$),
).subscribe();
}
private updateActiveConnectorKeys(key: string): void {
const wasEnabled = this.activeConnectors.includes(key);
if (wasEnabled) {
const index = this.activeConnectors.indexOf(key);
if (index !== -1) {
this.activeConnectors.splice(index, 1);
}
this.inactiveConnectors.push(key);
} else {
const index = this.inactiveConnectors.indexOf(key);
if (index !== -1) {
this.inactiveConnectors.splice(index, 1);
}
this.activeConnectors.push(key);
}
}
private executeAttributeUpdates(attribute: GatewayAttributeData): void {
forkJoin(this.getAttributeExecutionTasks(attribute))
.pipe(
take(1),
tap(() => this.updateData(true)),
takeUntil(this.destroy$),
)
.subscribe();
}
private getAttributeExecutionTasks(attribute: GatewayAttributeData): Observable<any>[] {
const isActive = this.activeConnectors.includes(attribute.key);
const scopeOld = isActive ? AttributeScope.SERVER_SCOPE : AttributeScope.SHARED_SCOPE;
const scopeNew = isActive ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
return [
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, [{
key: 'active_connectors',
value: this.activeConnectors
}]),
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, [{
key: 'inactive_connectors',
value: this.inactiveConnectors
}]),
this.attributeService.deleteEntityAttributes(this.device, scopeOld, [attribute]),
this.attributeService.saveEntityAttributes(this.device, scopeNew, [attribute])
];
}
private onDataUpdateError(e: any): void {
const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name;
if (exceptionData.message) {
errorText += ': ' + exceptionData.message;
}
console.error(errorText);
}
private onErrorsUpdated(): void {
this.cd.detectChanges();
}
private onDataUpdated(): void {
const dataSources = this.ctx.defaultSubscription.data;
const active = dataSources.find(data => data.dataKey.name === 'active').data[0][1];
const lastDisconnectedTime = dataSources.find(data => data.dataKey.name === 'lastDisconnectTime').data[0][1];
const lastConnectedTime = dataSources.find(data => data.dataKey.name === 'lastConnectTime').data[0][1];
this.isGatewayActive = this.getGatewayStatus(active, lastConnectedTime, lastDisconnectedTime);
this.cd.detectChanges();
}
private getGatewayStatus(active: boolean, lastConnectedTime: number, lastDisconnectedTime: number): boolean {
if (!active) {
return false;
}
return !lastDisconnectedTime || lastConnectedTime > lastDisconnectedTime;
}
private generateSubscription(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
if (this.device) {
const subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: this.device.id,
entityName: 'Gateway',
timeseries: []
}];
this.dataSource.data.forEach(value => {
subscriptionInfo[0].timeseries.push({name: `${value.key}_ERRORS_COUNT`, label: `${value.key}_ERRORS_COUNT`});
});
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.latest, subscriptionInfo, this.subscriptionOptions,
false, true).subscribe(subscription => {
this.subscription = subscription;
});
}
}
private createBasicConfigWatcher(): void {
if (this.basicConfigSub) {
this.basicConfigSub.unsubscribe();
}
this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe(
filter(() => !!this.initialConnector),
takeUntil(this.destroy$)
).subscribe((config) => {
const configJson = this.connectorForm.get('configurationJson');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (!isEqual(config, configJson?.value) && this.allowBasicConfig.has(type) && mode === ConfigurationModes.BASIC) {
const newConfig = {...configJson.value, ...config};
this.connectorForm.get('configurationJson').patchValue(newConfig, {emitEvent: false});
}
});
}
private createJsonConfigWatcher(): void {
if (this.jsonConfigSub) {
this.jsonConfigSub.unsubscribe();
}
this.jsonConfigSub = this.connectorForm.get('configurationJson').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((config) => {
const basicConfig = this.connectorForm.get('basicConfig');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConfigurationModes.ADVANCED) {
this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false});
}
});
}
private confirmConnectorChange(): Observable<boolean> {
if (this.initialConnector && this.connectorForm.dirty) {
return this.dialogService.confirm(
this.translate.instant('gateway.change-connector-title'),
this.translate.instant('gateway.change-connector-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
);
}
return of(true);
}
private setFormValue(connector: GatewayConnector): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
const connectorState = GatewayConnectorVersionMappingUtil.getConfig({
configuration: '',
key: 'auto',
configurationJson: {} as ConnectorBaseConfig,
...connector,
}, this.gatewayVersion);
if (this.gatewayVersion && !connectorState.configVersion) {
connectorState.configVersion = this.gatewayVersion;
}
connectorState.basicConfig = connectorState.configurationJson;
this.initialConnector = connectorState;
this.updateConnector(connectorState);
}
private updateConnector(connector: GatewayConnector): void {
this.jsonConfigSub?.unsubscribe();
switch (connector.type) {
case ConnectorType.MQTT:
case ConnectorType.OPCUA:
case ConnectorType.MODBUS:
this.updateBasicConfigConnector(connector);
break;
default:
this.connectorForm.patchValue({...connector, mode: null});
this.connectorForm.markAsPristine();
this.createJsonConfigWatcher();
}
}
private updateBasicConfigConnector(connector: GatewayConnector): void {
this.basicConfigSub?.unsubscribe();
const previousType = this.connectorForm.get('type').value;
this.setInitialConnectorValues(connector);
if (previousType === connector.type || !this.allowBasicConfig.has(connector.type)) {
this.patchBasicConfigConnector(connector);
} else {
this.basicConfigInitSubject.asObservable().pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector(connector);
});
}
}
private patchBasicConfigConnector(connector: GatewayConnector): void {
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
this.createJsonConfigWatcher();
}
private toggleReportStrategy(type: ConnectorType): void {
const reportStrategyControl = this.connectorForm.get('reportStrategy');
if (type === ConnectorType.MODBUS) {
reportStrategyControl.enable({emitEvent: false});
} else {
reportStrategyControl.disable({emitEvent: false});
}
}
private setClientData(data: PageData<GatewayAttributeData>): void {
if (this.initialConnector) {
const clientConnectorData = data.data.find(attr => attr.key === this.initialConnector.name);
if (clientConnectorData) {
clientConnectorData.value = typeof clientConnectorData.value === 'string' ?
JSON.parse(clientConnectorData.value) : clientConnectorData.value;
if (this.isConnectorSynced(clientConnectorData) && clientConnectorData.value.configurationJson) {
this.setFormValue({...clientConnectorData.value, mode: this.connectorForm.get('mode').value ?? clientConnectorData.value.mode});
}
}
}
}
}

286
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html

@ -1,286 +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.
-->
<form #formContainer class="gateway-form"
[formGroup]="gatewayConfigurationGroup"
tb-toast toastTarget="{{ toastTargetId }}"
(ngSubmit)="save()">
<mat-accordion multi="true" class="mat-body-2">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.thingsboard' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<tb-entity-gateway-select
formControlName="gateway"
[deviceName]="deviceNameForm"
[isStateForm]="isStateForm"
[newGatewayType]="gatewayType"
(gatewayNameExist)="gatewayExist()"
required
>
</tb-entity-gateway-select>
<mat-form-field class="flex">
<mat-label>{{'gateway.security-type' | translate }}</mat-label>
<mat-select formControlName="securityType" >
<mat-option *ngFor="let securityType of securityTypes | keyvalue" [value]="securityType.key">
{{ securityType.value.toString() | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<div class="flex" [class]="{
'gap-1.25': layoutGap,
'flex-row': alignment,
'flex-col': !alignment
}">
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.thingsboard-host' | translate }}</mat-label>
<input matInput type="text" formControlName="host">
<mat-error *ngIf="gatewayConfigurationGroup.get('host').hasError('required')" translate>
gateway.thingsboard-host-required
</mat-error>
</mat-form-field>
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.thingsboard-port' | translate }}</mat-label>
<input matInput type="number" formControlName="port">
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('required')" translate>
gateway.thingsboard-port-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('min')" translate>
gateway.thingsboard-port-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('max')" translate>
gateway.thingsboard-port-max
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('pattern')" translate>
gateway.thingsboard-port-pattern
</mat-error>
</mat-form-field>
</div>
<div *ngIf="gatewayConfigurationGroup.get('securityType').value == 'tls'" class="flex flex-col">
<mat-form-field>
<mat-label>{{ 'gateway.tls-path-ca-certificate' | translate }}</mat-label>
<input matInput type="text" formControlName="caCertPath">
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.tls-path-private-key' | translate }}</mat-label>
<input matInput type="text" formControlName="privateKeyPath">
</mat-form-field>
<mat-form-field>
<mat-label>{{ 'gateway.tls-path-client-certificate' | translate }}</mat-label>
<input matInput type="text" formControlName="certPath">
</mat-form-field>
</div>
<mat-checkbox formControlName="remoteConfiguration">{{ 'gateway.remote' | translate }}</mat-checkbox>
<div class="flex" [class]="{
'gap-1.25': layoutGap,
'flex-row': alignment,
'flex-col': !alignment
}">
<mat-form-field class="flex-1">
<mat-label>{{'gateway.remote-logging-level' | translate }}</mat-label>
<mat-select formControlName="remoteLoggingLevel">
<mat-option *ngFor="let logLevel of gatewayLogLevels" [value]="logLevel">
{{ logLevel }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.path-logs' | translate }}</mat-label>
<input matInput type="text" formControlName="remoteLoggingPathToLogs">
<mat-error *ngIf="gatewayConfigurationGroup.get('remoteLoggingPathToLogs').hasError('required')" translate>
gateway.path-logs-required
</mat-error>
</mat-form-field>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.storage' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="flex flex-col">
<mat-form-field>
<mat-label>{{'gateway.storage-type' | translate }}</mat-label>
<mat-select formControlName="storageType">
<mat-option *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key">
{{ storageType.value.toString() | translate}}
</mat-option>
</mat-select>
</mat-form-field>
<div class="flex" [class]="{
'gap-1.25': layoutGap,
'flex-row': alignment,
'flex-col': !alignment
}">
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.storage-pack-size' | translate }}</mat-label>
<input matInput type="number" formControlName="readRecordsCount">
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('required')" translate>
gateway.storage-pack-size-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('min')" translate>
gateway.storage-pack-size-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('pattern')" translate>
gateway.storage-pack-size-pattern
</mat-error>
</mat-form-field>
<mat-form-field class="flex-1">
<mat-label >
{{ (gatewayConfigurationGroup.get('storageType').value !== 'file' ? 'gateway.storage-max-records' : 'gateway.storage-max-file-records') | translate}}
</mat-label>
<input matInput type="number" formControlName="maxRecordsCount">
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('required')" translate>
gateway.storage-max-records-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('min')" translate>
gateway.storage-max-records-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('pattern')" translate>
gateway.storage-max-records-pattern
</mat-error>
</mat-form-field>
</div>
<div class="flex" [class]="{
'gap-1.25': layoutGap,
'flex-row': alignment,
'flex-col': !alignment
}" *ngIf="gatewayConfigurationGroup.get('storageType').value == 'file'">
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.storage-max-files' | translate }}</mat-label>
<input matInput type="number" formControlName="maxFilesCount">
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('required')" translate>
gateway.storage-max-files-required
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('min')" translate>
gateway.storage-max-files-min
</mat-error>
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('pattern')" translate>
gateway.storage-max-files-pattern
</mat-error>
</mat-form-field>
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.storage-path' | translate }}</mat-label>
<input matInput type="text" formControlName="dataFolderPath">
<mat-error *ngIf="gatewayConfigurationGroup.get('dataFolderPath').hasError('required')" translate>
gateway.storage-path-required
</mat-error>
</mat-form-field>
</div>
</div>
</mat-expansion-panel>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.connectors-config' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="gateway-config flex flex-col">
<section formArrayName="connectors" *ngFor="let connector of connectors.controls; let i = index;">
<div [formGroupName]="i" class="flex flex-row items-stretch justify-between gap-2">
<div class="flex flex-col justify-center">
<mat-slide-toggle formControlName="enabled"></mat-slide-toggle>
</div>
<div class="flex flex-full" [class]="{
'gap-1.25': layoutGap,
'flex-row': alignment,
'flex-col': !alignment
}">
<mat-form-field class="flex-1">
<mat-label>{{'gateway.connector-type' | translate }}</mat-label>
<mat-select formControlName="configType" (selectionChange)="changeConnectorType(connector)">
<mat-option *ngFor="let connectorType of connectorTypes" [value]="connectorType">
{{ connectorType }}
</mat-option>
</mat-select>
<mat-error *ngIf="connector.get('configType').hasError('required')" translate>
gateway.connector-type-required
</mat-error>
</mat-form-field>
<mat-form-field class="flex-1">
<mat-label>{{ 'gateway.connector-name' | translate }}</mat-label>
<input matInput type="text" formControlName="name" (blur)="changeConnectorName(connector, i)">
<mat-error *ngIf="connector.get('name').hasError('required')" translate>
gateway.connector-name-required
</mat-error>
</mat-form-field>
</div>
<div class="action-buttons flex" [class]="{
'gap-1.25': layoutGap,
'flex-row justify-end item-center': alignment,
'flex-col justify-evenly item-center': !alignment
}">
<button [disabled]="isReadOnlyForm" mat-icon-button (click)="openConfigDialog($event, i, connector.get('config').value, connector.get('name').value)"
matTooltip="{{ 'gateway.update-config' | translate }}"
matTooltipPosition="above"
[class.mat-warn]="connector.get('config').invalid">
<mat-icon>more_horiz</mat-icon>
</button>
<button [disabled]="isReadOnlyForm"
mat-icon-button (click)="removeConnector(i)"
matTooltip="{{ 'gateway.delete' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</section>
<span [class.!hidden]="connectors.length" class="no-data-found items-center justify-center">{{'gateway.no-connectors' | translate}}</span>
<div>
<button [class.!hidden]="isReadOnlyForm" mat-raised-button type="button" (click)="addNewConnector()"
matTooltip="{{ 'gateway.connector-add' | translate }}"
matTooltipPosition="above">
{{ 'action.add' | translate }}
</button>
</div>
</div >
</mat-expansion-panel>
</mat-accordion>
<section [class.!hidden]="isReadOnlyForm" class="form-action-buttons flex flex-row items-center justify-end">
<button mat-raised-button color="primary" type="button"
(click)="exportConfig()"
*ngIf="!gatewayConfigurationGroup.get('remoteConfiguration').value"
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid"
matTooltip="{{'gateway.download-tip' | translate }}">
{{'action.download' | translate }}
</button>
<button mat-raised-button color="primary" type="submit"
*ngIf="gatewayConfigurationGroup.get('remoteConfiguration').value"
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid"
matTooltip="{{'gateway.save-tip' | translate }}">
{{'action.save' | translate }}
</button>
</section>
</form>

36
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.scss

@ -1,36 +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.
*/
:host{
.gateway-form {
height: 100%;
padding: 5px;
background-color: transparent;
overflow-y: auto;
overflow-x: hidden;
.form-action-buttons{
padding-top: 8px;
}
.gateway-config {
.no-data-found {
position: relative;
display: flex;
height: 40px;
}
}
}
}

426
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.ts

@ -1,426 +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.
///
import { Component, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, NgForm, Validators } from '@angular/forms';
import { WidgetContext } from '@home/models/widget-component.models';
import { UtilsService } from '@core/services/utils.service';
import {
CONFIGURATION_ATTRIBUTE,
CONFIGURATION_DRAFT_ATTRIBUTE,
ConnectorType,
createFormConfig,
CURRENT_CONFIGURATION_ATTRIBUTE,
DEFAULT_CONNECTOR,
gatewayConfigJSON,
GatewayFormConnectorModel,
GatewayFormModels,
GatewayLogLevel,
generateConnectorConfigFiles,
generateLogConfigFile,
generateYAMLConfigFile,
getDraftConnectorsJSON,
getEntityId,
REMOTE_LOGGING_LEVEL_ATTRIBUTE,
SecurityType,
SecurityTypeTranslationMap,
StorageType,
StorageTypeTranslationMap,
ValidateJSON,
WidgetSetting
} from './gateway-form.models';
import { WINDOW } from '@core/services/window.service';
import { MatDialog } from '@angular/material/dialog';
import {
JsonObjectEditDialogComponent,
JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { DeviceService } from '@core/http/device.service';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { forkJoin, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ImportExportService } from '@shared/import-export/import-export.service';
// @dynamic
@Component({
selector: 'tb-gateway-form',
templateUrl: './gateway-form.component.html',
styleUrls: ['./gateway-form.component.scss']
})
export class GatewayFormComponent extends PageComponent implements OnInit, OnDestroy {
constructor(
protected store: Store<AppState>,
private elementRef: ElementRef,
private utils: UtilsService,
private ngZone: NgZone,
private fb: UntypedFormBuilder,
@Inject(WINDOW) private window: Window,
private dialog: MatDialog,
private translate: TranslateService,
private deviceService: DeviceService,
private attributeService: AttributeService,
private importExport: ImportExportService
) {
super(store);
}
get connectors(): UntypedFormArray {
return this.gatewayConfigurationGroup.get('connectors') as UntypedFormArray;
}
@ViewChild('formContainer', {static: true}) formContainerRef: ElementRef<HTMLElement>;
@ViewChild('gatewayConfigurationForm', {static: true}) multipleInputForm: NgForm;
private successfulSaved: string;
private gatewayNameExists: string;
private archiveFileName: string;
private formResize$: ResizeObserver;
private subscribeStorageType$: any;
private subscribeGateway$: any;
alignment = true;
layoutGap = true;
gatewayType: string;
gatewayConfigurationGroup: UntypedFormGroup;
securityTypes = SecurityTypeTranslationMap;
gatewayLogLevels = Object.keys(GatewayLogLevel).map(itm => GatewayLogLevel[itm]);
connectorTypes = Object.keys(ConnectorType);
storageTypes = StorageTypeTranslationMap;
toastTargetId = 'gateway-configuration-widget' + this.utils.guid();
@Input()
ctx: WidgetContext;
@Input()
isStateForm: boolean;
isReadOnlyForm = false;
deviceNameForm: string;
ngOnInit(): void {
this.initWidgetSettings(this.ctx.settings);
if (this.ctx.datasources && this.ctx.datasources.length) {
this.deviceNameForm = this.ctx.datasources[0].name;
}
this.buildForm();
this.ctx.updateWidgetParams();
this.formResize$ = new ResizeObserver(() => {
this.resize();
});
this.formResize$.observe(this.formContainerRef.nativeElement);
}
ngOnDestroy(): void {
if (this.formResize$) {
this.formResize$.disconnect();
}
this.subscribeGateway$.unsubscribe();
this.subscribeStorageType$.unsubscribe();
}
private initWidgetSettings(settings: WidgetSetting): void {
let widgetTitle;
if (settings.gatewayTitle && settings.gatewayTitle.length) {
widgetTitle = this.utils.customTranslation(settings.gatewayTitle, settings.gatewayTitle);
} else {
widgetTitle = this.translate.instant('gateway.gateway');
}
this.ctx.widgetTitle = widgetTitle;
this.isReadOnlyForm = (settings.readOnly) ? settings.readOnly : false;
this.archiveFileName = settings.archiveFileName?.length ? settings.archiveFileName : 'gatewayConfiguration';
this.gatewayType = settings.gatewayType?.length ? settings.gatewayType : 'Gateway';
this.gatewayNameExists = this.utils.customTranslation(settings.gatewayNameExists, settings.gatewayNameExists) || this.translate.instant('gateway.gateway-exists');
this.successfulSaved = this.utils.customTranslation(settings.successfulSave, settings.successfulSave) || this.translate.instant('gateway.gateway-saved');
this.updateWidgetDisplaying();
}
private resize(): void {
this.ngZone.run(() => {
this.updateWidgetDisplaying();
this.ctx.detectChanges();
});
}
private updateWidgetDisplaying(): void {
if(this.ctx.$container && this.ctx.$container[0].offsetWidth <= 425){
this.layoutGap = false;
this.alignment = false;
} else {
this.layoutGap = true;
this.alignment = true;
}
}
private saveAttribute(attributeName: string, attributeValue: string, attributeScope: AttributeScope): Observable<any> {
const gatewayId = this.gatewayConfigurationGroup.get('gateway').value;
const attributes: AttributeData = {
key: attributeName,
value: attributeValue
};
return this.attributeService.saveEntityAttributes(getEntityId(gatewayId), attributeScope, [attributes]);
}
private createConnector(setting: GatewayFormConnectorModel = DEFAULT_CONNECTOR): void {
this.connectors.push(this.fb.group({
enabled: [setting.enabled],
configType: [setting.configType, [Validators.required]],
name: [setting.name, [Validators.required]],
config: [setting.config, [Validators.nullValidator, ValidateJSON]]
}));
}
private getFormField(name: string): AbstractControl {
return this.gatewayConfigurationGroup.get(name);
}
private buildForm(): void {
this.gatewayConfigurationGroup = this.fb.group({
gateway: [null, []],
accessToken: [null, [Validators.required]],
securityType: [SecurityType.accessToken],
host: [this.window.location.hostname, [Validators.required]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteConfiguration: [true],
caCertPath: ['/etc/thingsboard-gateway/ca.pem'],
privateKeyPath: ['/etc/thingsboard-gateway/privateKey.pem'],
certPath: ['/etc/thingsboard-gateway/certificate.pem'],
remoteLoggingLevel: [GatewayLogLevel.debug],
remoteLoggingPathToLogs: ['./logs/', [Validators.required]],
storageType: [StorageType.memory],
readRecordsCount: [100, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
maxRecordsCount: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
maxFilesCount: [5, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
dataFolderPath: ['./data/', [Validators.required]],
connectors: this.fb.array([])
});
if (this.isReadOnlyForm) {
this.gatewayConfigurationGroup.disable({emitEvent: false});
}
this.subscribeStorageType$ = this.getFormField('storageType').valueChanges.subscribe((value: StorageType) => {
if (value === StorageType.memory) {
this.getFormField('maxFilesCount').disable();
this.getFormField('dataFolderPath').disable();
} else {
this.getFormField('maxFilesCount').enable();
this.getFormField('dataFolderPath').enable();
}
});
this.subscribeGateway$ = this.getFormField('gateway').valueChanges.subscribe((gatewayId: string) => {
if (gatewayId !== null) {
forkJoin([
this.deviceService.getDeviceCredentials(gatewayId).pipe(tap(deviceCredential => {
this.getFormField('accessToken').patchValue(deviceCredential.credentialsId);
})),
...this.getAttributes(gatewayId)]).subscribe(() => {
this.gatewayConfigurationGroup.markAsPristine();
this.ctx.detectChanges();
});
} else {
this.getFormField('accessToken').patchValue('');
}
})
}
gatewayExist(): void {
this.ctx.showErrorToast(this.gatewayNameExists, 'top', 'left', this.toastTargetId);
}
exportConfig(): void {
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value;
const filesZip: any = {};
filesZip['tb_gateway.yaml'] = generateYAMLConfigFile(gatewayConfiguration);
generateConnectorConfigFiles(filesZip, gatewayConfiguration.connectors);
generateLogConfigFile(filesZip, gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs);
this.importExport.exportJSZip(filesZip, this.archiveFileName);
this.saveAttribute(
REMOTE_LOGGING_LEVEL_ATTRIBUTE,
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(),
AttributeScope.SHARED_SCOPE);
}
addNewConnector(): void {
this.createConnector();
}
removeConnector(index: number): void {
if (index > -1) {
this.connectors.removeAt(index);
this.connectors.markAsDirty();
}
}
openConfigDialog($event: Event, index: number, config: object, type: string): void {
if ($event) {
$event.stopPropagation();
$event.preventDefault();
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: config,
required: true,
title: this.translate.instant('gateway.title-connectors-json', {typeName: type})
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.connectors.at(index).get('config').patchValue(res);
this.ctx.detectChanges();
}
}
);
}
private createConnectorName(connectors: GatewayFormConnectorModel[], name: string, index: number = 0): string {
const newKeyName = index ? name + index : name;
const indexRes = connectors.findIndex((element) => element.name === newKeyName);
return indexRes === -1 ? newKeyName : this.createConnectorName(connectors, name, ++index);
}
private validateConnectorName(connectors: GatewayFormConnectorModel[], name: string, connectorIndex: number, index = 0): string {
for (let i = 0; i < connectors.length; i++) {
const nameEq = (index === 0) ? name : name + index;
if (i !== connectorIndex && connectors[i].name === nameEq) {
this.validateConnectorName(connectors, name, connectorIndex, ++index);
}
}
return (index === 0) ? name : name + index;
}
changeConnectorType(connector: AbstractControl): void {
if (!connector.get('name').value) {
const typeConnector = connector.get('configType').value;
const connectors = this.gatewayConfigurationGroup.value.connectors;
connector.get('name').patchValue(this.createConnectorName(connectors, ConnectorType[typeConnector]));
}
}
changeConnectorName(connector: AbstractControl, index: number): void {
const connectors = this.gatewayConfigurationGroup.value.connectors;
connector.get('name').patchValue(this.validateConnectorName(connectors, connector.get('name').value, index));
}
save(): void {
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value;
forkJoin([
this.saveAttribute(
CONFIGURATION_ATTRIBUTE,
window.btoa(JSON.stringify(gatewayConfigJSON(gatewayConfiguration))),
AttributeScope.SHARED_SCOPE),
this.saveAttribute(
CONFIGURATION_DRAFT_ATTRIBUTE,
window.btoa(JSON.stringify(getDraftConnectorsJSON(gatewayConfiguration.connectors))),
AttributeScope.SERVER_SCOPE),
this.saveAttribute(
REMOTE_LOGGING_LEVEL_ATTRIBUTE,
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(),
AttributeScope.SHARED_SCOPE)
]).subscribe(() =>{
this.ctx.showSuccessToast(this.successfulSaved,
2000, 'top', 'left', this.toastTargetId);
this.gatewayConfigurationGroup.markAsPristine();
})
}
private getAttributes(gatewayId: string): Array<Observable<Array<AttributeData>>> {
const tasks = [];
tasks.push(forkJoin([this.getAttribute(CURRENT_CONFIGURATION_ATTRIBUTE, AttributeScope.CLIENT_SCOPE, gatewayId),
this.getAttribute(CONFIGURATION_DRAFT_ATTRIBUTE, AttributeScope.SERVER_SCOPE, gatewayId)]).pipe(
tap(([currentConfig, draftConfig]) => {
this.setFormGatewaySettings(currentConfig);
this.setFormConnectorsDraft(draftConfig);
if (this.isReadOnlyForm) {
this.gatewayConfigurationGroup.disable({emitEvent: false});
}
})
)
);
tasks.push(this.getAttribute(REMOTE_LOGGING_LEVEL_ATTRIBUTE, AttributeScope.SHARED_SCOPE, gatewayId).pipe(
tap(logsLevel => this.processLoggingLevel(logsLevel))
));
return tasks;
}
private getAttribute(attributeName: string, attributeScope: AttributeScope, gatewayId: string): Observable<Array<AttributeData>> {
return this.attributeService.getEntityAttributes(getEntityId(gatewayId), attributeScope, [attributeName]);
}
private setFormGatewaySettings(response: Array<AttributeData>): void {
this.connectors.clear();
if (response.length > 0) {
const attribute = JSON.parse(window.atob(response[0].value));
for (const attributeKey of Object.keys(attribute)) {
const keyValue = attribute[attributeKey];
if (attributeKey === 'thingsboard') {
if (keyValue !== null && Object.keys(keyValue).length > 0) {
this.gatewayConfigurationGroup.patchValue(createFormConfig(keyValue));
}
} else {
for (const connector of Object.keys(keyValue)) {
let name = 'No name';
if (Object.prototype.hasOwnProperty.call(keyValue[connector], 'name')) {
name = keyValue[connector].name;
}
const newConnector: GatewayFormConnectorModel = {
enabled: true,
configType: (attributeKey as ConnectorType),
config: keyValue[connector].config,
name
};
this.createConnector(newConnector);
}
}
}
}
}
private setFormConnectorsDraft(response: Array<AttributeData>): void {
if (response.length > 0) {
const attribute = JSON.parse(window.atob(response[0].value));
for (const connectorName of Object.keys(attribute)) {
const newConnector: GatewayFormConnectorModel = {
enabled: false,
configType: (attribute[connectorName].connector as ConnectorType),
config: attribute[connectorName].config,
name: connectorName
};
this.createConnector(newConnector);
}
}
}
private processLoggingLevel(value: Array<AttributeData>): void {
let logsLevel = GatewayLogLevel.debug;
if (value.length > 0 && GatewayLogLevel[value[0].value.toLowerCase()]) {
logsLevel = GatewayLogLevel[value[0].value.toLowerCase()];
}
this.getFormField('remoteLoggingLevel').patchValue(logsLevel);
}
}

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

@ -1,380 +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.
///
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export enum SecurityType {
tls = 'tls',
accessToken = 'accessToken'
}
export interface WidgetSetting {
widgetTitle: string;
gatewayTitle?: string;
readOnly?: boolean;
archiveFileName: string;
gatewayType: string;
successfulSave: string;
gatewayNameExists: string;
}
export const CURRENT_CONFIGURATION_ATTRIBUTE = 'current_configuration';
export const CONFIGURATION_DRAFT_ATTRIBUTE = 'configuration_drafts';
export const CONFIGURATION_ATTRIBUTE = 'configuration';
export const REMOTE_LOGGING_LEVEL_ATTRIBUTE = 'RemoteLoggingLevel';
export const SecurityTypeTranslationMap = new Map<SecurityType, string>(
[
[SecurityType.tls, 'gateway.security-types.tls'],
[SecurityType.accessToken, 'gateway.security-types.access-token']
]
);
export enum GatewayLogLevel {
none = 'NONE',
critical = 'CRITICAL',
error = 'ERROR',
warning = 'WARNING',
info = 'INFO',
debug = 'DEBUG'
}
export enum StorageType {
memory = 'memory',
file = 'file'
}
export const StorageTypeTranslationMap = new Map<StorageType, string>(
[
[StorageType.memory, 'gateway.storage-types.memory-storage'],
[StorageType.file, 'gateway.storage-types.file-storage']
]
);
export enum ConnectorType {
mqtt= 'MQTT',
modbus = 'Modbus',
opcua = 'OPC-UA',
ble = 'BLE',
request = 'Request',
can = 'CAN',
bacnet = 'BACnet',
custom = 'Custom'
}
export interface GatewayFormModels {
gateway?: string;
accessToken?: string;
securityType?: SecurityType;
host?: string;
port?: number;
remoteConfiguration?: boolean;
caCertPath?: string;
privateKeyPath?: string;
certPath?: string;
remoteLoggingLevel?: GatewayLogLevel;
remoteLoggingPathToLogs?:string;
storageType?: StorageType;
readRecordsCount?: number;
maxRecordsCount?: number;
maxFilesCount?: number;
dataFolderPath?: number;
connectors?: Array<GatewayFormConnectorModel>;
}
export interface GatewayFormConnectorModel {
config: object;
name: string;
configType: ConnectorType;
enabled: boolean;
}
export const DEFAULT_CONNECTOR: GatewayFormConnectorModel = {
config: {},
name: '',
configType: null,
enabled: false
};
type Connector = {
[key in ConnectorType]?: Array<ConnectorConfig>;
}
interface GatewaySetting extends Connector{
thingsboard: GatewayMainSetting;
}
interface ConnectorConfig {
name: string;
config: object;
}
interface GatewayMainSetting {
thingsboard: GatewayMainThingsboardSetting;
connectors: Array<GatewayMainConnector>,
logs: string,
storage: GatewayStorage
}
interface GatewayMainThingsboardSetting {
host: string,
remoteConfiguration: boolean,
port: number,
security: GatewaySecurity
}
type GatewaySecurity = SecurityToken | SecurityCertificate;
interface SecurityToken {
accessToken: string;
}
interface SecurityCertificate {
caCert: string;
privateKey: string;
cert: string;
}
type GatewayStorage = GatewayStorageMemory | GatewayStorageFile;
interface GatewayStorageMemory {
type: string;
max_records_count: number;
read_records_count: number;
}
interface GatewayStorageFile {
type: string;
data_folder_path: number;
max_file_count: number;
max_read_records_count: number;
max_records_per_file: number;
}
interface GatewayMainConnector {
configuration: string;
name: string;
type: ConnectorType;
}
export function ValidateJSON(control: AbstractControl): ValidationErrors | null {
if (JSON.stringify(control.value) === JSON.stringify({})) {
return { validJSON: true };
}
return null;
}
const TEMPLATE_LOGS_CONFIG = '[loggers]}}keys=root, service, connector, converter, tb_connection, storage, extension}}[handlers]}}keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}[formatters]}}keys=LogFormatter}}[logger_root]}}level=ERROR}}handlers=consoleHandler}}[logger_connector]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=connector}}[logger_storage]}}level={ERROR}}}handlers=storageHandler}}formatter=LogFormatter}}qualname=storage}}[logger_tb_connection]}}level={ERROR}}}handlers=tb_connectionHandler}}formatter=LogFormatter}}qualname=tb_connection}}[logger_service]}}level={ERROR}}}handlers=serviceHandler}}formatter=LogFormatter}}qualname=service}}[logger_converter]}}level={ERROR}}}handlers=converterHandler}}formatter=LogFormatter}}qualname=converter}}[logger_extension]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=extension}}[handler_consoleHandler]}}class=StreamHandler}}level={ERROR}}}formatter=LogFormatter}}args=(sys.stdout,)}}[handler_connectorHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}connector.log", "d", 1, 7,)}}[handler_storageHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}storage.log", "d", 1, 7,)}}[handler_serviceHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}service.log", "d", 1, 7,)}}[handler_converterHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}converter.log", "d", 1, 3,)}}[handler_extensionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}extension.log", "d", 1, 3,)}}[handler_tb_connectionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}tb_connection.log", "d", 1, 3,)}}[formatter_LogFormatter]}}format="%(asctime)s - %(levelname)s - [%(filename)s] - %(module)s - %(lineno)d - %(message)s" }}datefmt="%Y-%m-%d %H:%M:%S"';
export function generateYAMLConfigFile(gatewaySetting: GatewayFormModels): string {
let config;
config = 'thingsboard:\n';
config += ' host: ' + gatewaySetting.host + '\n';
config += ' remoteConfiguration: ' + gatewaySetting.remoteConfiguration + '\n';
config += ' port: ' + gatewaySetting.port + '\n';
config += ' security:\n';
if (gatewaySetting.securityType === SecurityType.accessToken) {
config += ' access-token: ' + gatewaySetting.accessToken + '\n';
} else {
config += ' ca_cert: ' + gatewaySetting.caCertPath + '\n';
config += ' privateKey: ' + gatewaySetting.privateKeyPath + '\n';
config += ' cert: ' + gatewaySetting.certPath + '\n';
}
config += 'storage:\n';
if (gatewaySetting.storageType === StorageType.memory) {
config += ' type: memory\n';
config += ' read_records_count: ' + gatewaySetting.readRecordsCount + '\n';
config += ' max_records_count: ' + gatewaySetting.maxRecordsCount + '\n';
} else {
config += ' type: file\n';
config += ' data_folder_path: ' + gatewaySetting.dataFolderPath + '\n';
config += ' max_file_count: ' + gatewaySetting.maxFilesCount + '\n';
config += ' max_read_records_count: ' + gatewaySetting.readRecordsCount + '\n';
config += ' max_records_per_file: ' + gatewaySetting.maxRecordsCount + '\n';
}
config += 'connectors:\n';
for(const connector of gatewaySetting.connectors){
if (connector.enabled) {
config += ' -\n';
config += ' name: ' + connector.name + '\n';
config += ' type: ' + connector.configType + '\n';
config += ' configuration: ' + generateFileName(connector.name) + '\n';
}
}
return config;
}
export function generateConnectorConfigFiles(fileZipAdd: object, connectors: Array<GatewayFormConnectorModel>): void {
for(const connector of connectors) {
if (connector.enabled) {
fileZipAdd[generateFileName(connector.name)] = JSON.stringify(connector.config);
}
}
}
function generateFileName(fileName): string {
return fileName.replace('_', '')
.replace('-', '')
.replace(/^\s+|\s+/g, '')
.toLowerCase() + '.json';
}
export function generateLogConfigFile(fileZipAdd: object, logsLevel: string, logsPath: string): void {
fileZipAdd['logs.conf'] = getLogsConfig(logsLevel, logsPath);
}
function getLogsConfig(logsLevel: string, logsPath: string): string {
return TEMPLATE_LOGS_CONFIG
.replace(/{ERROR}/g, logsLevel)
.replace(/{.\/logs\/}/g, logsPath);
}
export function getEntityId(gatewayId: string): EntityId {
return {
id: gatewayId,
entityType: EntityType.DEVICE
}
}
export function createFormConfig(keyValue: GatewayMainSetting): GatewayFormModels {
const formSetting: GatewayFormModels = {};
if (Object.prototype.hasOwnProperty.call(keyValue, 'thingsboard')) {
formSetting.host = keyValue.thingsboard.host;
formSetting.port = keyValue.thingsboard.port;
formSetting.remoteConfiguration = keyValue.thingsboard.remoteConfiguration;
if (Object.prototype.hasOwnProperty.call(keyValue.thingsboard.security, SecurityType.accessToken)) {
formSetting.securityType = SecurityType.accessToken;
formSetting.accessToken = (keyValue.thingsboard.security as SecurityToken).accessToken;
} else {
formSetting.securityType = SecurityType.tls;
formSetting.caCertPath = (keyValue.thingsboard.security as SecurityCertificate).caCert;
formSetting.privateKeyPath = (keyValue.thingsboard.security as SecurityCertificate).privateKey;
formSetting.certPath = (keyValue.thingsboard.security as SecurityCertificate).cert;
}
}
if (Object.prototype.hasOwnProperty.call(keyValue, 'storage') && Object.prototype.hasOwnProperty.call(keyValue.storage, 'type')) {
if (keyValue.storage.type === StorageType.memory) {
formSetting.storageType = StorageType.memory;
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count;
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count;
} else if (keyValue.storage.type === StorageType.file) {
formSetting.storageType = StorageType.file;
formSetting.dataFolderPath = (keyValue.storage as GatewayStorageFile).data_folder_path;
formSetting.maxFilesCount = (keyValue.storage as GatewayStorageFile).max_file_count;
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count;
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count;
}
}
return formSetting;
}
export function getDraftConnectorsJSON(currentConnectors: Array<GatewayFormConnectorModel>) {
const draftConnectors = {};
for(const connector of currentConnectors){
if (!connector.enabled) {
draftConnectors[connector.name] = {
connector: connector.configType,
config: connector.config
};
}
}
return draftConnectors;
}
export function gatewayConfigJSON(gatewayConfiguration: GatewayFormModels): GatewaySetting {
const gatewayConfig = {
thingsboard: gatewayMainConfigJSON(gatewayConfiguration)
};
gatewayConnectorJSON(gatewayConfig, gatewayConfiguration.connectors);
return gatewayConfig;
}
function gatewayMainConfigJSON(gatewayConfiguration: GatewayFormModels): GatewayMainSetting {
let security: GatewaySecurity;
if (gatewayConfiguration.securityType === SecurityType.accessToken) {
security = {
accessToken: gatewayConfiguration.accessToken
}
} else {
security = {
caCert: gatewayConfiguration.caCertPath,
privateKey: gatewayConfiguration.privateKeyPath,
cert: gatewayConfiguration.certPath
}
}
const thingsboard: GatewayMainThingsboardSetting = {
host: gatewayConfiguration.host,
remoteConfiguration: gatewayConfiguration.remoteConfiguration,
port: gatewayConfiguration.port,
security
};
let storage: GatewayStorage;
if (gatewayConfiguration.storageType === StorageType.memory) {
storage = {
type: StorageType.memory,
read_records_count: gatewayConfiguration.readRecordsCount,
max_records_count: gatewayConfiguration.maxRecordsCount
};
} else {
storage = {
type: StorageType.file,
data_folder_path: gatewayConfiguration.dataFolderPath,
max_file_count: gatewayConfiguration.maxFilesCount,
max_read_records_count: gatewayConfiguration.readRecordsCount,
max_records_per_file: gatewayConfiguration.maxRecordsCount
};
}
const connectors: Array<GatewayMainConnector> = [];
for (const connector of gatewayConfiguration.connectors) {
if (connector.enabled) {
const connectorConfig: GatewayMainConnector = {
configuration: generateFileName(connector.name),
name: connector.name,
type: connector.configType
};
connectors.push(connectorConfig);
}
}
return {
thingsboard,
connectors,
storage,
logs: window.btoa(getLogsConfig(gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs))
}
}
function gatewayConnectorJSON(gatewayConfiguration, currentConnectors: Array<GatewayFormConnectorModel>): void {
for (const connector of currentConnectors) {
if (connector.enabled) {
const typeConnector = connector.configType;
if (!Array.isArray(gatewayConfiguration[typeConnector])) {
gatewayConfiguration[typeConnector] = [];
}
const connectorConfig: ConnectorConfig = {
name: connector.name,
config: connector.config
};
gatewayConfiguration[typeConnector].push(connectorConfig);
}
}
}

56
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html

@ -1,56 +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.
-->
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link *ngFor="let link of logLinks"
(click)="onTabChanged(link)"
[active]="activeLink.name === link.name"> {{ link.name }} </a>
</nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<table mat-table [dataSource]="dataSource" [trackBy]="trackByLogTs"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear>
<ng-container matColumnDef="ts">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 20%">{{ 'widgets.gateway.created-time' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 10%">{{ 'widgets.gateway.level' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute">
<span [class]="statusClass(attribute.status)">{{ attribute.status }}</span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="message">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute" [class]="statusClassMsg(attribute.status)">
{{ attribute.message }}
</mat-cell>
</ng-container>
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row class="mat-row-select" *matRowDef="let attribute; columns: displayedColumns;"></mat-row>
</table>
<span [class.!hidden]="dataSource.data.length !== 0"
class="no-data-found flex-1 items-center justify-center">{{ 'attribute.no-telemetry-text' | translate }}</span>
<span class="flex-1" [class.!hidden]="dataSource.data.length === 0"></span>
<mat-divider></mat-divider>
<mat-paginator [length]="dataSource.data.length"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator>

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

Loading…
Cancel
Save