Browse Source
* Create gateway widget * Refacoring and add styled * Update bundle * Add model and refactoringpull/2562/head
committed by
GitHub
19 changed files with 1465 additions and 61 deletions
@ -0,0 +1,262 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2020 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<form #formContainer class="gateway-form" |
||||
|
[formGroup]="gatewayConfigurationGroup" |
||||
|
tb-toast toastTarget="{{ toastTargetId }}" |
||||
|
(ngSubmit)="save()"> |
||||
|
<mat-accordion multi="true" class="mat-body-1"> |
||||
|
<mat-expansion-panel> |
||||
|
<mat-expansion-panel-header> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-panel-title">{{ 'gateway.thingsboard' | translate | uppercase }}</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
<tb-entity-gateway-select |
||||
|
formControlName="gateway" |
||||
|
[newGatewayType]="gatewayType" |
||||
|
(gatewayNameExist)="gatewayExist()" |
||||
|
required |
||||
|
> |
||||
|
</tb-entity-gateway-select> |
||||
|
<div fxLayout="column"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{'gateway.security-type' | translate }}</mat-label> |
||||
|
<mat-select formControlName="securityType" > |
||||
|
<mat-option *ngFor="let securityType of securityTypes | keyvalue" [value]="securityType.key"> |
||||
|
{{ securityType.value.toString() | translate }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.thingsboard-host' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="host"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('host').hasError('required')" translate> |
||||
|
gateway.thingsboard-host-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.thingsboard-port' | translate }}</mat-label> |
||||
|
<input matInput type="number" formControlName="port"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('required')" translate> |
||||
|
gateway.thingsboard-port-required |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('min')" translate> |
||||
|
gateway.thingsboard-port-min |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('max')" translate> |
||||
|
gateway.thingsboard-port-max |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('port').hasError('pattern')" translate> |
||||
|
gateway.thingsboard-port-pattern |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
|
||||
|
<div *ngIf="gatewayConfigurationGroup.get('securityType').value == 'tls'" fxLayout="column"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.tls-path-ca-certificate' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="caCertPath"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.tls-path-private-key' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="privateKeyPath"> |
||||
|
</mat-form-field> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.tls-path-client-certificate' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="certPath"> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
|
||||
|
<mat-checkbox formControlName="remoteConfiguration">{{ 'gateway.remote' | translate }}</mat-checkbox> |
||||
|
|
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{'gateway.remote-logging-level' | translate }}</mat-label> |
||||
|
<mat-select formControlName="remoteLoggingLevel"> |
||||
|
<mat-option *ngFor="let logLevel of gatewayLogLevels" [value]="logLevel"> |
||||
|
{{ logLevel }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.path-logs' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="remoteLoggingPathToLogs"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('remoteLoggingPathToLogs').hasError('required')" translate> |
||||
|
gateway.path-logs-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
|
||||
|
</mat-expansion-panel> |
||||
|
|
||||
|
<mat-expansion-panel> |
||||
|
<mat-expansion-panel-header> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-panel-title">{{ 'gateway.storage' | translate | uppercase }}</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
|
||||
|
<div fxLayout="column"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{'gateway.storage-type' | translate }}</mat-label> |
||||
|
<mat-select formControlName="storageType"> |
||||
|
<mat-option *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key"> |
||||
|
{{ storageType.value.toString() | translate}} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.storage-pack-size' | translate }}</mat-label> |
||||
|
<input matInput type="number" formControlName="readRecordsCount"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('required')" translate> |
||||
|
gateway.storage-pack-size-required |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('min')" translate> |
||||
|
gateway.storage-pack-size-min |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('readRecordsCount').hasError('pattern')" translate> |
||||
|
gateway.storage-pack-size-pattern |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label > |
||||
|
{{ (gatewayConfigurationGroup.get('storageType').value !== 'file' ? 'gateway.storage-max-records' : 'gateway.storage-max-file-records') | translate}} |
||||
|
</mat-label> |
||||
|
<input matInput type="number" formControlName="maxRecordsCount"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('required')" translate> |
||||
|
gateway.storage-max-records-required |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('min')" translate> |
||||
|
gateway.storage-max-records-min |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxRecordsCount').hasError('pattern')" translate> |
||||
|
gateway.storage-max-records-pattern |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
|
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" *ngIf="gatewayConfigurationGroup.get('storageType').value == 'file'"> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.storage-max-files' | translate }}</mat-label> |
||||
|
<input matInput type="number" formControlName="maxFilesCount"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('required')" translate> |
||||
|
gateway.storage-max-files-required |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('min')" translate> |
||||
|
gateway.storage-max-files-min |
||||
|
</mat-error> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('maxFilesCount').hasError('pattern')" translate> |
||||
|
gateway.storage-max-files-pattern |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.storage-path' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="dataFolderPath"> |
||||
|
<mat-error *ngIf="gatewayConfigurationGroup.get('dataFolderPath').hasError('required')" translate> |
||||
|
gateway.storage-path-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
</div> |
||||
|
</mat-expansion-panel> |
||||
|
|
||||
|
<mat-expansion-panel> |
||||
|
<mat-expansion-panel-header> |
||||
|
<mat-panel-title> |
||||
|
<div class="tb-panel-title">{{ 'gateway.connectors' | translate | uppercase }}</div> |
||||
|
</mat-panel-title> |
||||
|
</mat-expansion-panel-header> |
||||
|
|
||||
|
<div fxLayout="column" class="gateway-config"> |
||||
|
<section formArrayName="connectors" *ngFor="let connector of connectors.controls; let i = index;"> |
||||
|
<div [formGroupName]="i" fxLayout="row" fxLayoutAlign="space-between stretch" fxLayoutGap="8px"> |
||||
|
<div fxLayout="column" fxLayoutAlign="center start"> |
||||
|
<mat-slide-toggle formControlName="enabled"></mat-slide-toggle> |
||||
|
</div> |
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" fxFlex> |
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{'gateway.connector-type' | translate }}</mat-label> |
||||
|
<mat-select formControlName="configType" (selectionChange)="changeConnectorType(connector)"> |
||||
|
<mat-option *ngFor="let connectorType of connectorTypes" [value]="connectorType"> |
||||
|
{{ connectorType }} |
||||
|
</mat-option> |
||||
|
</mat-select> |
||||
|
<mat-error *ngIf="connector.get('configType').hasError('required')" translate> |
||||
|
gateway.connector-type-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
|
||||
|
<mat-form-field fxFlex> |
||||
|
<mat-label>{{ 'gateway.connector-name' | translate }}</mat-label> |
||||
|
<input matInput type="text" formControlName="name" (blur)="changeConnectorName(connector, i)"> |
||||
|
<mat-error *ngIf="connector.get('name').hasError('required')" translate> |
||||
|
gateway.connector-name-required |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
|
</div> |
||||
|
<div [fxLayout]="alignment" [fxLayoutGap]="layoutGap" |
||||
|
fxLayoutAlign="{{alignment == 'row' ? 'end center' : 'space-evenly center'}}" class="action-buttons"> |
||||
|
<button mat-icon-button (click)="openConfigDialog($event, i, connector.get('config').value, connector.get('name').value)" |
||||
|
matTooltip="{{ 'gateway.update-config' | translate }}" |
||||
|
matTooltipPosition="above" |
||||
|
[ngClass]="{'mat-warn': connector.get('config').invalid}"> |
||||
|
<mat-icon>more_horiz</mat-icon> |
||||
|
</button> |
||||
|
<button mat-icon-button (click)="removeConnector(i)" |
||||
|
matTooltip="{{ 'gateway.delete' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
<mat-icon>close</mat-icon> |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
<span [fxShow]="!connectors.length" fxLayoutAlign="center center" class="no-data-found">{{'gateway.no-connectors' | translate}}</span> |
||||
|
<div> |
||||
|
<button mat-raised-button type="button" (click)="addNewConnector()" |
||||
|
matTooltip="{{ 'gateway.connector-add' | translate }}" |
||||
|
matTooltipPosition="above"> |
||||
|
{{ 'action.add' | translate }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div > |
||||
|
</mat-expansion-panel> |
||||
|
</mat-accordion> |
||||
|
<section fxLayout="row" fxLayoutAlign="end center" class="form-action-buttons"> |
||||
|
<button mat-raised-button color="primary" type="button" |
||||
|
(click)="exportConfig()" |
||||
|
*ngIf="!gatewayConfigurationGroup.get('remoteConfiguration').value" |
||||
|
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid" |
||||
|
matTooltip="{{'gateway.download-tip' | translate }}"> |
||||
|
{{'action.download' | translate }} |
||||
|
</button> |
||||
|
|
||||
|
<button mat-raised-button color="primary" type="submit" |
||||
|
*ngIf="gatewayConfigurationGroup.get('remoteConfiguration').value" |
||||
|
[disabled]="!gatewayConfigurationGroup.dirty || gatewayConfigurationGroup.invalid" |
||||
|
matTooltip="{{'gateway.save-tip' | translate }}"> |
||||
|
{{'action.save' | translate }} |
||||
|
</button> |
||||
|
</section> |
||||
|
</form> |
||||
@ -0,0 +1,41 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
:host{ |
||||
|
.gateway-form { |
||||
|
height: 100%; |
||||
|
padding: 5px; |
||||
|
background-color: transparent; |
||||
|
overflow-y: auto; |
||||
|
overflow-x: hidden; |
||||
|
|
||||
|
.form-action-buttons{ |
||||
|
padding-top: 8px; |
||||
|
} |
||||
|
|
||||
|
.gateway-config { |
||||
|
.no-data-found { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
height: 40px; |
||||
|
text-transform: uppercase; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
::ng-deep .mat-form-field-infix { |
||||
|
width: auto !important; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,407 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, ElementRef, Inject, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'; |
||||
|
import { PageComponent } from '@shared/components/page.component'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { AbstractControl, FormArray, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms'; |
||||
|
import { WidgetContext } from '@home/models/widget-component.models'; |
||||
|
import { UtilsService } from '@core/services/utils.service'; |
||||
|
import { |
||||
|
CONFIGURATION_ATTRIBUTE, |
||||
|
CONFIGURATION_DRAFT_ATTRIBUTE, |
||||
|
ConnectorType, |
||||
|
createFormConfig, |
||||
|
CURRENT_CONFIGURATION_ATTRIBUTE, |
||||
|
DEFAULT_CONNECTOR, |
||||
|
gatewayConfigJSON, |
||||
|
GatewayFormConnectorModel, |
||||
|
GatewayFormModels, |
||||
|
GatewayLogLevel, |
||||
|
generateConnectorConfigFiles, |
||||
|
generateLogConfigFile, |
||||
|
generateYAMLConfigFile, |
||||
|
getDraftConnectorsJSON, |
||||
|
getEntityId, |
||||
|
REMOTE_LOGGING_LEVEL_ATTRIBUTE, |
||||
|
SecurityType, |
||||
|
SecurityTypeTranslationMap, |
||||
|
StorageType, |
||||
|
StorageTypeTranslationMap, |
||||
|
ValidateJSON, |
||||
|
WidgetSetting |
||||
|
} from './gateway-form.models'; |
||||
|
import { WINDOW } from '@core/services/window.service'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { |
||||
|
JsonObjectEditDialogComponent, |
||||
|
JsonObjectEditDialogData |
||||
|
} from '@shared/components/dialog/json-object-edit-dialog.component'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DeviceService } from '@core/http/device.service'; |
||||
|
import { AttributeService } from '@core/http/attribute.service'; |
||||
|
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
||||
|
import { forkJoin, Observable } from 'rxjs'; |
||||
|
import { tap } from 'rxjs/operators'; |
||||
|
import { ImportExportService } from '@home/components/import-export/import-export.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-gateway-form', |
||||
|
templateUrl: './gateway-form.component.html', |
||||
|
styleUrls: ['./gateway-form.component.scss'] |
||||
|
}) |
||||
|
|
||||
|
export class GatewayFormComponent extends PageComponent implements OnInit, OnDestroy { |
||||
|
constructor( |
||||
|
protected store: Store<AppState>, |
||||
|
private elementRef: ElementRef, |
||||
|
private utils: UtilsService, |
||||
|
private ngZone: NgZone, |
||||
|
private fb: FormBuilder, |
||||
|
@Inject(WINDOW) private window: Window, |
||||
|
private dialog: MatDialog, |
||||
|
private translate: TranslateService, |
||||
|
private deviceService: DeviceService, |
||||
|
private attributeService: AttributeService, |
||||
|
private importExport: ImportExportService |
||||
|
) { |
||||
|
super(store); |
||||
|
} |
||||
|
|
||||
|
get connectors(): FormArray { |
||||
|
return this.gatewayConfigurationGroup.get('connectors') as FormArray; |
||||
|
} |
||||
|
|
||||
|
@ViewChild('formContainer', {static: true}) formContainerRef: ElementRef<HTMLElement>; |
||||
|
@ViewChild('gatewayConfigurationForm', {static: true}) multipleInputForm: NgForm; |
||||
|
|
||||
|
private successfulSaved: string; |
||||
|
private gatewayNameExists: string; |
||||
|
private archiveFileName: string; |
||||
|
private formResizeListener: any; |
||||
|
private subscribeStorageType$: any; |
||||
|
private subscribeGateway$: any; |
||||
|
|
||||
|
alignment = 'row'; |
||||
|
layoutGap = '5px'; |
||||
|
gatewayType: string; |
||||
|
gatewayConfigurationGroup: FormGroup; |
||||
|
securityTypes = SecurityTypeTranslationMap; |
||||
|
gatewayLogLevels = Object.values(GatewayLogLevel); |
||||
|
connectorTypes = Object.keys(ConnectorType); |
||||
|
storageTypes = StorageTypeTranslationMap; |
||||
|
|
||||
|
toastTargetId = 'gateway-configuration-widget' + this.utils.guid(); |
||||
|
|
||||
|
@Input() |
||||
|
ctx: WidgetContext; |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
this.initWidgetSettings(this.ctx.settings); |
||||
|
|
||||
|
this.buildForm(); |
||||
|
this.ctx.updateWidgetParams(); |
||||
|
this.formResizeListener = this.resize.bind(this); |
||||
|
// @ts-ignore
|
||||
|
addResizeListener(this.formContainerRef.nativeElement, this.formResizeListener); |
||||
|
} |
||||
|
|
||||
|
ngOnDestroy(): void { |
||||
|
if (this.formResizeListener) { |
||||
|
// @ts-ignore
|
||||
|
removeResizeListener(this.formContainerRef.nativeElement, this.formResizeListener); |
||||
|
} |
||||
|
this.subscribeGateway$.unsubscribe(); |
||||
|
this.subscribeStorageType$.unsubscribe(); |
||||
|
} |
||||
|
|
||||
|
private initWidgetSettings(settings: WidgetSetting): void { |
||||
|
let widgetTitle; |
||||
|
if (settings.widgetTitle && settings.widgetTitle.length) { |
||||
|
widgetTitle = this.utils.customTranslation(settings.widgetTitle, settings.widgetTitle); |
||||
|
} else { |
||||
|
widgetTitle = this.translate.instant('gateway.gateway'); |
||||
|
} |
||||
|
this.ctx.widgetTitle = widgetTitle; |
||||
|
|
||||
|
this.archiveFileName = settings.archiveFileName?.length ? settings.archiveFileName : 'gatewayConfiguration'; |
||||
|
this.gatewayType = settings.gatewayType?.length ? settings.gatewayType : 'Gateway'; |
||||
|
this.gatewayNameExists = this.utils.customTranslation(settings.gatewayNameExists, settings.gatewayNameExists) || this.translate.instant('gateway.gateway-exists'); |
||||
|
this.successfulSaved = this.utils.customTranslation(settings.successfulSave, settings.successfulSave) || this.translate.instant('gateway.gateway-saved'); |
||||
|
|
||||
|
this.updateWidgetDisplaying(); |
||||
|
} |
||||
|
|
||||
|
private resize(): void { |
||||
|
this.ngZone.run(() => { |
||||
|
this.updateWidgetDisplaying(); |
||||
|
this.ctx.detectChanges(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private updateWidgetDisplaying(): void { |
||||
|
if(this.ctx.$container && this.ctx.$container[0].offsetWidth <= 425){ |
||||
|
this.layoutGap = '0'; |
||||
|
this.alignment = 'column'; |
||||
|
} else { |
||||
|
this.layoutGap = '5px'; |
||||
|
this.alignment = 'row'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private saveAttribute(attributeName: string, attributeValue: string, attributeScope: AttributeScope): Observable<any> { |
||||
|
const gatewayId = this.gatewayConfigurationGroup.get('gateway').value; |
||||
|
const attributes: AttributeData = { |
||||
|
key: attributeName, |
||||
|
value: attributeValue |
||||
|
}; |
||||
|
return this.attributeService.saveEntityAttributes(getEntityId(gatewayId), attributeScope, [attributes]); |
||||
|
} |
||||
|
|
||||
|
private createConnector(setting: GatewayFormConnectorModel = DEFAULT_CONNECTOR): void { |
||||
|
this.connectors.push(this.fb.group({ |
||||
|
enabled: [setting.enabled], |
||||
|
configType: [setting.configType, [Validators.required]], |
||||
|
name: [setting.name, [Validators.required]], |
||||
|
config: [setting.config, [Validators.nullValidator, ValidateJSON]] |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
private getFormField(name: string): AbstractControl { |
||||
|
return this.gatewayConfigurationGroup.get(name); |
||||
|
} |
||||
|
|
||||
|
private buildForm(): void { |
||||
|
this.gatewayConfigurationGroup = this.fb.group({ |
||||
|
gateway: [null], |
||||
|
accessToken: [null, [Validators.required]], |
||||
|
securityType: [SecurityType.accessToken], |
||||
|
host: [this.window.location.hostname, [Validators.required]], |
||||
|
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], |
||||
|
remoteConfiguration: [true], |
||||
|
caCertPath: ['/etc/thingsboard-gateway/ca.pem'], |
||||
|
privateKeyPath: ['/etc/thingsboard-gateway/privateKey.pem'], |
||||
|
certPath: ['/etc/thingsboard-gateway/certificate.pem'], |
||||
|
remoteLoggingLevel: [GatewayLogLevel.debug], |
||||
|
remoteLoggingPathToLogs: ['./logs/', [Validators.required]], |
||||
|
storageType: [StorageType.memory], |
||||
|
readRecordsCount: [100, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
||||
|
maxRecordsCount: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
||||
|
maxFilesCount: [5, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
||||
|
dataFolderPath: ['./data/', [Validators.required]], |
||||
|
connectors: this.fb.array([]) |
||||
|
}); |
||||
|
|
||||
|
this.subscribeStorageType$ = this.getFormField('storageType').valueChanges.subscribe((value: StorageType) => { |
||||
|
if (value === StorageType.memory) { |
||||
|
this.getFormField('maxFilesCount').disable(); |
||||
|
this.getFormField('dataFolderPath').disable(); |
||||
|
} else { |
||||
|
this.getFormField('maxFilesCount').enable(); |
||||
|
this.getFormField('dataFolderPath').enable(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
this.subscribeGateway$ = this.getFormField('gateway').valueChanges.subscribe((gatewayId: string) => { |
||||
|
if (gatewayId !== null) { |
||||
|
forkJoin([ |
||||
|
this.deviceService.getDeviceCredentials(gatewayId).pipe(tap(deviceCredential => { |
||||
|
this.getFormField('accessToken').patchValue(deviceCredential.credentialsId); |
||||
|
})), |
||||
|
...this.getAttributes(gatewayId)]).subscribe(() => { |
||||
|
this.gatewayConfigurationGroup.markAsPristine(); |
||||
|
this.ctx.detectChanges(); |
||||
|
}); |
||||
|
} else { |
||||
|
this.getFormField('accessToken').patchValue(''); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
gatewayExist(): void { |
||||
|
this.ctx.showErrorToast(this.gatewayNameExists, 'top', 'left', this.toastTargetId); |
||||
|
} |
||||
|
|
||||
|
exportConfig(): void { |
||||
|
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value; |
||||
|
const filesZip: any = {}; |
||||
|
filesZip['tb_gateway.yaml'] = generateYAMLConfigFile(gatewayConfiguration); |
||||
|
generateConnectorConfigFiles(filesZip, gatewayConfiguration.connectors); |
||||
|
generateLogConfigFile(filesZip, gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs); |
||||
|
this.importExport.exportJSZip(filesZip, this.archiveFileName); |
||||
|
this.saveAttribute( |
||||
|
REMOTE_LOGGING_LEVEL_ATTRIBUTE, |
||||
|
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(), |
||||
|
AttributeScope.SHARED_SCOPE); |
||||
|
} |
||||
|
|
||||
|
addNewConnector(): void { |
||||
|
this.createConnector(); |
||||
|
} |
||||
|
|
||||
|
removeConnector(index: number): void { |
||||
|
if (index > -1) { |
||||
|
this.connectors.removeAt(index); |
||||
|
this.connectors.markAsDirty(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
openConfigDialog($event: Event, index: number, config: object, type: string): void { |
||||
|
if ($event) { |
||||
|
$event.stopPropagation(); |
||||
|
$event.preventDefault(); |
||||
|
} |
||||
|
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, { |
||||
|
disableClose: true, |
||||
|
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
||||
|
data: { |
||||
|
jsonValue: config, |
||||
|
title: this.translate.instant('gateway.title-connectors-json', {typeName: type}) |
||||
|
} |
||||
|
}).afterClosed().subscribe( |
||||
|
(res) => { |
||||
|
if (res) { |
||||
|
this.connectors.at(index).get('config').patchValue(res); |
||||
|
this.ctx.detectChanges(); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private createConnectorName(connectors: GatewayFormConnectorModel[], name: string, index: number = 0): string { |
||||
|
const newKeyName = index ? name + index : name; |
||||
|
const indexRes = connectors.findIndex((element) => element.name === newKeyName); |
||||
|
return indexRes === -1 ? newKeyName : this.createConnectorName(connectors, name, ++index); |
||||
|
} |
||||
|
|
||||
|
private validateConnectorName(connectors: GatewayFormConnectorModel[], name: string, connectorIndex: number, index = 0): string { |
||||
|
for (let i = 0; i < connectors.length; i++) { |
||||
|
const nameEq = (index === 0) ? name : name + index; |
||||
|
if (i !== connectorIndex && connectors[i].name === nameEq) { |
||||
|
this.validateConnectorName(connectors, name, connectorIndex, ++index); |
||||
|
} |
||||
|
} |
||||
|
return (index === 0) ? name : name + index; |
||||
|
} |
||||
|
|
||||
|
changeConnectorType(connector: AbstractControl): void { |
||||
|
if (!connector.get('name').value) { |
||||
|
const typeConnector = connector.get('configType').value; |
||||
|
const connectors = this.gatewayConfigurationGroup.value.connectors; |
||||
|
connector.get('name').patchValue(this.createConnectorName(connectors, ConnectorType[typeConnector])); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
changeConnectorName(connector: AbstractControl, index: number): void { |
||||
|
const connectors = this.gatewayConfigurationGroup.value.connectors; |
||||
|
connector.get('name').patchValue(this.validateConnectorName(connectors, connector.get('name').value, index)); |
||||
|
} |
||||
|
|
||||
|
save(): void { |
||||
|
const gatewayConfiguration: GatewayFormModels = this.gatewayConfigurationGroup.value; |
||||
|
forkJoin([ |
||||
|
this.saveAttribute( |
||||
|
CONFIGURATION_ATTRIBUTE, |
||||
|
window.btoa(JSON.stringify(gatewayConfigJSON(gatewayConfiguration))), |
||||
|
AttributeScope.SHARED_SCOPE), |
||||
|
this.saveAttribute( |
||||
|
CONFIGURATION_DRAFT_ATTRIBUTE, |
||||
|
window.btoa(JSON.stringify(getDraftConnectorsJSON(gatewayConfiguration.connectors))), |
||||
|
AttributeScope.SERVER_SCOPE), |
||||
|
this.saveAttribute( |
||||
|
REMOTE_LOGGING_LEVEL_ATTRIBUTE, |
||||
|
this.gatewayConfigurationGroup.value.remoteLoggingLevel.toUpperCase(), |
||||
|
AttributeScope.SHARED_SCOPE) |
||||
|
]).subscribe(() =>{ |
||||
|
this.ctx.showSuccessToast(this.successfulSaved, |
||||
|
2000, 'top', 'left', this.toastTargetId); |
||||
|
this.gatewayConfigurationGroup.markAsPristine(); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
private getAttributes(gatewayId: string): Array<Observable<Array<AttributeData>>> { |
||||
|
const tasks = []; |
||||
|
tasks.push(forkJoin([this.getAttribute(CURRENT_CONFIGURATION_ATTRIBUTE, AttributeScope.CLIENT_SCOPE, gatewayId), |
||||
|
this.getAttribute(CONFIGURATION_DRAFT_ATTRIBUTE, AttributeScope.SERVER_SCOPE, gatewayId)]).pipe( |
||||
|
tap(([currentConfig, draftConfig]) => { |
||||
|
this.setFormGatewaySettings(currentConfig); |
||||
|
this.setFormConnectorsDraft(draftConfig); |
||||
|
}) |
||||
|
) |
||||
|
); |
||||
|
tasks.push(this.getAttribute(REMOTE_LOGGING_LEVEL_ATTRIBUTE, AttributeScope.SHARED_SCOPE, gatewayId).pipe( |
||||
|
tap(logsLevel => this.processLoggingLevel(logsLevel)) |
||||
|
)); |
||||
|
return tasks; |
||||
|
} |
||||
|
|
||||
|
private getAttribute(attributeName: string, attributeScope: AttributeScope, gatewayId: string): Observable<Array<AttributeData>> { |
||||
|
return this.attributeService.getEntityAttributes(getEntityId(gatewayId), attributeScope, [attributeName]); |
||||
|
} |
||||
|
|
||||
|
private setFormGatewaySettings(response: Array<AttributeData>): void { |
||||
|
this.connectors.clear(); |
||||
|
if (response.length > 0) { |
||||
|
const attribute = JSON.parse(window.atob(response[0].value)); |
||||
|
for (const attributeKey of Object.keys(attribute)) { |
||||
|
const keyValue = attribute[attributeKey]; |
||||
|
if (attributeKey === 'thingsboard') { |
||||
|
if (keyValue !== null && Object.keys(keyValue).length > 0) { |
||||
|
this.gatewayConfigurationGroup.patchValue(createFormConfig(keyValue)); |
||||
|
} |
||||
|
} else { |
||||
|
for (const connector of Object.keys(keyValue)) { |
||||
|
let name = 'No name'; |
||||
|
if (Object.prototype.hasOwnProperty.call(keyValue[connector], 'name')) { |
||||
|
name = keyValue[connector].name; |
||||
|
} |
||||
|
const newConnector: GatewayFormConnectorModel = { |
||||
|
enabled: true, |
||||
|
configType: (attributeKey as ConnectorType), |
||||
|
config: keyValue[connector].config, |
||||
|
name |
||||
|
}; |
||||
|
this.createConnector(newConnector); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private setFormConnectorsDraft(response: Array<AttributeData>): void { |
||||
|
if (response.length > 0) { |
||||
|
const attribute = JSON.parse(window.atob(response[0].value)); |
||||
|
for (const connectorName of Object.keys(attribute)) { |
||||
|
const newConnector: GatewayFormConnectorModel = { |
||||
|
enabled: false, |
||||
|
configType: (attribute[connectorName].connector as ConnectorType), |
||||
|
config: attribute[connectorName].config, |
||||
|
name: connectorName |
||||
|
}; |
||||
|
this.createConnector(newConnector); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private processLoggingLevel(value: Array<AttributeData>): void { |
||||
|
let logsLevel = GatewayLogLevel.debug; |
||||
|
if (value.length > 0 && GatewayLogLevel[value[0].value.toLowerCase()]) { |
||||
|
logsLevel = GatewayLogLevel[value[0].value.toLowerCase()]; |
||||
|
} |
||||
|
this.getFormField('remoteLoggingLevel').patchValue(logsLevel); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,374 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { AbstractControl, ValidationErrors } from '@angular/forms'; |
||||
|
import { EntityId } from '@shared/models/id/entity-id'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
|
||||
|
export enum SecurityType { |
||||
|
tls = 'tls', |
||||
|
accessToken = 'accessToken' |
||||
|
} |
||||
|
|
||||
|
export interface WidgetSetting { |
||||
|
widgetTitle: string; |
||||
|
archiveFileName: string; |
||||
|
gatewayType: string; |
||||
|
successfulSave: string; |
||||
|
gatewayNameExists: string; |
||||
|
} |
||||
|
|
||||
|
export const CURRENT_CONFIGURATION_ATTRIBUTE = 'current_configuration'; |
||||
|
export const CONFIGURATION_DRAFT_ATTRIBUTE = 'configuration_drafts'; |
||||
|
export const CONFIGURATION_ATTRIBUTE = 'configuration'; |
||||
|
export const REMOTE_LOGGING_LEVEL_ATTRIBUTE = 'RemoteLoggingLevel'; |
||||
|
|
||||
|
export const SecurityTypeTranslationMap = new Map<SecurityType, string>( |
||||
|
[ |
||||
|
[SecurityType.tls, 'gateway.security-types.tls'], |
||||
|
[SecurityType.accessToken, 'gateway.security-types.access-token'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export enum GatewayLogLevel { |
||||
|
none = 'NONE', |
||||
|
critical = 'CRITICAL', |
||||
|
error = 'ERROR', |
||||
|
warning = 'WARNING', |
||||
|
info = 'INFO', |
||||
|
debug = 'DEBUG' |
||||
|
} |
||||
|
|
||||
|
export enum StorageType { |
||||
|
memory = 'memory', |
||||
|
file = 'file' |
||||
|
} |
||||
|
|
||||
|
export const StorageTypeTranslationMap = new Map<StorageType, string>( |
||||
|
[ |
||||
|
[StorageType.memory, 'gateway.storage-types.memory-storage'], |
||||
|
[StorageType.file, 'gateway.storage-types.file-storage'] |
||||
|
] |
||||
|
); |
||||
|
|
||||
|
export enum ConnectorType { |
||||
|
mqtt= 'MQTT', |
||||
|
modbus = 'Modbus', |
||||
|
opc_ua = 'OPC-UA', |
||||
|
ble = 'BLE' |
||||
|
} |
||||
|
|
||||
|
export interface GatewayFormModels { |
||||
|
gateway?: string; |
||||
|
accessToken?: string; |
||||
|
securityType?: SecurityType; |
||||
|
host?: string; |
||||
|
port?: number; |
||||
|
remoteConfiguration?: boolean; |
||||
|
caCertPath?: string; |
||||
|
privateKeyPath?: string; |
||||
|
certPath?: string; |
||||
|
remoteLoggingLevel?: GatewayLogLevel; |
||||
|
remoteLoggingPathToLogs?:string; |
||||
|
storageType?: StorageType; |
||||
|
readRecordsCount?: number; |
||||
|
maxRecordsCount?: number; |
||||
|
maxFilesCount?: number; |
||||
|
dataFolderPath?: number; |
||||
|
connectors?: Array<GatewayFormConnectorModel>; |
||||
|
} |
||||
|
|
||||
|
export interface GatewayFormConnectorModel { |
||||
|
config: object; |
||||
|
name: string; |
||||
|
configType: ConnectorType; |
||||
|
enabled: boolean; |
||||
|
} |
||||
|
|
||||
|
export const DEFAULT_CONNECTOR: GatewayFormConnectorModel = { |
||||
|
config: {}, |
||||
|
name: '', |
||||
|
configType: null, |
||||
|
enabled: false |
||||
|
}; |
||||
|
|
||||
|
type Connector = { |
||||
|
[key in ConnectorType]?: Array<ConnectorConfig>; |
||||
|
} |
||||
|
|
||||
|
interface GatewaySetting extends Connector{ |
||||
|
thingsboard: GatewayMainSetting; |
||||
|
} |
||||
|
|
||||
|
interface ConnectorConfig { |
||||
|
name: string; |
||||
|
config: object; |
||||
|
} |
||||
|
|
||||
|
interface GatewayMainSetting { |
||||
|
thingsboard: GatewayMainThingsboardSetting; |
||||
|
connectors: Array<GatewayMainConnector>, |
||||
|
logs: string, |
||||
|
storage: GatewayStorage |
||||
|
} |
||||
|
|
||||
|
interface GatewayMainThingsboardSetting { |
||||
|
host: string, |
||||
|
remoteConfiguration: boolean, |
||||
|
port: number, |
||||
|
security: GatewaySecurity |
||||
|
} |
||||
|
|
||||
|
type GatewaySecurity = SecurityToken | SecurityCertificate; |
||||
|
|
||||
|
interface SecurityToken { |
||||
|
accessToken: string; |
||||
|
} |
||||
|
|
||||
|
interface SecurityCertificate { |
||||
|
caCert: string; |
||||
|
privateKey: string; |
||||
|
cert: string; |
||||
|
} |
||||
|
|
||||
|
type GatewayStorage = GatewayStorageMemory | GatewayStorageFile; |
||||
|
|
||||
|
interface GatewayStorageMemory { |
||||
|
type: string; |
||||
|
max_records_count: number; |
||||
|
read_records_count: number; |
||||
|
} |
||||
|
|
||||
|
interface GatewayStorageFile { |
||||
|
type: string; |
||||
|
data_folder_path: number; |
||||
|
max_file_count: number; |
||||
|
max_read_records_count: number; |
||||
|
max_records_per_file: number; |
||||
|
} |
||||
|
|
||||
|
interface GatewayMainConnector { |
||||
|
configuration: string; |
||||
|
name: string; |
||||
|
type: ConnectorType; |
||||
|
} |
||||
|
|
||||
|
export function ValidateJSON(control: AbstractControl): ValidationErrors | null { |
||||
|
if (JSON.stringify(control.value) === JSON.stringify({})) { |
||||
|
return { validJSON: true }; |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const TEMPLATE_LOGS_CONFIG = '[loggers]}}keys=root, service, connector, converter, tb_connection, storage, extension}}[handlers]}}keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}[formatters]}}keys=LogFormatter}}[logger_root]}}level=ERROR}}handlers=consoleHandler}}[logger_connector]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=connector}}[logger_storage]}}level={ERROR}}}handlers=storageHandler}}formatter=LogFormatter}}qualname=storage}}[logger_tb_connection]}}level={ERROR}}}handlers=tb_connectionHandler}}formatter=LogFormatter}}qualname=tb_connection}}[logger_service]}}level={ERROR}}}handlers=serviceHandler}}formatter=LogFormatter}}qualname=service}}[logger_converter]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=converter}}[logger_extension]}}level={ERROR}}}handlers=connectorHandler}}formatter=LogFormatter}}qualname=extension}}[handler_consoleHandler]}}class=StreamHandler}}level={ERROR}}}formatter=LogFormatter}}args=(sys.stdout,)}}[handler_connectorHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}connector.log", "d", 1, 7,)}}[handler_storageHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}storage.log", "d", 1, 7,)}}[handler_serviceHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}service.log", "d", 1, 7,)}}[handler_converterHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}converter.log", "d", 1, 3,)}}[handler_extensionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}extension.log", "d", 1, 3,)}}[handler_tb_connectionHandler]}}level={ERROR}}}class=logging.handlers.TimedRotatingFileHandler}}formatter=LogFormatter}}args=("{./logs/}tb_connection.log", "d", 1, 3,)}}[formatter_LogFormatter]}}format="%(asctime)s - %(levelname)s - [%(filename)s] - %(module)s - %(lineno)d - %(message)s" }}datefmt="%Y-%m-%d %H:%M:%S"'; |
||||
|
|
||||
|
export function generateYAMLConfigFile(gatewaySetting: GatewayFormModels): string { |
||||
|
let config; |
||||
|
config = 'thingsboard:\n'; |
||||
|
config += ' host: ' + gatewaySetting.host + '\n'; |
||||
|
config += ' remoteConfiguration: ' + gatewaySetting.remoteConfiguration + '\n'; |
||||
|
config += ' port: ' + gatewaySetting.port + '\n'; |
||||
|
config += ' security:\n'; |
||||
|
if (gatewaySetting.securityType === SecurityType.accessToken) { |
||||
|
config += ' access-token: ' + gatewaySetting.accessToken + '\n'; |
||||
|
} else { |
||||
|
config += ' ca_cert: ' + gatewaySetting.caCertPath + '\n'; |
||||
|
config += ' privateKey: ' + gatewaySetting.privateKeyPath + '\n'; |
||||
|
config += ' cert: ' + gatewaySetting.certPath + '\n'; |
||||
|
} |
||||
|
config += 'storage:\n'; |
||||
|
if (gatewaySetting.storageType === StorageType.memory) { |
||||
|
config += ' type: memory\n'; |
||||
|
config += ' read_records_count: ' + gatewaySetting.readRecordsCount + '\n'; |
||||
|
config += ' max_records_count: ' + gatewaySetting.maxRecordsCount + '\n'; |
||||
|
} else { |
||||
|
config += ' type: file\n'; |
||||
|
config += ' data_folder_path: ' + gatewaySetting.dataFolderPath + '\n'; |
||||
|
config += ' max_file_count: ' + gatewaySetting.maxFilesCount + '\n'; |
||||
|
config += ' max_read_records_count: ' + gatewaySetting.readRecordsCount + '\n'; |
||||
|
config += ' max_records_per_file: ' + gatewaySetting.maxRecordsCount + '\n'; |
||||
|
} |
||||
|
config += 'connectors:\n'; |
||||
|
for(const connector of gatewaySetting.connectors){ |
||||
|
if (connector.enabled) { |
||||
|
config += ' -\n'; |
||||
|
config += ' name: ' + connector.name + '\n'; |
||||
|
config += ' type: ' + connector.configType + '\n'; |
||||
|
config += ' configuration: ' + generateFileName(connector.name) + '\n'; |
||||
|
} |
||||
|
} |
||||
|
return config; |
||||
|
} |
||||
|
|
||||
|
export function generateConnectorConfigFiles(fileZipAdd: object, connectors: Array<GatewayFormConnectorModel>): void { |
||||
|
for(const connector of connectors) { |
||||
|
if (connector.enabled) { |
||||
|
fileZipAdd[generateFileName(connector.name)] = JSON.stringify(connector.config); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function generateFileName(fileName): string { |
||||
|
return fileName.replace('_', '') |
||||
|
.replace('-', '') |
||||
|
.replace(/^\s+|\s+/g, '') |
||||
|
.toLowerCase() + '.json'; |
||||
|
} |
||||
|
|
||||
|
export function generateLogConfigFile(fileZipAdd: object, logsLevel: string, logsPath: string): void { |
||||
|
fileZipAdd['logs.conf'] = getLogsConfig(logsLevel, logsPath); |
||||
|
} |
||||
|
|
||||
|
function getLogsConfig(logsLevel: string, logsPath: string): string { |
||||
|
return TEMPLATE_LOGS_CONFIG |
||||
|
.replace(/{ERROR}/g, logsLevel) |
||||
|
.replace(/{.\/logs\/}/g, logsPath); |
||||
|
} |
||||
|
|
||||
|
export function getEntityId(gatewayId: string): EntityId { |
||||
|
return { |
||||
|
id: gatewayId, |
||||
|
entityType: EntityType.DEVICE |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function createFormConfig(keyValue: GatewayMainSetting): GatewayFormModels { |
||||
|
const formSetting: GatewayFormModels = {}; |
||||
|
if (Object.prototype.hasOwnProperty.call(keyValue, 'thingsboard')) { |
||||
|
formSetting.host = keyValue.thingsboard.host; |
||||
|
formSetting.port = keyValue.thingsboard.port; |
||||
|
formSetting.remoteConfiguration = keyValue.thingsboard.remoteConfiguration; |
||||
|
if (Object.prototype.hasOwnProperty.call(keyValue.thingsboard.security, SecurityType.accessToken)) { |
||||
|
formSetting.securityType = SecurityType.accessToken; |
||||
|
formSetting.accessToken = (keyValue.thingsboard.security as SecurityToken).accessToken; |
||||
|
} else { |
||||
|
formSetting.securityType = SecurityType.tls; |
||||
|
formSetting.caCertPath = (keyValue.thingsboard.security as SecurityCertificate).caCert; |
||||
|
formSetting.privateKeyPath = (keyValue.thingsboard.security as SecurityCertificate).privateKey; |
||||
|
formSetting.certPath = (keyValue.thingsboard.security as SecurityCertificate).cert; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (Object.prototype.hasOwnProperty.call(keyValue, 'storage') && Object.prototype.hasOwnProperty.call(keyValue.storage, 'type')) { |
||||
|
if (keyValue.storage.type === StorageType.memory) { |
||||
|
formSetting.storageType = StorageType.memory; |
||||
|
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count; |
||||
|
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count; |
||||
|
} else if (keyValue.storage.type === StorageType.file) { |
||||
|
formSetting.storageType = StorageType.file; |
||||
|
formSetting.dataFolderPath = (keyValue.storage as GatewayStorageFile).data_folder_path; |
||||
|
formSetting.maxFilesCount = (keyValue.storage as GatewayStorageFile).max_file_count; |
||||
|
formSetting.readRecordsCount = (keyValue.storage as GatewayStorageMemory).read_records_count; |
||||
|
formSetting.maxRecordsCount = (keyValue.storage as GatewayStorageMemory).max_records_count; |
||||
|
} |
||||
|
} |
||||
|
return formSetting; |
||||
|
} |
||||
|
|
||||
|
export function getDraftConnectorsJSON(currentConnectors: Array<GatewayFormConnectorModel>) { |
||||
|
const draftConnectors = {}; |
||||
|
for(const connector of currentConnectors){ |
||||
|
if (!connector.enabled) { |
||||
|
draftConnectors[connector.name] = { |
||||
|
connector: connector.configType, |
||||
|
config: connector.config |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
return draftConnectors; |
||||
|
} |
||||
|
|
||||
|
export function gatewayConfigJSON(gatewayConfiguration: GatewayFormModels): GatewaySetting { |
||||
|
const gatewayConfig = { |
||||
|
thingsboard: gatewayMainConfigJSON(gatewayConfiguration) |
||||
|
}; |
||||
|
gatewayConnectorJSON(gatewayConfig, gatewayConfiguration.connectors); |
||||
|
return gatewayConfig; |
||||
|
} |
||||
|
|
||||
|
function gatewayMainConfigJSON(gatewayConfiguration: GatewayFormModels): GatewayMainSetting { |
||||
|
let security: GatewaySecurity; |
||||
|
if (gatewayConfiguration.securityType === SecurityType.accessToken) { |
||||
|
security = { |
||||
|
accessToken: gatewayConfiguration.accessToken |
||||
|
} |
||||
|
} else { |
||||
|
security = { |
||||
|
caCert: gatewayConfiguration.caCertPath, |
||||
|
privateKey: gatewayConfiguration.privateKeyPath, |
||||
|
cert: gatewayConfiguration.certPath |
||||
|
} |
||||
|
} |
||||
|
const thingsboard: GatewayMainThingsboardSetting = { |
||||
|
host: gatewayConfiguration.host, |
||||
|
remoteConfiguration: gatewayConfiguration.remoteConfiguration, |
||||
|
port: gatewayConfiguration.port, |
||||
|
security |
||||
|
}; |
||||
|
|
||||
|
let storage: GatewayStorage; |
||||
|
if (gatewayConfiguration.storageType === StorageType.memory) { |
||||
|
storage = { |
||||
|
type: StorageType.memory, |
||||
|
read_records_count: gatewayConfiguration.readRecordsCount, |
||||
|
max_records_count: gatewayConfiguration.maxRecordsCount |
||||
|
}; |
||||
|
} else { |
||||
|
storage = { |
||||
|
type: StorageType.file, |
||||
|
data_folder_path: gatewayConfiguration.dataFolderPath, |
||||
|
max_file_count: gatewayConfiguration.maxFilesCount, |
||||
|
max_read_records_count: gatewayConfiguration.readRecordsCount, |
||||
|
max_records_per_file: gatewayConfiguration.maxRecordsCount |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
const connectors: Array<GatewayMainConnector> = []; |
||||
|
for (const connector of gatewayConfiguration.connectors) { |
||||
|
if (connector.enabled) { |
||||
|
const connectorConfig: GatewayMainConnector = { |
||||
|
configuration: generateFileName(connector.name), |
||||
|
name: connector.name, |
||||
|
type: connector.configType |
||||
|
}; |
||||
|
connectors.push(connectorConfig); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
thingsboard, |
||||
|
connectors, |
||||
|
storage, |
||||
|
logs: window.btoa(getLogsConfig(gatewayConfiguration.remoteLoggingLevel, gatewayConfiguration.remoteLoggingPathToLogs)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function gatewayConnectorJSON(gatewayConfiguration, currentConnectors: Array<GatewayFormConnectorModel>): void { |
||||
|
for (const connector of currentConnectors) { |
||||
|
if (connector.enabled) { |
||||
|
const typeConnector = connector.configType; |
||||
|
if (!Array.isArray(gatewayConfiguration[typeConnector])) { |
||||
|
gatewayConfiguration[typeConnector] = []; |
||||
|
} |
||||
|
|
||||
|
const connectorConfig: ConnectorConfig = { |
||||
|
name: connector.name, |
||||
|
config: connector.config |
||||
|
}; |
||||
|
gatewayConfiguration[typeConnector].push(connectorConfig); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2020 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<mat-form-field [formGroup]="selectDeviceGatewayFormGroup" class="mat-block"> |
||||
|
<input matInput type="text" #deviceGatewayInput placeholder="{{ 'gateway.gateway-name' | translate }}" |
||||
|
formControlName="gateway" |
||||
|
(focusin)="onFocus()" |
||||
|
[required]="required" |
||||
|
(keydown)="gatewayNameEnter($event)" |
||||
|
(keypress)="gatewayNameEnter($event)" |
||||
|
[matAutocomplete]="gatewayAutocomplete" |
||||
|
> |
||||
|
<button *ngIf="selectDeviceGatewayFormGroup.get('gateway').value" (click)="clear()" |
||||
|
type="button" matSuffix mat-icon-button aria-label="Clear"> |
||||
|
<mat-icon class="material-icons">close</mat-icon> |
||||
|
</button> |
||||
|
<mat-autocomplete class="tb-autocomplete" |
||||
|
#gatewayAutocomplete="matAutocomplete" |
||||
|
[displayWith]="displayGatewayFn"> |
||||
|
<mat-option *ngFor="let gateway of filteredGateways | async" [value]="gateway"> |
||||
|
<span [innerHTML]="gateway.name | highlight:searchText"></span> |
||||
|
</mat-option> |
||||
|
<mat-option *ngIf="!(filteredGateways | async)?.length" [value]="null" class="tb-not-found"> |
||||
|
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> |
||||
|
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> |
||||
|
<span translate>gateway.no-gateway-found</span> |
||||
|
</div> |
||||
|
<ng-template #searchNotEmpty> |
||||
|
<span translate [translateParams]="{item: searchText | truncate:true:6:'...'}"> |
||||
|
gateway.no-gateway-matching |
||||
|
</span> |
||||
|
</ng-template> |
||||
|
<span> |
||||
|
<a translate (click)="createGateway($event, searchText)">gateway.create-new-gateway</a> |
||||
|
</span> |
||||
|
</div> |
||||
|
</mat-option> |
||||
|
</mat-autocomplete> |
||||
|
<mat-error *ngIf="selectDeviceGatewayFormGroup.get('gateway').hasError('required')"> |
||||
|
{{ 'gateway.gateway-name-required' | translate }} |
||||
|
</mat-error> |
||||
|
</mat-form-field> |
||||
@ -0,0 +1,223 @@ |
|||||
|
///
|
||||
|
/// Copyright © 2016-2020 The Thingsboard Authors
|
||||
|
///
|
||||
|
/// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
/// you may not use this file except in compliance with the License.
|
||||
|
/// You may obtain a copy of the License at
|
||||
|
///
|
||||
|
/// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
///
|
||||
|
/// Unless required by applicable law or agreed to in writing, software
|
||||
|
/// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
|
/// See the License for the specific language governing permissions and
|
||||
|
/// limitations under the License.
|
||||
|
///
|
||||
|
|
||||
|
import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; |
||||
|
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; |
||||
|
import { Store } from '@ngrx/store'; |
||||
|
import { AppState } from '@core/core.state'; |
||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
||||
|
import { ENTER } from '@angular/cdk/keycodes'; |
||||
|
import { Observable, of } from 'rxjs'; |
||||
|
import { filter, map, mergeMap, reduce, share, switchMap, tap } from 'rxjs/operators'; |
||||
|
import { EntityService } from '@core/http/entity.service'; |
||||
|
import { EntityType } from '@shared/models/entity-type.models'; |
||||
|
import { Device } from '@shared/models/device.models'; |
||||
|
import { DialogService } from '@core/services/dialog.service'; |
||||
|
import { TranslateService } from '@ngx-translate/core'; |
||||
|
import { DeviceService } from '@core/http/device.service'; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'tb-entity-gateway-select', |
||||
|
templateUrl: './entity-gateway-select.component.html', |
||||
|
styleUrls: [], |
||||
|
providers: [{ |
||||
|
provide: NG_VALUE_ACCESSOR, |
||||
|
useExisting: forwardRef(() => EntityGatewaySelectComponent), |
||||
|
multi: true |
||||
|
}] |
||||
|
}) |
||||
|
|
||||
|
export class EntityGatewaySelectComponent implements ControlValueAccessor, OnInit { |
||||
|
get required(): boolean { |
||||
|
return this.requiredValue; |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
set required(value: boolean) { |
||||
|
this.requiredValue = coerceBooleanProperty(value); |
||||
|
} |
||||
|
|
||||
|
@Input() |
||||
|
set newGatewayType(value: string){ |
||||
|
this.gatewayType = value; |
||||
|
} |
||||
|
|
||||
|
@Output() |
||||
|
private gatewayNameExist = new EventEmitter(); |
||||
|
|
||||
|
constructor(private store: Store<AppState>, |
||||
|
private entityService: EntityService, |
||||
|
private dialogService: DialogService, |
||||
|
private deviceService: DeviceService, |
||||
|
private translate: TranslateService, |
||||
|
private fb: FormBuilder) { |
||||
|
this.loadGatewayList(); |
||||
|
this.selectDeviceGatewayFormGroup = this.fb.group({ |
||||
|
gateway: [null] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private gatewayType = 'Gateway'; |
||||
|
private dirty: boolean; |
||||
|
private requiredValue: boolean; |
||||
|
private gatewayList: Array<Device>; |
||||
|
|
||||
|
searchText = ''; |
||||
|
filteredGateways: Observable<Array<Device>>; |
||||
|
selectDeviceGatewayFormGroup: FormGroup; |
||||
|
modelValue: string | null; |
||||
|
|
||||
|
@ViewChild('deviceGatewayInput', {static: true}) deviceGatewayInput: ElementRef<HTMLInputElement>; |
||||
|
private propagateChange = (v: any) => { }; |
||||
|
|
||||
|
registerOnChange(fn: any): void { |
||||
|
this.propagateChange = fn; |
||||
|
} |
||||
|
|
||||
|
registerOnTouched(fn: any): void { |
||||
|
} |
||||
|
|
||||
|
ngOnInit() { |
||||
|
this.filteredGateways = this.selectDeviceGatewayFormGroup.get('gateway').valueChanges |
||||
|
.pipe( |
||||
|
tap(value => { |
||||
|
let modelValue; |
||||
|
if (typeof value === 'string' || !value) { |
||||
|
modelValue = null; |
||||
|
} else { |
||||
|
modelValue = value.id.id; |
||||
|
} |
||||
|
this.updateView(modelValue); |
||||
|
if (value === null) { |
||||
|
this.clear(); |
||||
|
} |
||||
|
}), |
||||
|
map(value => value ? (typeof value === 'string' ? value : value.name) : ''), |
||||
|
mergeMap(name => this.fetchGateway(name) ), |
||||
|
share() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
fetchGateway(searchText?: string): Observable<Array<Device>> { |
||||
|
this.searchText = searchText; |
||||
|
let result = []; |
||||
|
if (searchText && searchText.length) { |
||||
|
result = this.gatewayList.filter((gateway) => gateway.name.toLowerCase().includes(searchText.toLowerCase())); |
||||
|
} else { |
||||
|
result = this.gatewayList; |
||||
|
} |
||||
|
return of(result); |
||||
|
} |
||||
|
|
||||
|
onFocus() { |
||||
|
if (this.dirty) { |
||||
|
this.selectDeviceGatewayFormGroup.get('gateway').updateValueAndValidity({onlySelf: true, emitEvent: true}); |
||||
|
this.dirty = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
displayGatewayFn(gateway?: Device): string | undefined { |
||||
|
return gateway ? gateway.name : undefined; |
||||
|
} |
||||
|
|
||||
|
setDisabledState(isDisabled: boolean): void { |
||||
|
} |
||||
|
|
||||
|
writeValue(value: string | null): void { |
||||
|
if(value === null){ |
||||
|
this.searchText = ''; |
||||
|
this.selectDeviceGatewayFormGroup.get('gateway').patchValue('', {emitEvent: false}); |
||||
|
this.dirty = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
clear(value: string = '', hideList?: boolean) { |
||||
|
this.searchText = value; |
||||
|
this.selectDeviceGatewayFormGroup.get('gateway').patchValue(value, {emitEvent: true}); |
||||
|
if(!hideList) { |
||||
|
setTimeout(() => { |
||||
|
this.deviceGatewayInput.nativeElement.blur(); |
||||
|
this.deviceGatewayInput.nativeElement.focus(); |
||||
|
}, 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
textIsNotEmpty(text: string): boolean { |
||||
|
return !!text && text.length > 0; |
||||
|
} |
||||
|
|
||||
|
gatewayNameEnter($event: KeyboardEvent) { |
||||
|
if ($event.keyCode === ENTER) { |
||||
|
if (!this.modelValue) { |
||||
|
this.createGateway($event, this.searchText); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
createGateway($event: Event, gatewayName: string) { |
||||
|
$event.preventDefault(); |
||||
|
$event.stopPropagation(); |
||||
|
const title = this.translate.instant('gateway.create-new-gateway'); |
||||
|
const content = this.translate.instant('gateway.create-new-gateway-text', {gatewayName}); |
||||
|
this.dialogService.confirm(title, content, null, null, true).subscribe(value => { |
||||
|
if(value){ |
||||
|
this.createDeviceGateway(gatewayName); |
||||
|
} else { |
||||
|
this.clear('', true); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private createDeviceGateway(gatewayName: string){ |
||||
|
this.deviceService.findByName(gatewayName, {ignoreErrors: true}).subscribe(value => { |
||||
|
this.gatewayNameExist.emit(gatewayName) |
||||
|
}, () => { |
||||
|
const newGateway: Device = { |
||||
|
name: gatewayName, |
||||
|
label: null, |
||||
|
type: this.gatewayType, |
||||
|
additionalInfo: { |
||||
|
gateway: true |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
this.deviceService.saveDevice(newGateway).subscribe( |
||||
|
(device) => { |
||||
|
this.searchText = ''; |
||||
|
this.gatewayList.push(device); |
||||
|
this.selectDeviceGatewayFormGroup.get('gateway').patchValue(device, {emitEvent: true}); |
||||
|
} |
||||
|
); |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
private loadGatewayList(): void{ |
||||
|
this.entityService.getEntitiesByNameFilter(EntityType.DEVICE, '', -1).pipe( |
||||
|
switchMap(results => results), |
||||
|
filter((device) => (device as Device)?.additionalInfo?.gateway), |
||||
|
reduce((acc, val) => acc.concat(val), []) |
||||
|
).subscribe((devices) => { |
||||
|
this.gatewayList = devices; |
||||
|
if(!this.searchText && this.gatewayList.length){ |
||||
|
this.selectDeviceGatewayFormGroup.get('gateway').patchValue(this.gatewayList[0], {emitEvent: true}); |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
private updateView(modelValue: any) { |
||||
|
this.propagateChange(modelValue); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue