Browse Source

Merge pull request #11462 from maxunbearable/improvement/4193-gateway-config-advanced

Added Flow to edit Gateway Config as JSON in Advanced config tab
pull/11569/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
53efb6747e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/src/main/data/json/tenant/dashboards/gateways.json
  2. 25
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.html
  3. 23
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.scss
  4. 95
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/advanced/gateway-advanced-configuration.component.ts
  5. 223
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html
  6. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.scss
  7. 676
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts
  8. 64
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.html
  9. 64
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.scss
  10. 394
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts
  11. 164
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts
  12. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  13. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  14. 10
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

2
application/src/main/data/json/tenant/dashboards/gateways.json

@ -200,7 +200,7 @@
"useShowWidgetActionFunction": null,
"showWidgetActionFunction": "return true;",
"type": "customPretty",
"customHtml": "<mat-toolbar fxLayout=\"row\" color=\"primary\">\n <h2 translate>gateway.gateway-configuration</h2>\n <span fxFlex></span>\n <button mat-icon-button (click)=\"cancel()\" type=\"button\">\n <mat-icon class=\"material-icons\">close</mat-icon>\n </button>\n</mat-toolbar>\n<mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n</mat-progress-bar>\n<div style=\"height: 4px;\" *ngIf=\"!(isLoading$ | async)\"></div>\n<tb-gateway-configuration\n class=\"gateway-config\"\n mat-dialog-content\n [device]=\"device\"\n [dialogRef]=\"dialogRef\">\n</tb-gateway-configuration>",
"customHtml": "<mat-progress-bar color=\"warn\" mode=\"indeterminate\" *ngIf=\"isLoading$ | async\">\n</mat-progress-bar>\n<tb-gateway-configuration\n class=\"gateway-config\"\n mat-dialog-content\n [device]=\"device\"\n [dialogRef]=\"dialogRef\">\n</tb-gateway-configuration>",
"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": [],

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

@ -0,0 +1,25 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<tb-json-object-edit
fillHeight="true"
class="tb-flex config-container"
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
[formControl]="advancedFormControl"
/>

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

@ -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;
}
}

95
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<void>();
constructor(private fb: FormBuilder) {
this.advancedFormControl = this.fb.control('');
this.advancedFormControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: (value: unknown) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
writeValue(basicConfig: unknown): void {
this.advancedFormControl.reset(basicConfig, {emitEvent: false});
}
validate(): ValidationErrors | null {
return this.advancedFormControl.valid ? null : {
advancedFormControl: {valid: false}
};
}
}

223
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html → 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.
-->
<mat-toolbar color="primary" *ngIf="!dialogRef">
<h2 translate>gateway.gateway-configuration</h2>
</mat-toolbar>
<mat-tab-group class="tab-group-block" [formGroup]="gatewayConfigGroup">
<mat-tab-group class="tab-group-block" [formGroup]="basicFormGroup" [class.dialog-mode]="dialogMode">
<mat-tab label="{{ 'gateway.general' | translate }}">
<ng-template matTabContent>
<div formGroupName="thingsboard" class="mat-content mat-padding configuration-block">
@ -42,23 +39,23 @@
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.host' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.host').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.host').hasError('required')">
{{ 'gateway.thingsboard-host-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.thingsboard-port</mat-label>
<input matInput formControlName="port" type="number" min="0"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.port').hasError('required')">
{{ 'gateway.thingsboard-port-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.port').hasError('min')">
{{ 'gateway.thingsboard-port-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('max')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.port').hasError('max')">
{{ 'gateway.thingsboard-port-max' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.port').hasError('pattern')">
{{ 'gateway.thingsboard-port-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -76,17 +73,17 @@
</tb-toggle-option>
</tb-toggle-select>
<mat-form-field appearance="outline"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('accesstoken')">
*ngIf="basicFormGroup.get('thingsboard.security.type').value.toLowerCase().includes('accesstoken')">
<mat-label translate>security.access-token</mat-label>
<input matInput formControlName="accessToken"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.security.accessToken').hasError('required')">
{{ 'security.access-token-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.accessToken').value"
*ngIf="basicFormGroup.get('thingsboard.security.accessToken').value"
[copyText]="basicFormGroup.get('thingsboard.security.accessToken').value"
tooltipText="{{ 'device.copy-access-token' | translate }}"
tooltipPosition="above"
icon="content_copy">
@ -97,18 +94,18 @@
</mat-form-field>
<section>
<div class="tb-form-row no-border no-padding tb-standard-fields"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'">
*ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>security.clientId</mat-label>
<input matInput formControlName="clientId"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.security.clientId').hasError('required')">
{{ 'security.clientId-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.clientId').value"
*ngIf="basicFormGroup.get('thingsboard.security.clientId').value"
[copyText]="basicFormGroup.get('thingsboard.security.clientId').value"
tooltipText="{{ 'gateway.copy-client-id' | translate }}"
tooltipPosition="above"
icon="content_copy">
@ -120,14 +117,14 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>security.username</mat-label>
<input matInput formControlName="username"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.username').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.security.username').hasError('required')">
{{ 'security.username-required' | translate }}
</mat-error>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.username').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.username').value"
*ngIf="basicFormGroup.get('thingsboard.security.username').value"
[copyText]="basicFormGroup.get('thingsboard.security.username').value"
tooltipText="{{ 'gateway.copy-username' | translate }}"
tooltipPosition="above"
icon="content_copy">
@ -138,14 +135,14 @@
</mat-form-field>
</div>
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 100%"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'">
*ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'">
<mat-label translate>gateway.password</mat-label>
<input matInput formControlName="password"/>
<tb-copy-button
matSuffix
miniButton="false"
*ngIf="gatewayConfigGroup.get('thingsboard.security.password').value"
[copyText]="gatewayConfigGroup.get('thingsboard.security.password').value"
*ngIf="basicFormGroup.get('thingsboard.security.password').value"
[copyText]="basicFormGroup.get('thingsboard.security.password').value"
tooltipText="{{ 'gateway.copy-password' | translate }}"
tooltipPosition="above"
icon="content_copy">
@ -156,13 +153,13 @@
</mat-form-field>
</section>
<tb-error style="margin-top: -12px; display: block;" fxFlex="100"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'"
[error]="gatewayConfigGroup.get('thingsboard.security').hasError('atLeastOne') ?
*ngIf="basicFormGroup.get('thingsboard.security.type').value === 'usernamePassword'"
[error]="basicFormGroup.get('thingsboard.security').hasError('atLeastOne') ?
('device.client-id-or-user-name-necessary' | translate) : ''"></tb-error>
<tb-file-input
fxFlex="100"
hint="{{ 'gateway.hints.ca-cert' | translate }}"
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')"
*ngIf="basicFormGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')"
formControlName="caCert"
label="{{ 'security.ca-cert' | translate }}"
[allowedExtensions]="'pem, cert, key'"
@ -182,7 +179,7 @@
<mat-form-field appearance="outline">
<mat-label translate>gateway.logs.date-format</mat-label>
<input matInput formControlName="dateFormat"/>
<mat-error *ngIf="gatewayConfigGroup.get('logs.dateFormat').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('logs.dateFormat').hasError('required')">
{{ 'gateway.logs.date-format-required' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -192,7 +189,7 @@
<mat-form-field appearance="outline">
<mat-label translate>gateway.logs.log-format</mat-label>
<textarea matInput formControlName="logFormat" rows="2"></textarea>
<mat-error *ngIf="gatewayConfigGroup.get('logs.logFormat').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('logs.logFormat').hasError('required')">
{{ 'gateway.logs.log-format-required' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -233,7 +230,7 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.logs.file-path</mat-label>
<input matInput formControlName="filePath"/>
<mat-error *ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.filePath').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.filePath').hasError('required')">
{{ 'gateway.logs.file-path-required' | translate }}
</mat-error>
</mat-form-field>
@ -244,11 +241,11 @@
<mat-label translate>gateway.logs.saving-period</mat-label>
<input matInput formControlName="savingTime" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('required')">
*ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('required')">
{{ 'gateway.logs.saving-period-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('min')">
*ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('min')">
{{ 'gateway.logs.saving-period-min' | translate }}
</mat-error>
</mat-form-field>
@ -264,11 +261,11 @@
<mat-label translate>gateway.logs.backup-count</mat-label>
<input matInput formControlName="backupCount" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('required')">
*ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('required')">
{{ 'gateway.logs.backup-count-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')">
*ngIf="basicFormGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')">
{{ 'gateway.logs.backup-count-min' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -292,19 +289,19 @@
{{ storageTypesTranslationMap.get(storageType) | translate }}
</tb-toggle-option>
</tb-toggle-select>
<div class="tb-form-panel-hint">{{ 'gateway.hints.' + gatewayConfigGroup.get('storage.type').value | translate }}</div>
<ng-container [ngSwitch]="gatewayConfigGroup.get('storage.type').value">
<div class="tb-form-panel-hint">{{ 'gateway.hints.' + basicFormGroup.get('storage.type').value | translate }}</div>
<ng-container [ngSwitch]="basicFormGroup.get('storage.type').value">
<section *ngSwitchCase="StorageTypes.MEMORY" class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-read-record-count</mat-label>
<input type="number" matInput formControlName="read_records_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('storage.read_records_count').hasError('required')">
{{ 'gateway.storage-read-record-count-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('storage.read_records_count').hasError('min')">
{{ 'gateway.storage-read-record-count-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('storage.read_records_count').hasError('pattern')">
{{ 'gateway.storage-read-record-count-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -314,13 +311,13 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-max-records</mat-label>
<input type="number" matInput formControlName="max_records_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('storage.max_records_count').hasError('required')">
{{ 'gateway.storage-max-records-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('storage.max_records_count').hasError('min')">
{{ 'gateway.storage-max-records-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('storage.max_records_count').hasError('pattern')">
{{ 'gateway.storage-max-records-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -333,7 +330,7 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-data-folder-path</mat-label>
<input matInput formControlName="data_folder_path"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.data_folder_path').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('storage.data_folder_path').hasError('required')">
{{ 'gateway.storage-data-folder-path-required' | translate }}
</mat-error>
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" aria-hidden="false"
@ -345,13 +342,13 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-max-files</mat-label>
<input matInput type="number" formControlName="max_file_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('storage.max_file_count').hasError('required')">
{{ 'gateway.storage-max-files-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('storage.max_file_count').hasError('min')">
{{ 'gateway.storage-max-files-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('storage.max_file_count').hasError('pattern')">
{{ 'gateway.storage-max-files-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -363,13 +360,13 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-max-read-record-count</mat-label>
<input matInput type="number" formControlName="max_read_records_count"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('storage.max_read_records_count').hasError('required')">
{{ 'gateway.storage-max-read-record-count-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('storage.max_read_records_count').hasError('min')">
{{ 'gateway.storage-max-read-record-count-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('storage.max_read_records_count').hasError('pattern')">
{{ 'gateway.storage-max-read-record-count-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -379,13 +376,13 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-max-file-records</mat-label>
<input matInput type="number" formControlName="max_records_per_file"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('storage.max_records_per_file').hasError('required')">
{{ 'gateway.storage-max-records-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('storage.max_records_per_file').hasError('min')">
{{ 'gateway.storage-max-records-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('storage.max_records_per_file').hasError('pattern')">
{{ 'gateway.storage-max-records-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -399,7 +396,7 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.storage-path</mat-label>
<input matInput formControlName="data_file_path"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.data_file_path').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('storage.data_file_path').hasError('required')">
{{ 'gateway.storage-path-required' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -409,13 +406,13 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.messages-ttl-check-in-hours</mat-label>
<input matInput type="number" formControlName="messages_ttl_check_in_hours"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_check_in_hours').hasError('required')">
{{ 'gateway.messages-ttl-check-in-hours-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_check_in_hours').hasError('min')">
{{ 'gateway.messages-ttl-check-in-hours-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_check_in_hours').hasError('pattern')">
{{ 'gateway.messages-ttl-check-in-hours-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -426,13 +423,13 @@
<mat-form-field appearance="outline" class="mat-block">
<mat-label translate>gateway.messages-ttl-in-days</mat-label>
<input matInput type="number" formControlName="messages_ttl_in_days"/>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_in_days').hasError('required')">
{{ 'gateway.messages-ttl-in-days-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_in_days').hasError('min')">
{{ 'gateway.messages-ttl-in-days-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('storage.messages_ttl_in_days').hasError('pattern')">
{{ 'gateway.messages-ttl-in-days-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -466,16 +463,16 @@
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.server-port' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('grpc.serverPort').hasError('required')">
{{ 'gateway.thingsboard-port-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('grpc.serverPort').hasError('min')">
{{ 'gateway.thingsboard-port-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('max')">
<mat-error *ngIf="basicFormGroup.get('grpc.serverPort').hasError('max')">
{{ 'gateway.thingsboard-port-max' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('grpc.serverPort').hasError('pattern')">
{{ 'gateway.thingsboard-port-pattern' | translate }}
</mat-error>
</mat-form-field>
@ -485,13 +482,13 @@
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-keep-alive-timeout' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeoutMs').hasError('required')">
{{ 'gateway.grpc-keep-alive-timeout-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeoutMs').hasError('min')">
{{ 'gateway.grpc-keep-alive-timeout-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeoutMs').hasError('pattern')">
{{ 'gateway.grpc-keep-alive-timeout-pattern' | translate }}
</mat-error>
</mat-form-field>
@ -503,13 +500,13 @@
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-keep-alive' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeMs').hasError('required')">
{{ 'gateway.grpc-keep-alive-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeMs').hasError('min')">
{{ 'gateway.grpc-keep-alive-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('grpc.keepAliveTimeMs').hasError('pattern')">
{{ 'gateway.grpc-keep-alive-pattern' | translate }}
</mat-error>
</mat-form-field>
@ -519,13 +516,13 @@
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-min-time-between-pings' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('grpc.minTimeBetweenPingsMs').hasError('required')">
{{ 'gateway.grpc-min-time-between-pings-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('grpc.minTimeBetweenPingsMs').hasError('min')">
{{ 'gateway.grpc-min-time-between-pings-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('grpc.minTimeBetweenPingsMs').hasError('pattern')">
{{ 'gateway.grpc-min-time-between-pings-pattern' | translate }}
</mat-error>
</mat-form-field>
@ -537,13 +534,13 @@
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-max-pings-without-data' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('grpc.maxPingsWithoutData').hasError('required')">
{{ 'gateway.grpc-max-pings-without-data-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('grpc.maxPingsWithoutData').hasError('min')">
{{ 'gateway.grpc-max-pings-without-data-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('grpc.maxPingsWithoutData').hasError('pattern')">
{{ 'gateway.grpc-max-pings-without-data-pattern' | translate }}
</mat-error>
</mat-form-field>
@ -553,13 +550,13 @@
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.grpc-min-ping-interval-without-data' | translate }}">info_outlined
</mat-icon>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('required')">
{{ 'gateway.grpc-min-ping-interval-without-data-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('min')">
{{ 'gateway.grpc-min-ping-interval-without-data-min' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('pattern')">
<mat-error *ngIf="basicFormGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('pattern')">
{{ 'gateway.grpc-min-ping-interval-without-data-pattern' | translate }}
</mat-error>
</mat-form-field>
@ -580,15 +577,15 @@
<mat-label translate>gateway.statistics.send-period</mat-label>
<input matInput formControlName="statsSendPeriodInSeconds" type="number" min="60"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('required')">
*ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('required')">
{{ 'gateway.statistics.send-period-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('min')">
*ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('min')">
{{ 'gateway.statistics.send-period-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('pattern')">
*ngIf="basicFormGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('pattern')">
{{ 'gateway.statistics.send-period-pattern' | translate }}
</mat-error>
</mat-form-field>
@ -643,7 +640,7 @@
</mat-form-field>
</section>
<button mat-icon-button (click)="removeCommandControl($index, $event)"
[disabled]="!gatewayConfigGroup.get('thingsboard.remoteConfiguration').value"
[disabled]="!basicFormGroup.get('thingsboard.remoteConfiguration').value"
matTooltip="{{ 'gateway.statistics.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
@ -652,7 +649,7 @@
<button mat-stroked-button color="primary"
style="width: fit-content;"
type="button"
[disabled]="!gatewayConfigGroup.get('thingsboard.remoteConfiguration').value"
[disabled]="!basicFormGroup.get('thingsboard.remoteConfiguration').value"
(click)="addCommand()">
{{ 'gateway.statistics.add' | translate }}
</button>
@ -665,7 +662,7 @@
<ng-template matTabContent>
<div formGroupName="thingsboard" class="mat-content mat-padding configuration-block">
<div class="tb-form-panel" formGroupName="checkingDeviceActivity"
[class.no-padding-bottom]="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value">
[class.no-padding-bottom]="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value">
<div tb-hint-tooltip-icon="{{ 'gateway.hints.check-device-activity' | translate }}"
class="tb-form-row no-border no-padding">
<mat-slide-toggle class="mat-slide" color="primary" formControlName="checkDeviceInactivity">
@ -673,20 +670,20 @@
</mat-slide-toggle>
</div>
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs"
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value">
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.inactivity-timeout-seconds</mat-label>
<input matInput formControlName="inactivityTimeoutSeconds" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('required')">
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('required')">
{{ 'gateway.inactivity-timeout-seconds-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')">
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')">
{{ 'gateway.inactivity-timeout-seconds-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('pattern')">
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('pattern')">
{{ 'gateway.inactivity-timeout-seconds-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -697,15 +694,15 @@
<mat-label translate>gateway.inactivity-check-period-seconds</mat-label>
<input matInput formControlName="inactivityCheckPeriodSeconds"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')">
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')">
{{ 'gateway.inactivity-check-period-seconds-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')">
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')">
{{ 'gateway.inactivity-check-period-seconds-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('pattern')">
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('pattern')">
{{ 'gateway.inactivity-check-period-seconds-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -720,15 +717,15 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.min-pack-send-delay</mat-label>
<input matInput formControlName="minPackSendDelayMS" type="number" min="0"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.minPackSendDelayMS').hasError('required')">
{{ 'gateway.min-pack-send-delay-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.minPackSendDelayMS').hasError('min')">
{{ 'gateway.min-pack-send-delay-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('pattern')">
{{ 'gateway.min-pack-send-delay-min-pattern' | translate }}
*ngIf="basicFormGroup.get('thingsboard.minPackSendDelayMS').hasError('pattern')">
{{ 'gateway.min-pack-send-delay-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.minimal-pack-delay' | translate }}">info_outlined
@ -737,13 +734,13 @@
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.mqtt-qos</mat-label>
<input matInput formControlName="qos" type="number" min="0" max="1"/>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('required')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.qos').hasError('required')">
{{ 'gateway.mqtt-qos-required' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('min')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.qos').hasError('min')">
{{ 'gateway.mqtt-qos-range' | translate }}
</mat-error>
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('max')">
<mat-error *ngIf="basicFormGroup.get('thingsboard.qos').hasError('max')">
{{ 'gateway.mqtt-qos-range' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -756,15 +753,15 @@
<mat-label translate>gateway.statistics.check-connectors-configuration</mat-label>
<input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')">
*ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')">
{{ 'gateway.statistics.check-connectors-configuration-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')">
*ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')">
{{ 'gateway.statistics.check-connectors-configuration-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')">
*ngIf="basicFormGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')">
{{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }}
</mat-error>
</mat-form-field>
@ -772,15 +769,15 @@
<mat-label translate>gateway.statistics.max-payload-size-bytes</mat-label>
<input matInput formControlName="maxPayloadSizeBytes" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.maxPayloadSizeBytes').hasError('required')">
*ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('required')">
{{ 'gateway.statistics.max-payload-size-bytes-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.maxPayloadSizeBytes').hasError('min')">
*ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('min')">
{{ 'gateway.statistics.max-payload-size-bytes-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.maxPayloadSizeBytes').hasError('pattern')">
*ngIf="basicFormGroup.get('thingsboard.maxPayloadSizeBytes').hasError('pattern')">
{{ 'gateway.statistics.max-payload-size-bytes-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -793,15 +790,15 @@
<mat-label translate>gateway.statistics.min-pack-size-to-send</mat-label>
<input matInput formControlName="minPackSizeToSend" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.minPackSizeToSend').hasError('required')">
*ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('required')">
{{ 'gateway.statistics.min-pack-size-to-send-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.minPackSizeToSend').hasError('min')">
*ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('min')">
{{ 'gateway.statistics.min-pack-size-to-send-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.minPackSizeToSend').hasError('pattern')">
*ngIf="basicFormGroup.get('thingsboard.minPackSizeToSend').hasError('pattern')">
{{ 'gateway.statistics.min-pack-size-to-send-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
@ -814,17 +811,3 @@
</ng-template>
</mat-tab>
</mat-tab-group>
<div class="actions">
<button mat-button color="primary"
type="button"
*ngIf="dialogRef"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
[disabled]="gatewayConfigGroup.invalid || !gatewayConfigGroup.dirty"
(click)="saveConfig()">
{{ 'action.save' | translate }}
</button>
</div>

16
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss → 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;

676
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts → 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<DeviceCredentials>();
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<any>;
logSelector: FormControl;
basicFormGroup: FormGroup;
private onChange: (value: GatewayConfigValue) => void;
private onTouched: () => void;
private initialCredentials: DeviceCredentials;
private destroy$ = new Subject<void>();
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, GatewayRemoteConfigurationDialogData>
(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<any> {
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, GatewayRemoteConfigurationDialogData>
(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});
}
}
);
});
}
}

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

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

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

@ -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);
}
}

394
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<GatewayConfigurationComponent>;
initialCredentials: DeviceCredentials;
gatewayConfigGroup: FormGroup;
ConfigurationModes = ConfigurationModes;
private destroy$ = new Subject<void>();
private readonly gatewayConfigAttributeKeys =
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel', 'mode'];
constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private deviceService: DeviceService,
private cd: ChangeDetectorRef,
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<string, unknown>, ts: new Date().getTime()};
addAttribute(key, val);
};
addAttribute('RemoteLoggingLevel', value.logs?.remote?.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE);
delete value.connectors;
addAttribute('logs_configuration', this.generateLogsFile(value.logs));
addTimestampedAttribute('grpc_configuration', value.grpc);
addTimestampedAttribute('storage_configuration', value.storage);
addTimestampedAttribute('general_configuration', value.thingsboard);
addAttribute('mode', value.mode);
return attributes;
}
private updateCredentials(securityConfig: GatewayConfigSecurity): Observable<DeviceCredentials> {
let newCredentials: Partial<DeviceCredentials> = {};
switch (securityConfig.type) {
case SecurityTypes.USERNAME_PASSWORD:
if (this.shouldUpdateCredentials(securityConfig)) {
newCredentials = this.generateMqttCredentials(securityConfig);
}
break;
case SecurityTypes.ACCESS_TOKEN:
case SecurityTypes.TLS_ACCESS_TOKEN:
if (this.shouldUpdateAccessToken(securityConfig)) {
newCredentials = {
credentialsType: DeviceCredentialsType.ACCESS_TOKEN,
credentialsId: securityConfig.accessToken
};
}
break;
}
return Object.keys(newCredentials).length
? this.deviceService.saveDeviceCredentials({ ...this.initialCredentials, ...newCredentials })
: of(null);
}
private shouldUpdateCredentials(securityConfig: GatewayConfigSecurity): boolean {
if (this.initialCredentials.credentialsType !== DeviceCredentialsType.MQTT_BASIC) {
return true;
}
const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue);
return !(
parsedCredentials.clientId === securityConfig.clientId &&
parsedCredentials.userName === securityConfig.username &&
parsedCredentials.password === securityConfig.password
);
}
private generateMqttCredentials(securityConfig: GatewayConfigSecurity): Partial<DeviceCredentials> {
const { clientId, username, password } = securityConfig;
const credentialsValue = {
...(clientId && { clientId }),
...(username && { userName: username }),
...(password && { password }),
};
return {
credentialsType: DeviceCredentialsType.MQTT_BASIC,
credentialsValue: JSON.stringify(credentialsValue)
};
}
private shouldUpdateAccessToken(securityConfig: GatewayConfigSecurity): boolean {
return this.initialCredentials.credentialsType !== DeviceCredentialsType.ACCESS_TOKEN ||
this.initialCredentials.credentialsId !== securityConfig.accessToken;
}
cancel(): void {
if (this.dialogRef) {
this.dialogRef.close();
}
}
private removeEmpty(obj: Record<string, unknown>): Record<string, unknown> {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v != null)
.map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v as Record<string, unknown>) : v])
);
}
private generateLogsFile(logsObj: GatewayLogsConfig): LogAttribute {
const logAttrObj = {
version: 1,
disable_existing_loggers: false,
formatters: {
LogFormatter: {
class: 'logging.Formatter',
format: logsObj.logFormat,
datefmt: logsObj.dateFormat,
}
},
handlers: {
consoleHandler: {
class: 'logging.StreamHandler',
formatter: 'LogFormatter',
level: '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 };
}
}

164
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<LocalLogsConfigs, LogConfig>;
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;
}

16
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<GatewayAttributeData>;
connectorForm: FormGroup;
activeConnectors: Array<string>;
mode: ConnectorConfigurationModes = this.ConnectorConfigurationModes.BASIC;
mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
private inactiveConnectors: Array<string>;
@ -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();

4
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'
}

10
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,

Loading…
Cancel
Save