committed by
GitHub
158 changed files with 45 additions and 23789 deletions
@ -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; |
|||
} |
|||
@ -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>; |
|||
} |
|||
@ -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>; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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>; |
|||
} |
|||
} |
|||
@ -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" |
|||
/> |
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
@ -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} |
|||
}; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
@ -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 }); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
|
|||
@ -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 }; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
|
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
@ -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 ''; |
|||
} |
|||
} |
|||
@ -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> </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> |
|||
@ -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 |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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, {})); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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%; |
|||
} |
|||
@ -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, |
|||
}; |
|||
} |
|||
} |
|||
@ -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, |
|||
}; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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> </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> |
|||
@ -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 |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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(); |
|||
}); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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 }); |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
@ -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> |
|||
|
|||
@ -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; |
|||
} |
|||
@ -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 ?? [], |
|||
}; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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> |
|||
|
|||
@ -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%; |
|||
} |
|||
} |
|||
@ -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[]> |
|||
}; |
|||
} |
|||
} |
|||
@ -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[]>)) |
|||
}; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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} |
|||
}; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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} |
|||
}; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
@ -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 }); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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%; |
|||
} |
|||
} |
|||
@ -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, |
|||
}; |
|||
} |
|||
} |
|||
@ -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), |
|||
}; |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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}); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
@ -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> |
|||
|
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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}); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
|
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
|
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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}); |
|||
}); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
@ -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}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
@ -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]}))); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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(() => {}); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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`); |
|||
}; |
|||
} |
|||
@ -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> |
|||
@ -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%; |
|||
} |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue