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