110 changed files with 12112 additions and 242 deletions
File diff suppressed because it is too large
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,25 @@ |
|||
#!/bin/bash |
|||
# |
|||
# 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. |
|||
# |
|||
|
|||
echo "Building ThingsBoard protobuf-containing packages..." |
|||
MAVEN_OPTS="-Xmx1024m" NODE_OPTIONS="--max_old_space_size=3072" \ |
|||
mvn clean compile -T4 --also-make --projects=' |
|||
common/cluster-api, |
|||
common/edge-api, |
|||
common/transport/coap, |
|||
common/transport/mqtt, |
|||
common/transport/transport-api' |
|||
@ -0,0 +1,158 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.server.dao.sql.rule; |
|||
|
|||
import com.datastax.oss.driver.api.core.uuid.Uuids; |
|||
import com.google.common.util.concurrent.ListeningExecutorService; |
|||
import org.junit.After; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.id.RuleChainId; |
|||
import org.thingsboard.server.common.data.id.RuleNodeId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.common.data.rule.RuleChain; |
|||
import org.thingsboard.server.common.data.rule.RuleNode; |
|||
import org.thingsboard.server.dao.AbstractJpaDaoTest; |
|||
import org.thingsboard.server.dao.rule.RuleChainDao; |
|||
import org.thingsboard.server.dao.rule.RuleNodeDao; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.junit.Assert.assertNotNull; |
|||
|
|||
public class JpaRuleNodeDaoTest extends AbstractJpaDaoTest { |
|||
|
|||
public static final int COUNT = 40; |
|||
public static final String PREFIX_FOR_RULE_NODE_NAME = "SEARCH_TEXT_"; |
|||
List<UUID> ruleNodeIds; |
|||
TenantId tenantId1; |
|||
TenantId tenantId2; |
|||
RuleChainId ruleChainId1; |
|||
RuleChainId ruleChainId2; |
|||
|
|||
@Autowired |
|||
private RuleChainDao ruleChainDao; |
|||
|
|||
@Autowired |
|||
private RuleNodeDao ruleNodeDao; |
|||
|
|||
ListeningExecutorService executor; |
|||
|
|||
@Before |
|||
public void setUp() { |
|||
tenantId1 = TenantId.fromUUID(Uuids.timeBased()); |
|||
ruleChainId1 = new RuleChainId(UUID.randomUUID()); |
|||
tenantId2 = TenantId.fromUUID(Uuids.timeBased()); |
|||
ruleChainId2 = new RuleChainId(UUID.randomUUID()); |
|||
|
|||
ruleNodeIds = createRuleNodes(tenantId1, tenantId2, ruleChainId1, ruleChainId2, COUNT); |
|||
} |
|||
|
|||
@After |
|||
public void tearDown() throws Exception { |
|||
ruleNodeDao.removeAllByIds(ruleNodeIds); |
|||
if (executor != null) { |
|||
executor.shutdownNow(); |
|||
} |
|||
} |
|||
|
|||
@Test |
|||
public void testSaveRuleName0x00_thenSomeDatabaseException() { |
|||
RuleNode ruleNode = getRuleNode(ruleChainId1, "T", "\u0000"); |
|||
assertThatThrownBy(() -> ruleNodeIds.add(ruleNodeDao.save(tenantId1, ruleNode).getUuidId())); |
|||
} |
|||
|
|||
@Test |
|||
public void testFindRuleNodesByTenantIdAndType() { |
|||
List<RuleNode> ruleNodes1 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId1, "A", PREFIX_FOR_RULE_NODE_NAME); |
|||
assertEquals(20, ruleNodes1.size()); |
|||
|
|||
List<RuleNode> ruleNodes2 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId2, "B", PREFIX_FOR_RULE_NODE_NAME); |
|||
assertEquals(20, ruleNodes2.size()); |
|||
|
|||
ruleNodes1 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId1, "A", null); |
|||
assertEquals(20, ruleNodes1.size()); |
|||
|
|||
ruleNodes2 = ruleNodeDao.findRuleNodesByTenantIdAndType(tenantId2, "B", null); |
|||
assertEquals(20, ruleNodes2.size()); |
|||
} |
|||
|
|||
@Test |
|||
public void testFindRuleNodesByType() { |
|||
PageData<RuleNode> ruleNodes = ruleNodeDao.findAllRuleNodesByType( "A", new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME)); |
|||
assertEquals(20, ruleNodes.getTotalElements()); |
|||
assertEquals(2, ruleNodes.getTotalPages()); |
|||
assertEquals(10, ruleNodes.getData().size()); |
|||
|
|||
ruleNodes = ruleNodeDao.findAllRuleNodesByType( "A", new PageLink(10, 0)); |
|||
assertEquals(20, ruleNodes.getTotalElements()); |
|||
assertEquals(2, ruleNodes.getTotalPages()); |
|||
assertEquals(10, ruleNodes.getData().size()); |
|||
} |
|||
|
|||
@Test |
|||
public void testFindRuleNodesByTypeAndVersionLessThan() { |
|||
PageData<RuleNode> ruleNodes = ruleNodeDao.findAllRuleNodesByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0, PREFIX_FOR_RULE_NODE_NAME)); |
|||
assertEquals(20, ruleNodes.getTotalElements()); |
|||
assertEquals(2, ruleNodes.getTotalPages()); |
|||
assertEquals(10, ruleNodes.getData().size()); |
|||
|
|||
ruleNodes = ruleNodeDao.findAllRuleNodesByTypeAndVersionLessThan( "A", 1, new PageLink(10, 0)); |
|||
assertEquals(20, ruleNodes.getTotalElements()); |
|||
assertEquals(2, ruleNodes.getTotalPages()); |
|||
assertEquals(10, ruleNodes.getData().size()); |
|||
} |
|||
|
|||
private List<UUID> createRuleNodes(TenantId tenantId1, TenantId tenantId2, RuleChainId ruleChainId1, RuleChainId ruleChainId2, int count) { |
|||
var chain1 = new RuleChain(ruleChainId1); |
|||
chain1.setTenantId(tenantId1); |
|||
chain1.setName(ruleChainId1.toString()); |
|||
ruleChainDao.save(tenantId1, chain1); |
|||
var chain2 = new RuleChain(ruleChainId2); |
|||
chain2.setTenantId(tenantId2); |
|||
chain2.setName(ruleChainId2.toString()); |
|||
ruleChainDao.save(tenantId2, chain2); |
|||
List<UUID> savedRuleNodeIds = new ArrayList<>(); |
|||
for (int i = 0; i < count / 2; i++) { |
|||
savedRuleNodeIds.add(ruleNodeDao.save(tenantId1, getRuleNode(ruleChainId1, "A", Integer.toString(i))).getUuidId()); |
|||
savedRuleNodeIds.add(ruleNodeDao.save(tenantId2, getRuleNode(ruleChainId2, "B", Integer.toString(i + count / 2))).getUuidId()); |
|||
} |
|||
return savedRuleNodeIds; |
|||
} |
|||
|
|||
private RuleNode getRuleNode(RuleChainId ruleChainId, String type, String nameSuffix) { |
|||
return getRuleNode(ruleChainId, Uuids.timeBased(), type, nameSuffix); |
|||
} |
|||
|
|||
private RuleNode getRuleNode(RuleChainId ruleChainId, UUID ruleNodeId, String type, String nameSuffix) { |
|||
RuleNode ruleNode = new RuleNode(); |
|||
ruleNode.setId(new RuleNodeId(ruleNodeId)); |
|||
ruleNode.setRuleChainId(ruleChainId); |
|||
ruleNode.setName(nameSuffix); |
|||
ruleNode.setType(type); |
|||
ruleNode.setConfiguration(JacksonUtil.newObjectNode().put("searchHint", PREFIX_FOR_RULE_NODE_NAME + nameSuffix)); |
|||
ruleNode.setConfigurationVersion(0); |
|||
return ruleNode; |
|||
} |
|||
} |
|||
@ -1,35 +0,0 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
package org.thingsboard.rule.engine.api; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import org.thingsboard.server.common.data.util.TbPair; |
|||
|
|||
public interface TbVersionedNode extends TbNode { |
|||
|
|||
/** |
|||
* Upgrades the configuration from a specific version to the current version specified in the |
|||
* {@link RuleNode} annotation for the instance of {@link TbVersionedNode}. |
|||
* |
|||
* @param fromVersion The version from which the configuration needs to be upgraded. |
|||
* @param oldConfiguration The old configuration to be upgraded. |
|||
* @return A pair consisting of a Boolean flag indicating the success of the upgrade |
|||
* and a JsonNode representing the upgraded configuration. |
|||
* @throws TbNodeException If an error occurs during the upgrade process. |
|||
*/ |
|||
TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException; |
|||
|
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
<!-- |
|||
|
|||
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" style="padding-top: 5px"> |
|||
<div fxLayout="row" fxLayoutAlign="space-between center"> |
|||
<span class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</span> |
|||
<div fxFlexAlign="end" class="tb-help" [tb-help]="helpLink"></div> |
|||
</div> |
|||
<div fxFlex fxLayout="row" style="justify-content: space-between; flex-grow: 0; align-items: center"> |
|||
</div> |
|||
<mat-tab-group [(selectedIndex)]="tabIndex"> |
|||
<mat-tab> |
|||
<ng-template mat-tab-label> |
|||
<mat-icon class="tabs-icon" svgIcon="windows"></mat-icon> |
|||
Windows |
|||
</ng-template> |
|||
<ng-template matTabContent> |
|||
<div class="tb-form-panel no-padding no-border tb-tab-body"> |
|||
<tb-markdown usePlainMarkdown containerClass="start-code" data=" |
|||
```bash |
|||
{{windowsCode}} |
|||
{:copy-code} |
|||
``` |
|||
"></tb-markdown> |
|||
</div> |
|||
</ng-template> |
|||
</mat-tab> |
|||
<mat-tab> |
|||
<ng-template mat-tab-label> |
|||
<mat-icon class="tabs-icon" svgIcon="linux"></mat-icon> |
|||
Linux |
|||
</ng-template> |
|||
<ng-template matTabContent> |
|||
<div class="tb-form-panel no-padding no-border tb-tab-body"> |
|||
<tb-markdown usePlainMarkdown containerClass="start-code" data=" |
|||
```bash |
|||
{{linuxCode}} |
|||
{:copy-code} |
|||
``` |
|||
"></tb-markdown> |
|||
</div> |
|||
</ng-template> |
|||
</mat-tab> |
|||
<mat-tab> |
|||
<ng-template mat-tab-label> |
|||
<mat-icon class="tabs-icon" svgIcon="macos"></mat-icon> |
|||
MacOS |
|||
</ng-template> |
|||
<ng-template matTabContent> |
|||
<div class="tb-form-panel no-padding no-border tb-tab-body"> |
|||
<tb-markdown usePlainMarkdown containerClass="start-code" data=" |
|||
```bash |
|||
{{linuxCode}} |
|||
{:copy-code} |
|||
``` |
|||
"></tb-markdown> |
|||
</div> |
|||
</ng-template> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
</div> |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* 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; |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-markdown-view { |
|||
.start-code { |
|||
code[class*="language-"], pre[class*="language-"] { |
|||
overflow: hidden; |
|||
white-space: break-spaces; |
|||
word-break: break-all; |
|||
} |
|||
|
|||
.code-wrapper { |
|||
padding: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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 { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; |
|||
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 { DeviceService } from '@core/http/device.service'; |
|||
import { helpBaseUrl } from '@shared/models/constants'; |
|||
import { getOS } from '@core/utils'; |
|||
|
|||
|
|||
@Component({ |
|||
selector: 'tb-gateway-command', |
|||
templateUrl: './device-gateway-command.component.html', |
|||
styleUrls: ['./device-gateway-command.component.scss'] |
|||
}) |
|||
|
|||
export class DeviceGatewayCommandComponent implements OnInit { |
|||
|
|||
@Input() |
|||
token: string; |
|||
|
|||
@Input() |
|||
deviceId: string; |
|||
|
|||
linuxCode: string; |
|||
windowsCode: string; |
|||
|
|||
helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/'; |
|||
|
|||
tabIndex = 0; |
|||
|
|||
constructor(protected router: Router, |
|||
protected store: Store<AppState>, |
|||
private translate: TranslateService, |
|||
private cd: ChangeDetectorRef, |
|||
private deviceService: DeviceService) { |
|||
} |
|||
|
|||
|
|||
ngOnInit(): void { |
|||
if (this.deviceId) { |
|||
this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => { |
|||
this.createRunCode(commands.mqtt); |
|||
this.cd.detectChanges(); |
|||
}); |
|||
} |
|||
const currentOS = getOS(); |
|||
switch (currentOS) { |
|||
case 'linux': |
|||
case 'android': |
|||
this.tabIndex = 1; |
|||
break; |
|||
case 'macos': |
|||
case 'ios': |
|||
this.tabIndex = 2; |
|||
break; |
|||
case 'windows': |
|||
this.tabIndex = 0; |
|||
break; |
|||
default: |
|||
this.tabIndex = 1; |
|||
} |
|||
} |
|||
|
|||
createRunCode(commands) { |
|||
this.linuxCode = commands.linux; |
|||
this.windowsCode = commands.windows; |
|||
} |
|||
|
|||
onDockerCodeCopied() { |
|||
this.store.dispatch(new ActionNotificationShow( |
|||
{ |
|||
message: this.translate.instant('gateway.command-copied-message'), |
|||
type: 'success', |
|||
target: 'dockerCommandDialogContent', |
|||
duration: 1200, |
|||
verticalPosition: 'bottom', |
|||
horizontalPosition: 'left' |
|||
})); |
|||
} |
|||
} |
|||
@ -0,0 +1,833 @@ |
|||
<!-- |
|||
|
|||
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% - 117px); flex-grow: 1"> |
|||
<mat-tab label="{{'gateway.thingsboard-general' | translate}}"> |
|||
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding"> |
|||
<div style="padding-top: 20px" class="tb-form-panel"> |
|||
<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 slider-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 slider-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 appearance="outline" 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 pointer-event 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 appearance="outline" 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 pointer-event 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> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div translate class="expansion-panel-header tb-form-panel-title tb-form-panel-title">security.security</div> |
|||
<div fxLayout="column" formGroupName="security"> |
|||
<tb-toggle-header fxFlex="100" class="security-toggle-group" |
|||
[value]="gatewayConfigGroup.get('thingsboard.security.type').value" |
|||
name="securityTypeToggle" (valueChange)="updateSecurityValidators($event)" |
|||
useSelectOnMdLg="false" ignoreMdLgSize="true"> |
|||
<tb-toggle-option *ngFor="let securityType of securityTypes | keyvalue" |
|||
[value]="securityType.key">{{ securityType.value | translate }}</tb-toggle-option> |
|||
</tb-toggle-header> |
|||
<div fxLayout="row wrap"> |
|||
<mat-form-field appearance="outline" |
|||
*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 pointer-event 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 appearance="outline" *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 pointer-event 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 appearance="outline" *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 pointer-event 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 appearance="outline" *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-icon class="mat-form-field-infix pointer-event 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-error style="margin-top: -12px; display: block;" fxFlex="100" |
|||
[error]="gatewayConfigGroup.get('thingsboard.security').hasError('atLeastOne') ? |
|||
('device.client-id-or-user-name-necessary' | translate) : ''"></tb-error> |
|||
<tb-file-input |
|||
fxFlex="100" |
|||
hint="{{'gateway.hints.ca-cert' | translate}}" |
|||
*ngIf="gatewayConfigGroup.get('thingsboard.security.type').value.toLowerCase().includes('tls')" |
|||
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> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.logs.logs' | translate}}"> |
|||
<div formGroupName="logs" fxLayout="column" class="mat-content mat-padding"> |
|||
<div class="tb-form-panel" style="padding-top: 20px"> |
|||
<div fxLayout="row wrap"> |
|||
<mat-form-field appearance="outline" 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 pointer-event 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 appearance="outline" 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 pointer-event 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> |
|||
</div> |
|||
<div class="tb-form-panel" formGroupName="remote"> |
|||
<div translate class="expansion-panel-header tb-form-panel-title">gateway.logs.remote</div> |
|||
<mat-slide-toggle color="primary" formControlName="enabled"> |
|||
{{ 'gateway.logs.remote-logs' | translate }} |
|||
<mat-icon class="mat-form-field-infix pointer-event suffix-icon slider-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 appearance="outline" 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> |
|||
</div> |
|||
<div class="tb-form-panel" formGroupName="local"> |
|||
<div translate class="expansion-panel-header tb-form-panel-title">gateway.logs.local</div> |
|||
<div fxLayout="column"> |
|||
<tb-toggle-header fxFlex="100" class="security-toggle-group" |
|||
[value]="logSelector.value" name="logTypeToggle" (valueChange)="updateLogType($event)" |
|||
useSelectOnMdLg="false" ignoreMdLgSize="true"> |
|||
<tb-toggle-option *ngFor="let logConfig of localLogsConfigLabels| keyvalue" [value]="logConfig.key" |
|||
class="first-capital">{{ logConfig.value }}</tb-toggle-option> |
|||
</tb-toggle-header> |
|||
<div fxFlex="100" fxLayout="row wrap" fxLayout.lt-sm="column" |
|||
[formGroup]="getLogFormGroup(logSelector.value)"> |
|||
<mat-form-field appearance="outline" 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 appearance="outline" 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> |
|||
<div fxLayout="row" fxFlex="50" fxLayoutGap="15px"> |
|||
<mat-form-field appearance="outline" class="mat-block tb-value-type" fxFlex> |
|||
<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 appearance="outline" class="mat-block tb-value-type" style="min-width: 110px; width: 30%"> |
|||
<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> |
|||
</div> |
|||
<mat-form-field appearance="outline" 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 pointer-event 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> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.storage' | translate}}"> |
|||
<div formGroupName="storage" fxLayout="column" class="mat-content mat-padding"> |
|||
<div class="tb-form-panel"> |
|||
<div translate class="expansion-panel-header tb-form-panel-title">gateway.storage |
|||
<span class="tb-hint">{{'gateway.hints.storage' | translate}}</span> |
|||
</div> |
|||
<div fxLayout="column"> |
|||
<tb-toggle-header fxFlex="100" class="security-toggle-group" |
|||
[value]="gatewayConfigGroup.get('storage.type').value" name="storageType" |
|||
(valueChange)="updateStorageType($event)" |
|||
useSelectOnMdLg="false" ignoreMdLgSize="true"> |
|||
<tb-toggle-option *ngFor="let storageType of storageTypes | keyvalue" [value]="storageType.key"> |
|||
{{ storageType.value | translate }}</tb-toggle-option> |
|||
|
|||
</tb-toggle-header> |
|||
<div class="line-break "></div> |
|||
<span class="tb-hint" style="padding-left: 0"> |
|||
{{'gateway.hints.' + gatewayConfigGroup.get('storage.type').value | translate}}</span> |
|||
<div fxLayout="row wrap" fxLayout.lt-sm="column"> |
|||
<mat-form-field appearance="outline" *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 type="number" 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 pointer-event 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 appearance="outline" *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 type="number" 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 pointer-event 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 appearance="outline" *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 pointer-event 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 appearance="outline" *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 type="number" 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 pointer-event 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 appearance="outline" *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 type="number" 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 pointer-event 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 appearance="outline" *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 type="number" 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 pointer-event 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 appearance="outline" *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 pointer-event 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 appearance="outline" *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 type="number" 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 pointer-event 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 appearance="outline" *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 type="number" 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 pointer-event 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> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.grpc' | translate}}"> |
|||
<div formGroupName="grpc" fxLayout="column" class="mat-content mat-padding"> |
|||
<div class="tb-form-panel" style="padding-top: 20px"> |
|||
<div fxLayout="row wrap" fxLayout.lt-md="column"> |
|||
<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-icon class="material-icons-outlined suffix-icon slider-icon " aria-hidden="false" aria-label="help-icon" |
|||
style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.permit-without-calls' | translate }}">info |
|||
</mat-icon> |
|||
</mat-slide-toggle> |
|||
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.server-port</mat-label> |
|||
<input matInput formControlName="serverPort" type="number" min="0"/> |
|||
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.server-port' | translate }}">info_outlined |
|||
</mat-icon> |
|||
<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 appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" 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-icon class="mat-form-field-infix pointer-event suffix-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.grpc-keep-alive-timeout' | translate }}">info_outlined |
|||
</mat-icon> |
|||
<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 appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" class="mat-block tb-value-type"> |
|||
<mat-label translate>gateway.grpc-keep-alive</mat-label> |
|||
<input matInput formControlName="keepAliveTimeMs" type="number" min="0"/> |
|||
<mat-icon class="mat-form-field-infix pointer-event suffix-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.grpc-keep-alive' | translate }}">info_outlined |
|||
</mat-icon> |
|||
<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 appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" 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-icon class="mat-form-field-infix pointer-event suffix-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.grpc-min-time-between-pings' | translate }}">info_outlined |
|||
</mat-icon> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minTimeBetweenPingsMs').hasError('required')"> |
|||
{{'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 appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" 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-icon class="mat-form-field-infix pointer-event suffix-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.grpc-max-pings-without-data' | translate }}">info_outlined |
|||
</mat-icon> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.maxPingsWithoutData').hasError('required')"> |
|||
{{'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 appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md="100" 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-icon class="mat-form-field-infix pointer-event suffix-icon" |
|||
matSuffix style="cursor:pointer;" |
|||
matTooltip="{{'gateway.hints.grpc-min-ping-interval-without-data' | translate }}">info_outlined |
|||
</mat-icon> |
|||
<mat-error *ngIf="gatewayConfigGroup.get('grpc.minPingIntervalWithoutDataMs').hasError('required')"> |
|||
{{'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> |
|||
</div> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.statistics.statistics' | translate}}"> |
|||
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding"> |
|||
<div class="tb-form-panel" 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 appearance="outline" 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="60"/> |
|||
<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> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div class="expansion-panel-header tb-form-panel-title">{{"gateway.statistics.commands" |translate}} |
|||
<span class="tb-hint"> |
|||
{{'gateway.hints.commands' | translate}}</span></div> |
|||
<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 fxFlex.lt-md fxLayout="row wrap" fxLayout.lt-md="column" [formGroupName]="$index" |
|||
class="statistics-block"> |
|||
<mat-form-field appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md 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 pointer-event 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 appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md 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 pointer-event 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 appearance="outline" fxFlex="calc(50%-15px)" fxFlex.lt-md 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 pointer-event 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, $event)" |
|||
[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> |
|||
</div> |
|||
</div> |
|||
</mat-tab> |
|||
<mat-tab label="{{'gateway.other' | translate}}"> |
|||
<div formGroupName="thingsboard" fxLayout="column" class="mat-content mat-padding"> |
|||
<div class="tb-form-panel" formGroupName="checkingDeviceActivity"> |
|||
<div class="expansion-panel-header tb-form-panel-title"> |
|||
<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> |
|||
</div> |
|||
<div fxLayout="row wrap" fxLayout.lt-sm="column"> |
|||
<mat-form-field |
|||
appearance="outline" |
|||
*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 pointer-event 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 |
|||
appearance="outline" |
|||
*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 pointer-event 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> |
|||
</div> |
|||
<div class="tb-form-panel"> |
|||
<div translate class="expansion-panel-header tb-form-panel-title">gateway.advanced</div> |
|||
<div fxLayout="row wrap" fxLayout.lt-sm="column"> |
|||
<mat-form-field appearance="outline" 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 pointer-event 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 appearance="outline" 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 pointer-event 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 appearance="outline" 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> |
|||
</div> |
|||
|
|||
</div> |
|||
</mat-tab> |
|||
</mat-tab-group> |
|||
<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,143 @@ |
|||
/** |
|||
* 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); |
|||
} |
|||
|
|||
.tb-form-panel { |
|||
margin-bottom: 20px; |
|||
} |
|||
|
|||
.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; |
|||
} |
|||
|
|||
.slider-icon { |
|||
position: absolute; |
|||
transform: translateY(-3px); |
|||
} |
|||
|
|||
.block-title { |
|||
font-size: 20px; |
|||
font-weight: 400; |
|||
padding-top: 16px; |
|||
} |
|||
|
|||
.hover-cursor { |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
.security-toggle-group { |
|||
margin-bottom: 15px; |
|||
margin-right: auto; |
|||
} |
|||
|
|||
.logs-label { |
|||
font-weight: 500; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.statistics-block { |
|||
margin-bottom: 15px; |
|||
padding-left: 15px; |
|||
padding-top: 15px; |
|||
} |
|||
|
|||
.first-capital { |
|||
text-transform: capitalize; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.mat-tab-label-active { |
|||
color: white; |
|||
opacity: 1; |
|||
} |
|||
|
|||
.mat-tab-label, .mat-tab-label-active{ |
|||
min-width: 50px !important; |
|||
padding: 3px !important; |
|||
margin: 3px !important; |
|||
flex-grow: 1; |
|||
} |
|||
|
|||
.mat-ink-bar { |
|||
height: 100%; |
|||
z-index: -10; |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
.pointer-event { |
|||
pointer-events: all; |
|||
} |
|||
|
|||
.mat-mdc-form-field-icon-suffix { |
|||
z-index: 100; |
|||
} |
|||
|
|||
.security-toggle-group span { |
|||
padding: 0 25px; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,721 @@ |
|||
///
|
|||
/// 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, |
|||
UntypedFormGroup, |
|||
ValidationErrors, |
|||
ValidatorFn, |
|||
Validators |
|||
} from '@angular/forms'; |
|||
import { EntityId } from '@shared/models/id/entity-id'; |
|||
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; |
|||
import { AttributeService } from '@core/http/attribute.service'; |
|||
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; |
|||
import { |
|||
GatewayRemoteConfigurationDialogComponent, |
|||
GatewayRemoteConfigurationDialogData |
|||
} from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog'; |
|||
import { DeviceService } from '@core/http/device.service'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { mergeMap } from 'rxjs/operators'; |
|||
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
|
|||
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; |
|||
|
|||
securityType: SecurityTypes; |
|||
|
|||
initialCredentials: DeviceCredentials; |
|||
|
|||
|
|||
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(60), 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.security.password').valueChanges.subscribe(password => { |
|||
if (password && password !== '') { |
|||
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]); |
|||
} else { |
|||
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([]); |
|||
} |
|||
this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); |
|||
}); |
|||
|
|||
this.toggleRpcFields(false); |
|||
|
|||
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); |
|||
} |
|||
}); |
|||
|
|||
this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => { |
|||
this.toggleRpcFields(value); |
|||
}); |
|||
|
|||
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.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]) |
|||
// 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); |
|||
} |
|||
|
|||
atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) { |
|||
return (group: UntypedFormGroup): ValidationErrors | null => { |
|||
if (!controls) { |
|||
controls = Object.keys(group.controls); |
|||
} |
|||
const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); |
|||
|
|||
return hasAtLeastOne ? null : {atLeastOne: true}; |
|||
}; |
|||
} |
|||
|
|||
updateSecurityValidators(value: SecurityTypes) { |
|||
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(value, {emitEvent: true}); |
|||
this.gatewayConfigGroup.get('thingsboard.security.type').markAsDirty(); |
|||
} |
|||
|
|||
updateLogType(value: LocalLogsConfigs) { |
|||
this.logSelector.setValue(value); |
|||
} |
|||
|
|||
updateStorageType(value: StorageTypes) { |
|||
this.gatewayConfigGroup.get('storage.type').setValue(value, {emitEvent: true}); |
|||
this.gatewayConfigGroup.get('storage.type').markAsDirty(); |
|||
} |
|||
|
|||
fetchConfigAttribute(entityId: EntityId) { |
|||
if (entityId.id === NULL_UUID) return; |
|||
this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, |
|||
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe( |
|||
mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( |
|||
entityId, AttributeScope.SHARED_SCOPE, ['general_configuration', 'grpc_configuration', |
|||
'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel'])) |
|||
).subscribe(attributes => { |
|||
if (attributes.length) { |
|||
const general_configuration = attributes.find(attribute => attribute.key === 'general_configuration')?.value; |
|||
const grpc_configuration = attributes.find(attribute => attribute.key === 'grpc_configuration')?.value; |
|||
const logs_configuration = attributes.find(attribute => attribute.key === 'logs_configuration')?.value; |
|||
const storage_configuration = attributes.find(attribute => attribute.key === 'storage_configuration')?.value; |
|||
const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel')?.value; |
|||
if (general_configuration) { |
|||
const configObj = {thingsboard: general_configuration}; |
|||
if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) { |
|||
for (const command of configObj.thingsboard.statistics.commands) { |
|||
this.addCommand(command); |
|||
} |
|||
delete configObj.thingsboard.statistics.commands; |
|||
} |
|||
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); |
|||
this.gatewayConfigGroup.markAsPristine(); |
|||
if (!configObj.thingsboard.remoteConfiguration) { |
|||
this.gatewayConfigGroup.disable({emitEvent: false}); |
|||
} |
|||
this.checkAndFetchCredentials(configObj.thingsboard.security); |
|||
} |
|||
if (grpc_configuration) { |
|||
const configObj = {grpc: grpc_configuration}; |
|||
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); |
|||
this.toggleRpcFields(grpc_configuration.enabled); |
|||
} |
|||
if (logs_configuration) { |
|||
const configObj = {logs: this.logsToObj(logs_configuration)}; |
|||
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); |
|||
this.cd.detectChanges(); |
|||
} |
|||
if (storage_configuration) { |
|||
const configObj = {storage: storage_configuration}; |
|||
this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); |
|||
} |
|||
if (remoteLoggingLevel) { |
|||
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); |
|||
remoteLogsFormGroup.patchValue({ |
|||
enabled: remoteLoggingLevel !== GatewayLogLevel.none, |
|||
logLevel: remoteLoggingLevel |
|||
}, {emitEvent: false}); |
|||
remoteLogsFormGroup.markAsPristine(); |
|||
} |
|||
this.cd.detectChanges(); |
|||
} else { |
|||
this.checkAndFetchCredentials({}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
checkAndFetchCredentials(security): void { |
|||
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) { |
|||
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => { |
|||
this.initialCredentials = credentials; |
|||
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) { |
|||
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN? SecurityTypes.TLS_ACCESS_TOKEN : SecurityTypes.ACCESS_TOKEN); |
|||
this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId); |
|||
if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) { |
|||
this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert); |
|||
} |
|||
} else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) { |
|||
const parsedValue = JSON.parse(credentials.credentialsValue); |
|||
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD); |
|||
this.gatewayConfigGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId); |
|||
this.gatewayConfigGroup.get('thingsboard.security.username').setValue(parsedValue.userName); |
|||
this.gatewayConfigGroup.get('thingsboard.security.password').setValue(parsedValue.password, {emitEvent: false}); |
|||
} 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}; |
|||
} |
|||
|
|||
toggleRpcFields(enable: boolean) { |
|||
const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup; |
|||
if (enable) { |
|||
grpcGroup.get('serverPort').enable(); |
|||
grpcGroup.get('keepAliveTimeMs').enable(); |
|||
grpcGroup.get('keepAliveTimeoutMs').enable(); |
|||
grpcGroup.get('keepalivePermitWithoutCalls').enable(); |
|||
grpcGroup.get('maxPingsWithoutData').enable(); |
|||
grpcGroup.get('minTimeBetweenPingsMs').enable(); |
|||
grpcGroup.get('minPingIntervalWithoutDataMs').enable(); |
|||
} else { |
|||
grpcGroup.get('serverPort').disable(); |
|||
grpcGroup.get('keepAliveTimeMs').disable(); |
|||
grpcGroup.get('keepAliveTimeoutMs').disable(); |
|||
grpcGroup.get('keepalivePermitWithoutCalls').disable(); |
|||
grpcGroup.get('maxPingsWithoutData').disable(); |
|||
grpcGroup.get('minTimeBetweenPingsMs').disable(); |
|||
grpcGroup.get('minPingIntervalWithoutDataMs').disable(); |
|||
} |
|||
} |
|||
|
|||
|
|||
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, event: any): void { |
|||
if (event.pointerType === '') return; |
|||
this.commandFormArray().removeAt(index); |
|||
this.gatewayConfigGroup.markAsDirty(); |
|||
} |
|||
|
|||
removeAllSecurityValidators(): void { |
|||
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; |
|||
securityGroup.clearValidators(); |
|||
for (const controlsKey in securityGroup.controls) { |
|||
if (controlsKey !== 'type') { |
|||
securityGroup.controls[controlsKey].clearValidators(); |
|||
securityGroup.controls[controlsKey].setErrors(null); |
|||
securityGroup.controls[controlsKey].updateValueAndValidity(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
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_handler.TimedRotatingFileHandler', |
|||
formatter: 'LogFormatter', |
|||
filename: './logs/database.log', |
|||
backupCount: 1, |
|||
encoding: 'utf-8' |
|||
} |
|||
}, |
|||
loggers: { |
|||
database: { |
|||
handlers: ['databaseHandler', 'consoleHandler'], |
|||
level: 'DEBUG', |
|||
propagate: false |
|||
} |
|||
}, |
|||
root: { |
|||
level: 'ERROR', |
|||
handlers: [ |
|||
'consoleHandler' |
|||
] |
|||
}, |
|||
ts: new Date().getTime() |
|||
}; |
|||
for (const key of Object.keys(logsObj.local)) { |
|||
logAttrObj.handlers[key + 'Handler'] = this.createHandlerObj(logsObj.local[key], key); |
|||
logAttrObj.loggers[key] = this.createLoggerObj(logsObj.local[key], key); |
|||
} |
|||
return logAttrObj; |
|||
} |
|||
|
|||
createHandlerObj(logObj, key) { |
|||
return { |
|||
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', |
|||
formatter: 'LogFormatter', |
|||
filename: `${logObj.filePath}/${key}.log`, |
|||
backupCount: logObj.backupCount, |
|||
interval: logObj.savingTime, |
|||
when: logObj.savingPeriod, |
|||
encoding: 'utf-8' |
|||
}; |
|||
} |
|||
|
|||
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) |
|||
}); |
|||
value.grpc.ts = new Date().getTime(); |
|||
attributes.push({ |
|||
key: 'grpc_configuration', |
|||
value: value.grpc |
|||
}); |
|||
value.storage.ts = new Date().getTime(); |
|||
attributes.push({ |
|||
key: 'storage_configuration', |
|||
value: value.storage |
|||
}); |
|||
value.thingsboard.ts = new Date().getTime(); |
|||
attributes.push({ |
|||
key: 'general_configuration', |
|||
value: value.thingsboard |
|||
}); |
|||
|
|||
|
|||
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { |
|||
this.updateCredentials(value.thingsboard.security).subscribe(_ => { |
|||
if (this.dialogRef) { |
|||
this.dialogRef.close(); |
|||
} else { |
|||
this.gatewayConfigGroup.markAsPristine(); |
|||
this.cd.detectChanges(); |
|||
} |
|||
}) |
|||
}); |
|||
} |
|||
|
|||
updateCredentials(securityConfig): Observable<any> { |
|||
let updateCredentials = false; |
|||
let newCredentials = {}; |
|||
if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) { |
|||
if (this.initialCredentials.credentialsType !== DeviceCredentialsType.MQTT_BASIC) { |
|||
updateCredentials = true; |
|||
} else { |
|||
const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue); |
|||
updateCredentials = !(parsedCredentials.clientId === securityConfig.clientId && parsedCredentials.userName === securityConfig.username && parsedCredentials.password === securityConfig.password); |
|||
} |
|||
if (updateCredentials) { |
|||
let credentialsValue: { clientId?, userName?, password? } = {}; |
|||
const credentialsType = DeviceCredentialsType.MQTT_BASIC; |
|||
if (securityConfig.clientId) credentialsValue.clientId = securityConfig.clientId; |
|||
if (securityConfig.username) credentialsValue.userName = securityConfig.username; |
|||
if (securityConfig.password) credentialsValue.password = securityConfig.password; |
|||
newCredentials = { |
|||
credentialsType, |
|||
credentialsValue: JSON.stringify(credentialsValue) |
|||
}; |
|||
} |
|||
} else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) { |
|||
if (this.initialCredentials.credentialsType !== DeviceCredentialsType.ACCESS_TOKEN) { |
|||
updateCredentials = true; |
|||
} else { |
|||
updateCredentials = this.initialCredentials.credentialsId !== securityConfig.accessToken; |
|||
} |
|||
if (updateCredentials) { |
|||
newCredentials = { |
|||
credentialsType: DeviceCredentialsType.ACCESS_TOKEN, |
|||
credentialsId: securityConfig.accessToken |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (updateCredentials) { |
|||
return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials}) |
|||
} |
|||
return of(null); |
|||
} |
|||
|
|||
cancel(): void { |
|||
if (this.dialogRef) { |
|||
this.dialogRef.close(); |
|||
} |
|||
} |
|||
|
|||
private openConfigurationConfirmDialog(): void { |
|||
this.deviceService.getDevice(this.device.id).subscribe(gateway => { |
|||
this.dialog.open<GatewayRemoteConfigurationDialogComponent, GatewayRemoteConfigurationDialogData> |
|||
(GatewayRemoteConfigurationDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
|||
data: { |
|||
gatewayName: gateway.name |
|||
} |
|||
}).afterClosed().subscribe( |
|||
(res) => { |
|||
if (!res) { |
|||
this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false}); |
|||
} |
|||
} |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,198 @@ |
|||
<!-- |
|||
|
|||
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" fxLayout.lt-lg="column" fxLayoutGap="15px" fxFlex class="connector-container"> |
|||
<mat-card fxLayout="column" fxFlex.lt-lg style="overflow: auto; min-height: 35vh"> |
|||
<mat-toolbar class="mat-mdc-table-toolbar"> |
|||
<h2>{{ 'gateway.connectors' | translate }}</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;"> |
|||
{{ 'gateway.connectors-table-enabled' | translate }} |
|||
</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%"> |
|||
{{ 'gateway.connectors-table-name' | translate }}</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%"> |
|||
{{ 'gateway.connectors-table-type' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase"> |
|||
{{ returnType(attribute) }} |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="syncStatus"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%"> |
|||
{{ 'gateway.configuration' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase"> |
|||
<div |
|||
[ngClass]="{ |
|||
'status-block': true, |
|||
'status-sync':isConnectorSynced(attribute), |
|||
'status-unsync':!isConnectorSynced(attribute) |
|||
}">{{isConnectorSynced(attribute)?'sync' : 'out of sync'}}</div> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="errors"> |
|||
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 30%"> |
|||
{{ 'gateway.connectors-table-status' | translate }} |
|||
</mat-header-cell> |
|||
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase"> |
|||
<span class="dot" |
|||
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}" |
|||
matTooltipPosition="above" |
|||
[ngClass]="{ |
|||
'hasErrors': +getErrorsCount(attribute) > 0, |
|||
'noErrors': +getErrorsCount(attribute) == 0 || getErrorsCount(attribute) == '' |
|||
}"></span> |
|||
</mat-cell> |
|||
</ng-container> |
|||
<ng-container matColumnDef="actions" stickyEnd> |
|||
<mat-header-cell *matHeaderCellDef |
|||
[ngStyle.gt-md]="{ minWidth: '144px', maxWidth: '144px', width: '144px'}"> |
|||
{{ 'gateway.connectors-table-actions' | translate }} |
|||
</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, |
|||
'tb-current-entity': isSameConnector(attribute)}" |
|||
*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" fxLayout.lt-lg="column" fxLayoutGap="15px" fxLayoutGap.lt-lg="5px"> |
|||
<mat-form-field class="mat-block tb-value-type"> |
|||
<mat-label>{{ 'gateway.connectors-table-name' | translate }}</mat-label> |
|||
<input matInput formControlName="name" #nameInput/> |
|||
</mat-form-field> |
|||
<mat-form-field class="mat-block tb-value-type"> |
|||
<mat-label>{{ 'gateway.connectors-table-type' | translate }}</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>{{ 'gateway.connectors-table-key' | translate }}</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>{{ 'gateway.connectors-table-class' | translate }}</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="logLevel"> |
|||
<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" |
|||
jsonRequired |
|||
label="{{ 'gateway.configuration' | 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,114 @@ |
|||
/** |
|||
* 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: calc(50% - 15px); |
|||
} |
|||
|
|||
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; |
|||
} |
|||
|
|||
mat-card { |
|||
padding-left: 10px; |
|||
background: transparent; |
|||
} |
|||
|
|||
.mat-card-selected { |
|||
background-color: rgba(48, 86, 128, 0.1); |
|||
} |
|||
|
|||
.mat-mdc-slide-toggle { |
|||
margin: 15px; |
|||
} |
|||
|
|||
.status-block { |
|||
text-align: center; |
|||
border-radius: 16px; |
|||
font-weight: 500; |
|||
width: fit-content; |
|||
padding: 5px 15px; |
|||
} |
|||
|
|||
.status-sync { |
|||
background: rgba(25, 128, 56, .06); |
|||
color: rgb(25, 128, 56); |
|||
} |
|||
|
|||
.status-unsync { |
|||
background: rgba(203, 37, 48, .06); |
|||
color: rgb(203, 37, 48); |
|||
} |
|||
|
|||
.action-btns { |
|||
margin: 10px 10px 0; |
|||
} |
|||
|
|||
mat-row { |
|||
cursor: pointer; |
|||
} |
|||
|
|||
.dot { |
|||
height: 12px; |
|||
width: 12px; |
|||
background-color: #bbb; |
|||
border-radius: 50%; |
|||
display: inline-block; |
|||
} |
|||
|
|||
.hasErrors { |
|||
background-color: rgb(203, 37, 48); |
|||
} |
|||
|
|||
.noErrors { |
|||
background-color: rgb(25, 128, 56); |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep tb-json-object-edit > div { |
|||
flex-grow: 1; |
|||
} |
|||
|
|||
@ -0,0 +1,517 @@ |
|||
///
|
|||
/// 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, UntypedFormControl, ValidatorFn, 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 } 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 { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; |
|||
import { MatTableDataSource } from '@angular/material/table'; |
|||
import { GatewayLogLevel } from '@home/components/widget/lib/gateway/gateway-configuration.component'; |
|||
import { ActionNotificationShow } from '@core/notification/notification.actions'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { deepClone } from '@core/utils'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; |
|||
import { DatasourceType, widgetType } from '@shared/models/widget.models'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { EntityType } from '@shared/models/entity-type.models'; |
|||
|
|||
|
|||
export interface gatewayConnector { |
|||
name: string; |
|||
type: string; |
|||
configuration?: string; |
|||
configurationJson: string; |
|||
logLevel: 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; |
|||
|
|||
serverDataSource: AttributeDatasource; |
|||
|
|||
dataSource: MatTableDataSource<AttributeData>; |
|||
|
|||
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; |
|||
|
|||
gatewayConnectorDefaultTypes = GatewayConnectorDefaultTypesTranslates; |
|||
|
|||
@Input() |
|||
ctx: WidgetContext; |
|||
|
|||
@Input() |
|||
device: EntityId; |
|||
|
|||
@ViewChild('nameInput') nameInput: ElementRef; |
|||
@ViewChild(MatSort, {static: false}) 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> = []; |
|||
|
|||
sharedAttributeData: Array<AttributeData> = []; |
|||
|
|||
initialConnector: gatewayConnector; |
|||
|
|||
subscriptionOptions: WidgetSubscriptionOptions = { |
|||
callbacks: { |
|||
onDataUpdated: () => this.ctx.ngZone.run(() => { |
|||
this.onDataUpdated(); |
|||
}), |
|||
onDataUpdateError: (subscription, e) => this.ctx.ngZone.run(() => { |
|||
this.onDataUpdateError(e); |
|||
}) |
|||
} |
|||
}; |
|||
|
|||
subscription: IWidgetSubscription; |
|||
|
|||
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 utils: UtilsService, |
|||
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.serverDataSource = new AttributeDatasource(this.attributeService, this.telemetryWsService, this.zone, this.translate); |
|||
this.dataSource = new MatTableDataSource<AttributeData>([]); |
|||
this.connectorForm = this.fb.group({ |
|||
name: ['', [Validators.required, this.uniqNameRequired()]], |
|||
type: ['', [Validators.required]], |
|||
logLevel: ['', [Validators.required]], |
|||
key: ['auto'], |
|||
class: [''], |
|||
configuration: [''], |
|||
configurationJson: [{}, [Validators.required]] |
|||
}); |
|||
this.connectorForm.disable(); |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
this.connectorForm.valueChanges.subscribe(() => { |
|||
this.cd.detectChanges(); |
|||
}); |
|||
|
|||
this.dataSource.sort = this.sort; |
|||
this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => { |
|||
if (sortHeaderId === 'syncStatus') { |
|||
return this.isConnectorSynced(data) ? 1 : 0; |
|||
} else if (sortHeaderId === 'enabled') { |
|||
return this.activeConnectors.includes(data.key) ? 1 : 0; |
|||
} |
|||
return data[sortHeaderId] || data.value[sortHeaderId]; |
|||
}; |
|||
|
|||
this.viewsInited = true; |
|||
if (this.device) { |
|||
if (this.device.id === NULL_UUID) return; |
|||
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.activeConnectors = typeof this.activeConnectors === 'string' ? JSON.parse(this.activeConnectors) : this.activeConnectors; |
|||
this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : []; |
|||
this.inactiveConnectors = typeof this.inactiveConnectors === 'string' ? JSON.parse(this.inactiveConnectors) : this.inactiveConnectors; |
|||
this.updateData(true); |
|||
} else { |
|||
this.activeConnectors = []; |
|||
this.inactiveConnectors = []; |
|||
this.updateData(true); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
uniqNameRequired(): ValidatorFn { |
|||
return (c: UntypedFormControl) => { |
|||
const newName = c.value.trim().toLowerCase(); |
|||
const found = this.dataSource.data.find((connectorAttr) => { |
|||
const connectorData = connectorAttr.value; |
|||
return connectorData.name.toLowerCase() === newName; |
|||
}); |
|||
if (found) { |
|||
if (this.initialConnector && this.initialConnector.name.toLowerCase() === newName) { |
|||
return null; |
|||
} |
|||
return { |
|||
duplicateName: { |
|||
valid: false |
|||
} |
|||
}; |
|||
} |
|||
return null; |
|||
}; |
|||
} |
|||
|
|||
saveConnector(): void { |
|||
const value = this.connectorForm.value; |
|||
value.configuration = this.camelize(value.name) + '.json'; |
|||
if (value.type !== 'grpc') { |
|||
delete value.key; |
|||
} |
|||
if (value.type !== 'custom') { |
|||
delete value.class; |
|||
} |
|||
value.ts = new Date().getTime(); |
|||
const attributesToSave = [{ |
|||
key: value.name, |
|||
value |
|||
}]; |
|||
const attributesToDelete = []; |
|||
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? 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(inactiveIndex, 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, 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, scope, attributesToDelete)); |
|||
} |
|||
forkJoin(tasks).subscribe(_ => { |
|||
this.initialConnector = value; |
|||
this.showToast('Update Successful'); |
|||
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.generateSubscription(); |
|||
}); |
|||
this.inactiveConnectorsDataSource.loadAttributes(this.device, AttributeScope.SHARED_SCOPE, this.pageLink, reload).subscribe(data => { |
|||
this.sharedAttributeData = data.data.filter(value => this.activeConnectors.includes(value.key)); |
|||
this.combineData(); |
|||
}); |
|||
this.serverDataSource.loadAttributes(this.device, AttributeScope.SERVER_SCOPE, this.pageLink, reload).subscribe(data => { |
|||
this.inactiveData = data.data.filter(value => this.inactiveConnectors.includes(value.key)); |
|||
this.combineData(); |
|||
}); |
|||
} |
|||
|
|||
isConnectorSynced(attribute: AttributeData) { |
|||
const connectorData = attribute.value; |
|||
if (!connectorData.ts) return false; |
|||
const clientIndex = this.activeData.findIndex(data => { |
|||
const sharedData = data.value; |
|||
return sharedData.name === connectorData.name; |
|||
}) |
|||
if (clientIndex == -1) return false; |
|||
const sharedIndex = this.sharedAttributeData.findIndex(data => { |
|||
const sharedData = data.value; |
|||
return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts; |
|||
}) |
|||
return sharedIndex !== -1; |
|||
} |
|||
|
|||
combineData() { |
|||
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => |
|||
index === self.findIndex((t) => t.key === item.key) |
|||
).map(attribute=>{ |
|||
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value; |
|||
return attribute |
|||
}); |
|||
} |
|||
|
|||
addAttribute(): void { |
|||
if (this.connectorForm.disabled) { |
|||
this.connectorForm.enable(); |
|||
} |
|||
this.nameInput.nativeElement.focus(); |
|||
this.clearOutConnectorForm(); |
|||
|
|||
} |
|||
|
|||
clearOutConnectorForm(): void { |
|||
this.connectorForm.setValue({ |
|||
name: '', |
|||
type: 'mqtt', |
|||
logLevel: 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 = attribute.value; |
|||
if (!connector.configuration) { |
|||
connector.configuration = ''; |
|||
} |
|||
if (!connector.key) { |
|||
connector.key = 'auto'; |
|||
} |
|||
this.initialConnector = connector; |
|||
this.connectorForm.patchValue(connector); |
|||
this.connectorForm.markAsPristine(); |
|||
} |
|||
|
|||
isSameConnector(attribute): boolean { |
|||
if (!this.initialConnector) return false; |
|||
const connector = attribute.value; |
|||
return this.initialConnector.name === connector.name; |
|||
} |
|||
|
|||
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 = 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.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; |
|||
tasks.push(this.attributeService.deleteEntityAttributes(this.device, 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(inactiveIndex, 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(true); |
|||
}); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
camelize(str): string { |
|||
return str.toLowerCase().replace(/\s+/g, '_'); |
|||
} |
|||
|
|||
connectorLogs(attribute: AttributeData, $event: Event): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const params = deepClone(this.ctx.stateController.getStateParams()); |
|||
params.connector_logs = attribute; |
|||
params.targetEntityParamName = 'connector_logs'; |
|||
this.ctx.stateController.openState('connector_logs', params); |
|||
} |
|||
|
|||
connectorRpc(attribute: AttributeData, $event: Event): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
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); |
|||
const scopeOld = wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; |
|||
const scopeNew = !wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; |
|||
attribute.value.ts = new Date().getTime(); |
|||
const tasks = [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 |
|||
}]), this.attributeService.deleteEntityAttributes(this.device, scopeOld, [attribute]), |
|||
this.attributeService.saveEntityAttributes(this.device, scopeNew, [attribute])]; |
|||
if (wasEnabled) { |
|||
const index = this.activeConnectors.indexOf(attribute.key); |
|||
this.activeConnectors.splice(index, 1); |
|||
this.inactiveConnectors.push(attribute.key); |
|||
} else { |
|||
const index = this.inactiveConnectors.indexOf(attribute.key); |
|||
this.inactiveConnectors.splice(index, 1); |
|||
this.activeConnectors.push(attribute.key); |
|||
} |
|||
forkJoin(tasks).subscribe(_ => { |
|||
this.updateData(true); |
|||
}); |
|||
} |
|||
|
|||
onDataUpdateError(e: any) { |
|||
const exceptionData = this.utils.parseException(e); |
|||
let errorText = exceptionData.name; |
|||
if (exceptionData.message) { |
|||
errorText += ': ' + exceptionData.message; |
|||
} |
|||
console.error(errorText); |
|||
} |
|||
|
|||
onDataUpdated() { |
|||
this.cd.detectChanges(); |
|||
} |
|||
|
|||
generateSubscription() { |
|||
if (this.subscription) { |
|||
this.subscription.unsubscribe(); |
|||
} |
|||
if (this.device) { |
|||
const subscriptionInfo = [{ |
|||
type: DatasourceType.entity, |
|||
entityType: EntityType.DEVICE, |
|||
entityId: this.device.id, |
|||
entityName: "Gateway", |
|||
timeseries: [] |
|||
}]; |
|||
this.dataSource.data.forEach(value => { |
|||
subscriptionInfo[0].timeseries.push({name: `${value.key}_ERRORS_COUNT`, label: `${value.key}_ERRORS_COUNT`}) |
|||
}) |
|||
this.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.latest, subscriptionInfo,this.subscriptionOptions, false, true).subscribe(subscription => { |
|||
this.subscription = subscription; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
getErrorsCount(attribute) { |
|||
const connectorName = attribute.key; |
|||
const connector = this.subscription && this.subscription.data.find(data=>data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`); |
|||
return (connector && this.activeConnectors.includes(connectorName))? connector.data[0][1]: 'Inactive'; |
|||
} |
|||
} |
|||
@ -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. |
|||
|
|||
--> |
|||
<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> |
|||
</table> |
|||
<span [fxShow]="dataSource.data.length === 0" |
|||
fxFlex |
|||
fxLayoutAlign="center center" |
|||
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span> |
|||
<span fxFlex [fxShow]="dataSource.data.length !== 0"></span> |
|||
<mat-divider></mat-divider> |
|||
<mat-paginator #paginator |
|||
[length]="dataSource.data.length" |
|||
[pageIndex]="pageLink.page" |
|||
[pageSize]="pageLink.pageSize" |
|||
[pageSizeOptions]="[10, 20, 30]"></mat-paginator> |
|||
|
|||
@ -0,0 +1,56 @@ |
|||
/** |
|||
* 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: flex; |
|||
flex-direction: column; |
|||
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: blue; |
|||
background: rgba(0, 0, 128, 0.1); |
|||
} |
|||
|
|||
.msg-status-exception { |
|||
color: red; |
|||
} |
|||
|
|||
} |
|||
|
|||
@ -0,0 +1,237 @@ |
|||
///
|
|||
/// 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 '@home/components/widget/lib/gateway/gateway-configuration.component'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { WidgetContext } from '@home/models/widget-component.models'; |
|||
import { MatPaginator } from '@angular/material/paginator'; |
|||
|
|||
|
|||
export interface GatewayConnector { |
|||
name: string; |
|||
type: string; |
|||
configuration?: string; |
|||
configurationJson: string; |
|||
logLevel: 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; |
|||
@ViewChild(MatPaginator) paginator: MatPaginator; |
|||
|
|||
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(10, 0, null, sortOrder); |
|||
this.dataSource = new MatTableDataSource<AttributeData>([]); |
|||
|
|||
} |
|||
|
|||
|
|||
ngAfterViewInit() { |
|||
this.dataSource.sort = this.sort; |
|||
this.dataSource.paginator = this.paginator; |
|||
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 && this.ctx.defaultSubscription.data[0]) { |
|||
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 && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) { |
|||
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,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]="gatewayControl" required/> |
|||
<mat-error |
|||
*ngIf="gatewayControl.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]="gatewayControl.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: 'gateway-remote-configuration-dialog', |
|||
templateUrl: './gateway-remote-configuration-dialog.html' |
|||
}) |
|||
|
|||
export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<GatewayRemoteConfigurationDialogComponent, |
|||
boolean> implements OnInit { |
|||
|
|||
gatewayName: string; |
|||
|
|||
gatewayControl: 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.gatewayControl = this.fb.control(null); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
close(): void { |
|||
this.dialogRef.close(); |
|||
} |
|||
|
|||
turnOff(): void { |
|||
this.dialogRef.close(true); |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
<!-- |
|||
|
|||
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 fxLayout="row" fxLayout.lt-sm="column" 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" min="1"/> |
|||
<mat-error |
|||
*ngIf="commandForm.get('time').hasError('min')"> |
|||
{{'gateway.statistics.timeout-min' | translate }} |
|||
</mat-error> |
|||
</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="{{'gateway.rpc-command-edit-params' | translate}}">edit |
|||
</mat-icon> |
|||
</mat-form-field> |
|||
<button mat-raised-button color="primary" (click)="sendCommand()" |
|||
[disabled]="commandForm.invalid">{{'gateway.rpc-command-send' | translate}}</button> |
|||
</div> |
|||
<mat-card class="result-block" [formGroup]="commandForm" fxFlex> |
|||
<span>{{'gateway.rpc-command-result' | translate}}</span> |
|||
<mat-divider></mat-divider> |
|||
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result" fxFlex></tb-json-content> |
|||
</mat-card> |
|||
|
|||
@ -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%; |
|||
overflow-x: auto; |
|||
display: flex; |
|||
flex-direction: column; |
|||
padding: 0; |
|||
|
|||
.command-form { |
|||
width: 100%; |
|||
flex-wrap: nowrap; |
|||
padding: 0 15px 5px; |
|||
margin-bottom: 5px; |
|||
|
|||
& > button { |
|||
margin-top: 10px; |
|||
} |
|||
} |
|||
|
|||
.result-block { |
|||
padding: 0 15px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
& > span { |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-json-content { |
|||
height: 100%; |
|||
} |
|||
|
|||
.mat-mdc-form-field-icon-suffix { |
|||
z-index: 100; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,130 @@ |
|||
///
|
|||
/// 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 '@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'; |
|||
import { jsonRequired } from '@shared/components/json-object-edit.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, Validators.min(1)]], |
|||
params: [{}, [jsonRequired]], |
|||
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), |
|||
required: true |
|||
} |
|||
}).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" fxLayout.lt-md="column"> |
|||
<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-error |
|||
*ngIf="!statisticsKeys.length && !commands.length"> |
|||
{{'gateway.statistics.statistic-commands-empty' | translate }} |
|||
</mat-error> |
|||
<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>{{'audit-log.timestamp' | translate}}</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%">{{"event.message" | translate}}</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,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. |
|||
*/ |
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
padding: 0; |
|||
|
|||
|
|||
.statistics-container { |
|||
height: 100%; |
|||
overflow-y: auto; |
|||
|
|||
mat-card { |
|||
width: 40%; |
|||
height: 100%; |
|||
margin-right: 35px; |
|||
padding: 15px; |
|||
} |
|||
|
|||
@media only screen and (max-width: 750px) { |
|||
mat-card { |
|||
width: 100%; |
|||
} |
|||
} |
|||
|
|||
.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 '@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 { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
|
|||
|
|||
@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 = false; |
|||
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 (gateway.id.id === NULL_UUID) return; |
|||
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.commands && this.commands.length) { |
|||
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 => !isNaN(+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,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"> |
|||
{{"widgets.gateway.is-connector" | translate}} |
|||
</mat-slide-toggle> |
|||
<mat-form-field fxFlex class="mat-block" *ngIf="GatewayLogSettingForm.get('isConnectorLog').value"> |
|||
<mat-label>{{"widgets.gateway.state-param-name" | translate}}</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,22 @@ |
|||
<!-- |
|||
|
|||
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="isConnector"> |
|||
{{"widgets.gateway.is-connector" | translate}} |
|||
</mat-slide-toggle> |
|||
</section> |
|||
@ -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, []] |
|||
}); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue