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