diff --git a/application/src/main/data/json/tenant/dashboards/gateways.json b/application/src/main/data/json/tenant/dashboards/gateways.json index a62550ed95..8030d3e731 100644 --- a/application/src/main/data/json/tenant/dashboards/gateways.json +++ b/application/src/main/data/json/tenant/dashboards/gateways.json @@ -200,7 +200,7 @@ "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", "type": "customPretty", - "customHtml": "\n

gateway.gateway-configuration

\n \n \n
\n\n\n
\n\n", + "customHtml": "\n\n\n", "customCss": ".gateway-config {\n width: 800px !important;\n padding: 0 !important;\n min-height: 75vh;\n max-width: 100%;\n display: grid !important;\n}\n\n@media screen and (max-width: 599px) {\n .mat-mdc-dialog-content {\n max-height: calc(100% - 60px) !important;\n }\n}", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\n\nopenAddEntityDialog();\n\nfunction openAddEntityDialog() {\n customDialog.customDialog(htmlTemplate, AddEntityDialogController).subscribe();\n}\n\nfunction AddEntityDialogController(instance) {\n let vm = instance;\n \n vm.device = additionalParams.entity.id;\n\n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n}\n", "customResources": [], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html new file mode 100644 index 0000000000..843e67b0fe --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html @@ -0,0 +1,25 @@ + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss new file mode 100644 index 0000000000..4825ff0a64 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts new file mode 100644 index 0000000000..c806130975 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts @@ -0,0 +1,95 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT 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'; + +@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(); + + 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(basicConfig: unknown): void { + this.advancedFormControl.reset(basicConfig, {emitEvent: false}); + } + + validate(): ValidationErrors | null { + return this.advancedFormControl.valid ? null : { + advancedFormControl: {valid: false} + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html similarity index 77% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html index 36b26cc7a8..9c67bd2eaf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html @@ -15,10 +15,7 @@ limitations under the License. --> - -

gateway.gateway-configuration

-
- +
@@ -42,23 +39,23 @@ info_outlined - + {{ 'gateway.thingsboard-host-required' | translate }} gateway.thingsboard-port - + {{ 'gateway.thingsboard-port-required' | translate }} - + {{ 'gateway.thingsboard-port-min' | translate }} - + {{ 'gateway.thingsboard-port-max' | translate }} - + {{ 'gateway.thingsboard-port-pattern' | translate }} + *ngIf="basicFormGroup.get('thingsboard.security.type').value.toLowerCase().includes('accesstoken')"> security.access-token - + {{ 'security.access-token-required' | translate }} @@ -97,18 +94,18 @@
+ *ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'"> security.clientId - + {{ 'security.clientId-required' | translate }} @@ -120,14 +117,14 @@ security.username - + {{ 'security.username-required' | translate }} @@ -138,14 +135,14 @@
+ *ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'"> gateway.password @@ -156,13 +153,13 @@
gateway.logs.date-format - + {{ 'gateway.logs.date-format-required' | translate }} gateway.logs.log-format - + {{ 'gateway.logs.log-format-required' | translate }} gateway.logs.file-path - + {{ 'gateway.logs.file-path-required' | translate }}
@@ -244,11 +241,11 @@ gateway.logs.saving-period + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('required')"> {{ 'gateway.logs.saving-period-required' | translate }} + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('min')"> {{ 'gateway.logs.saving-period-min' | translate }} @@ -264,11 +261,11 @@ gateway.logs.backup-count + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('required')"> {{ 'gateway.logs.backup-count-required' | translate }} + *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')"> {{ 'gateway.logs.backup-count-min' | translate }} -
{{ 'gateway.hints.' + gatewayConfigGroup.get('storage.type').value | translate }}
- +
{{ 'gateway.hints.' + basicFormGroup.get('storage.type').value | translate }}
+
gateway.storage-read-record-count - + {{ 'gateway.storage-read-record-count-required' | translate }} - + {{ 'gateway.storage-read-record-count-min' | translate }} - + {{ 'gateway.storage-read-record-count-pattern' | translate }} gateway.storage-max-records - + {{ 'gateway.storage-max-records-required' | translate }} - + {{ 'gateway.storage-max-records-min' | translate }} - + {{ 'gateway.storage-max-records-pattern' | translate }} gateway.storage-data-folder-path - + {{ 'gateway.storage-data-folder-path-required' | translate }} gateway.storage-max-files - + {{ 'gateway.storage-max-files-required' | translate }} - + {{ 'gateway.storage-max-files-min' | translate }} - + {{ 'gateway.storage-max-files-pattern' | translate }} gateway.storage-max-read-record-count - + {{ 'gateway.storage-max-read-record-count-required' | translate }} - + {{ 'gateway.storage-max-read-record-count-min' | translate }} - + {{ 'gateway.storage-max-read-record-count-pattern' | translate }} gateway.storage-max-file-records - + {{ 'gateway.storage-max-records-required' | translate }} - + {{ 'gateway.storage-max-records-min' | translate }} - + {{ 'gateway.storage-max-records-pattern' | translate }} gateway.storage-path - + {{ 'gateway.storage-path-required' | translate }} gateway.messages-ttl-check-in-hours - + {{ 'gateway.messages-ttl-check-in-hours-required' | translate }} - + {{ 'gateway.messages-ttl-check-in-hours-min' | translate }} - + {{ 'gateway.messages-ttl-check-in-hours-pattern' | translate }} gateway.messages-ttl-in-days - + {{ 'gateway.messages-ttl-in-days-required' | translate }} - + {{ 'gateway.messages-ttl-in-days-min' | translate }} - + {{ 'gateway.messages-ttl-in-days-pattern' | translate }} info_outlined - + {{ 'gateway.thingsboard-port-required' | translate }} - + {{ 'gateway.thingsboard-port-min' | translate }} - + {{ 'gateway.thingsboard-port-max' | translate }} - + {{ 'gateway.thingsboard-port-pattern' | translate }} @@ -485,13 +482,13 @@ info_outlined - + {{ 'gateway.grpc-keep-alive-timeout-required' | translate }} - + {{ 'gateway.grpc-keep-alive-timeout-min' | translate }} - + {{ 'gateway.grpc-keep-alive-timeout-pattern' | translate }} @@ -503,13 +500,13 @@ info_outlined - + {{ 'gateway.grpc-keep-alive-required' | translate }} - + {{ 'gateway.grpc-keep-alive-min' | translate }} - + {{ 'gateway.grpc-keep-alive-pattern' | translate }} @@ -519,13 +516,13 @@ info_outlined - + {{ 'gateway.grpc-min-time-between-pings-required' | translate }} - + {{ 'gateway.grpc-min-time-between-pings-min' | translate }} - + {{ 'gateway.grpc-min-time-between-pings-pattern' | translate }} @@ -537,13 +534,13 @@ info_outlined - + {{ 'gateway.grpc-max-pings-without-data-required' | translate }} - + {{ 'gateway.grpc-max-pings-without-data-min' | translate }} - + {{ 'gateway.grpc-max-pings-without-data-pattern' | translate }} @@ -553,13 +550,13 @@ info_outlined - + {{ 'gateway.grpc-min-ping-interval-without-data-required' | translate }} - + {{ 'gateway.grpc-min-ping-interval-without-data-min' | translate }} - + {{ 'gateway.grpc-min-ping-interval-without-data-pattern' | translate }} @@ -580,15 +577,15 @@ gateway.statistics.send-period + *ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('required')"> {{ 'gateway.statistics.send-period-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('min')"> {{ 'gateway.statistics.send-period-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('pattern')"> {{ 'gateway.statistics.send-period-pattern' | translate }} @@ -643,7 +640,7 @@
@@ -665,7 +662,7 @@
+ [class.no-padding-bottom]="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value">
@@ -673,20 +670,20 @@
+ *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value"> gateway.inactivity-timeout-seconds + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('required')"> {{ 'gateway.inactivity-timeout-seconds-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')"> {{ 'gateway.inactivity-timeout-seconds-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('pattern')"> {{ 'gateway.inactivity-timeout-seconds-pattern' | translate }} gateway.inactivity-check-period-seconds + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')"> {{ 'gateway.inactivity-check-period-seconds-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')"> {{ 'gateway.inactivity-check-period-seconds-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('pattern')"> {{ 'gateway.inactivity-check-period-seconds-pattern' | translate }} gateway.min-pack-send-delay - + {{ 'gateway.min-pack-send-delay-required' | translate }} - + {{ 'gateway.min-pack-send-delay-min' | translate }} - {{ 'gateway.min-pack-send-delay-min-pattern' | translate }} + *ngIf="basicFormGroup.get('thingsboard.minPackSendDelayMS').hasError('pattern')"> + {{ 'gateway.min-pack-send-delay-pattern' | translate }} info_outlined @@ -737,13 +734,13 @@ gateway.mqtt-qos - + {{ 'gateway.mqtt-qos-required' | translate }} - + {{ 'gateway.mqtt-qos-range' | translate }} - + {{ 'gateway.mqtt-qos-range' | translate }} gateway.statistics.check-connectors-configuration + *ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')"> {{ 'gateway.statistics.check-connectors-configuration-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')"> {{ 'gateway.statistics.check-connectors-configuration-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')"> {{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }} @@ -772,15 +769,15 @@ gateway.statistics.max-payload-size-bytes + *ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('required')"> {{ 'gateway.statistics.max-payload-size-bytes-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('min')"> {{ 'gateway.statistics.max-payload-size-bytes-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('pattern')"> {{ 'gateway.statistics.max-payload-size-bytes-pattern' | translate }} gateway.statistics.min-pack-size-to-send + *ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('required')"> {{ 'gateway.statistics.min-pack-size-to-send-required' | translate }} + *ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('min')"> {{ 'gateway.statistics.min-pack-size-to-send-min' | translate }} + *ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('pattern')"> {{ 'gateway.statistics.min-pack-size-to-send-pattern' | translate }} -
- - -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss similarity index 93% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss index 9d72d1d759..9807fb71c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss @@ -23,6 +23,13 @@ display: flex; flex-direction: column; gap: 16px; + max-height: 70vh; + } + + .dialog-mode { + .configuration-block { + max-height: 60vh; + } } .mat-toolbar { @@ -62,15 +69,6 @@ } } - .actions { - grid-row: 3; - padding: 8px; - display: flex; - gap: 8px; - justify-content: flex-end; - flex: 1; - } - mat-form-field { mat-error { display: none !important; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts similarity index 52% rename from ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts rename to ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts index 55121da8c5..4b47dd15c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts @@ -14,29 +14,37 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; 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, MatDialogRef } from '@angular/material/dialog'; -import { AttributeService } from '@core/http/attribute.service'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +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 { Observable, of } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; -import { NULL_UUID } from '@shared/models/id/has-uuid'; import { GatewayLogLevel, GecurityTypesTranslationsMap, @@ -46,51 +54,228 @@ import { LogSavingPeriodTranslations, SecurityTypes, StorageTypes, - StorageTypesTranslationMap -} from './gateway-widget.models'; -import { deepTrim } from '@core/utils'; + StorageTypesTranslationMap, +} from '../../gateway-widget.models'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { + GatewayConfigCommand, + GatewayConfigValue, LogConfig +} from '@home/components/widget/lib/gateway/configuration/models/gateway-configuration.models'; @Component({ - selector: 'tb-gateway-configuration', - templateUrl: './gateway-configuration.component.html', - styleUrls: ['./gateway-configuration.component.scss'] + 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 GatewayConfigurationComponent implements OnInit { +export class GatewayBasicConfigurationComponent implements OnDestroy, ControlValueAccessor, Validators { + + @Input() + device: EntityId; + + @coerceBoolean() + @Input() + dialogMode = false; - gatewayConfigGroup: FormGroup; + @Output() + initialCredentialsUpdated = new EventEmitter(); StorageTypes = StorageTypes; storageTypes = Object.values(StorageTypes) as StorageTypes[]; storageTypesTranslationMap = StorageTypesTranslationMap; - logSavingPeriods = LogSavingPeriodTranslations; - localLogsConfigs = Object.keys(LocalLogsConfigs) as LocalLogsConfigs[]; localLogsConfigTranslateMap = LocalLogsConfigTranslateMap; - securityTypes = GecurityTypesTranslationsMap; - gatewayLogLevel = Object.values(GatewayLogLevel); - - @Input() - device: EntityId; - - @Input() - dialogRef: MatDialogRef; - logSelector: FormControl; + basicFormGroup: FormGroup; + + private onChange: (value: GatewayConfigValue) => void; + private onTouched: () => void; - private initialCredentials: DeviceCredentials; + private destroy$ = new Subject(); constructor(private fb: FormBuilder, - private attributeService: AttributeService, private deviceService: DeviceService, private cd: ChangeDetectorRef, private dialog: MatDialog) { + this.initBasicFormGroup(); + 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}); + if (basicConfig?.thingsboard?.security) { + this.checkAndFetchCredentials(basicConfig.thingsboard.security); + } + if (basicConfig?.grpc) { + this.toggleRpcFields(basicConfig.grpc.enabled); + } + const commands = basicConfig?.thingsboard?.statistics?.commands || []; + commands.forEach(command => 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}; + }; } - ngOnInit() { - this.gatewayConfigGroup = this.fb.group({ + 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, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + gatewayName: gateway.name + } + }).afterClosed().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.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]+$/)]], @@ -125,8 +310,14 @@ export class GatewayConfigurationComponent implements OnInit { }), storage: this.fb.group({ type: [StorageTypes.MEMORY, [Validators.required]], - read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], - max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]], + read_records_count: [ + 100, [Validators.min(1), + Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)] + ], + max_records_count: [ + 100000, + [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)] + ], 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]+$/)]], @@ -160,18 +351,18 @@ export class GatewayConfigurationComponent implements OnInit { }) }); - this.gatewayConfigGroup.get('thingsboard.security.password').valueChanges.subscribe(password => { + this.basicFormGroup.get('thingsboard.security.password').valueChanges.subscribe(password => { if (password && password !== '') { - this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]); + this.basicFormGroup.get('thingsboard.security.username').setValidators([Validators.required]); } else { - this.gatewayConfigGroup.get('thingsboard.security.username').clearValidators(); + this.basicFormGroup.get('thingsboard.security.username').clearValidators(); } - this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); }); this.toggleRpcFields(false); - this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => { + this.basicFormGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => { if (!enabled) { this.openConfigurationConfirmDialog(); } @@ -180,15 +371,17 @@ export class GatewayConfigurationComponent implements OnInit { this.logSelector = this.fb.control(LocalLogsConfigs.service); for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) { - this.addLocalLogConfig(localLogsConfigsKey, {}); + this.addLocalLogConfig(localLogsConfigsKey, {} as LogConfig); } - const checkingDeviceActivityGroup = this.gatewayConfigGroup.get('thingsboard.checkingDeviceActivity') as FormGroup; + const checkingDeviceActivityGroup = this.basicFormGroup.get('thingsboard.checkingDeviceActivity') as FormGroup; checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => { checkingDeviceActivityGroup.updateValueAndValidity(); if (enabled) { - checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); - checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); + checkingDeviceActivityGroup.get('inactivityTimeoutSeconds') + .setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); + checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds') + .setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]); } else { checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators(); checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators(); @@ -197,11 +390,11 @@ export class GatewayConfigurationComponent implements OnInit { checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({emitEvent: false}); }); - this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => { + this.basicFormGroup.get('grpc.enabled').valueChanges.subscribe(value => { this.toggleRpcFields(value); }); - const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; + const securityGroup = this.basicFormGroup.get('thingsboard.security') as FormGroup; securityGroup.get('type').valueChanges.subscribe(type => { this.removeAllSecurityValidators(); if (type === SecurityTypes.ACCESS_TOKEN) { @@ -229,7 +422,7 @@ export class GatewayConfigurationComponent implements OnInit { securityGroup.get('privateKey').valueChanges.subscribe(() => this.cd.detectChanges()); securityGroup.get('cert').valueChanges.subscribe(() => this.cd.detectChanges()); - const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; + const storageGroup = this.basicFormGroup.get('storage') as FormGroup; storageGroup.get('type').valueChanges.subscribe(type => { this.removeAllStorageValidators(); if (type === StorageTypes.MEMORY) { @@ -262,403 +455,32 @@ export class GatewayConfigurationComponent implements OnInit { storageGroup.get('messages_ttl_in_days').updateValueAndValidity({emitEvent: false}); } }); - - this.fetchConfigAttribute(this.device); - } - - 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 fetchConfigAttribute(entityId: EntityId) { - if (entityId.id === NULL_UUID) { - return; - } - this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, - ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe( - mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( - entityId, AttributeScope.SHARED_SCOPE, ['general_configuration', 'grpc_configuration', - 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel'])) - ).subscribe(attributes => { - if (attributes.length) { - const general_configuration = attributes.find(attribute => attribute.key === 'general_configuration')?.value; - const grpc_configuration = attributes.find(attribute => attribute.key === 'grpc_configuration')?.value; - const logs_configuration = attributes.find(attribute => attribute.key === 'logs_configuration')?.value; - const storage_configuration = attributes.find(attribute => attribute.key === 'storage_configuration')?.value; - const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel')?.value; - if (general_configuration) { - const configObj = {thingsboard: general_configuration}; - if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) { - for (const command of configObj.thingsboard.statistics.commands) { - this.addCommand(command); - } - delete configObj.thingsboard.statistics.commands; - } - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - this.gatewayConfigGroup.markAsPristine(); - if (!configObj.thingsboard.remoteConfiguration) { - this.gatewayConfigGroup.disable({emitEvent: false}); - } - this.checkAndFetchCredentials(configObj.thingsboard.security); - } - if (grpc_configuration) { - const configObj = {grpc: grpc_configuration}; - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - this.toggleRpcFields(grpc_configuration.enabled); - } - if (logs_configuration) { - const configObj = {logs: this.logsToObj(logs_configuration)}; - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - this.cd.detectChanges(); - } - if (storage_configuration) { - const configObj = {storage: storage_configuration}; - this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); - } - if (remoteLoggingLevel) { - const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); - remoteLogsFormGroup.patchValue({ - enabled: remoteLoggingLevel !== GatewayLogLevel.NONE, - logLevel: remoteLoggingLevel - }, {emitEvent: false}); - remoteLogsFormGroup.markAsPristine(); - } - this.cd.detectChanges(); - } else { - this.checkAndFetchCredentials(); - } - }); } private checkAndFetchCredentials(security: any = {}): void { if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) { this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => { - this.initialCredentials = credentials; + this.initialCredentialsUpdated.emit(credentials); if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) { - this.gatewayConfigGroup.get('thingsboard.security.type').setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN - ? SecurityTypes.TLS_ACCESS_TOKEN - : SecurityTypes.ACCESS_TOKEN); - this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId); + this.basicFormGroup.get('thingsboard.security.type') + .setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN + ? SecurityTypes.TLS_ACCESS_TOKEN + : SecurityTypes.ACCESS_TOKEN, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId, {emitEvent: false}); if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) { - this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert); + this.basicFormGroup.get('thingsboard.security.caCert').setValue(security.caCert, {emitEvent: false}); } } else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) { const parsedValue = JSON.parse(credentials.credentialsValue); - this.gatewayConfigGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD); - this.gatewayConfigGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId); - this.gatewayConfigGroup.get('thingsboard.security.username').setValue(parsedValue.userName); - this.gatewayConfigGroup.get('thingsboard.security.password').setValue(parsedValue.password, {emitEvent: false}); + this.basicFormGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD, {emitEvent: false}); + 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}); } else if (credentials.credentialsType === DeviceCredentialsType.X509_CERTIFICATE) { //if sertificate is present set sertificate as present } }); } } - - private logsToObj(logsConfig: any) { - const logsObject = { - local: {} - }; - const logFormat = logsConfig.formatters.LogFormatter.format; - const dateFormat = logsConfig.formatters.LogFormatter.datefmt; - for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) { - const handlerKey = localLogsConfigsKey + 'Handler'; - logsObject[localLogsConfigsKey] = { - logLevel: logsConfig.loggers[localLogsConfigsKey].level, - filePath: logsConfig.handlers[handlerKey].filename.split('/' + localLogsConfigsKey)[0], - backupCount: logsConfig.handlers[handlerKey].backupCount, - savingTime: logsConfig.handlers[handlerKey].interval, - savingPeriod: logsConfig.handlers[handlerKey].when, - }; - } - - - return {local: logsObject, logFormat, dateFormat}; - } - - private toggleRpcFields(enable: boolean) { - const grpcGroup = this.gatewayConfigGroup.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}); - } - } - - - addCommand(command: any = {}): void { - const commandsFormArray = this.commandFormArray(); - const commandFormGroup = this.fb.group({ - attributeOnGateway: [command.attributeOnGateway || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]], - command: [command.command || null, [Validators.required, Validators.pattern(/^(?=\S).*\S$/)]], - timeout: [command.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]], - }); - commandsFormArray.push(commandFormGroup); - } - - private addLocalLogConfig(name: string, config: any): void { - const localLogsFormGroup = this.gatewayConfigGroup.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.gatewayConfigGroup.get(`logs.local.${value}`) as FormGroup; - } - - commandFormArray(): FormArray { - return this.gatewayConfigGroup.get('thingsboard.statistics.commands') as FormArray; - } - - removeCommandControl(index: number, event: any): void { - if (event.pointerType === '') { - return; - } - this.commandFormArray().removeAt(index); - this.gatewayConfigGroup.markAsDirty(); - } - - private removeAllSecurityValidators(): void { - const securityGroup = this.gatewayConfigGroup.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.gatewayConfigGroup.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 removeEmpty(obj: any) { - return Object.fromEntries( - Object.entries(obj) - .filter(([_, v]) => v != null) - .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v]) - ); - } - - private generateLogsFile(logsObj: any) { - 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: 'DEBUG', - 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() - }; - for (const key of Object.keys(logsObj.local)) { - logAttrObj.handlers[key + 'Handler'] = this.createHandlerObj(logsObj.local[key], key); - logAttrObj.loggers[key] = this.createLoggerObj(logsObj.local[key], key); - } - return logAttrObj; - } - - private createHandlerObj(logObj: any, 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: any, key: string) { - return { - handlers: [`${key}Handler`, 'consoleHandler'], - level: logObj.logLevel, - propagate: false - }; - } - - saveConfig(): void { - const value = deepTrim(this.removeEmpty(this.gatewayConfigGroup.value)); - value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands); - const attributes = []; - attributes.push({ - key: 'RemoteLoggingLevel', - value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE - }); - delete value.connectors; - attributes.push({ - key: 'logs_configuration', - value: this.generateLogsFile(value.logs) - }); - value.grpc.ts = new Date().getTime(); - attributes.push({ - key: 'grpc_configuration', - value: value.grpc - }); - value.storage.ts = new Date().getTime(); - attributes.push({ - key: 'storage_configuration', - value: value.storage - }); - value.thingsboard.ts = new Date().getTime(); - attributes.push({ - key: 'general_configuration', - value: value.thingsboard - }); - - - this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { - this.updateCredentials(value.thingsboard.security).subscribe(() => { - if (this.dialogRef) { - this.dialogRef.close(); - } else { - this.gatewayConfigGroup.markAsPristine(); - this.cd.detectChanges(); - } - }); - }); - } - - private updateCredentials(securityConfig: any): Observable { - let updateCredentials = false; - let newCredentials = {}; - if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) { - if (this.initialCredentials.credentialsType !== DeviceCredentialsType.MQTT_BASIC) { - updateCredentials = true; - } else { - const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue); - updateCredentials = !( - parsedCredentials.clientId === securityConfig.clientId && - parsedCredentials.userName === securityConfig.username && - parsedCredentials.password === securityConfig.password); - } - if (updateCredentials) { - const credentialsValue: { clientId?: string; userName?: string; password?: string } = {}; - const credentialsType = DeviceCredentialsType.MQTT_BASIC; - if (securityConfig.clientId) { - credentialsValue.clientId = securityConfig.clientId; - } - if (securityConfig.username) { - credentialsValue.userName = securityConfig.username; - } - if (securityConfig.password) { - credentialsValue.password = securityConfig.password; - } - newCredentials = { - credentialsType, - credentialsValue: JSON.stringify(credentialsValue) - }; - } - } else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) { - if (this.initialCredentials.credentialsType !== DeviceCredentialsType.ACCESS_TOKEN) { - updateCredentials = true; - } else { - updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken; - if (updateCredentials) { - this.initialCredentials.credentialsId = securityConfig.accessToken; - } - } - if (updateCredentials) { - newCredentials = { - credentialsType: DeviceCredentialsType.ACCESS_TOKEN, - credentialsId: securityConfig.accessToken - }; - } - } - - if (updateCredentials) { - return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials}); - } - return of(null); - } - - cancel(): void { - if (this.dialogRef) { - this.dialogRef.close(); - } - } - - private openConfigurationConfirmDialog(): void { - this.deviceService.getDevice(this.device.id).subscribe(gateway => { - this.dialog.open - (GatewayRemoteConfigurationDialogComponent, { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - gatewayName: gateway.name - } - }).afterClosed().subscribe( - (res) => { - if (!res) { - this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false}); - } - } - ); - }); - } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html new file mode 100644 index 0000000000..01fc12f167 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html @@ -0,0 +1,64 @@ + +
+
+ +
+

gateway.gateway-configuration

+
+ + + {{ 'gateway.basic' | translate }} + + + {{ 'gateway.advanced' | translate }} + + + +
+
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss new file mode 100644 index 0000000000..4d1d62ddad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts new file mode 100644 index 0000000000..317b616110 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts @@ -0,0 +1,394 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT 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 { MatDialog, 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, + GatewayLogsConfig, + 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; + + initialCredentials: DeviceCredentials; + gatewayConfigGroup: FormGroup; + ConfigurationModes = ConfigurationModes; + + private destroy$ = new Subject(); + 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, + private dialog: MatDialog) { + + 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)) { + 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)) { + 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, 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 { + let newCredentials: Partial = {}; + + 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 { + 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): Record { + return Object.fromEntries( + Object.entries(obj) + .filter(([_, v]) => v != null) + .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v as Record) : 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: 'DEBUG', + 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: null, + grpc: null, + logs: null, + storage: null, + 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 }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts new file mode 100644 index 0000000000..a23d8e7900 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts @@ -0,0 +1,164 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT 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: { + 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; + }; + grpc: { + enabled: boolean; + serverPort: number; + keepAliveTimeMs: number; + keepAliveTimeoutMs: number; + keepalivePermitWithoutCalls: boolean; + maxPingsWithoutData: number; + minTimeBetweenPingsMs: number; + minPingIntervalWithoutDataMs: number; + }; + connectors?: GatewayConnector[]; + logs: GatewayLogsConfig; +} + +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; + +interface LogFormatterConfig { + class: string; + format: string; + datefmt: string; +} + +interface StreamHandlerConfig { + class: string; + formatter: string; + level: string; + 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; +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index 2915cb831e..c4cbf9d698 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -52,7 +52,7 @@ import { AddConnectorConfigData, ConnectorBaseConfig, ConnectorBaseInfo, - ConnectorConfigurationModes, + ConfigurationModes, ConnectorType, GatewayAttributeData, GatewayConnector, @@ -97,13 +97,13 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie readonly gatewayLogLevel = Object.values(GatewayLogLevel); readonly displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; readonly GatewayConnectorTypesTranslatesMap = GatewayConnectorDefaultTypesTranslatesMap; - readonly ConnectorConfigurationModes = ConnectorConfigurationModes; + readonly ConnectorConfigurationModes = ConfigurationModes; pageLink: PageLink; dataSource: MatTableDataSource; connectorForm: FormGroup; activeConnectors: Array; - mode: ConnectorConfigurationModes = this.ConnectorConfigurationModes.BASIC; + mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC; initialConnector: GatewayConnector; private inactiveConnectors: Array; @@ -330,7 +330,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private clearOutConnectorForm(): void { this.initialConnector = null; this.connectorForm.setValue({ - mode: ConnectorConfigurationModes.BASIC, + mode: ConfigurationModes.BASIC, name: '', type: ConnectorType.MQTT, sendDataOnlyOnChange: false, @@ -516,7 +516,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private initConnectorForm(): void { this.connectorForm = this.fb.group({ - mode: [ConnectorConfigurationModes.BASIC], + mode: [ConfigurationModes.BASIC], name: ['', [Validators.required, this.uniqNameRequired(), Validators.pattern(noLeadTrailSpacesRegex)]], type: ['', [Validators.required]], enableRemoteLogging: [false], @@ -680,7 +680,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie 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 === ConnectorConfigurationModes.BASIC) { + 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}); } @@ -697,7 +697,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie 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 === ConnectorConfigurationModes.ADVANCED) { + if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConfigurationModes.ADVANCED) { this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false}); } }); @@ -738,7 +738,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie case ConnectorType.MQTT: case ConnectorType.OPCUA: case ConnectorType.MODBUS: - this.connectorForm.get('mode').setValue(connector.mode || ConnectorConfigurationModes.BASIC, {emitEvent: false}); + this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false}); setTimeout(() => { this.connectorForm.patchValue(connector, {emitEvent: false}); this.connectorForm.markAsPristine(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 2dde0b8f54..4b0f684bc7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -123,7 +123,7 @@ export interface GatewayConnector { logLevel: string; key?: string; class?: string; - mode?: ConnectorConfigurationModes; + mode?: ConfigurationModes; } export interface DataMapping { @@ -493,7 +493,7 @@ export interface ModbusSlaveInfo { buttonTitle: string; } -export enum ConnectorConfigurationModes { +export enum ConfigurationModes { BASIC = 'basic', ADVANCED = 'advanced' } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 955fd1ecf6..2acc299586 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -56,7 +56,7 @@ import { GatewayServiceRPCConnectorTemplatesComponent } from '@home/components/widget/lib/gateway/gateway-service-rpc-connector-templates.component'; import { DeviceGatewayCommandComponent } from '@home/components/widget/lib/gateway/device-gateway-command.component'; -import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/gateway-configuration.component'; +import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/configuration/gateway-configuration.component'; import { GatewayRemoteConfigurationDialogComponent } from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog'; @@ -145,6 +145,12 @@ import { ModbusRpcParametersComponent } from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; +import { + GatewayBasicConfigurationComponent +} from '@home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component'; +import { + GatewayAdvancedConfigurationComponent +} from '@home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component'; @NgModule({ declarations: [ @@ -233,6 +239,8 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc ModbusBasicConfigComponent, EllipsisChipListDirective, ModbusRpcParametersComponent, + GatewayBasicConfigurationComponent, + GatewayAdvancedConfigurationComponent, ], exports: [ EntitiesTableWidgetComponent,