major refactoring

This commit is contained in:
mpetrov 2024-07-25 12:09:26 +03:00
parent e9d58525bc
commit 7b9d8e0a55
19 changed files with 345 additions and 350 deletions

View File

@ -68,12 +68,6 @@ import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master
overflow: hidden !important;
}
}
:host ::ng-deep {
.mat-mdc-tab-group, .mat-mdc-tab-body-wrapper {
height: 100%;
}
}
`]
})
@ -83,10 +77,9 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
basicFormGroup: FormGroup;
onChange: (value: string) => void;
onChange: (value: ModbusBasicConfig) => void;
onTouched: () => void;
protected readonly connectorType = ConnectorType;
private destroy$ = new Subject<void>();
constructor(private fb: FormBuilder) {
@ -108,7 +101,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
registerOnChange(fn: (value: ModbusBasicConfig) => void): void {
this.onChange = fn;
}
@ -118,8 +111,8 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
writeValue(basicConfig: ModbusBasicConfig): void {
const editedBase = {
slave: basicConfig.slave || {},
master: basicConfig.master || {},
slave: basicConfig.slave ?? {},
master: basicConfig.master ?? {},
};
this.basicFormGroup.setValue(editedBase, {emitEvent: false});

View File

@ -20,7 +20,7 @@
<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;">
*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">
@ -99,10 +99,19 @@
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.address</div>
<div class="fixed-title-width tb-required" translate>gateway.address</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" max="50000" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.address-required') | translate"
*ngIf="keyControl.get('address').hasError('required') &&
keyControl.get('address').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>

View File

@ -14,14 +14,21 @@
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { AbstractControl, FormGroup, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms';
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import {
AbstractControl,
FormArray,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
ModbusDataType,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusRegisterType,
ModbusValue,
ModbusValueKey,
noLeadTrailSpacesRegex,
@ -31,6 +38,8 @@ import { SharedModule } from '@shared/shared.module';
import { GatewayHelpLinkPipe } from '@home/pipes/gateway-help-link.pipe';
import { generateSecret } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
@Component({
selector: 'tb-modbus-data-keys-panel',
@ -43,7 +52,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
GatewayHelpLinkPipe,
]
})
export class ModbusDataKeysPanelComponent implements OnInit {
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
@coerceBoolean()
@Input() isMaster = false;
@ -51,15 +60,13 @@ export class ModbusDataKeysPanelComponent implements OnInit {
@Input() addKeyTitle: string;
@Input() deleteKeyTitle: string;
@Input() noKeysText: string;
@Input() register: ModbusRegisterType;
@Input() keysType: ModbusValueKey;
@Input() values: ModbusValue[];
@Input() popover: TbPopoverComponent<ModbusDataKeysPanelComponent>;
@Output() keysDataApplied = new EventEmitter<Array<ModbusValue>>();
keysListFormArray: UntypedFormArray;
errorText = '';
keysListFormArray: FormArray<UntypedFormGroup>;
modbusDataTypes = Object.values(ModbusDataType);
withFunctionCode = true;
functionCodesMap = new Map();
@ -67,9 +74,12 @@ export class ModbusDataKeysPanelComponent implements OnInit {
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING];
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
readonly defaultReadFunctionCodes = [3, 4];
readonly defaultWriteFunctionCodes = [5, 6, 15, 16];
readonly stringAttrUpdatesWriteFunctionCodes = [6, 16];
private destroy$ = new Subject<void>();
private readonly defaultReadFunctionCodes = [3, 4];
private readonly defaultWriteFunctionCodes = [5, 6, 15, 16];
private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16];
constructor(private fb: UntypedFormBuilder) {}
@ -79,8 +89,13 @@ export class ModbusDataKeysPanelComponent implements OnInit {
this.defaultFunctionCodes = this.getDefaultFunctionCodes();
}
trackByKey(_: number, keyControl: AbstractControl): AbstractControl {
return keyControl;
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
trackByControlId(_: number, keyControl: AbstractControl): string {
return keyControl.value.id;
}
addKey(): void {
@ -88,7 +103,7 @@ export class ModbusDataKeysPanelComponent implements OnInit {
tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{value: '', disabled: !this.isMaster}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusDataType.BYTES, [Validators.required]],
address: [0, [Validators.required]],
address: [null, [Validators.required]],
objectsCount: [1, [Validators.required]],
functionCode: [this.getDefaultFunctionCodes()[0]],
id: [{value: generateSecret(5), disabled: true}],
@ -107,7 +122,7 @@ export class ModbusDataKeysPanelComponent implements OnInit {
}
cancel(): void {
this.popover?.hide();
this.popover.hide();
}
applyKeysData(): void {
@ -116,32 +131,38 @@ export class ModbusDataKeysPanelComponent implements OnInit {
private prepareKeysFormArray(values: ModbusValue[]): UntypedFormArray {
const keysControlGroups: Array<AbstractControl> = [];
if (values) {
values.forEach(keyData => {
const { tag, value, type, address, objectsCount, functionCode } = keyData;
const dataKeyFormGroup = 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: [functionCode, []],
id: [{value: generateSecret(5), disabled: true}],
});
values.forEach(value => {
const dataKeyFormGroup = this.createDataKeyFormGroup(value);
this.observeKeyDataType(dataKeyFormGroup);
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(type));
this.functionCodesMap.set(dataKeyFormGroup.get('id').value, this.getFunctionCodes(value.type));
keysControlGroups.push(dataKeyFormGroup);
});
}
return this.fb.array(keysControlGroups);
}
private createDataKeyFormGroup(modbusValue: ModbusValue): FormGroup {
const { tag, value, type, address, objectsCount, functionCode } = modbusValue;
return this.fb.group({
tag: [tag, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
value: [{ value, disabled: !this.isMaster }, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [type, [Validators.required]],
address: [address, [Validators.required]],
objectsCount: [objectsCount, [Validators.required]],
functionCode: [functionCode, [Validators.required]],
id: [{ value: generateSecret(5), disabled: true }],
});
}
private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.subscribe(dataType => {
const objectsCountControl = keyFormGroup.get('objectsCount');
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.editableDataTypes.includes(dataType)) {
objectsCountControl.patchValue(ModbusObjectCountByDataType[dataType]);
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
}
this.functionCodesMap.set(keyFormGroup.get('id').value, this.getFunctionCodes(dataType));
});
@ -149,20 +170,21 @@ export class ModbusDataKeysPanelComponent implements OnInit {
private getFunctionCodes(dataType: ModbusDataType): number[] {
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
if (dataType === ModbusDataType.STRING) {
return this.stringAttrUpdatesWriteFunctionCodes;
}
return this.defaultWriteFunctionCodes;
return dataType === ModbusDataType.STRING
? this.stringAttrUpdatesWriteFunctionCodes
: this.defaultWriteFunctionCodes;
}
const functionCodes = [...this.defaultReadFunctionCodes];
if (dataType === ModbusDataType.BITS) {
const bitsFunctionCodes = [1, 2];
bitsFunctionCodes.forEach(code => functionCodes.push(code));
functionCodes.push(...bitsFunctionCodes);
functionCodes.sort();
}
if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
this.defaultWriteFunctionCodes.forEach(code => functionCodes.push(code));
functionCodes.push(...this.defaultWriteFunctionCodes);
}
return functionCodes;
}

View File

@ -60,18 +60,18 @@
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="'name'">
<mat-header-cell *matHeaderCellDef class="table-value-column request-column">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.name' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column request-column">
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ mapping['name'] }}
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column request-column">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.client-communication-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column request-column">
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(mapping['type']) }}
</mat-cell>
</ng-container>
@ -111,7 +111,7 @@
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let mapping; columns: ['name', 'type', 'actions'];"></mat-row>
<mat-row *matRowDef="let mapping; columns: ['name', 'type', 'actions']"></mat-row>
</table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">

View File

@ -19,19 +19,15 @@
width: 100%;
height: 100%;
display: block;
.tb-master-table {
.tb-master-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;
}
@ -49,25 +45,17 @@
.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%;
}
width: 38%;
}
}
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
@ -91,6 +79,7 @@
:host ::ng-deep {
mat-cell.tb-value-cell {
cursor: pointer;
.mat-icon {
height: 24px;
width: 24px;

View File

@ -17,6 +17,7 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
@ -76,15 +77,12 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
textSearchMode = false;
dataSource: SlavesDatasource;
hidePageSize = false;
activeValue = false;
dirtyValue = false;
masterFormGroup: UntypedFormGroup;
textSearch = this.fb.control('', {nonNullable: true});
readonly ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
private onChange: (value: string) => void = () => {};
private onChange: (value: ModbusMasterConfig) => void = () => {};
private onTouched: () => void = () => {};
private destroy$ = new Subject<void>();
@ -93,10 +91,10 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
public translate: TranslateService,
public dialog: MatDialog,
private dialogService: DialogService,
private fb: FormBuilder
private fb: FormBuilder,
private cdr: ChangeDetectorRef,
) {
this.masterFormGroup = this.fb.group({ slaves: this.fb.array([])});
this.dirtyValue = !this.activeValue;
this.masterFormGroup = this.fb.group({ slaves: this.fb.array([]) });
this.dataSource = new SlavesDatasource();
}
@ -124,13 +122,10 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
debounceTime(150),
distinctUntilChanged((prev, current) => (prev ?? '') === current.trim()),
takeUntil(this.destroy$)
).subscribe((text) => {
const searchText = text.trim();
this.updateTableData(this.slaves.value, searchText.trim());
});
).subscribe(text => this.updateTableData(this.slaves.value, text.trim()));
}
registerOnChange(fn: (value: string) => void): void {
registerOnChange(fn: (value: ModbusMasterConfig) => void): void {
this.onChange = fn;
}
@ -151,10 +146,10 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
enterFilterMode(): void {
this.textSearchMode = true;
setTimeout(() => {
this.searchInputField.nativeElement.focus();
this.searchInputField.nativeElement.setSelectionRange(0, 0);
}, 10);
this.cdr.detectChanges();
const searchInput = this.searchInputField.nativeElement;
searchInput.focus();
searchInput.setSelectionRange(0, 0);
}
exitFilterMode(): void {
@ -167,19 +162,20 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
if ($event) {
$event.stopPropagation();
}
const value = isDefinedAndNotNull(index) ? this.slaves.at(index).value : {};
const withIndex = isDefinedAndNotNull(index);
const value = withIndex ? this.slaves.at(index).value : {};
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value,
buttonTitle: isUndefinedOrNull(index) ? 'action.add' : 'action.apply'
buttonTitle: withIndex ? 'action.add' : 'action.apply'
}
}).afterClosed()
.pipe(take(1), takeUntil(this.destroy$))
.subscribe(res => {
if (res) {
if (isDefinedAndNotNull(index)) {
if (withIndex) {
this.slaves.at(index).patchValue(res);
} else {
this.slaves.push(this.fb.control(res));
@ -199,7 +195,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
true
).subscribe((result) => {
).pipe(take(1), takeUntil(this.destroy$)).subscribe((result) => {
if (result) {
this.slaves.removeAt(index);
this.masterFormGroup.markAsDirty();
@ -208,15 +204,14 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
}
private updateTableData(data: SlaveConfig[], textSearch?: string): void {
let tableValue = data;
if (textSearch) {
tableValue = tableValue.filter(value =>
Object.values(value).some(val =>
val.toString().toLowerCase().includes(textSearch.toLowerCase())
data = data.filter(item =>
Object.values(item).some(value =>
value.toString().toLowerCase().includes(textSearch.toLowerCase())
)
);
}
this.dataSource.loadData(tableValue);
this.dataSource.loadData(data);
}
private pushDataAsFormArrays(slaves: SlaveConfig[]): void {

View File

@ -85,7 +85,7 @@ export class ModbusSecurityConfigComponent implements ControlValueAccessor, Vali
keyfile: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
password: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
server_hostname: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
reqclicert: [{value: false, disabled: true}, []],
reqclicert: [{value: false, disabled: true}],
});
this.observeValueChanges();

View File

@ -86,11 +86,8 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('serialPort') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('serialPort').hasError('required') ||
slaveConfigFormGroup.get('serialPort').hasError('min') ||
slaveConfigFormGroup.get('serialPort').hasError('max')) &&
slaveConfigFormGroup.get('serialPort').touched"
[matTooltip]="'gateway.port-required' | translate"
*ngIf="slaveConfigFormGroup.get('port').hasError('required') && slaveConfigFormGroup.get('port').touched"
class="tb-error">
warning
</mat-icon>
@ -188,7 +185,7 @@
<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" [formGroup]="slaveConfigFormGroup">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.byte-order</div>
<div class="tb-flex no-gap">

View File

@ -16,11 +16,6 @@
$server-config-header-height: 132px;
:host {
.nested-expansion-header {
::ng-deep .mat-content {
height: 100%;
}
}
.slave-content {
height: calc(100% - #{$server-config-header-height});

View File

@ -18,9 +18,9 @@ import { ChangeDetectionStrategy, Component, forwardRef, OnDestroy } from '@angu
import {
ControlValueAccessor,
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors,
Validator,
@ -78,7 +78,7 @@ import { isEqual } from '@core/utils';
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
slaveConfigFormGroup: UntypedFormGroup;
showSecurityControl: UntypedFormControl;
showSecurityControl: FormControl<boolean>;
ModbusProtocolLabelsMap = ModbusProtocolLabelsMap;
ModbusMethodLabelsMap = ModbusMethodLabelsMap;
portLimits = PortLimits;
@ -89,8 +89,9 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
readonly modbusOrderType = Object.values(ModbusOrderType);
readonly ModbusProtocolType = ModbusProtocolType;
readonly modbusBaudrates = ModbusBaudrates;
readonly serialSpecificControlKeys = ['serialPort', 'baudrate'];
readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host'];
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host'];
private onChange: (value: SlaveConfig) => void;
private onTouched: () => void;
@ -100,18 +101,18 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
constructor(private fb: FormBuilder) {
this.showSecurityControl = this.fb.control(false);
this.slaveConfigFormGroup = this.fb.group({
type: [ModbusProtocolType.TCP, []],
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.RTU, []],
method: [ModbusMethodType.SOCKET],
unitId: [0, [Validators.required]],
baudrate: [this.ModbusProtocolType[0], []],
baudrate: [this.modbusBaudrates[0]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
pollPeriod: [5000, []],
sendDataToThingsBoard: [false, []],
byteOrder:[ModbusOrderType.BIG, []],
pollPeriod: [5000],
sendDataToThingsBoard: [false],
byteOrder:[ModbusOrderType.BIG],
security: [],
identity: this.fb.group({
vendorName: ['', [Validators.pattern(noLeadTrailSpacesRegex)]],
@ -123,22 +124,16 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
values: [],
});
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();
});
this.observeValueChanges();
this.observeTypeChange();
this.observeFormEnable();
this.observeShowSecurity();
}
get isSlaveEnabled(): boolean {
return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
@ -154,7 +149,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
validate(): ValidationErrors | null {
return this.slaveConfigFormGroup.valid ? null : {
serverConfigFormGroup: { valid: false }
slaveConfigFormGroup: { valid: false }
};
}
@ -164,18 +159,29 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
this.updateFormEnableState(slaveConfig.sendDataToThingsBoard);
}
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.updateFormEnableState(this.slaveConfigFormGroup.get('sendDataToThingsBoard').value);
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(() => this.updateFormEnableState(this.isSlaveEnabled));
}
private observeFormEnable(): void {
this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges
.pipe(startWith(this.slaveConfigFormGroup.get('sendDataToThingsBoard').value), takeUntil(this.destroy$))
.subscribe(value => {
this.updateFormEnableState(value);
});
.pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$))
.subscribe(value => this.updateFormEnableState(value));
}
private updateFormEnableState(enabled: boolean): void {
@ -192,11 +198,13 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
}
private observeShowSecurity(): void {
this.showSecurityControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => this.updateSecurityEnable(value));
this.showSecurityControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateSecurityEnable(value));
}
private updateSecurityEnable(isEnabled: boolean): void {
if (isEnabled && this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) {
private updateSecurityEnable(securityEnabled: boolean): void {
if (securityEnabled && this.isSlaveEnabled) {
this.slaveConfigFormGroup.get('security').enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.get('security').disable({emitEvent: false});
@ -204,63 +212,58 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
}
private updateEnablingByProtocol(type: ModbusProtocolType): void {
if (type === ModbusProtocolType.Serial) {
if (this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) {
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false}));
}
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false}));
} else {
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false}));
if (this.slaveConfigFormGroup.get('sendDataToThingsBoard').value) {
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false}));
}
const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys;
const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys;
if (this.isSlaveEnabled) {
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false }));
}
};
disableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({ emitEvent: false }));
}
private updateSlaveConfig(slaveConfig: ModbusSlave): void {
const {
type,
method,
unitId,
deviceName,
deviceType,
pollPeriod,
sendDataToThingsBoard,
byteOrder,
security,
identity,
values,
baudrate,
host,
port,
} = slaveConfig;
let slaveState: ModbusSlave = {
host: host ?? '',
type: type ?? ModbusProtocolType.TCP,
method: method ?? ModbusMethodType.RTU,
unitId: unitId ?? 0,
deviceName: deviceName ?? '',
deviceType: deviceType ?? '',
pollPeriod: pollPeriod ?? 5000,
sendDataToThingsBoard: !!sendDataToThingsBoard,
byteOrder: byteOrder ?? ModbusOrderType.BIG,
security: security ?? {},
identity: identity ?? {
type = ModbusProtocolType.TCP,
method = ModbusMethodType.RTU,
unitId = 0,
deviceName = '',
deviceType = '',
pollPeriod = 5000,
sendDataToThingsBoard = false,
byteOrder = ModbusOrderType.BIG,
security = {},
identity = {
vendorName: '',
productCode: '',
vendorUrl: '',
productName: '',
modelName: '',
},
values: values ?? {} as ModbusRegisterValues,
port: port ?? null,
baudrate: baudrate ?? this.modbusBaudrates[0],
values = {} as ModbusRegisterValues,
baudrate = this.modbusBaudrates[0],
host = '',
port = null,
} = slaveConfig;
const slaveState: ModbusSlave = {
type,
method,
unitId,
deviceName,
deviceType,
pollPeriod,
sendDataToThingsBoard: !!sendDataToThingsBoard,
byteOrder,
security,
identity,
values,
baudrate,
host: type === ModbusProtocolType.Serial ? '' : host,
port: type === ModbusProtocolType.Serial ? null : port,
serialPort: (type === ModbusProtocolType.Serial ? port : '') as string,
};
if (slaveConfig.type === ModbusProtocolType.Serial) {
slaveState = { ...slaveState, serialPort: port, host: '', port: null } as ModbusSlave;
} else {
slaveState = { ...slaveState, serialPort: '' } as ModbusSlave;
}
this.slaveConfigFormGroup.setValue(slaveState, {emitEvent: false});
this.slaveConfigFormGroup.setValue(slaveState, { emitEvent: false });
}
}

View File

@ -105,10 +105,8 @@
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="slaveConfigFormGroup.get('serialPort') | getGatewayPortTooltip"
*ngIf="(slaveConfigFormGroup.get('serialPort').hasError('required') ||
slaveConfigFormGroup.get('serialPort').hasError('min') ||
slaveConfigFormGroup.get('serialPort').hasError('max')) &&
[matTooltip]="'gateway.port-required' | translate"
*ngIf="slaveConfigFormGroup.get('serialPort').hasError('required') &&
slaveConfigFormGroup.get('serialPort').touched"
class="tb-error">
warning

View File

@ -17,14 +17,13 @@
import { ChangeDetectionStrategy, Component, forwardRef, Inject, OnDestroy } from '@angular/core';
import {
FormBuilder,
FormControl,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormControl,
UntypedFormGroup,
Validators,
} from '@angular/forms';
import {
MappingInfo,
ModbusBaudrates,
ModbusByteSizes,
ModbusMethodLabelsMap,
@ -35,6 +34,7 @@ import {
ModbusProtocolLabelsMap,
ModbusProtocolType,
ModbusSerialMethodType,
ModbusSlaveInfo,
noLeadTrailSpacesRegex,
PortLimits,
SlaveConfig,
@ -80,15 +80,15 @@ import { isEqual } from '@core/utils';
styles: [`
:host {
.slaves-config-container {
width: 900px;
}
width: 80vw;
max-width: 900px; }
}
`],
})
export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialogComponent, SlaveConfig> implements OnDestroy {
slaveConfigFormGroup: UntypedFormGroup;
showSecurityControl: UntypedFormControl;
showSecurityControl: FormControl<boolean>;
portLimits = PortLimits;
readonly modbusProtocolTypes = Object.values(ModbusProtocolType);
@ -104,8 +104,9 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
readonly ModbusMethodLabelsMap = ModbusMethodLabelsMap;
readonly modbusHelpLink =
'https://thingsboard.io/docs/iot-gateway/config/modbus/#section-master-description-and-configuration-parameters';
readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict'];
readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host', 'wordOrder'];
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate', 'stopbits', 'bytesize', 'parity', 'strict'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host', 'wordOrder'];
private destroy$ = new Subject<void>();
@ -113,7 +114,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
private fb: FormBuilder,
protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MappingInfo,
@Inject(MAT_DIALOG_DATA) public data: ModbusSlaveInfo,
public dialogRef: MatDialogRef<ModbusSlaveDialogComponent, SlaveConfig>,
) {
super(store, router, dialogRef);
@ -121,42 +122,38 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
this.showSecurityControl = this.fb.control(false);
this.slaveConfigFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusProtocolType.TCP, [Validators.required]],
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.RTU, []],
baudrate: [this.modbusBaudrates[0], []],
stopbits: [null, []],
bytesize: [ModbusByteSizes[0], []],
parity: [ModbusParity.None, []],
strict: [false, []],
method: [ModbusMethodType.SOCKET, [Validators.required]],
baudrate: [this.modbusBaudrates[0]],
stopbits: [1],
bytesize: [ModbusByteSizes[0]],
parity: [ModbusParity.None],
strict: [true],
unitId: [0, [Validators.required]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
sendDataOnlyOnChange: [false, []],
sendDataOnlyOnChange: [false],
timeout: [35],
byteOrder: [ModbusOrderType.BIG, []],
wordOrder: [ModbusOrderType.BIG, []],
retries: [true, []],
retryOnEmpty: [true, []],
retryOnInvalid: [true, []],
pollPeriod: [5000, []],
connectAttemptTimeMs: [5000, []],
connectAttemptCount: [5, []],
waitAfterFailedAttemptsMs: [300000, []],
values: [{}, []],
byteOrder: [ModbusOrderType.BIG],
wordOrder: [ModbusOrderType.BIG],
retries: [true],
retryOnEmpty: [true],
retryOnInvalid: [true],
pollPeriod: [5000],
connectAttemptTimeMs: [5000],
connectAttemptCount: [5],
waitAfterFailedAttemptsMs: [300000],
values: [{}],
security: [{}],
});
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
: '',
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 ?? [],
@ -180,40 +177,49 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
}
add(): void {
if (this.slaveConfigFormGroup.valid) {
const slaveResult = {...this.slaveConfigFormGroup.value, ...this.slaveConfigFormGroup.value.values};
delete slaveResult.values;
if (slaveResult.type === ModbusProtocolType.Serial) {
slaveResult.port = slaveResult.serialPort;
}
delete slaveResult.serialPort;
this.dialogRef.close(slaveResult);
if (!this.slaveConfigFormGroup.valid) {
return;
}
const { values, type, serialPort, ...rest } = this.slaveConfigFormGroup.value;
const slaveResult = { ...rest, ...values };
if (type === ModbusProtocolType.Serial) {
slaveResult.port = serialPort;
}
this.dialogRef.close(slaveResult);
}
private observeTypeChange(): void {
this.slaveConfigFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.updateControlsEnabling(type);
});
this.slaveConfigFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(type => this.updateControlsEnabling(type));
}
private updateControlsEnabling(type: ModbusProtocolType): void {
if (type === ModbusProtocolType.Serial) {
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false}));
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false}));
} else {
this.serialSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.disable({emitEvent: false}));
this.tcpUdpSpecificControlKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({emitEvent: false}));
}
};
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 => {
if (value) {
this.slaveConfigFormGroup.get('security').enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.get('security').disable({emitEvent: false});
}
});
this.showSecurityControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => this.updateSecurityEnabling(value));
}
private updateSecurityEnabling(isEnabled: boolean): void {
if (isEnabled) {
this.slaveConfigFormGroup.get('security').enable({emitEvent: false});
} else {
this.slaveConfigFormGroup.get('security').disable({emitEvent: false});
}
}
}

View File

@ -42,6 +42,7 @@ import {
ModbusRegisterTranslationsMap,
ModbusRegisterType,
ModbusRegisterValues,
ModbusValue,
ModbusValueKey,
ModbusValues,
ModbusValuesState,
@ -103,7 +104,7 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O
ModbusValueKey = ModbusValueKey;
valuesFormGroup: FormGroup;
private onChange: (value: string) => void;
private onChange: (value: ModbusValuesState) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
@ -125,7 +126,7 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
registerOnChange(fn: (value: ModbusValuesState) => void): void {
this.onChange = fn;
}
@ -135,15 +136,15 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O
writeValue(values: ModbusValuesState): void {
if (this.singleMode) {
this.valuesFormGroup.setValue(this.getSingleRegisterState(values as ModbusValues), {emitEvent: false});
this.valuesFormGroup.setValue(this.getSingleRegisterState(values as ModbusValues), { emitEvent: false });
} else {
const registers = values as ModbusRegisterValues;
const { holding_registers, coils_initializer, input_registers, discrete_inputs } = values as ModbusRegisterValues;
this.valuesFormGroup.setValue({
holding_registers: this.getSingleRegisterState(registers.holding_registers),
coils_initializer: this.getSingleRegisterState(registers.coils_initializer),
input_registers: this.getSingleRegisterState(registers.input_registers),
discrete_inputs: this.getSingleRegisterState(registers.discrete_inputs),
}, {emitEvent: false});
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();
}
@ -159,69 +160,63 @@ export class ModbusValuesComponent implements ControlValueAccessor, Validator, O
this.cdr.markForCheck();
}
getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType) {
getValueGroup(valueKey: ModbusValueKey, register?: ModbusRegisterType): FormGroup {
return register ? this.valuesFormGroup.get(register).get(valueKey).value : this.valuesFormGroup.get(valueKey).value;
}
manageKeys($event: Event, matButton: MatButton, keysType: ModbusValueKey, register?: ModbusRegisterType): void {
if ($event) {
$event.stopPropagation();
}
$event.stopPropagation();
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const group = this.valuesFormGroup;
const keysControl = register ? group.get(register).get(keysType) : group.get(keysType);
const ctx = {
values: keysControl.value,
isMaster: !this.singleMode,
keysType,
panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType),
addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType),
deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType),
noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType)
};
const dataKeysPanelPopover = this.popoverService.displayPopover(
trigger,
this.renderer,
this.viewContainerRef,
ModbusDataKeysPanelComponent,
'leftBottom',
false,
null,
ctx,
{},
{},
{},
true
);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
this.cdr.markForCheck();
});
return;
}
const keysControl = this.getValueGroup(keysType, register);
const ctx = {
values: keysControl.value,
isMaster: !this.singleMode,
keysType,
panelTitle: ModbusKeysPanelTitleTranslationsMap.get(keysType),
addKeyTitle: ModbusKeysAddKeyTranslationsMap.get(keysType),
deleteKeyTitle: ModbusKeysDeleteKeyTranslationsMap.get(keysType),
noKeysText: ModbusKeysNoKeysTextTranslationsMap.get(keysType)
};
const dataKeysPanelPopover = this.popoverService.displayPopover(
trigger,
this.renderer,
this.viewContainerRef,
ModbusDataKeysPanelComponent,
'leftBottom',
false,
null,
ctx,
{},
{},
{},
true
);
dataKeysPanelPopover.tbComponentRef.instance.popover = dataKeysPanelPopover;
dataKeysPanelPopover.tbComponentRef.instance.keysDataApplied.pipe(takeUntil(this.destroy$)).subscribe((keysData: ModbusValue[]) => {
dataKeysPanelPopover.hide();
keysControl.patchValue(keysData);
keysControl.markAsDirty();
this.cdr.markForCheck();
});
}
private initializeValuesFormGroup(): void {
const getValuesFormGroup = () => this.fb.group(this.modbusValueKeys.reduce((acc, key) => {
acc[key] = this.fb.control([[], []]);
return acc;
}, {}));
if (this.singleMode) {
this.valuesFormGroup = this.fb.group(this.modbusValueKeys.reduce((acc, key) => {
acc[key] = this.fb.control([[], []]);
return acc;
}, {}));
this.valuesFormGroup = getValuesFormGroup();
} else {
this.valuesFormGroup = this.fb.group(
this.modbusRegisterTypes.reduce((registersAcc, register) => {
registersAcc[register] = this.fb.group(this.modbusValueKeys.reduce((acc, key) => {
acc[key] = this.fb.control([[], []]);
return acc;
}, {}));
registersAcc[register] = getValuesFormGroup();
return registersAcc;
}, {})
);

View File

@ -485,6 +485,11 @@ export interface MappingInfo {
buttonTitle: string;
}
export interface ModbusSlaveInfo {
value: SlaveConfig;
buttonTitle: string;
}
export enum ConnectorConfigurationModes {
BASIC = 'basic',
ADVANCED = 'advanced'
@ -852,24 +857,6 @@ export enum ModbusObjectCountByDataType {
'64float' = 4,
}
export enum ModbusValueField {
Tag = 'tag',
Type = 'type',
ObjectsCount = 'objectsCount',
Address = 'address',
Value = 'value',
}
export const ModbusFieldsTranslationsMap = new Map<ModbusValueField, string>(
[
[ModbusValueField.Tag, 'gateway.tag'],
[ModbusValueField.Type, 'gateway.type'],
[ModbusValueField.ObjectsCount, 'gateway.objects_count'],
[ModbusValueField.Address, 'gateway.address'],
[ModbusValueField.Value, 'gateway.value']
]
);
export enum ModbusValueKey {
ATTRIBUTES = 'attributes',
TIMESERIES = 'timeseries',
@ -946,7 +933,7 @@ export interface SlaveConfig {
pollPeriod: number;
unitId: number;
deviceName: string;
deviceType?: string;
deviceType: string;
sendDataOnlyOnChange: boolean;
connectAttemptTimeMs: number;
connectAttemptCount: number;
@ -959,7 +946,7 @@ export interface SlaveConfig {
baudrate?: number;
stopbits?: number;
bytesize?: number;
parity?: string;
parity?: ModbusParity;
strict?: boolean;
}

View File

@ -208,25 +208,26 @@ import {
LabelCardWidgetComponent,
LabelValueCardWidgetComponent,
UnreadNotificationWidgetComponent,
NotificationTypeFilterPanelComponent],
imports: [
CommonModule,
SharedModule,
RpcWidgetsModule,
HomePageWidgetsModule,
SharedHomeComponentsModule,
RestConnectorSecurityComponent,
GatewayHelpLinkPipe,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
OpcServerConfigComponent,
MqttBasicConfigComponent,
MappingTableComponent,
OpcUaBasicConfigComponent,
KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent,
EllipsisChipListDirective,
],
NotificationTypeFilterPanelComponent
],
imports: [
CommonModule,
SharedModule,
RpcWidgetsModule,
HomePageWidgetsModule,
SharedHomeComponentsModule,
RestConnectorSecurityComponent,
GatewayHelpLinkPipe,
BrokerConfigControlComponent,
WorkersConfigControlComponent,
OpcServerConfigComponent,
MqttBasicConfigComponent,
MappingTableComponent,
OpcUaBasicConfigComponent,
KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent,
EllipsisChipListDirective,
],
exports: [
EntitiesTableWidgetComponent,
AlarmsTableWidgetComponent,

View File

@ -30,12 +30,12 @@ export class GatewayPortTooltipPipe implements PipeTransform {
transform(portControl: AbstractControl): string {
if (portControl.hasError('required')) {
return this.translate.instant('gateway.port-required');
} else if (
portControl.hasError('min') ||
portControl.hasError('max')
) {
return this.translate.instant('gateway.port-limits-error',
{min: PortLimits.MIN, max: PortLimits.MAX});
}
if (portControl.hasError('min') || portControl.hasError('max')) {
return this.translate.instant('gateway.port-limits-error', {
min: PortLimits.MIN,
max: PortLimits.MAX,
});
}
return '';
}

View File

@ -61,8 +61,11 @@ export class EllipsisChipListDirective implements OnDestroy {
).subscribe(() => {
this.adjustChips();
});
this.observeIntersection();
}
this.intersectionObserver = new IntersectionObserver((entries) => {
private observeIntersection(): void {
this.intersectionObserver = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.adjustChips();

View File

@ -2756,6 +2756,7 @@
},
"gateway": {
"address": "Address",
"address-required": "Address required",
"add-entry": "Add configuration",
"add-attribute": "Add attribute",
"add-attribute-update": "Add attribute update",

View File

@ -16,6 +16,7 @@
"pollPeriod": 5000,
"unitId": 1,
"deviceName": "Temp Sensor",
"deviceType": "default",
"sendDataOnlyOnChange": true,
"connectAttemptTimeMs": 5000,
"connectAttemptCount": 5,