45 changed files with 4206 additions and 408 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,86 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<div mat-dialog-content tb-toast fxLayout="column" toastTarget="dockerCommandDialogContent"> |
|||
<mat-button-toggle-group [formControl]="protocolCtrl" class="protocol-toggle"> |
|||
<mat-button-toggle value="mqtt" aria-label="mqtt"> |
|||
MQTT |
|||
</mat-button-toggle> |
|||
<mat-button-toggle value="coap" aria-label="coap"> |
|||
COAP |
|||
</mat-button-toggle> |
|||
<mat-button-toggle value="http" aria-label="html"> |
|||
HTTP |
|||
</mat-button-toggle> |
|||
</mat-button-toggle-group> |
|||
<span class="help-title">In order to publish telemetry data to (<a [href]="helpLink" target="_blank">ThingsBoard device</a>) with credentials of the current device you can use the following commands.</span> |
|||
<div class="mat-content" fxLayout="column"> |
|||
<div class="mat-content" fxLayout="column" *ngIf="protocolCtrl.value === 'mqtt'"> |
|||
<span>MQTT </span> |
|||
<span>1. Setup: <span [innerHTML]="mqttSetup"></span></span> |
|||
<span class="protocol-send-command">2. Send command:</span> |
|||
<div fxLayout="row wrap" fxLayoutAlign="start center"> |
|||
<pre class="tb-highlight" fxFlex><code class="pre-wrap">{{ mqttCode }}</code></pre> |
|||
<button mat-icon-button |
|||
color="primary" |
|||
ngxClipboard |
|||
cbContent="{{ mqttCode }}" |
|||
(cbOnSuccess)="onDockerCodeCopied()" |
|||
matTooltip="{{ 'device.transportCommandCopiedMessage' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div class="mat-content" fxLayout="column" *ngIf="protocolCtrl.value === 'coap'"> |
|||
<span>COAP </span> |
|||
<span>1. Setup: <span [innerHTML]="coapSetup"></span></span> |
|||
<span class="protocol-send-command">2. Send command: (based on CoAP cli)</span> |
|||
<div fxLayout="row wrap" fxLayoutAlign="start center"> |
|||
<pre class="tb-highlight" fxFlex><code class="pre-wrap">{{ coapCode }}</code></pre> |
|||
<button mat-icon-button |
|||
color="primary" |
|||
ngxClipboard |
|||
cbContent="{{ coapCode }}" |
|||
(cbOnSuccess)="onDockerCodeCopied()" |
|||
matTooltip="{{ 'device.transportCommandCopiedMessage' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> |
|||
</button> |
|||
<pre class="tb-highlight" fxFlex="100"><code |
|||
class="pre-wrap">JSON example: {{'[{"temperature": 40}, {"isRainy": true}]'}}</code></pre> |
|||
</div> |
|||
</div> |
|||
<div class="mat-content" fxLayout="column" *ngIf="protocolCtrl.value === 'http'"> |
|||
<span>HTTP </span> |
|||
<span>1. Setup: <span [innerHTML]="httpSetup"></span></span> |
|||
<span class="protocol-send-command">2. Send command:</span> |
|||
<div fxLayout="row wrpa"fxLayoutAlign="start center"> |
|||
<pre class="tb-highlight" fxFlex><code class="pre-wrap">{{ httpCode }}</code></pre> |
|||
<button mat-icon-button |
|||
color="primary" |
|||
ngxClipboard |
|||
cbContent="{{ httpCode }}" |
|||
(cbOnSuccess)="onDockerCodeCopied()" |
|||
matTooltip="{{ 'device.transportCommandCopiedMessage' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,41 @@ |
|||
/** |
|||
* Copyright © 2016-2023 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 { |
|||
|
|||
mat-button-toggle-group, .protocol-title { |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
span { |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.protocol-toggle { |
|||
width: fit-content; |
|||
} |
|||
|
|||
.help-title { |
|||
padding-bottom: 5px; |
|||
} |
|||
|
|||
.protocol-send-command { |
|||
width: 100%; |
|||
} |
|||
|
|||
.pre-wrap { |
|||
white-space: pre-wrap; |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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, Input, OnInit } from '@angular/core'; |
|||
import { FormControl } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
import { helpBaseUrl } from '@shared/models/constants'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-device-example-command', |
|||
templateUrl: './device-example-command.component.html', |
|||
styleUrls: ['./device-example-command.component.scss'] |
|||
}) |
|||
|
|||
export class DeviceExampleCommandComponent implements OnInit { |
|||
|
|||
@Input() |
|||
token: string; |
|||
|
|||
helpLink: string = helpBaseUrl + '/docs/reference/protocols/'; |
|||
|
|||
mqttCode: string; |
|||
|
|||
coapCode: string; |
|||
|
|||
httpCode: string; |
|||
|
|||
mqttSetup: string; |
|||
|
|||
coapSetup: string; |
|||
|
|||
httpSetup: string; |
|||
|
|||
protocolCtrl: FormControl; |
|||
|
|||
constructor(protected router: Router, |
|||
protected store: Store<AppState>, |
|||
private translate: TranslateService) { |
|||
} |
|||
|
|||
|
|||
ngOnInit(): void { |
|||
const HOST = window.location.hostname; |
|||
this.mqttCode = `mosquitto_pub -d -q 1 -h ${HOST} -t "v1/devices/me/telemetry" -u "${this.token}" -m "{"temperature":42}"`; |
|||
this.coapCode = `cat telemetry-data.json | coap post coap://${HOST}/api/v1/${this.token}/telemetry`; |
|||
this.httpCode = `curl -v -X POST --data "{"temperature":42,"humidity":73}" http://${HOST}/api/v1/${this.token}/telemetry --header "Content-Type:application/json"`; |
|||
|
|||
this.protocolCtrl = new FormControl('mqtt'); |
|||
const mqttLink = '<a href="https://thingsboard.io/docs/getting-started-guides/helloworld/?connectdevice=mqtt-windows"> Mqtt Guide</a>'; |
|||
|
|||
// @ts-ignore
|
|||
const platform = window.navigator?.userAgentData?.platform || window.navigator.platform, |
|||
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], |
|||
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']; |
|||
if (macosPlatforms.indexOf(platform) !== -1) { |
|||
this.mqttSetup = 'brew install mosquitto-clients'; |
|||
this.coapSetup = 'npm install coap-cli -g'; |
|||
this.httpSetup = 'brew install curl'; |
|||
} else if (windowsPlatforms.indexOf(platform) !== -1) { |
|||
this.mqttSetup = mqttLink; |
|||
this.coapSetup = 'npm install coap-cli -g'; |
|||
this.httpSetup = 'not required, available by default in windows 10+'; |
|||
} else if (/Linux/.test(platform)) { |
|||
this.mqttSetup = 'sudo apt-get install mosquitto-clients'; |
|||
this.coapSetup = 'npm install coap-cli -g'; |
|||
this.httpSetup = 'sudo apt-get install curl'; |
|||
} |
|||
} |
|||
|
|||
onDockerCodeCopied() { |
|||
this.store.dispatch(new ActionNotificationShow( |
|||
{ |
|||
message: this.translate.instant('gateway.command-copied-message'), |
|||
type: 'success', |
|||
target: 'dockerCommandDialogContent', |
|||
duration: 1200, |
|||
verticalPosition: 'bottom', |
|||
horizontalPosition: 'left' |
|||
})); |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2022 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 style="min-width: 400px; position: relative;"> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>gateway.command</h2> |
|||
<span fxFlex></span> |
|||
<button mat-button mat-icon-button |
|||
(click)="close()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<tb-gateway-command [token]="data.credentials.credentialsId"></tb-gateway-command> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
cdkFocusInitial |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="close()"> |
|||
{{ 'action.ok' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -1,58 +0,0 @@ |
|||
///
|
|||
/// Copyright © 2016-2022 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, Inject, OnInit} from '@angular/core'; |
|||
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; |
|||
import {Store} from '@ngrx/store'; |
|||
import {AppState} from '@core/core.state'; |
|||
import {Router} from '@angular/router'; |
|||
import {DialogComponent} from '@app/shared/components/dialog.component'; |
|||
import {TranslateService} from '@ngx-translate/core'; |
|||
import {Device, DeviceCredentials} from "@shared/models/device.models"; |
|||
|
|||
export interface GatewayCommandDialogData { |
|||
device: Device, |
|||
credentials: DeviceCredentials |
|||
} |
|||
|
|||
enum OsType { |
|||
linux = 'linux', |
|||
macos = 'macos', |
|||
windows = 'win' |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-command-dialog', |
|||
templateUrl: './gateway-command-dialog.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class GatewayCommandDialogComponent extends DialogComponent<GatewayCommandDialogComponent> implements OnInit { |
|||
|
|||
constructor(protected router: Router, |
|||
protected store: Store<AppState>, |
|||
@Inject(MAT_DIALOG_DATA) public data: GatewayCommandDialogData, |
|||
public dialogRef: MatDialogRef<GatewayCommandDialogComponent, boolean>,) { |
|||
super(store, router, dialogRef); |
|||
} |
|||
|
|||
ngOnInit() { |
|||
} |
|||
|
|||
close(): void { |
|||
this.dialogRef.close(); |
|||
} |
|||
|
|||
} |
|||
@ -1,130 +0,0 @@ |
|||
///
|
|||
/// Copyright © 2016-2022 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 { |
|||
DateEntityTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@home/models/entity/entities-table-config.models'; |
|||
import {EntityType, EntityTypeResource} from '@shared/models/entity-type.models'; |
|||
import {TranslateService} from '@ngx-translate/core'; |
|||
import {DatePipe} from '@angular/common'; |
|||
import {Direction} from '@shared/models/page/sort-order'; |
|||
import {MatDialog} from '@angular/material/dialog'; |
|||
import {TimePageLink} from '@shared/models/page/page-link'; |
|||
import {Observable} from 'rxjs'; |
|||
import {emptyPageData, PageData} from '@shared/models/page/page-data'; |
|||
import {UtilsService} from '@core/services/utils.service'; |
|||
import {DeviceService} from "@core/http/device.service"; |
|||
import {AttributeService} from "@core/http/attribute.service"; |
|||
import {Device} from "@shared/models/device.models"; |
|||
import { |
|||
GatewayCommandDialogComponent, |
|||
GatewayCommandDialogData |
|||
} from "@home/components/gateway/gateway-command-dialog.component"; |
|||
import {ActionNotificationShow} from "@core/notification/notification.actions"; |
|||
import {Store} from "@ngrx/store"; |
|||
import {AppState} from "@core/core.state"; |
|||
import {map} from "rxjs/operators"; |
|||
import {EntityService} from "@core/http/entity.service"; |
|||
|
|||
export class GatewayListTableConfig extends EntityTableConfig<Device, TimePageLink> { |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private deviceService: DeviceService, |
|||
private attributeService: AttributeService, |
|||
private entityService: EntityService, |
|||
private datePipe: DatePipe, |
|||
private translate: TranslateService, |
|||
private utils: UtilsService, |
|||
private dialog: MatDialog, |
|||
updateOnInit = true) { |
|||
super(); |
|||
this.loadDataOnInit = updateOnInit; |
|||
this.tableTitle = 'Gateway list'; |
|||
this.useTimePageLink = false; |
|||
this.pageMode = false; |
|||
this.displayPagination = false; |
|||
this.detailsPanelEnabled = false; |
|||
this.selectionEnabled = false; |
|||
this.searchEnabled = false; |
|||
this.addEnabled = false; |
|||
this.entitiesDeleteEnabled = false; |
|||
this.actionsColumnTitle = ''; |
|||
this.entityTranslations = { |
|||
noEntities: 'gateway.no-gateway-found', |
|||
search: 'gateway.gateway-search' |
|||
}; |
|||
this.entityResources = {} as EntityTypeResource<Device>; |
|||
|
|||
this.entitiesFetchFunction = () => this.fetchGateways(); |
|||
|
|||
this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; |
|||
|
|||
this.columns.push( |
|||
new DateEntityTableColumn<Device>('createdTime', 'gateway.created-time', this.datePipe, '150px')); |
|||
|
|||
this.columns.push( |
|||
new EntityTableColumn<Device>('entityName', 'gateway.gateway-name', '20%', |
|||
(entity => this.utils.customTranslation(entity.name, entity.name)) |
|||
) |
|||
); |
|||
|
|||
this.cellActionDescriptors.push( |
|||
{ |
|||
name: this.translate.instant('gateway.command'), |
|||
icon: 'vpn_key', |
|||
isEnabled: () => true, |
|||
onAction: ($event, entity) => this.showGatewayDockerCommand(entity) |
|||
} |
|||
); |
|||
} |
|||
|
|||
showGatewayDockerCommand(entity: Device) { |
|||
this.deviceService.getDeviceCredentials(entity.id.id).subscribe(credentials => { |
|||
this.dialog.open<GatewayCommandDialogComponent, GatewayCommandDialogData, boolean> |
|||
(GatewayCommandDialogComponent, |
|||
{ |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
credentials: credentials, |
|||
device: entity |
|||
} |
|||
}).afterClosed().subscribe( |
|||
(res) => { |
|||
if (res) { |
|||
this.updateData(); |
|||
} |
|||
} |
|||
); |
|||
}, error => { |
|||
const messageToShow = `<div>${error}</div>`; |
|||
this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'})); |
|||
}) |
|||
} |
|||
|
|||
fetchGateways(): Observable<PageData<Device>> { |
|||
return this.entityService.getEntitiesByNameFilter(EntityType.DEVICE, "", -1).pipe( |
|||
map((array: Array<Device>) => { |
|||
const pageData = emptyPageData<Device>(); |
|||
pageData.data = array.filter(device => device.additionalInfo?.gateway); |
|||
pageData.totalPages = 1; |
|||
pageData.totalElements = array.length; |
|||
return pageData; |
|||
})) |
|||
} |
|||
} |
|||
@ -1,63 +0,0 @@ |
|||
///
|
|||
/// Copyright © 2016-2022 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, OnInit} from '@angular/core'; |
|||
import {UtilsService} from '@core/services/utils.service'; |
|||
import {TranslateService} from '@ngx-translate/core'; |
|||
import {DeviceService} from '@core/http/device.service'; |
|||
import {AttributeService} from '@core/http/attribute.service'; |
|||
import {GatewayListTableConfig} from "@home/components/gateway/gateway-list-table-config"; |
|||
import {DatePipe} from "@angular/common"; |
|||
import {MatDialog} from "@angular/material/dialog"; |
|||
import {Store} from "@ngrx/store"; |
|||
import {AppState} from "@core/core.state"; |
|||
import {EntityService} from "@core/http/entity.service"; |
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-list', |
|||
templateUrl: './gateway-list.component.html', |
|||
styleUrls: ['./gateway-list.component.scss'] |
|||
}) |
|||
|
|||
|
|||
export class GatewayListComponent implements OnInit { |
|||
gatewayListTableConfig: GatewayListTableConfig; |
|||
|
|||
constructor( |
|||
protected store: Store<AppState>, |
|||
private utils: UtilsService, |
|||
private translate: TranslateService, |
|||
private datePipe: DatePipe, |
|||
private deviceService: DeviceService, |
|||
private entityService: EntityService, |
|||
private attributeService: AttributeService, |
|||
private dialog: MatDialog, |
|||
) { |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.gatewayListTableConfig = new GatewayListTableConfig( |
|||
this.store, |
|||
this.deviceService, |
|||
this.attributeService, |
|||
this.entityService, |
|||
this.datePipe, |
|||
this.translate, |
|||
this.utils, |
|||
this.dialog |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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. |
|||
|
|||
--> |
|||
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column"> |
|||
<mat-slide-toggle class="mat-slide" formControlName="isConnectorLog"> |
|||
Is Connector |
|||
</mat-slide-toggle> |
|||
<mat-form-field fxFlex class="mat-block" *ngIf="GatewayLogSettingForm.get('isConnectorLog').value"> |
|||
<mat-label translate>State parameter connector name</mat-label> |
|||
<input matInput formControlName="connectorLogState"> |
|||
</mat-form-field> |
|||
</section> |
|||
@ -0,0 +1,54 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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 } from '@angular/core'; |
|||
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
|||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-logs-settings', |
|||
templateUrl: './gateway-logs-settings.component.html', |
|||
styleUrls: ['../widget-settings.scss'] |
|||
}) |
|||
export class GatewayLogsSettingsComponent extends WidgetSettingsComponent { |
|||
|
|||
GatewayLogSettingForm: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
protected settingsForm(): UntypedFormGroup { |
|||
return this.GatewayLogSettingForm; |
|||
} |
|||
|
|||
protected defaultSettings(): WidgetSettings { |
|||
return { |
|||
isConnectorLog: false, |
|||
connectorLogState: 'default' |
|||
}; |
|||
} |
|||
|
|||
protected onSettingsSet(settings: WidgetSettings) { |
|||
this.GatewayLogSettingForm = this.fb.group({ |
|||
isConnectorLog: [false, []], |
|||
connectorLogState: ['default', []] |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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 } from '@angular/core'; |
|||
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; |
|||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-service-rpc-settings', |
|||
templateUrl: './gateway-service-rpc-settings.component.html', |
|||
styleUrls: ['../widget-settings.scss'] |
|||
}) |
|||
export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent { |
|||
|
|||
GatewayLogSettingForm: UntypedFormGroup; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private fb: UntypedFormBuilder) { |
|||
super(store); |
|||
} |
|||
|
|||
protected settingsForm(): UntypedFormGroup { |
|||
return this.GatewayLogSettingForm; |
|||
} |
|||
|
|||
protected defaultSettings(): WidgetSettings { |
|||
return { |
|||
isConnector: false, |
|||
}; |
|||
} |
|||
|
|||
protected onSettingsSet(settings: WidgetSettings) { |
|||
this.GatewayLogSettingForm = this.fb.group({ |
|||
isConnector: [false, []] |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,789 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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 [formGroup]="gatewayConfigGroup" style="height: 100%" fxLayout="column"> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>gateway.gateway-configuration</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
type="button" |
|||
(click)="cancel()" |
|||
*ngIf="dialogRef"> |
|||
<mat-icon class="material-icons" style="color:#000;">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-tab-group style="max-height: calc(100% - 115px)"> |
|||
<mat-tab label="{{'gateway.thingsboard-general' | translate}}"> |
|||
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding"> |
|||
<mat-accordion multi> |
|||
<mat-expansion-panel expanded="true" disabled style="padding-top: 20px"> |
|||
<div fxLayout="row wrap"> |
|||
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteConfiguration"> |
|||
{{ 'gateway.remote-configuration' | translate }} |
|||
<mat-icon class="material-icons-outlined suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.remote-configuration' | translate }}">info |
|||
</mat-icon> |
|||
</mat-slide-toggle> |
|||
<mat-slide-toggle color="primary" fxFlex="100" formControlName="remoteShell"> |
|||
{{ 'gateway.remote-shell' | translate }} |
|||
<mat-icon class="material-icons-outlined suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.remote-shell' | translate }}">info |
|||
</mat-icon> |
|||
</mat-slide-toggle> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.thingsboard-host</mat-label> |
|||
<input matInput formControlName="host"/> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.host' | translate }}">info_outlined |
|||
</mat-icon> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.host').hasError('required')"> |
|||
{{ 'gateway.thingsboard-host-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.thingsboard-port</mat-label> |
|||
<input matInput formControlName="port" type="number" min="0" /> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('required')"> |
|||
{{'gateway.thingsboard-port-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('min')"> |
|||
{{'gateway.thingsboard-port-min' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('max')"> |
|||
{{'gateway.thingsboard-port-max' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.port').hasError('pattern')"> |
|||
{{'gateway.thingsboard-port-pattern' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.port' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
|
|||
</div> |
|||
</mat-expansion-panel> |
|||
<mat-expansion-panel expanded="true" disabled> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title translate class="expansion-panel-header">security.security</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div fxLayout="row wrap" formGroupName="security"> |
|||
<mat-button-toggle-group class="security-toggle-group" formControlName="type" fxFlex="100"> |
|||
<mat-button-toggle *ngFor="let securityType of securityTypes | keyvalue" [value]="securityType.key"> |
|||
{{ securityType.value | translate }} |
|||
</mat-button-toggle> |
|||
</mat-button-toggle-group> |
|||
<mat-form-field |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('accesstoken')" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>security.access-token</mat-label> |
|||
<input matInput formControlName="accessToken"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.accessToken').hasError('required')"> |
|||
{{'security.access-token-required' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.token' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>security.clientId</mat-label> |
|||
<input matInput formControlName="clientId"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.clientId').hasError('required')"> |
|||
{{'security.clientId-required' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.client-id' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>security.username</mat-label> |
|||
<input matInput formControlName="username"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.username').hasError('required')"> |
|||
{{'security.username-required' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.username' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'usernamePassword'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>security.password</mat-label> |
|||
<input matInput formControlName="password"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.security.password').hasError('required')"> |
|||
{{'security.password-required' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.password' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<tb-file-input |
|||
fxFlex="100" |
|||
hint="{{'gateway.hints.ca-cert' | translate}}" |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')" |
|||
formControlName="caCert" |
|||
label="{{'security.ca-cert' | translate}}" |
|||
[allowedExtensions]="'pem, cert, key'" |
|||
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'" |
|||
dropLabel="{{'gateway.drop-file' | translate}}"> |
|||
</tb-file-input> |
|||
<tb-file-input |
|||
fxFlex="100" |
|||
hint="{{'gateway.hints.cert' | translate}}" |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'" |
|||
formControlName="cert" |
|||
label="{{'security.cert' | translate}}" |
|||
[allowedExtensions]="'pem, cert, key'" |
|||
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'" |
|||
dropLabel="{{'gateway.drop-file' | translate}}"> |
|||
</tb-file-input> |
|||
<tb-file-input |
|||
fxFlex="100" |
|||
hint="{{'gateway.hints.private-key' | translate}}" |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value === 'tlsPrivateKey'" |
|||
formControlName="privateKey" |
|||
label="{{'security.private-key' | translate}}" |
|||
[allowedExtensions]="'pem, cert, key'" |
|||
[accept]="'.pem, application/pem,.cert, application/cert, .key,application/key'" |
|||
dropLabel="{{'gateway.drop-file' | translate}}"> |
|||
</tb-file-input> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
</mat-accordion> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.logs.logs' | translate}}"> |
|||
<div formGroupName="logs" fxLayout="column" class="mat-content mat-padding"> |
|||
<mat-accordion multi> |
|||
<mat-expansion-panel expanded="true" disabled style="padding-top: 20px"> |
|||
<div fxLayout="row wrap"> |
|||
<mat-form-field fxFlex="100" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.logs.date-format</mat-label> |
|||
<input matInput formControlName="dateFormat"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('logs.dateFormat').hasError('required')"> |
|||
{{'gateway.logs.date-format-required' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.date-form' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="100" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.logs.log-format</mat-label> |
|||
<textarea matInput formControlName="logFormat" rows="2"></textarea> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('logs.logFormat').hasError('required')"> |
|||
{{'gateway.logs.log-format-required' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.log-format' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
<mat-expansion-panel expanded="true" disabled formGroupName="remote" > |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title translate class="expansion-panel-header">gateway.logs.remote</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<mat-slide-toggle color="primary" formControlName="enabled"> |
|||
{{ 'gateway.logs.remote-logs' | translate }} |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.remote-log' | translate }}">info |
|||
</mat-icon> |
|||
</mat-slide-toggle> |
|||
<mat-form-field class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.logs.level</mat-label> |
|||
<mat-select formControlName="logLevel"> |
|||
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</mat-expansion-panel> |
|||
<mat-expansion-panel expanded="true" disabled formGroupName="local" > |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title translate class="expansion-panel-header">gateway.logs.local</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<mat-button-toggle-group class="security-toggle-group" [formControl]="logSelector" fxFlex="100" |
|||
name="logSelector"> |
|||
<mat-button-toggle *ngFor="let logConfig of localLogsConfigLabels| keyvalue" [value]="logConfig.key" |
|||
class="first-capital"> |
|||
{{ logConfig.value }} |
|||
</mat-button-toggle> |
|||
</mat-button-toggle-group> |
|||
<div fxFlex="100" fxLayout="row wrap" [formGroup]="getLogFormGroup(logSelector.value)"> |
|||
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(50%-15px)"> |
|||
<mat-label translate>gateway.logs.level</mat-label> |
|||
<mat-select formControlName="logLevel"> |
|||
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(50%-15px)"> |
|||
<mat-label translate>gateway.logs.file-path</mat-label> |
|||
<input matInput formControlName="filePath"/> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.filePath').hasError('required')"> |
|||
{{'gateway.logs.file-path-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(35%-30px)"> |
|||
<mat-label translate>gateway.logs.saving-period</mat-label> |
|||
<input matInput formControlName="savingTime" type="number" min="0" /> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('required')"> |
|||
{{'gateway.logs.saving-period-required' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.savingTime').hasError('min')"> |
|||
{{'gateway.logs.saving-period-min' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(15%-30px)"> |
|||
<mat-label translate></mat-label> |
|||
<mat-select formControlName="savingPeriod"> |
|||
<mat-option *ngFor="let period of logSavingPeriods | keyvalue" [value]="period.key"> |
|||
{{period.value | translate}} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type" fxFlex="calc(50%-30px)"> |
|||
<mat-label translate>gateway.logs.backup-count</mat-label> |
|||
<input matInput formControlName="backupCount" type="number" min="0" /> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('required')"> |
|||
{{'gateway.logs.backup-count-required' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('logs.local.' + logSelector.value + '.backupCount').hasError('min')"> |
|||
{{'gateway.logs.backup-count-min' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.backup-count' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
</mat-accordion> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.storage' | translate}}"> |
|||
<div formGroupName="storage" fxLayout="column" class="mat-content mat-padding"> |
|||
<mat-accordion> |
|||
<mat-expansion-panel expanded="true" disabled> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title translate class="expansion-panel-header">gateway.storage |
|||
<span class="tb-hint">{{'gateway.hints.storage' | translate}}</span> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div fxLayout="row wrap"> |
|||
<mat-button-toggle-group fxLayout="row wrap" class="security-toggle-group" formControlName="type" |
|||
fxFlex="100"> |
|||
<mat-button-toggle *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key"> |
|||
{{ storageType.value | translate }} |
|||
</mat-button-toggle> |
|||
<div class="line-break "></div> |
|||
<span class="tb-hint" style="padding-left: 0"> |
|||
{{'gateway.hints.'+gatewayConfigGroup.get('storage.type').value | translate}}</span> |
|||
</mat-button-toggle-group> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.storage-read-record-count</mat-label> |
|||
<input matInput formControlName="read_records_count"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('required')"> |
|||
{{'gateway.storage-read-record-count-required' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('min')"> |
|||
{{'gateway.storage-read-record-count-min' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.read_records_count').hasError('pattern')"> |
|||
{{'gateway.storage-read-record-count-pattern' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.read-record-count' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'memory'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.storage-max-records</mat-label> |
|||
<input matInput formControlName="max_records_count"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('required')"> |
|||
{{'gateway.storage-max-records-required' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('min')"> |
|||
{{'gateway.storage-max-records-min' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_count').hasError('pattern')"> |
|||
{{'gateway.storage-max-records-pattern' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.max-records-count' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.storage-data-folder-path</mat-label> |
|||
<input matInput formControlName="data_folder_path"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.data_folder_path').hasError('required')"> |
|||
{{'gateway.storage-data-folder-path-required' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.data-folder' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.storage-max-files</mat-label> |
|||
<input matInput formControlName="max_file_count"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('required')"> |
|||
{{'gateway.storage-max-files-required' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('min')"> |
|||
{{'gateway.storage-max-files-min' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_file_count').hasError('pattern')"> |
|||
{{'gateway.storage-max-files-pattern' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.max-file-count' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.storage-max-read-record-count</mat-label> |
|||
<input matInput formControlName="max_read_records_count"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('required')"> |
|||
{{'gateway.storage-max-read-record-count-required' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('min')"> |
|||
{{'gateway.storage-max-read-record-count-min' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_read_records_count').hasError('pattern')"> |
|||
{{'gateway.storage-max-read-record-count-pattern' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.max-read-count' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'file'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.storage-max-file-records</mat-label> |
|||
<input matInput formControlName="max_records_per_file"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('required')"> |
|||
{{'gateway.storage-max-records-required' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('min')"> |
|||
{{'gateway.storage-max-records-min' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.max_records_per_file').hasError('pattern')"> |
|||
{{'gateway.storage-max-records-pattern' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.max-records' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.storage-path</mat-label> |
|||
<input matInput formControlName="data_file_path"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.data_file_path').hasError('required')"> |
|||
{{'gateway.storage-path-required' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.data-folder' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.messages-ttl-check-in-hours</mat-label> |
|||
<input matInput formControlName="messages_ttl_check_in_hours"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('required')"> |
|||
{{'gateway.messages-ttl-check-in-hours-required' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('min')"> |
|||
{{'gateway.messages-ttl-check-in-hours-min' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_check_in_hours').hasError('pattern')"> |
|||
{{'gateway.messages-ttl-check-in-hours-pattern' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.ttl-check-hour' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field *ngIf="gatewayConfigGroup.get('storage.type').value === 'sqlite'" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type" style="flex-grow: 0"> |
|||
<mat-label translate>gateway.messages-ttl-in-days</mat-label> |
|||
<input matInput formControlName="messages_ttl_in_days"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('required')"> |
|||
{{'gateway.messages-ttl-in-days-required' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('min')"> |
|||
{{'gateway.messages-ttl-in-days-min' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('storage.messages_ttl_in_days').hasError('pattern')"> |
|||
{{'gateway.messages-ttl-in-days-pattern' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.ttl-messages-day' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
</mat-accordion> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.grpc' | translate}}"> |
|||
<div formGroupName="grpc" fxLayout="column" class="mat-content mat-padding"> |
|||
<mat-accordion multi> |
|||
<mat-expansion-panel expanded="true" disabled style="padding-top: 20px"> |
|||
<div fxLayout="row wrap"> |
|||
<mat-slide-toggle color="primary" fxFlex="100" formControlName="enabled"> |
|||
{{ 'gateway.grpc' | translate }} |
|||
</mat-slide-toggle> |
|||
<mat-slide-toggle color="primary" fxFlex="100" formControlName="keepalivePermitWithoutCalls"> |
|||
{{ 'gateway.permit-without-calls' | translate }} |
|||
</mat-slide-toggle> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.server-port</mat-label> |
|||
<input matInput formControlName="serverPort" type="number" min="0" /> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('required')"> |
|||
{{'gateway.thingsboard-port-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('min')"> |
|||
{{'gateway.thingsboard-port-min' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('max')"> |
|||
{{'gateway.thingsboard-port-max' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.serverPort').hasError('pattern')"> |
|||
{{'gateway.thingsboard-port-pattern' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.grpc-keep-alive-timeout</mat-label> |
|||
<input matInput formControlName="keepAliveTimeoutMs" type="number" min="0" /> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('required')"> |
|||
{{'gateway.grpc-keep-alive-timeout-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('min')"> |
|||
{{'gateway.grpc-keep-alive-timeout-min' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeoutMs').hasError('pattern')"> |
|||
{{'gateway.grpc-keep-alive-timeout-pattern' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.grpc-keep-alive</mat-label> |
|||
<input matInput formControlName="keepAliveTimeMs" type="number" min="0" /> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('required')"> |
|||
{{'gateway.grpc-keep-alive-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('min')"> |
|||
{{'gateway.grpc-keep-alive-min' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.keepAliveTimeMs').hasError('pattern')"> |
|||
{{'gateway.grpc-keep-alive-pattern' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.grpc-min-time-between-pings</mat-label> |
|||
<input matInput formControlName="minTimeBetweenPingsMs" type="number" min="0" /> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('required')"> |
|||
{{'gateway.grpc-min-time-between-pings-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('min')"> |
|||
{{'gateway.grpc-min-time-between-pings-min' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('pattern')"> |
|||
{{'gateway.grpc-min-time-between-pings-pattern' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.grpc-max-pings-without-data</mat-label> |
|||
<input matInput formControlName="maxPingsWithoutData" type="number" min="0" /> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('required')"> |
|||
{{'gateway.grpc-max-pings-without-data-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('min')"> |
|||
{{'gateway.grpc-max-pings-without-data-min' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('pattern')"> |
|||
{{'gateway.grpc-max-pings-without-data-pattern' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.grpc-min-ping-interval-without-data</mat-label> |
|||
<input matInput formControlName="minPingIntervalWithoutDataMs" type="number" min="0" /> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('required')"> |
|||
{{'gateway.grpc-min-ping-interval-without-data-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('min')"> |
|||
{{'gateway.grpc-min-ping-interval-without-data-min' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('pattern')"> |
|||
{{'gateway.grpc-min-ping-interval-without-data-pattern' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
</mat-accordion> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.statistics.statistics' | translate}}"> |
|||
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding"> |
|||
<mat-accordion multi> |
|||
<mat-expansion-panel expanded="true" disabled style="padding-top: 20px"> |
|||
<div fxLayout="row wrap" formGroupName="statistics"> |
|||
<mat-slide-toggle color="primary" fxFlex="100" formControlName="enable"> |
|||
{{ 'gateway.statistics.statistics' | translate }} |
|||
</mat-slide-toggle> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.statistics.send-period</mat-label> |
|||
<input matInput formControlName="statsSendPeriodInSeconds" type="number" min="1" /> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('required')"> |
|||
{{'gateway.statistics.send-period-required' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('min')"> |
|||
{{'gateway.statistics.send-period-min' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.statistics.statsSendPeriodInSeconds').hasError('pattern')"> |
|||
{{'gateway.statistics.send-period-pattern' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
<mat-expansion-panel expanded="true" disabled> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title class="expansion-panel-header">{{"gateway.statistics.commands" |translate}} |
|||
<span class="tb-hint"> |
|||
{{'gateway.hints.commands' | translate}}</span></mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div fxLayout="column" formGroupName="statistics"> |
|||
<div fxLayout="row" formArrayName="commands" |
|||
*ngFor="let commandControl of commandFormArray().controls; let $index = index" |
|||
style="margin-bottom: 15px"> |
|||
<mat-card fxLayout="row wrap" [formGroupName]="$index" class="statistics-block"> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.statistics.attribute-name</mat-label> |
|||
<input matInput formControlName="attributeOnGateway"/> |
|||
<mat-error |
|||
*ngIf="commandControl.get('attributeOnGateway').hasError('required')"> |
|||
{{'gateway.statistics.attribute-name-required' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.attribute' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.statistics.timeout</mat-label> |
|||
<input matInput formControlName="timeout" type="number" min="0" /> |
|||
<mat-error |
|||
*ngIf="commandControl.get('timeout').hasError('required')"> |
|||
{{'gateway.statistics.timeout-required' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="commandControl.get('timeout').hasError('min')"> |
|||
{{'gateway.statistics.timeout-min' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="commandControl.get('timeout').hasError('pattern')"> |
|||
{{'gateway.statistics.timeout-pattern' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.timeout' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type" style="flex-grow: 0"> |
|||
<mat-label translate>gateway.statistics.command</mat-label> |
|||
<input matInput formControlName="command"/> |
|||
<mat-error |
|||
*ngIf="commandControl.get('command').hasError('required')"> |
|||
{{'gateway.statistics.command-required' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.command' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</mat-card> |
|||
<button mat-icon-button (click)="removeCommandControl($index)" |
|||
[disabled]="!gatewayConfigGroup.get('thingsboard.remoteConfiguration').value" |
|||
matTooltip="{{ 'gateway.statistics.remove' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>close</mat-icon> |
|||
</button> |
|||
</div> |
|||
<button mat-stroked-button color="primary" |
|||
style="width: fit-content;" |
|||
type="button" |
|||
[disabled]="!gatewayConfigGroup.get('thingsboard.remoteConfiguration').value" |
|||
(click)="addCommand()"> |
|||
{{ 'gateway.statistics.add' | translate }} |
|||
</button> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
</mat-accordion> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.other' | translate}}"> |
|||
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding"> |
|||
<mat-accordion multi> |
|||
<mat-expansion-panel expanded="true" disabled formGroupName="checkingDeviceActivity"> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title class="expansion-panel-header"> |
|||
<mat-slide-toggle color="primary" fxFlex="100" formControlName="checkDeviceInactivity"> |
|||
{{ 'gateway.checking-device-activity' | translate }} |
|||
</mat-slide-toggle> |
|||
<span class="tb-hint" style="padding-left: 40px; margin-top: -15px;">{{'gateway.hints.check-device-activity' | translate}}</span> |
|||
</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div fxLayout="row wrap"> |
|||
<mat-form-field |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.inactivity-timeout-seconds</mat-label> |
|||
<input matInput formControlName="inactivityTimeoutSeconds" type="number" min="0" /> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('required')"> |
|||
{{'gateway.inactivity-timeout-seconds-required' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityTimeoutSeconds').hasError('min')"> |
|||
{{'gateway.inactivity-timeout-seconds-min' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.inactivity-timeout' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.checkDeviceInactivity').value" |
|||
fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.inactivity-check-period-seconds</mat-label> |
|||
<input matInput formControlName="inactivityCheckPeriodSeconds"/> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')"> |
|||
{{'gateway.inactivity-check-period-seconds-required' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('min')"> |
|||
{{'gateway.inactivity-check-period-seconds-min' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.inactivity-period' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
<mat-expansion-panel expanded="true" disabled> |
|||
<mat-expansion-panel-header> |
|||
<mat-panel-title translate class="expansion-panel-header">gateway.advanced</mat-panel-title> |
|||
</mat-expansion-panel-header> |
|||
<div fxLayout="row wrap"> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.min-pack-send-delay</mat-label> |
|||
<input matInput formControlName="minPackSendDelayMS" type="number" min="0" /> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('required')"> |
|||
{{ 'gateway.min-pack-send-delay-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('min')"> |
|||
{{ 'gateway.min-pack-send-delay-min' | translate }} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.minimal-pack-delay' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.mqtt-qos</mat-label> |
|||
<input matInput formControlName="qos" type="number" min="0" max="1"/> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('required')"> |
|||
{{ 'gateway.mqtt-qos-required' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('min')"> |
|||
{{ 'gateway.mqtt-qos-range' | translate}} |
|||
</mat-error> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.qos').hasError('max')"> |
|||
{{ 'gateway.mqtt-qos-range' | translate}} |
|||
</mat-error> |
|||
<mat-icon class="mat-form-field-infix pe suffix-icon" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.qos' | translate }}">info_outlined |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<mat-form-field fxFlex="calc(50%-15px)" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.statistics.check-connectors-configuration</mat-label> |
|||
<input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number" min="0" /> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')"> |
|||
{{'gateway.statistics.check-connectors-configuration-required' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')"> |
|||
{{'gateway.statistics.check-connectors-configuration-min' | translate }} |
|||
</mat-error> |
|||
<mat-error |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')"> |
|||
{{'gateway.statistics.check-connectors-configuration-pattern' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</mat-expansion-panel> |
|||
|
|||
</mat-accordion> |
|||
</div> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
<div fxFlex></div> |
|||
<div mat-dialog-actions fxLayoutAlign="start center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
*ngIf="dialogRef" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
type="button" |
|||
[disabled]="gatewayConfigGroup.invalid || !gatewayConfigGroup.dirty" |
|||
(click)="saveConfig()"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
|
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,201 @@ |
|||
/** |
|||
* Copyright © 2016-2023 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: block; |
|||
|
|||
.mat-icon { |
|||
color: rgba(0, 0, 0, .12); |
|||
} |
|||
|
|||
.mat-toolbar { |
|||
background: transparent; |
|||
color: rgba(0, 0, 0, .87) !important; |
|||
} |
|||
|
|||
.mat-content { |
|||
.expansion-panel-header { |
|||
font-weight: 600; |
|||
color: rgba(0, 0, 0, .87) !important; |
|||
} |
|||
|
|||
mat-slide-toggle { |
|||
margin-bottom: 16px; |
|||
} |
|||
|
|||
mat-form-field { |
|||
margin-right: 15px; |
|||
} |
|||
|
|||
.block-title { |
|||
font-size: 20px; |
|||
font-weight: 400; |
|||
padding-top: 16px; |
|||
} |
|||
|
|||
.hover-cursor { |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
.security-toggle-group { |
|||
padding-bottom: 0; |
|||
background: transparent !important; |
|||
margin-bottom: 15px; |
|||
border: none; |
|||
|
|||
.mat-button-toggle { |
|||
border-radius: 15px; |
|||
border: 1px solid rgba(0, 0, 0, 0); |
|||
background: rgba(0, 0, 0, .05); |
|||
margin: 5px; |
|||
color: rgba(0, 0, 0, .2); |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.mat-button-toggle-checked { |
|||
border-radius: 15px; |
|||
border: 1px solid #305680; |
|||
background: white; |
|||
color: #305680; |
|||
} |
|||
|
|||
::ng-deep span { |
|||
text-transform: capitalize; |
|||
line-height: 20px; |
|||
font-size: 13px; |
|||
padding: 5px 25px; |
|||
} |
|||
|
|||
::ng-deep .mat-button-toggle-ripple { |
|||
border-radius: 15px; |
|||
} |
|||
|
|||
::ng-deep .mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { |
|||
opacity: 0 !important; |
|||
} |
|||
} |
|||
|
|||
.logs-label { |
|||
font-weight: 500; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.logs-toggle-group { |
|||
border: none; |
|||
border-radius: 15px; |
|||
background: rgba(0, 0, 0, .05); |
|||
color: rgba(0, 0, 0, .2); |
|||
font-weight: 500; |
|||
|
|||
.mat-button-toggle { |
|||
border: none; |
|||
background: rgba(0, 0, 0, .0); |
|||
color: rgba(0, 0, 0, .2); |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.mat-button-toggle-checked { |
|||
border-radius: 15px; |
|||
color: white; |
|||
background: #305680; |
|||
} |
|||
|
|||
::ng-deep span { |
|||
line-height: 20px; |
|||
font-size: 13px; |
|||
padding: 5px 25px; |
|||
} |
|||
|
|||
::ng-deep .mat-button-toggle-ripple { |
|||
border-radius: 15px; |
|||
} |
|||
|
|||
::ng-deep .mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { |
|||
opacity: 0 !important; |
|||
} |
|||
} |
|||
|
|||
.statistics-block { |
|||
margin-bottom: 15px; |
|||
padding-left: 15px; |
|||
padding-top: 15px; |
|||
} |
|||
|
|||
.first-capital { |
|||
text-transform: capitalize; |
|||
} |
|||
|
|||
::ng-deep.mat-tab-label-active { |
|||
color: white; |
|||
opacity: 1; |
|||
} |
|||
|
|||
::ng-deep.mat-tab-label, ::ng-deep.mat-tab-label-active { |
|||
min-width: 50px !important; |
|||
padding: 3px !important; |
|||
margin: 3px !important; |
|||
flex-grow: 1; |
|||
} |
|||
|
|||
::ng-deep.mat-ink-bar { |
|||
height: 100%; |
|||
z-index: -10; |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
::ng-deep.pe { |
|||
pointer-events: all; |
|||
} |
|||
|
|||
::ng-deep.mat-mdc-form-field-icon-suffix { |
|||
z-index: 100; |
|||
} |
|||
|
|||
.suffix-icon { |
|||
position: relative; |
|||
transform: translateY(5px); |
|||
} |
|||
|
|||
mat-panel-title { |
|||
display: block; |
|||
padding-top: 20px; |
|||
} |
|||
|
|||
mat-panel-title span { |
|||
display: block; |
|||
padding-left: 0; |
|||
padding-top: 5px; |
|||
} |
|||
|
|||
.tb-hint { |
|||
font-size: 13px; |
|||
color: rgba(0, 0, 0, .54); |
|||
width: fit-content; |
|||
cursor: pointer; |
|||
text-transform: none!important; |
|||
} |
|||
|
|||
.line-break { |
|||
width: 100%; |
|||
} |
|||
|
|||
textarea { |
|||
resize: none; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,589 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; |
|||
import { AttributeService } from '@core/http/attribute.service'; |
|||
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
|||
import { |
|||
GatewayRemoteConfigurationDialogComponent, |
|||
GatewayRemoteConfigurationDialogData |
|||
} from '@shared/components/dialog/gateway-remote-configuration-dialog'; |
|||
import { DeviceService } from '@core/http/device.service'; |
|||
import { of } from 'rxjs'; |
|||
import { mergeMap } from 'rxjs/operators'; |
|||
import { DeviceCredentialsType } from '@shared/models/device.models'; |
|||
|
|||
export enum StorageTypes { |
|||
MEMORY = 'memory', |
|||
FILE = 'file', |
|||
SQLITE = 'sqlite' |
|||
} |
|||
|
|||
export enum GatewayLogLevel { |
|||
none = 'NONE', |
|||
critical = 'CRITICAL', |
|||
error = 'ERROR', |
|||
warning = 'WARNING', |
|||
info = 'INFO', |
|||
debug = 'DEBUG' |
|||
} |
|||
|
|||
export enum LogSavingPeriod { |
|||
days = 'D', |
|||
hours = 'H', |
|||
minutes = 'M', |
|||
seconds = 'S' |
|||
} |
|||
|
|||
export enum LocalLogsConfigs { |
|||
service = 'service', |
|||
connector = 'connector', |
|||
converter = 'converter', |
|||
tb_connection = 'tb_connection', |
|||
storage = 'storage', |
|||
extension = 'extension' |
|||
} |
|||
|
|||
export const localLogsConfigLabels = new Map<LocalLogsConfigs, string>([ |
|||
[LocalLogsConfigs.service, 'Service'], |
|||
[LocalLogsConfigs.connector, 'Connector'], |
|||
[LocalLogsConfigs.converter, 'Converter'], |
|||
[LocalLogsConfigs.tb_connection, 'TB Connection'], |
|||
[LocalLogsConfigs.storage, 'Storage'], |
|||
[LocalLogsConfigs.extension, 'Extension'] |
|||
]) |
|||
|
|||
export const logSavingPeriodTranslations = new Map<LogSavingPeriod, string>( |
|||
[ |
|||
[LogSavingPeriod.days, 'gateway.logs.days'], |
|||
[LogSavingPeriod.hours, 'gateway.logs.hours'], |
|||
[LogSavingPeriod.minutes, 'gateway.logs.minutes'], |
|||
[LogSavingPeriod.seconds, 'gateway.logs.seconds'] |
|||
] |
|||
); |
|||
|
|||
export const storageTypesTranslations = new Map<StorageTypes, string>( |
|||
[ |
|||
[StorageTypes.MEMORY, 'gateway.storage-types.memory-storage'], |
|||
[StorageTypes.FILE, 'gateway.storage-types.file-storage'], |
|||
[StorageTypes.SQLITE, 'gateway.storage-types.sqlite'] |
|||
] |
|||
); |
|||
|
|||
export enum SecurityTypes { |
|||
ACCESS_TOKEN = 'accessToken', |
|||
USERNAME_PASSWORD = 'usernamePassword', |
|||
TLS_ACCESS_TOKEN = 'tlsAccessToken', |
|||
TLS_PRIVATE_KEY = 'tlsPrivateKey' |
|||
} |
|||
|
|||
export const securityTypesTranslationsMap = new Map<SecurityTypes, string>( |
|||
[ |
|||
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'], |
|||
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'], |
|||
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'], |
|||
// [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
|
|||
] |
|||
); |
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-configuration', |
|||
templateUrl: './gateway-configuration.component.html', |
|||
styleUrls: ['./gateway-configuration.component.scss'] |
|||
}) |
|||
export class GatewayConfigurationComponent implements OnInit { |
|||
|
|||
gatewayConfigGroup: FormGroup; |
|||
|
|||
storageTypes = storageTypesTranslations; |
|||
|
|||
logSavingPeriods = logSavingPeriodTranslations; |
|||
|
|||
localLogsConfigLabels = localLogsConfigLabels; |
|||
|
|||
securityTypes = securityTypesTranslationsMap; |
|||
|
|||
gatewayLogLevel = Object.values(GatewayLogLevel); |
|||
|
|||
@Input() |
|||
device: EntityId; |
|||
|
|||
@Input() |
|||
dialogRef: MatDialogRef<any>; |
|||
|
|||
logSelector: FormControl; |
|||
|
|||
|
|||
constructor(protected router: Router, |
|||
protected store: Store<AppState>, |
|||
protected fb: FormBuilder, |
|||
protected attributeService: AttributeService, |
|||
protected deviceService: DeviceService, |
|||
private cd: ChangeDetectorRef, |
|||
public dialog: MatDialog) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.gatewayConfigGroup = this.fb.group({ |
|||
thingsboard: this.fb.group({ |
|||
host: [window.location.hostname, [Validators.required]], |
|||
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], |
|||
remoteShell: [false, []], |
|||
remoteConfiguration: [true, []], |
|||
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
statistics: this.fb.group({ |
|||
enable: [true, []], |
|||
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
commands: this.fb.array([], []) |
|||
}), |
|||
maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
minPackSendDelayMS: [200, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]], |
|||
minPackSizeToSend: [500, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
handleDeviceRenaming: [true, []], |
|||
checkingDeviceActivity: this.fb.group({ |
|||
checkDeviceInactivity: [false, []], |
|||
inactivityTimeoutSeconds: [200, [Validators.min(1)]], |
|||
inactivityCheckPeriodSeconds: [500, [Validators.min(1)]] |
|||
}), |
|||
security: this.fb.group({ |
|||
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]], |
|||
accessToken: [null, [Validators.required]], |
|||
clientId: [null, []], |
|||
username: [null, []], |
|||
password: [null, []], |
|||
caCert: [null, []], |
|||
cert: [null, []], |
|||
privateKey: [null, []], |
|||
}), |
|||
qos: [1, [Validators.min(0), Validators.max(1), Validators.required]] |
|||
}), |
|||
storage: this.fb.group({ |
|||
type: [StorageTypes.MEMORY, [Validators.required]], |
|||
read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]], |
|||
max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]], |
|||
data_folder_path: ['./data/', []], |
|||
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
data_file_path: ['./data/data.db', []], |
|||
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
|
|||
}), |
|||
grpc: this.fb.group({ |
|||
enabled: [false, []], |
|||
serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], |
|||
keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
keepalivePermitWithoutCalls: [true, []], |
|||
maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]], |
|||
minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
}), |
|||
connectors: this.fb.array([]), |
|||
logs: this.fb.group({ |
|||
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required]], |
|||
logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s', |
|||
[Validators.required]], |
|||
type: ['remote', [Validators.required]], |
|||
remote: this.fb.group({ |
|||
enabled: [false], |
|||
logLevel: [GatewayLogLevel.info, [Validators.required]], |
|||
}), |
|||
local: this.fb.group({}) |
|||
}) |
|||
}); |
|||
|
|||
this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => { |
|||
if (!enabled) { |
|||
this.openConfigurationConfirmDialog(); |
|||
} |
|||
}); |
|||
|
|||
this.logSelector = this.fb.control(LocalLogsConfigs.service); |
|||
|
|||
for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) { |
|||
this.addLocalLogConfig(localLogsConfigsKey, {}); |
|||
} |
|||
|
|||
const checkingDeviceActivityGroup = this.gatewayConfigGroup.get('thingsboard.checkingDeviceActivity') as FormGroup; |
|||
checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => { |
|||
checkingDeviceActivityGroup.updateValueAndValidity(); |
|||
if (enabled) { |
|||
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators([Validators.min(1), Validators.required]); |
|||
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]); |
|||
} else { |
|||
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators(); |
|||
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setErrors(null); |
|||
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators(); |
|||
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setErrors(null); |
|||
} |
|||
}); |
|||
|
|||
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; |
|||
securityGroup.get('type').valueChanges.subscribe(type => { |
|||
this.removeAllSecurityValidators(); |
|||
if (type === SecurityTypes.ACCESS_TOKEN) { |
|||
securityGroup.get('accessToken').addValidators([Validators.required]); |
|||
securityGroup.get('accessToken').updateValueAndValidity(); |
|||
} else if (type === SecurityTypes.TLS_PRIVATE_KEY) { |
|||
securityGroup.get('caCert').addValidators([Validators.required]); |
|||
securityGroup.get('caCert').updateValueAndValidity(); |
|||
securityGroup.get('privateKey').addValidators([Validators.required]); |
|||
securityGroup.get('privateKey').updateValueAndValidity(); |
|||
securityGroup.get('cert').addValidators([Validators.required]); |
|||
securityGroup.get('cert').updateValueAndValidity(); |
|||
} else if (type === SecurityTypes.TLS_ACCESS_TOKEN) { |
|||
securityGroup.get('accessToken').addValidators([Validators.required]); |
|||
securityGroup.get('accessToken').updateValueAndValidity(); |
|||
securityGroup.get('caCert').addValidators([Validators.required]); |
|||
securityGroup.get('caCert').updateValueAndValidity(); |
|||
} else if (type === SecurityTypes.USERNAME_PASSWORD) { |
|||
securityGroup.get('clientId').addValidators([Validators.required]); |
|||
securityGroup.get('clientId').updateValueAndValidity(); |
|||
securityGroup.get('username').addValidators([Validators.required]); |
|||
securityGroup.get('username').updateValueAndValidity(); |
|||
securityGroup.get('password').addValidators([Validators.required]); |
|||
securityGroup.get('password').updateValueAndValidity(); |
|||
} |
|||
securityGroup.updateValueAndValidity(); |
|||
}); |
|||
|
|||
securityGroup.get('caCert').valueChanges.subscribe(_ => this.cd.detectChanges()) |
|||
securityGroup.get('privateKey').valueChanges.subscribe(_ => this.cd.detectChanges()) |
|||
securityGroup.get('cert').valueChanges.subscribe(_ => this.cd.detectChanges()) |
|||
|
|||
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; |
|||
storageGroup.get('type').valueChanges.subscribe(type => { |
|||
this.removeAllStorageValidators(); |
|||
if (type === StorageTypes.MEMORY) { |
|||
storageGroup.get('read_records_count').addValidators( |
|||
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]); |
|||
storageGroup.get('max_records_count').addValidators( |
|||
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); |
|||
} else if (type === StorageTypes.FILE) { |
|||
storageGroup.get('data_folder_path').addValidators([Validators.required]); |
|||
storageGroup.get('max_file_count').addValidators( |
|||
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); |
|||
storageGroup.get('max_read_records_count').addValidators( |
|||
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); |
|||
storageGroup.get('max_records_per_file').addValidators( |
|||
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); |
|||
} else if (type === StorageTypes.SQLITE) { |
|||
storageGroup.get('data_file_path').addValidators([Validators.required]); |
|||
storageGroup.get('messages_ttl_check_in_hours').addValidators( |
|||
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); |
|||
storageGroup.get('messages_ttl_in_days').addValidators( |
|||
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); |
|||
} |
|||
}); |
|||
|
|||
this.fetchConfigAttribute(this.device); |
|||
} |
|||
|
|||
fetchConfigAttribute(entityId: EntityId) { |
|||
this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, |
|||
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe( |
|||
mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( |
|||
entityId, AttributeScope.SHARED_SCOPE, ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel'])) |
|||
).subscribe(attributes => { |
|||
if (attributes.length) { |
|||
const general_configuration = attributes.find(attribute => attribute.key === 'general_configuration')?.value; |
|||
const grpc_configuration = attributes.find(attribute => attribute.key === 'grpc_configuration')?.value; |
|||
const logs_configuration = attributes.find(attribute => attribute.key === 'logs_configuration')?.value; |
|||
const storage_configuration = attributes.find(attribute => attribute.key === 'storage_configuration')?.value; |
|||
const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel')?.value; |
|||
if (general_configuration) { |
|||
const configObj = {thingsboard: general_configuration}; |
|||
if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) { |
|||
for (const command of configObj.thingsboard.statistics.commands) { |
|||
this.addCommand(command); |
|||
} |
|||
delete configObj.thingsboard.statistics.commands; |
|||
} |
|||
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); |
|||
this.gatewayConfigGroup.markAsPristine(); |
|||
if (!configObj.thingsboard.remoteConfiguration) { |
|||
this.gatewayConfigGroup.disable({emitEvent: false}); |
|||
} |
|||
this.checkAndFetchCredentials(configObj.thingsboard.security); |
|||
} |
|||
if (grpc_configuration) { |
|||
const configObj = {grpc: grpc_configuration}; |
|||
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); |
|||
} |
|||
if (logs_configuration) { |
|||
const configObj = {logs: this.logsToObj(logs_configuration)}; |
|||
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); |
|||
this.cd.detectChanges(); |
|||
} |
|||
if (storage_configuration) { |
|||
const configObj = {storage: storage_configuration}; |
|||
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); |
|||
} |
|||
if (remoteLoggingLevel) { |
|||
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); |
|||
remoteLogsFormGroup.patchValue({ |
|||
enabled: remoteLoggingLevel !== GatewayLogLevel.none, |
|||
logLevel: remoteLoggingLevel |
|||
}, {emitEvent: false}); |
|||
remoteLogsFormGroup.markAsPristine(); |
|||
} |
|||
this.cd.detectChanges(); |
|||
} else { |
|||
this.checkAndFetchCredentials({}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
checkAndFetchCredentials(security): void { |
|||
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) { |
|||
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => { |
|||
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type !== SecurityTypes.TLS_ACCESS_TOKEN) { |
|||
this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId); |
|||
} else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) { |
|||
const parsedValue = JSON.parse(credentials.credentialsValue); |
|||
this.gatewayConfigGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId); |
|||
this.gatewayConfigGroup.get('thingsboard.security.username').setValue(parsedValue.userName); |
|||
this.gatewayConfigGroup.get('thingsboard.security.password').setValue(parsedValue.password); |
|||
} else if (credentials.credentialsType === DeviceCredentialsType.X509_CERTIFICATE ) { |
|||
//if sertificate is present set sertificate as present
|
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
logsToObj(logsConfig) { |
|||
const logsObject = { |
|||
local: {} |
|||
} |
|||
const logFormat = logsConfig.formatters.LogFormatter.format; |
|||
const dateFormat = logsConfig.formatters.LogFormatter.datefmt; |
|||
for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) { |
|||
const handlerKey = localLogsConfigsKey + "Handler"; |
|||
logsObject[localLogsConfigsKey] = { |
|||
logLevel: logsConfig.loggers[localLogsConfigsKey].level, |
|||
filePath: logsConfig.handlers[handlerKey].filename.split('/'+localLogsConfigsKey)[0], |
|||
backupCount: logsConfig.handlers[handlerKey].backupCount, |
|||
savingTime: logsConfig.handlers[handlerKey].interval, |
|||
savingPeriod: logsConfig.handlers[handlerKey].when, |
|||
} |
|||
} |
|||
|
|||
|
|||
return {local: logsObject, logFormat, dateFormat}; |
|||
} |
|||
|
|||
addCommand(command?): void { |
|||
const data = command || {}; |
|||
const commandsFormArray = this.commandFormArray(); |
|||
const commandFormGroup = this.fb.group({ |
|||
attributeOnGateway: [data.attributeOnGateway || null, [Validators.required]], |
|||
command: [data.command || null, [Validators.required]], |
|||
timeout: [data.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], |
|||
}); |
|||
commandsFormArray.push(commandFormGroup); |
|||
} |
|||
|
|||
addLocalLogConfig(name, config): void { |
|||
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup; |
|||
const configGroup = this.fb.group({ |
|||
logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]], |
|||
filePath: [config.filePath || './logs', [Validators.required]], |
|||
backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]], |
|||
savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]], |
|||
savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]] |
|||
}); |
|||
localLogsFormGroup.addControl(name, configGroup); |
|||
} |
|||
|
|||
getLogFormGroup(value: string): FormGroup { |
|||
return this.gatewayConfigGroup.get(`logs.local.${value}`) as FormGroup; |
|||
} |
|||
|
|||
commandFormArray(): FormArray { |
|||
return this.gatewayConfigGroup.get('thingsboard.statistics.commands') as FormArray; |
|||
} |
|||
|
|||
removeCommandControl(index: number): void { |
|||
this.commandFormArray().removeAt(index); |
|||
this.gatewayConfigGroup.markAsDirty(); |
|||
} |
|||
|
|||
removeAllSecurityValidators(): void { |
|||
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; |
|||
for (const controlsKey in securityGroup.controls) { |
|||
if (controlsKey !== 'type') { |
|||
securityGroup.controls[controlsKey].clearValidators(); |
|||
securityGroup.controls[controlsKey].setErrors(null); |
|||
securityGroup.controls[controlsKey].updateValueAndValidity(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
removeAllStorageValidators(): void { |
|||
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; |
|||
for (const storageKey in storageGroup.controls) { |
|||
if (storageKey !== 'type') { |
|||
storageGroup.controls[storageKey].clearValidators(); |
|||
storageGroup.controls[storageKey].setErrors(null); |
|||
storageGroup.controls[storageKey].updateValueAndValidity(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
removeEmpty(obj) { |
|||
return Object.fromEntries( |
|||
Object.entries(obj) |
|||
.filter(([_, v]) => v != null) |
|||
.map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v]) |
|||
); |
|||
} |
|||
|
|||
generateLogsFile(logsObj) { |
|||
const logAttrObj = { |
|||
version: 1, |
|||
disable_existing_loggers: false, |
|||
formatters: { |
|||
LogFormatter: { |
|||
class: "logging.Formatter", |
|||
format: logsObj.logFormat, |
|||
datefmt: logsObj.dateFormat, |
|||
} |
|||
}, |
|||
handlers: { |
|||
consoleHandler: { |
|||
class: "logging.StreamHandler", |
|||
formatter: "LogFormatter", |
|||
level: "DEBUG", |
|||
stream: "ext://sys.stdout" |
|||
}, |
|||
databaseHandler: { |
|||
class: "thingsboard_gateway.tb_utility.tb_logger.TimedRotatingFileHandler", |
|||
formatter: "LogFormatter", |
|||
filename: "./logs/database.log", |
|||
backupCount: 1, |
|||
encoding: "utf-8" |
|||
} |
|||
}, |
|||
loggers: { |
|||
database: { |
|||
handlers: ["databaseHandler", "consoleHandler"], |
|||
level: "DEBUG", |
|||
propagate: false |
|||
} |
|||
}, |
|||
root: { |
|||
level: "ERROR", |
|||
handlers: [ |
|||
"consoleHandler" |
|||
] |
|||
} |
|||
} |
|||
for (const key of Object.keys(logsObj.local)) { |
|||
logAttrObj.handlers[key+"Handler"] = this.createHandlerObj(logsObj.local[key], key); |
|||
logAttrObj.loggers[key] = this.createLoggerObj(logsObj.local[key], key); |
|||
} |
|||
return logAttrObj; |
|||
} |
|||
|
|||
createHandlerObj(logObj, key) { |
|||
return { |
|||
class: "thingsboard_gateway.tb_utility.tb_logger.TimedRotatingFileHandler", |
|||
formatter: "LogFormatter", |
|||
filename: `${logObj.filePath}/${key}.log`, |
|||
backupCount: logObj.backupCount, |
|||
interval: logObj.savingTime, |
|||
when: logObj.savingPeriod, |
|||
encoding: "utf-8" |
|||
} |
|||
} |
|||
|
|||
createLoggerObj(logObj, key) { |
|||
return { |
|||
handlers: [`${key}Handler`, "consoleHandler"], |
|||
level: logObj.logLevel, |
|||
propagate: false |
|||
} |
|||
} |
|||
|
|||
saveConfig(): void { |
|||
const value = this.removeEmpty(this.gatewayConfigGroup.value); |
|||
value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands); |
|||
const attributes = []; |
|||
attributes.push({ |
|||
key: 'RemoteLoggingLevel', |
|||
value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.none |
|||
}); |
|||
delete value.connectors; |
|||
attributes.push({ |
|||
key: 'logs_configuration', |
|||
value: this.generateLogsFile(value.logs) |
|||
}); |
|||
attributes.push({ |
|||
key: 'grpc_configuration', |
|||
value: value.grpc |
|||
}); |
|||
attributes.push({ |
|||
key: 'storage_configuration', |
|||
value: value.storage |
|||
}); |
|||
attributes.push({ |
|||
key: 'general_configuration', |
|||
value: value.thingsboard |
|||
}); |
|||
|
|||
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { |
|||
if (this.dialogRef) { |
|||
this.dialogRef.close(); |
|||
} else { |
|||
this.gatewayConfigGroup.markAsPristine(); |
|||
this.cd.detectChanges(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
cancel(): void { |
|||
if (this.dialogRef) { |
|||
this.dialogRef.close(); |
|||
} |
|||
} |
|||
|
|||
private openConfigurationConfirmDialog(): void { |
|||
this.deviceService.getDevice(this.device.id).subscribe(gateway => { |
|||
this.dialog.open<GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogData> |
|||
(GatewayRemoteConfigurationDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
gatewayName: gateway.name |
|||
} |
|||
}).afterClosed().subscribe( |
|||
(res) => { |
|||
if (!res) { |
|||
this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false}); |
|||
} |
|||
} |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,167 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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 style="height: calc(100% - 7px)" fxLayout="column"> |
|||
<div fxLayout="row" fxLayoutGap="15px" fxFlex class="connector-container"> |
|||
<mat-card fxLayout="column" style="overflow: auto;"> |
|||
<mat-toolbar class="mat-mdc-table-toolbar"> |
|||
<h2>Connectors</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
[disabled]="isLoading$ | async" |
|||
(click)="addAttribute()" |
|||
matTooltip="{{ 'action.add' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>add</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<table mat-table [dataSource]="dataSource" |
|||
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" |
|||
matSortDisableClear> |
|||
<ng-container matColumnDef="enabled" sticky> |
|||
<mat-header-cell *matHeaderCellDef style="width: 30px;"> |
|||
Enabled |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute"> |
|||
<mat-slide-toggle [checked]="activeConnectors.includes(attribute.key)" |
|||
(click)="$event.stopPropagation(); enableConnector(attribute)"></mat-slide-toggle> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="key"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 40%"> Name</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute"> |
|||
{{ attribute.key }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="type"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%"> Type</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase"> |
|||
{{ returnType(attribute) }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<mat-header-cell *matHeaderCellDef |
|||
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}"> |
|||
Actions |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute" |
|||
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}"> |
|||
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> |
|||
<button mat-icon-button |
|||
matTooltip="RPC" |
|||
matTooltipPosition="above" |
|||
(click)="connectorRpc(attribute, $event)"> |
|||
<mat-icon>private_connectivity</mat-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
matTooltip="Logs" |
|||
matTooltipPosition="above" |
|||
(click)="connectorLogs(attribute, $event)"> |
|||
<mat-icon>list</mat-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
matTooltip="Delete connector" |
|||
matTooltipPosition="above" |
|||
(click)="deleteConnector(attribute, $event)"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</div> |
|||
<div fxHide fxShow.lt-lg> |
|||
<button mat-icon-button |
|||
(click)="$event.stopPropagation()" |
|||
[matMenuTriggerFor]="cellActionsMenu"> |
|||
<mat-icon class="material-icons">more_vert</mat-icon> |
|||
</button> |
|||
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> |
|||
<button mat-icon-button |
|||
matTooltip="RPC" |
|||
matTooltipPosition="above" |
|||
(click)="connectorRpc(attribute, $event)"> |
|||
<mat-icon>private_connectivity</mat-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
matTooltip="Delete connector" |
|||
matTooltipPosition="above" |
|||
(click)="connectorLogs(attribute, $event)"> |
|||
<mat-icon>list</mat-icon> |
|||
</button> |
|||
<button mat-icon-button |
|||
matTooltip="Delete connector" |
|||
matTooltipPosition="above" |
|||
(click)="deleteConnector(attribute, $event)"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
</mat-menu> |
|||
</div> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row [ngClass]="{'mat-row-select': true}" |
|||
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
|||
<mat-row [ngClass]="{'mat-row-select': true}" |
|||
*matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row> |
|||
</table> |
|||
<mat-divider></mat-divider> |
|||
</mat-card> |
|||
<div [formGroup]="connectorForm" fxLayout="column"> |
|||
<mat-card fxLayout="row" fxLayoutGap="15px"> |
|||
<mat-form-field class="mat-block tb-value-type"> |
|||
<mat-label>Name</mat-label> |
|||
<input matInput formControlName="name"/> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type"> |
|||
<mat-label>Type</mat-label> |
|||
<mat-select formControlName="type"> |
|||
<mat-option style="text-transform: uppercase" |
|||
*ngFor="let type of gatewayConnectorDefaultTypes | keyvalue" [value]="type.key">{{type.value}}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'grpc'"> |
|||
<mat-label>Key</mat-label> |
|||
<input matInput formControlName="key"/> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type" *ngIf="connectorForm.get('type').value === 'custom'"> |
|||
<mat-label>Class</mat-label> |
|||
<input matInput formControlName="class"/> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.remote-logging-level</mat-label> |
|||
<mat-select formControlName="log_level"> |
|||
<mat-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{logLevel}}</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
</mat-card> |
|||
<mat-card fxLayout="column" fxFlex> |
|||
<tb-json-object-edit |
|||
fxFlex |
|||
fxLayout="column" |
|||
required |
|||
label="{{ 'gateway.connector-json' | translate }}" |
|||
formControlName="configurationJson"> |
|||
</tb-json-object-edit> |
|||
<div fxLayoutAlign="start center"> |
|||
<button mat-raised-button color="primary" |
|||
class="action-btns" |
|||
type="button" |
|||
[disabled]="!connectorForm.dirty || connectorForm.invalid" |
|||
(click)="saveConnector()"> |
|||
{{ 'action.save' | translate }} |
|||
</button> |
|||
</div> |
|||
</mat-card> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,90 @@ |
|||
/** |
|||
* Copyright © 2016-2023 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: block; |
|||
overflow-x: auto; |
|||
padding: 0; |
|||
|
|||
.connector-container { |
|||
width: 100%; |
|||
|
|||
&>mat-card, &>div { |
|||
min-width: 50%; |
|||
} |
|||
|
|||
mat-card { |
|||
margin: 10px; |
|||
padding: 10px; |
|||
max-width: 100%; |
|||
} |
|||
} |
|||
|
|||
.tb-entity-table { |
|||
.tb-entity-table-content { |
|||
width: 100%; |
|||
height: 100%; |
|||
background: #fff; |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
|
|||
.mat-toolbar { |
|||
background: transparent; |
|||
color: rgba(0, 0, 0, .87) !important; |
|||
} |
|||
|
|||
.mat-mdc-form-field { |
|||
flex-grow: 1; |
|||
//min-width: 200px; |
|||
//flex-shrink: 1; |
|||
} |
|||
|
|||
mat-expansion-panel { |
|||
border: 1px solid #305680; |
|||
} |
|||
|
|||
mat-card { |
|||
padding-left: 10px; |
|||
background: transparent; |
|||
} |
|||
|
|||
.mat-card-selected { |
|||
background-color: rgba(48, 86, 128, 0.1); |
|||
} |
|||
|
|||
.mat-mdc-slide-toggle { |
|||
margin: 15px; |
|||
} |
|||
|
|||
::ng-deep tb-json-object-edit > div { |
|||
flex-grow: 1; |
|||
} |
|||
|
|||
.mat-accordion .mat-expansion-panel { |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
.action-btns { |
|||
margin: 10px 10px 0; |
|||
} |
|||
|
|||
mat-row { |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,390 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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 { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { AttributeService } from '@core/http/attribute.service'; |
|||
import { DeviceService } from '@core/http/device.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { forkJoin, merge } from 'rxjs'; |
|||
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource'; |
|||
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
|||
import { MatSort } from '@angular/material/sort'; |
|||
import { tap } from 'rxjs/operators'; |
|||
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; |
|||
import { MatTableDataSource } from '@angular/material/table'; |
|||
import { GatewayLogLevel } from '@shared/components/device/gateway-configuration.component'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
import { DialogService } from '@app/core/services/dialog.service'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { deepClone } from '@core/utils'; |
|||
|
|||
|
|||
export interface gatewayConnector { |
|||
name: string; |
|||
type: string; |
|||
configuration?: string; |
|||
configurationJson: string; |
|||
log_level: string; |
|||
key?: string; |
|||
} |
|||
|
|||
|
|||
export const GatewayConnectorDefaultTypesTranslates = new Map<string, string>([ |
|||
['mqtt', 'MQTT'], |
|||
['modbus', 'MODBUS'], |
|||
['grpc', 'GRPC'], |
|||
['opcua', 'OPCUA'], |
|||
['opcua_asyncio', 'OPCUA ASYNCIO'], |
|||
['ble', 'BLE'], |
|||
['request', 'REQUEST'], |
|||
['can', 'CAN'], |
|||
['bacnet', 'BACNET'], |
|||
['odbc', 'ODBC'], |
|||
['rest', 'REST'], |
|||
['snmp', 'SNMP'], |
|||
['ftp', 'FTP'], |
|||
['socket', 'SOCKET'], |
|||
['xmpp', 'XMPP'], |
|||
['ocpp', 'OCCP'], |
|||
['custom', 'CUSTOM'] |
|||
]); |
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-connector', |
|||
templateUrl: './gateway-connectors.component.html', |
|||
styleUrls: ['./gateway-connectors.component.scss'] |
|||
}) |
|||
export class GatewayConnectorComponent extends PageComponent implements AfterViewInit { |
|||
|
|||
pageLink: PageLink; |
|||
|
|||
attributeDataSource: AttributeDatasource; |
|||
|
|||
inactiveConnectorsDataSource: AttributeDatasource; |
|||
|
|||
dataSource: MatTableDataSource<AttributeData> |
|||
|
|||
displayedColumns = ['enabled', 'key', 'type', 'actions']; |
|||
|
|||
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslates; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
device: EntityId; |
|||
|
|||
@ViewChild('searchInput') searchInputField: ElementRef; |
|||
@ViewChild(MatSort) sort: MatSort; |
|||
|
|||
connectorForm: FormGroup; |
|||
|
|||
viewsInited = false; |
|||
|
|||
textSearchMode: boolean; |
|||
|
|||
activeConnectors: Array<string>; |
|||
|
|||
inactiveConnectors: Array<string>; |
|||
|
|||
InitialActiveConnectors: Array<string>; |
|||
|
|||
gatewayLogLevel = Object.values(GatewayLogLevel); |
|||
|
|||
activeData: Array<any> = []; |
|||
|
|||
inactiveData: Array<any> = []; |
|||
|
|||
constructor(protected router: Router, |
|||
protected store: Store<AppState>, |
|||
protected fb: FormBuilder, |
|||
protected translate: TranslateService, |
|||
protected attributeService: AttributeService, |
|||
protected deviceService: DeviceService, |
|||
protected dialogService: DialogService, |
|||
private telemetryWsService: TelemetryWebsocketService, |
|||
private zone: NgZone, |
|||
private cd: ChangeDetectorRef, |
|||
public dialog: MatDialog) { |
|||
super(store); |
|||
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC}; |
|||
this.pageLink = new PageLink(1000, 0, null, sortOrder); |
|||
this.attributeDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); |
|||
this.inactiveConnectorsDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); |
|||
this.dataSource = new MatTableDataSource<AttributeData>([]); |
|||
this.connectorForm = this.fb.group({ |
|||
name: ['', [Validators.required]], |
|||
type: ['', [Validators.required]], |
|||
log_level: ['', [Validators.required]], |
|||
key: ['auto'], |
|||
class: [''], |
|||
configuration: [''], |
|||
configurationJson: [{}, [Validators.required]] |
|||
}) |
|||
this.connectorForm.disable(); |
|||
} |
|||
|
|||
|
|||
initialConnector: gatewayConnector; |
|||
|
|||
ngAfterViewInit() { |
|||
this.connectorForm.valueChanges.subscribe(() => { |
|||
this.cd.detectChanges(); |
|||
}); |
|||
merge(this.sort.sortChange) |
|||
.pipe( |
|||
tap(() => this.updateData()) |
|||
) |
|||
.subscribe(); |
|||
|
|||
this.viewsInited = true; |
|||
if (this.device) { |
|||
forkJoin(this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']), |
|||
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors'])).subscribe(attributes => { |
|||
if (attributes.length) { |
|||
this.activeConnectors = attributes[0].length ? attributes[0][0].value : []; |
|||
this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : []; |
|||
this.updateData(true); |
|||
} else { |
|||
this.activeConnectors = []; |
|||
this.inactiveConnectors = []; |
|||
this.updateData(true); |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
saveConnector(): void { |
|||
const value = this.connectorForm.value; |
|||
value.configuration = this.camelize(value.name) + ".json"; |
|||
if (value.type !== 'grpc') { |
|||
delete value.key; |
|||
} |
|||
const attributesToSave = [{ |
|||
key: value.name, |
|||
value: value |
|||
}]; |
|||
const attributesToDelete = []; |
|||
const scope = (this.activeConnectors.includes(value.name) || !this.initialConnector) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; |
|||
let updateActiveConnectors = false; |
|||
if (this.initialConnector && this.initialConnector.name !== value.name) { |
|||
attributesToDelete.push({key: this.initialConnector.name}); |
|||
updateActiveConnectors = true; |
|||
const activeIndex = this.activeConnectors.indexOf(this.initialConnector.name); |
|||
const inactiveIndex = this.inactiveConnectors.indexOf(this.initialConnector.name); |
|||
if (activeIndex !== -1) this.activeConnectors.splice(activeIndex, 1); |
|||
if (inactiveIndex !== -1) this.inactiveConnectors.splice(activeIndex, 1); |
|||
} |
|||
if (!this.activeConnectors.includes(value.name) && scope == AttributeScope.SHARED_SCOPE) { |
|||
this.activeConnectors.push(value.name); |
|||
updateActiveConnectors = true; |
|||
} |
|||
if (!this.inactiveConnectors.includes(value.name) && scope == AttributeScope.SERVER_SCOPE) { |
|||
this.inactiveConnectors.push(value.name); |
|||
updateActiveConnectors = true; |
|||
} |
|||
const tasks = [this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributesToSave)]; |
|||
if (updateActiveConnectors) { |
|||
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ |
|||
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', |
|||
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors |
|||
}])); |
|||
} |
|||
|
|||
if (attributesToDelete.length) { |
|||
tasks.push(this.attributeService.deleteEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributesToDelete)); |
|||
} |
|||
forkJoin(tasks).subscribe(_ => { |
|||
this.showToast("Update Successful") |
|||
this.updateData(true); |
|||
}) |
|||
} |
|||
|
|||
resetSortAndFilter(update: boolean = true) { |
|||
this.textSearchMode = false; |
|||
this.pageLink.textSearch = null; |
|||
if (this.viewsInited) { |
|||
const sortable = this.sort.sortables.get('key'); |
|||
this.sort.active = sortable.id; |
|||
this.sort.direction = 'asc'; |
|||
if (update) { |
|||
this.updateData(true); |
|||
} |
|||
} |
|||
} |
|||
|
|||
updateData(reload: boolean = false) { |
|||
this.pageLink.sortOrder.property = this.sort.active; |
|||
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; |
|||
this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => { |
|||
this.activeData = data.data.filter(value => this.activeConnectors.includes(value.key)); |
|||
this.combineData() |
|||
}); |
|||
this.inactiveConnectorsDataSource.loadAttributes(this.device, AttributeScope.SHARED_SCOPE, this.pageLink, reload).subscribe(data => { |
|||
this.inactiveData = data.data.filter(value =>this.inactiveConnectors.includes(value.key)); |
|||
this.combineData() |
|||
}); |
|||
} |
|||
|
|||
combineData () { |
|||
this.dataSource.data = [...this.activeData, ...this.inactiveData]; |
|||
} |
|||
|
|||
addAttribute(): void { |
|||
if (this.connectorForm.disabled) { |
|||
this.connectorForm.enable(); |
|||
} |
|||
this.clearOutConnectorForm(); |
|||
} |
|||
|
|||
clearOutConnectorForm(): void { |
|||
this.connectorForm.setValue({ |
|||
name: '', |
|||
type: 'mqtt', |
|||
log_level: GatewayLogLevel.info, |
|||
key: 'auto', |
|||
class: '', |
|||
configuration: '', |
|||
configurationJson: {} |
|||
}) |
|||
this.initialConnector = null; |
|||
this.connectorForm.markAsPristine(); |
|||
} |
|||
|
|||
selectConnector(attribute): void { |
|||
if (this.connectorForm.disabled) { |
|||
this.connectorForm.enable(); |
|||
} |
|||
const connector = typeof attribute.value === 'string' ? JSON.parse(attribute.value): attribute.value; |
|||
if (!connector.configuration) { |
|||
connector.configuration = ""; |
|||
} |
|||
if (!connector.key) { |
|||
connector.key = 'auto'; |
|||
} |
|||
this.initialConnector = connector; |
|||
this.connectorForm.setValue(connector); |
|||
this.connectorForm.markAsPristine(); |
|||
} |
|||
|
|||
showToast(message: string) { |
|||
this.store.dispatch(new ActionNotificationShow( |
|||
{ |
|||
message, |
|||
type: 'success', |
|||
duration: 1000, |
|||
verticalPosition: 'top', |
|||
horizontalPosition: 'right', |
|||
target: 'dashboardRoot', |
|||
// panelClass: this.widgetNamespace,
|
|||
forceDismiss: true |
|||
})); |
|||
} |
|||
|
|||
returnType (attribute) { |
|||
const value = typeof attribute.value === 'string' ? JSON.parse(attribute.value): attribute.value; |
|||
return this.gatewayConnectorDefaultTypes.get(value.type) |
|||
} |
|||
|
|||
deleteConnector(attribute: AttributeData, $event: Event): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const title = `Delete connector ${attribute.key}?`; |
|||
const content = `All connector data will be deleted.`; |
|||
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => { |
|||
if (result) { |
|||
const tasks = []; |
|||
const scope = (this.activeConnectors.includes(attribute.key) || !this.initialConnector) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; |
|||
tasks.push(this.attributeService.deleteEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, [attribute])); |
|||
const activeIndex = this.activeConnectors.indexOf(attribute.key); |
|||
const inactiveIndex = this.inactiveConnectors.indexOf(attribute.key); |
|||
if (activeIndex !== -1) this.activeConnectors.splice(activeIndex, 1); |
|||
if (inactiveIndex !== -1) this.inactiveConnectors.splice(activeIndex, 1); |
|||
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ |
|||
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', |
|||
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors |
|||
}])); |
|||
forkJoin(tasks).subscribe(_ => { |
|||
if (this.initialConnector ? this.initialConnector.name === attribute.key : true) { |
|||
this.clearOutConnectorForm(); |
|||
this.cd.detectChanges(); |
|||
this.connectorForm.disable(); |
|||
} |
|||
this.updateData() |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
camelize(str): string { |
|||
return str.toLowerCase().replace(/\s+/g, '_'); |
|||
} |
|||
|
|||
connectorLogs(attribute: AttributeData, $event: Event): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value): attribute.value; |
|||
const params = deepClone(this.ctx.stateController.getStateParams()); |
|||
params.targetEntityParamName = "connector_logs"; |
|||
this.ctx.stateController.openState("connector_logs", params); |
|||
} |
|||
|
|||
connectorRpc(attribute: AttributeData, $event: Event): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value): attribute.value; |
|||
const params = deepClone(this.ctx.stateController.getStateParams()); |
|||
params.connector_rpc = attribute; |
|||
params.targetEntityParamName = "connector_rpc"; |
|||
this.ctx.stateController.openState("connector_rpc", params); |
|||
} |
|||
|
|||
|
|||
enableConnector(attribute): void { |
|||
const wasEnabled = this.activeConnectors.includes(attribute.key); |
|||
if (wasEnabled) { |
|||
let index = this.activeConnectors.indexOf(attribute.key); |
|||
this.activeConnectors.splice(index, 1); |
|||
this.inactiveConnectors.push(attribute.key); |
|||
} else { |
|||
let index = this.inactiveConnectors.indexOf(attribute.key); |
|||
this.inactiveConnectors.splice(index, 1); |
|||
this.activeConnectors.push(attribute.key); |
|||
} |
|||
forkJoin([this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, [{ |
|||
key: 'active_connectors', |
|||
value: this.activeConnectors |
|||
}]), this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, [{ |
|||
key: 'inactive_connectors', |
|||
value: this.inactiveConnectors |
|||
}]),]).subscribe(_ => { |
|||
this.updateData(); |
|||
}) |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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. |
|||
|
|||
--> |
|||
<nav mat-tab-nav-bar [tabPanel]="tabPanel"> |
|||
<a mat-tab-link *ngFor="let link of logLinks" |
|||
(click)="onTabChanged(link)" |
|||
[active]="activeLink.name == link.name"> {{link.name}} </a> |
|||
</nav> |
|||
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel> |
|||
<table mat-table [dataSource]="dataSource" |
|||
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" |
|||
matSortDisableClear> |
|||
<ng-container matColumnDef="ts"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header> Created time</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute"> |
|||
{{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss'}} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="status"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header> Status</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute"> |
|||
<span [ngClass]="statusClass(attribute.status )">{{ attribute.status }}</span> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="message"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">Message</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute" [ngClass]="statusClassMsg(attribute.status )"> |
|||
{{ attribute.message }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row [ngClass]="{'mat-row-select': true}" |
|||
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
|||
<mat-row [ngClass]="{'mat-row-select': true}" |
|||
*matRowDef="let attribute; columns: displayedColumns;"></mat-row> |
|||
<!-- <ng-container matColumnDef="lastUpdateTs">--> |
|||
<!-- <mat-header-cell *matHeaderCellDef mat-sort-header style="min-width: 150px; max-width: 150px; width: 150px;"> {{ 'attribute.last-update-time' | translate }} </mat-header-cell>--> |
|||
<!-- <mat-cell *matCellDef="let attribute">--> |
|||
<!-- {{ attribute.lastUpdateTs | date:'yyyy-MM-dd HH:mm:ss' }}--> |
|||
<!-- </mat-cell>--> |
|||
<!-- </ng-container>--> |
|||
</table> |
|||
<span [fxShow]="dataSource.data.length === 0" |
|||
fxLayoutAlign="center center" |
|||
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span> |
|||
<mat-divider></mat-divider> |
|||
|
|||
@ -0,0 +1,55 @@ |
|||
/** |
|||
* Copyright © 2016-2023 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
display: block; |
|||
overflow-x: auto; |
|||
padding: 0; |
|||
|
|||
|
|||
.status { |
|||
border-radius: 20px; |
|||
font-weight: 500; |
|||
padding: 5px 15px; |
|||
} |
|||
|
|||
.status-debug { |
|||
color: green; |
|||
background: rgba(0, 128, 0, 0.1); |
|||
} |
|||
|
|||
.status-warning { |
|||
color: orange; |
|||
background: rgba(255, 165, 0, 0.1); |
|||
} |
|||
|
|||
.status-error { |
|||
color: red; |
|||
background: rgba(255, 0, 0, 0.1); |
|||
} |
|||
|
|||
.status-info { |
|||
color: black; |
|||
background: rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.msg-status-exception { |
|||
color: red; |
|||
} |
|||
|
|||
} |
|||
|
|||
@ -0,0 +1,234 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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 { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { FormBuilder, FormGroup } from '@angular/forms'; |
|||
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; |
|||
import { AttributeService } from '@core/http/attribute.service'; |
|||
import { DeviceService } from '@core/http/device.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { AttributeData, DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { AttributeDatasource } from "@home/models/datasource/attribute-datasource"; |
|||
import { Direction, SortOrder } from "@shared/models/page/sort-order"; |
|||
import { MatSort } from '@angular/material/sort'; |
|||
import { MatTableDataSource } from '@angular/material/table'; |
|||
import { GatewayLogLevel } from '@shared/components/device/gateway-configuration.component'; |
|||
import { DialogService } from '@app/core/services/dialog.service'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
|
|||
|
|||
export interface GatewayConnector { |
|||
name: string; |
|||
type: string; |
|||
configuration?: string; |
|||
configurationJson: string; |
|||
log_level: string; |
|||
key?: string; |
|||
} |
|||
|
|||
export interface LogLink { |
|||
name: string; |
|||
key: string; |
|||
filterFn?: Function; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-logs', |
|||
templateUrl: './gateway-logs.component.html', |
|||
styleUrls: ['./gateway-logs.component.scss'] |
|||
}) |
|||
export class GatewayLogsComponent extends PageComponent implements AfterViewInit { |
|||
|
|||
pageLink: PageLink; |
|||
|
|||
attributeDataSource: AttributeDatasource; |
|||
|
|||
dataSource: MatTableDataSource<any> |
|||
|
|||
displayedColumns = ['ts', 'status', 'message']; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
dialogRef: MatDialogRef<any>; |
|||
|
|||
@ViewChild('searchInput') searchInputField: ElementRef; |
|||
@ViewChild(MatSort) sort: MatSort; |
|||
|
|||
connectorForm: FormGroup; |
|||
|
|||
viewsInited = false; |
|||
|
|||
textSearchMode: boolean; |
|||
|
|||
activeConnectors: Array<string>; |
|||
|
|||
inactiveConnectors: Array<string>; |
|||
|
|||
InitialActiveConnectors: Array<string>; |
|||
|
|||
gatewayLogLevel = Object.values(GatewayLogLevel); |
|||
|
|||
logLinks: Array<LogLink>; |
|||
|
|||
initialConnector: GatewayConnector; |
|||
|
|||
activeLink: LogLink; |
|||
|
|||
gatewayLogLinks: Array<LogLink> = [ |
|||
{ |
|||
name: "General", |
|||
key: "LOGS" |
|||
}, { |
|||
name: "Service", |
|||
key: "SERVICE_LOGS" |
|||
}, |
|||
{ |
|||
name: "Connection", |
|||
key: "CONNECTION_LOGS" |
|||
}, { |
|||
name: "Storage", |
|||
key: "STORAGE_LOGS" |
|||
}, |
|||
{ |
|||
key: 'EXTENSIONS_LOGS', |
|||
name: "Extension" |
|||
}] |
|||
|
|||
|
|||
constructor(protected router: Router, |
|||
protected store: Store<AppState>, |
|||
protected fb: FormBuilder, |
|||
protected translate: TranslateService, |
|||
protected attributeService: AttributeService, |
|||
protected deviceService: DeviceService, |
|||
protected dialogService: DialogService, |
|||
public dialog: MatDialog) { |
|||
super(store); |
|||
const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC}; |
|||
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder); |
|||
this.dataSource = new MatTableDataSource<AttributeData>([]); |
|||
|
|||
} |
|||
|
|||
|
|||
ngAfterViewInit() { |
|||
this.dataSource.sort = this.sort; |
|||
this.ctx.defaultSubscription.onTimewindowChangeFunction = timewindow => { |
|||
this.ctx.defaultSubscription.options.timeWindowConfig = timewindow; |
|||
this.ctx.defaultSubscription.updateDataSubscriptions(); |
|||
return timewindow; |
|||
} |
|||
if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) { |
|||
const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState]; |
|||
this.logLinks = [{ |
|||
key: `${connector.key}_LOGS`, |
|||
name: "Connector", |
|||
filterFn: (attrData)=>{ |
|||
return !attrData.message.includes(`_converter.py`) |
|||
} |
|||
},{ |
|||
key: `${connector.key}_LOGS`, |
|||
name: "Converter", |
|||
filterFn: (attrData)=>{ |
|||
return attrData.message.includes(`_converter.py`) |
|||
} |
|||
}] |
|||
} else { |
|||
this.logLinks = this.gatewayLogLinks; |
|||
} |
|||
this.activeLink = this.logLinks[0]; |
|||
this.changeSubscription(); |
|||
} |
|||
|
|||
|
|||
updateData(sort?) { |
|||
if (this.ctx.defaultSubscription.data.length) { |
|||
let attrData = this.ctx.defaultSubscription.data[0].data.map(data => { |
|||
let result = { |
|||
ts: data[0], |
|||
key: this.activeLink.key, |
|||
message: /\[(.*)/.exec(data[1])[0], |
|||
status: 'INVALID LOG FORMAT' |
|||
}; |
|||
|
|||
try { |
|||
result.status= data[1].match(/\|(\w+)\|/)[1]; |
|||
} catch (e) { |
|||
result.status = 'INVALID LOG FORMAT' |
|||
} |
|||
|
|||
return result; |
|||
}); |
|||
if (this.activeLink.filterFn) { |
|||
attrData = attrData.filter(data => this.activeLink.filterFn(data)); |
|||
} |
|||
this.dataSource.data = attrData; |
|||
if (sort) { |
|||
this.dataSource.sortData(this.dataSource.data, this.sort); |
|||
} |
|||
} |
|||
} |
|||
|
|||
onTabChanged(link) { |
|||
this.activeLink = link; |
|||
this.changeSubscription(); |
|||
} |
|||
|
|||
statusClass(status) { |
|||
switch (status) { |
|||
case GatewayLogLevel.debug: |
|||
return "status status-debug"; |
|||
case GatewayLogLevel.warning: |
|||
return "status status-warning"; |
|||
case GatewayLogLevel.error: |
|||
case "EXCEPTION": |
|||
return "status status-error"; |
|||
case GatewayLogLevel.info: |
|||
default: |
|||
return "status status-info"; |
|||
} |
|||
} |
|||
|
|||
statusClassMsg(status) { |
|||
if (status === "EXCEPTION") { |
|||
return 'msg-status-exception'; |
|||
} |
|||
} |
|||
|
|||
changeSubscription() { |
|||
if (this.ctx.datasources[0].entity) { |
|||
this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{ |
|||
name: this.activeLink.key, |
|||
type: DataKeyType.timeseries, |
|||
settings: {} |
|||
}]; |
|||
this.ctx.defaultSubscription.unsubscribe(); |
|||
this.ctx.defaultSubscription.updateDataSubscriptions(); |
|||
this.ctx.defaultSubscription.callbacks.onDataUpdated = () => { |
|||
this.updateData(); |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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-card fxLayout="row" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm"> |
|||
<mat-form-field class="mat-block tb-value-type"> |
|||
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label> |
|||
<mat-select formControlName="command" *ngIf="!isConnector"> |
|||
<mat-option *ngFor="let command of RPCCommands" [value]="command"> |
|||
{{command}} |
|||
</mat-option> |
|||
</mat-select> |
|||
<input matInput formControlName="command" *ngIf="isConnector"/> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="!isConnector"> |
|||
<mat-label>{{'gateway.statistics.timeout-ms' | translate}}</mat-label> |
|||
<input matInput formControlName="time" type="number"/> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="isConnector"> |
|||
<mat-label>{{'widget-config.datasource-parameters' | translate}}</mat-label> |
|||
<input matInput formControlName="params" type="JSON"/> |
|||
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
(click)="openEditJSONDialog($event)" |
|||
matTooltip="Edit params">edit |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<button mat-raised-button color="primary" (click)="sendCommand()" |
|||
[disabled]="commandForm.invalid">Send</button> |
|||
</mat-card> |
|||
<mat-card class="result-block" [formGroup]="commandForm" > |
|||
<span>Result</span> |
|||
<mat-divider></mat-divider> |
|||
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content> |
|||
</mat-card> |
|||
|
|||
@ -0,0 +1,128 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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 { AfterViewInit, Component, Input } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
|||
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; |
|||
import { AttributeService } from '@core/http/attribute.service'; |
|||
import { DeviceService } from '@core/http/device.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { PageComponent } from "@shared/components/page.component"; |
|||
import { DialogService } from '@app/core/services/dialog.service'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { ContentType } from '@shared/models/constants'; |
|||
import { |
|||
JsonObjectEditDialogComponent, |
|||
JsonObjectEditDialogData |
|||
} from '@shared/components/dialog/json-object-edit-dialog.component'; |
|||
|
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-service-rpc', |
|||
templateUrl: './gateway-service-rpc.component.html', |
|||
styleUrls: ['./gateway-service-rpc.component.scss'] |
|||
}) |
|||
export class GatewayServiceRPCComponent extends PageComponent implements AfterViewInit { |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
contentTypes = ContentType; |
|||
|
|||
@Input() |
|||
dialogRef: MatDialogRef<any>; |
|||
|
|||
commandForm: FormGroup; |
|||
|
|||
isConnector: boolean; |
|||
|
|||
connectorType: string; |
|||
|
|||
RPCCommands: Array<string> = [ |
|||
"Ping", |
|||
"Stats", |
|||
"Devices", |
|||
"Update", |
|||
"Version", |
|||
"Restart", |
|||
"Reboot" |
|||
] |
|||
|
|||
|
|||
constructor(protected router: Router, |
|||
protected store: Store<AppState>, |
|||
protected fb: FormBuilder, |
|||
protected translate: TranslateService, |
|||
protected attributeService: AttributeService, |
|||
protected deviceService: DeviceService, |
|||
protected dialogService: DialogService, |
|||
public dialog: MatDialog) { |
|||
super(store); |
|||
this.commandForm = this.fb.group({ |
|||
command: [null,[Validators.required]], |
|||
time: [60, [Validators.required]], |
|||
params: ["{}", [Validators.required]], |
|||
result: [null] |
|||
}) |
|||
|
|||
|
|||
} |
|||
|
|||
|
|||
ngAfterViewInit() { |
|||
this.isConnector = this.ctx.settings.isConnector; |
|||
if (!this.isConnector) { |
|||
this.commandForm.get('command').setValue(this.RPCCommands[0]); |
|||
} else { |
|||
this.connectorType = this.ctx.stateController.getStateParams().connector_rpc.value.type; |
|||
} |
|||
} |
|||
|
|||
|
|||
sendCommand() { |
|||
const formValues = this.commandForm.value; |
|||
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_'; |
|||
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe(resp=>{ |
|||
this.commandForm.get('result').setValue(JSON.stringify(resp)); |
|||
},error => { |
|||
console.log(error); |
|||
this.commandForm.get('result').setValue(JSON.stringify(error.error)); |
|||
}) |
|||
} |
|||
|
|||
openEditJSONDialog($event: Event) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
jsonValue: JSON.parse(this.commandForm.get('params').value) |
|||
} |
|||
}).afterClosed().subscribe( |
|||
(res) => { |
|||
if (res) { |
|||
this.commandForm.get('params').setValue(JSON.stringify(res)); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
|
|||
<div class="statistics-container" fxLayout="row"> |
|||
<mat-card [formGroup]="statisticForm" *ngIf="!general"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label>{{'gateway.statistics.statistic' | translate}}</mat-label> |
|||
<mat-select formControlName="statisticKey"> |
|||
<mat-option *ngFor="let key of statisticsKeys" [value]="key"> |
|||
{{key}} |
|||
</mat-option> |
|||
<mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway"> |
|||
{{command.attributeOnGateway}} |
|||
</mat-option> |
|||
</mat-select> |
|||
</mat-form-field> |
|||
<!-- <mat-form-field class="mat-block" *ngIf="commandObj">--> |
|||
<!-- <mat-label>Timeout</mat-label>--> |
|||
<!-- <input matInput [value]="commandObj.timeout" disabled>--> |
|||
<!-- </mat-form-field>--> |
|||
<mat-form-field class="mat-block" *ngIf="commandObj"> |
|||
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label> |
|||
<input matInput [value]="commandObj.command" disabled> |
|||
</mat-form-field> |
|||
</mat-card> |
|||
<div class="chart-box" fxLayout="column"> |
|||
<div class="chart-container" #statisticChart [fxShow]="isNumericData"> |
|||
|
|||
</div> |
|||
<table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource" |
|||
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" |
|||
matSortDisableClear> |
|||
<ng-container matColumnDef="0"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell> |
|||
<mat-cell *matCellDef="let row; let rowIndex = index"> |
|||
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="1"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">Message</mat-header-cell> |
|||
<mat-cell *matCellDef="let row"> |
|||
{{ row[1] }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<mat-header-row [ngClass]="{'mat-row-select': true}" |
|||
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> |
|||
<mat-row [ngClass]="{'mat-row-select': true}" |
|||
*matRowDef="let row; columns: displayedColumns;"></mat-row> |
|||
</table> |
|||
<span [fxShow]="dataSource.data.length === 0 && !isNumericData" |
|||
fxLayoutAlign="center center" |
|||
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span> |
|||
<div fxFlex class="legend" fxLayout="row" fxLayoutAlign="center center" [fxShow]="isNumericData"> |
|||
<div class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row" |
|||
fxLayoutAlign="center center"> |
|||
<span class="legend-line" [ngStyle]="{backgroundColor: legendKey.dataKey.color}"></span> |
|||
<div class="legend-label" |
|||
(click)="onLegendKeyHiddenChange(legendKey.dataIndex)" |
|||
[ngClass]="{ 'hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden }" |
|||
[innerHTML]="legendKey.dataKey.label"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
@ -0,0 +1,78 @@ |
|||
/** |
|||
* Copyright © 2016-2023 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0 |
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
padding: 0; |
|||
|
|||
|
|||
.statistics-container { |
|||
height: 100%; |
|||
|
|||
mat-card { |
|||
width: 40%; |
|||
margin-right: 35px; |
|||
padding: 15px; |
|||
} |
|||
|
|||
.chart-box, .chart-container { |
|||
height: 100%; |
|||
flex-grow: 1; |
|||
} |
|||
|
|||
.chart-box { |
|||
overflow: auto; |
|||
} |
|||
|
|||
& > * { |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
.legend { |
|||
flex-wrap: wrap; |
|||
width: 100%; |
|||
padding-top: 8px; |
|||
padding-bottom: 4px; |
|||
margin-top: 15px; |
|||
|
|||
.legend-keys { |
|||
.legend-label { |
|||
padding: 2px 20px 2px 10px; |
|||
white-space: nowrap; |
|||
|
|||
&.hidden-label { |
|||
text-decoration: line-through; |
|||
opacity: .6; |
|||
} |
|||
|
|||
&:focus { |
|||
outline: none; |
|||
} |
|||
} |
|||
|
|||
.legend-line { |
|||
display: inline-block; |
|||
width: 15px; |
|||
height: 3px; |
|||
text-align: left; |
|||
vertical-align: middle; |
|||
outline: none; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,299 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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 { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { Router } from '@angular/router'; |
|||
import { FormBuilder, FormGroup } from '@angular/forms'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { AttributeService } from '@core/http/attribute.service'; |
|||
import { DeviceService } from '@core/http/device.service'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { DialogService } from '@app/core/services/dialog.service'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { TbFlot } from '@home/components/widget/lib/flot-widget'; |
|||
import { ResizeObserver } from '@juggle/resize-observer'; |
|||
import { IWidgetSubscription, SubscriptionInfo, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { DatasourceType, LegendConfig, LegendData, LegendPosition, widgetType } from '@shared/models/widget.models'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { BaseData } from '@shared/models/base-data'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Direction, SortOrder } from '@shared/models/page/sort-order'; |
|||
import { MatTableDataSource } from '@angular/material/table'; |
|||
import { MatSort } from '@angular/material/sort'; |
|||
import { isNumber } from '@core/utils'; |
|||
|
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-statistics', |
|||
templateUrl: './gateway-statistics.component.html', |
|||
styleUrls: ['./gateway-statistics.component.scss'] |
|||
}) |
|||
export class GatewayStatisticsComponent extends PageComponent implements AfterViewInit { |
|||
|
|||
@ViewChild(MatSort) sort: MatSort; |
|||
@ViewChild('statisticChart') statisticChart: ElementRef; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
public general: boolean; |
|||
|
|||
public isNumericData: boolean = true; |
|||
public chartInited: boolean; |
|||
private flot: TbFlot; |
|||
private flotCtx; |
|||
public statisticForm: FormGroup; |
|||
public statisticsKeys = []; |
|||
public commands = []; |
|||
public commandObj: any; |
|||
public dataSource: MatTableDataSource<any>; |
|||
public pageLink: PageLink; |
|||
private resize$: ResizeObserver; |
|||
private subscription: IWidgetSubscription; |
|||
private subscriptionInfo: SubscriptionInfo []; |
|||
public legendData: LegendData; |
|||
public displayedColumns: Array<string>; |
|||
private subscriptionOptions: WidgetSubscriptionOptions = { |
|||
callbacks: { |
|||
onDataUpdated: () => this.ctx.ngZone.run(() => { |
|||
this.onDataUpdated(); |
|||
}), |
|||
onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => { |
|||
this.onDataUpdateError(e); |
|||
}) |
|||
}, |
|||
useDashboardTimewindow: false, |
|||
legendConfig: { |
|||
position: LegendPosition.bottom |
|||
} as LegendConfig |
|||
}; |
|||
|
|||
|
|||
constructor(protected router: Router, |
|||
protected store: Store<AppState>, |
|||
protected fb: FormBuilder, |
|||
protected translate: TranslateService, |
|||
protected attributeService: AttributeService, |
|||
protected deviceService: DeviceService, |
|||
protected dialogService: DialogService, |
|||
private cd: ChangeDetectorRef, |
|||
private utils: UtilsService, |
|||
public dialog: MatDialog) { |
|||
super(store); |
|||
const sortOrder: SortOrder = {property: '0', direction: Direction.DESC}; |
|||
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder); |
|||
this.displayedColumns = ['0', '1']; |
|||
this.dataSource = new MatTableDataSource<any>([]); |
|||
this.statisticForm = this.fb.group({ |
|||
statisticKey: [null, []] |
|||
}) |
|||
|
|||
this.statisticForm.get('statisticKey').valueChanges.subscribe(value => { |
|||
this.commandObj = null; |
|||
if (this.commands.length) { |
|||
this.commandObj = this.commands.find(command => command.attributeOnGateway === value); |
|||
} |
|||
if (this.subscriptionInfo) this.createChartsSubscription(this.ctx.defaultSubscription.datasources[0].entity, value); |
|||
}) |
|||
} |
|||
|
|||
|
|||
ngAfterViewInit() { |
|||
this.dataSource.sort = this.sort; |
|||
this.sort.sortChange.subscribe(_=>{ |
|||
this.sortData(); |
|||
}) |
|||
this.init(); |
|||
if (this.ctx.defaultSubscription.datasources.length) { |
|||
|
|||
const gateway = this.ctx.defaultSubscription.datasources[0].entity; |
|||
if (!this.general) { |
|||
this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ["general_configuration"]).subscribe((resp: AttributeData[]) => { |
|||
if (resp && resp.length) { |
|||
this.commands = resp[0].value.statistics.commands; |
|||
if (!this.statisticForm.get('statisticKey').value) { |
|||
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway); |
|||
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway); |
|||
} |
|||
} |
|||
}) |
|||
} else { |
|||
let connectorsTs; |
|||
this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe( |
|||
data => { |
|||
connectorsTs = Object.keys(data) |
|||
.filter(el => el.includes( |
|||
'ConnectorEventsProduced' |
|||
) || el.includes( |
|||
'ConnectorEventsSent')) |
|||
this.createGeneralChartsSubscription(gateway, connectorsTs); |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
|
|||
public sortData () { |
|||
this.dataSource.sortData(this.dataSource.data, this.sort); |
|||
} |
|||
|
|||
public onLegendKeyHiddenChange(index: number) { |
|||
this.legendData.keys[index].dataKey.hidden = !this.legendData.keys[index].dataKey.hidden; |
|||
this.subscription.updateDataVisibility(index); |
|||
} |
|||
|
|||
private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) { |
|||
let subscriptionInfo = [{ |
|||
type: DatasourceType.entity, |
|||
entityType: EntityType.DEVICE, |
|||
entityId: gateway.id.id, |
|||
entityName: gateway.name, |
|||
timeseries: [] |
|||
}]; |
|||
|
|||
subscriptionInfo[0].timeseries = [{name: attr, label: attr}]; |
|||
this.subscriptionInfo = subscriptionInfo; |
|||
this.changeSubscription(subscriptionInfo); |
|||
} |
|||
|
|||
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: [string]) { |
|||
let subscriptionInfo = [{ |
|||
type: DatasourceType.entity, |
|||
entityType: EntityType.DEVICE, |
|||
entityId: gateway.id.id, |
|||
entityName: gateway.name, |
|||
timeseries: [] |
|||
}]; |
|||
subscriptionInfo[0].timeseries = []; |
|||
if (attrData && attrData.length) { |
|||
attrData.forEach(attr => { |
|||
subscriptionInfo[0].timeseries.push({name: attr, label: attr}) |
|||
}) |
|||
} |
|||
this.ctx.defaultSubscription.datasources[0].dataKeys.forEach(dataKey => { |
|||
subscriptionInfo[0].timeseries.push({name: dataKey.name, label: dataKey.label}) |
|||
}) |
|||
|
|||
this.subscriptionInfo = subscriptionInfo; |
|||
this.changeSubscription(subscriptionInfo); |
|||
} |
|||
|
|||
init = () => { |
|||
this.flotCtx = { |
|||
$scope: this.ctx.$scope, |
|||
$injector: this.ctx.$injector, |
|||
utils: this.ctx.utils, |
|||
isMobile: this.ctx.isMobile, |
|||
isEdit: this.ctx.isEdit, |
|||
subscriptionApi: this.ctx.subscriptionApi, |
|||
detectChanges: this.ctx.detectChanges, |
|||
settings: this.ctx.settings |
|||
}; |
|||
} |
|||
|
|||
updateChart = () => { |
|||
if (this.flot && this.ctx.defaultSubscription.data.length) { |
|||
this.flot.update(); |
|||
} |
|||
} |
|||
|
|||
resize = () => { |
|||
if (this.flot) { |
|||
this.flot.resize(); |
|||
} |
|||
} |
|||
|
|||
private reset() { |
|||
if (this.resize$) { |
|||
this.resize$.disconnect(); |
|||
} |
|||
if (this.subscription) { |
|||
this.subscription.unsubscribe(); |
|||
} |
|||
if (this.flot) { |
|||
this.flot.destroy(); |
|||
} |
|||
} |
|||
|
|||
private onDataUpdateError(e: any) { |
|||
const exceptionData = this.utils.parseException(e); |
|||
let errorText = exceptionData.name; |
|||
if (exceptionData.message) { |
|||
errorText += ': ' + exceptionData.message; |
|||
} |
|||
console.error(errorText); |
|||
} |
|||
|
|||
private onDataUpdated() { |
|||
this.isDataOnlyNumbers(); |
|||
if (this.isNumericData) { |
|||
if (this.chartInited) { |
|||
if (this.flot) { |
|||
this.flot.update(); |
|||
} |
|||
} else { |
|||
this.initChart(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private initChart() { |
|||
this.chartInited = true; |
|||
this.flotCtx.$container = $(this.statisticChart.nativeElement); |
|||
this.resize$.observe(this.statisticChart.nativeElement); |
|||
this.flot = new TbFlot(this.flotCtx as WidgetContext, "line"); |
|||
this.flot.update(); |
|||
} |
|||
|
|||
private isDataOnlyNumbers() { |
|||
if (this.general) { |
|||
this.isNumericData = true; |
|||
return; |
|||
} |
|||
this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : []; |
|||
this.isNumericData = this.dataSource.data.every(data => isNumber(data[1])); |
|||
} |
|||
|
|||
|
|||
changeSubscription(subscriptionInfo: SubscriptionInfo[]) { |
|||
if (this.subscription) { |
|||
this.reset(); |
|||
} |
|||
if (this.ctx.datasources[0].entity) { |
|||
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.timeseries, subscriptionInfo, this.subscriptionOptions, false, true).subscribe(subscription => { |
|||
this.subscription = subscription; |
|||
this.isDataOnlyNumbers(); |
|||
this.legendData = this.subscription.legendData; |
|||
this.flotCtx.defaultSubscription = subscription; |
|||
this.resize$ = new ResizeObserver(() => { |
|||
this.resize(); |
|||
}); |
|||
this.ctx.detectChanges(); |
|||
if (this.isNumericData) { |
|||
this.initChart(); |
|||
} |
|||
}) |
|||
|
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2023 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 style="width: 600px; position: relative;"> |
|||
<mat-toolbar color="warn"> |
|||
<mat-icon>warning</mat-icon> |
|||
<h2 translate>gateway.configuration-delete-dialog-header</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="close()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent"> |
|||
<div class="mat-content" fxLayout="column"> |
|||
<span innerHTML="{{'gateway.configuration-delete-dialog-body' | translate}} <b>{{gatewayName}}</b>" > </span> |
|||
<mat-form-field class="mat-block tb-value-type" style="flex-grow: 0"> |
|||
<mat-label translate>gateway.configuration-delete-dialog-input</mat-label> |
|||
<input matInput [formControl]="gatewayForm" required/> |
|||
<mat-error |
|||
*ngIf="gatewayForm.hasError('required')"> |
|||
{{'gateway.configuration-delete-dialog-input-required' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
</div> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="warn" |
|||
type="button" |
|||
cdkFocusInitial |
|||
(click)="close()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-button color="warn" |
|||
type="button" |
|||
cdkFocusInitial |
|||
[disabled]="gatewayForm.value !== gatewayName" |
|||
(click)="turnOff()"> |
|||
{{ 'gateway.configuration-delete-dialog-confirm' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -0,0 +1,61 @@ |
|||
///
|
|||
/// Copyright © 2016-2023 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, Inject, OnInit } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Router } from '@angular/router'; |
|||
import { FormBuilder, FormControl } from '@angular/forms'; |
|||
|
|||
export interface GatewayRemoteConfigurationDialogData { |
|||
gatewayName: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-activation-link-dialog', |
|||
templateUrl: './gateway-remote-configuration-dialog.html' |
|||
}) |
|||
|
|||
export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<GatewayRemoteConfigurationDialogComponent, |
|||
boolean> implements OnInit { |
|||
|
|||
gatewayName: string; |
|||
|
|||
gatewayForm: FormControl; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: GatewayRemoteConfigurationDialogData, |
|||
public dialogRef: MatDialogRef<GatewayRemoteConfigurationDialogComponent, boolean>, |
|||
private fb: FormBuilder) { |
|||
super(store, router, dialogRef); |
|||
this.gatewayName = this.data.gatewayName; |
|||
this.gatewayForm = this.fb.control(null); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
close(): void { |
|||
this.dialogRef.close(); |
|||
} |
|||
|
|||
turnOff(): void { |
|||
this.dialogRef.close(true); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue