Browse Source

Merge pull request #8337 from MrKartoshka/gateway-list

Gateway list dashboard and widgets
pull/9219/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
96efdb3e20
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6772
      application/src/main/data/json/demo/dashboards/gateway_list.json
  2. 9
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  3. 19
      application/src/main/data/json/system/widget_types/gateway_configuration2.json
  4. 19
      application/src/main/data/json/system/widget_types/gateway_connector.json
  5. 23
      application/src/main/data/json/system/widget_types/gateway_general_statistics.json
  6. 20
      application/src/main/data/json/system/widget_types/gateway_logs.json
  7. 23
      application/src/main/data/json/system/widget_types/gateway_statistics.json
  8. 20
      application/src/main/data/json/system/widget_types/service_rpc.json
  9. 36
      application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java
  10. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java
  11. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  12. 35
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  13. 49
      dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java
  14. 6
      ui-ngx/src/app/core/http/device.service.ts
  15. 7
      ui-ngx/src/app/core/utils.ts
  16. 75
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html
  17. 37
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.scss
  18. 98
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts
  19. 833
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  20. 143
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss
  21. 721
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts
  22. 198
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  23. 114
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  24. 517
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  25. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html
  26. 61
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html
  27. 56
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.scss
  28. 237
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts
  29. 57
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.html
  30. 61
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.ts
  31. 53
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html
  32. 55
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss
  33. 128
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  34. 81
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html
  35. 86
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss
  36. 299
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts
  37. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html
  38. 54
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts
  39. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html
  40. 52
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts
  41. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  42. 23
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  43. 13
      ui-ngx/src/app/shared/components/file-input.component.html
  44. 4
      ui-ngx/src/app/shared/components/file-input.component.scss
  45. 3
      ui-ngx/src/app/shared/components/file-input.component.ts
  46. 7
      ui-ngx/src/app/shared/models/device.models.ts
  47. 247
      ui-ngx/src/assets/locale/locale.constant-en_US.json

6772
application/src/main/data/json/demo/dashboards/gateway_list.json

File diff suppressed because it is too large

9
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

@ -10,6 +10,13 @@
"widgetTypeFqns": [
"gateway_widgets.gateway_configuration",
"gateway_widgets.attributes_card",
"gateway_widgets.config_form_latest"
"gateway_widgets.gateway_configuration2",
"gateway_widgets.config_form_latest",
"gateway_widgets.gateway_events",
"gateway_widgets.gateway_connector",
"gateway_widgets.gateway_logs",
"gateway_widgets.gateway_statistics",
"gateway_widgets.gateway_general_statistics",
"gateway_widgets.service_rpc"
]
}

19
application/src/main/data/json/system/widget_types/gateway_configuration2.json

File diff suppressed because one or more lines are too long

19
application/src/main/data/json/system/widget_types/gateway_connector.json

File diff suppressed because one or more lines are too long

23
application/src/main/data/json/system/widget_types/gateway_general_statistics.json

File diff suppressed because one or more lines are too long

20
application/src/main/data/json/system/widget_types/gateway_logs.json

File diff suppressed because one or more lines are too long

23
application/src/main/data/json/system/widget_types/gateway_statistics.json

File diff suppressed because one or more lines are too long

20
application/src/main/data/json/system/widget_types/service_rpc.json

File diff suppressed because one or more lines are too long

36
application/src/main/java/org/thingsboard/server/controller/DeviceConnectivityController.java

@ -31,7 +31,9 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.dao.device.DeviceConnectivityService;
@ -87,6 +89,36 @@ public class DeviceConnectivityController extends BaseController {
return deviceConnectivityService.findDevicePublishTelemetryCommands(baseUrl, device);
}
@ApiOperation(value = "Get commands to launch gateway (getGatewayLaunchCommands)",
notes = "Fetch the list of commands for different operation systems to launch a gateway using docker." +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK",
examples = @io.swagger.annotations.Example(
value = {
@io.swagger.annotations.ExampleProperty(
mediaType = "application/json",
value = "{\"mqtt\": {\n" +
" \"linux\": \"docker run --rm -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v ~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name tbGateway127001 -e host=localhost -e port=1883 -e accessToken=qTe5oDBHPJf0KCSKO8J3 --restart always thingsboard/tb-gateway\",\n" +
" \"windows\": \"docker run --rm -it -v %HOMEPATH%/tb-gateway/logs:/thingsboard_gateway/logs -v %HOMEPATH%/tb-gateway/extensions:/thingsboard_gateway/extensions -v %HOMEPATH%/tb-gateway/config:/thingsboard_gateway/config --name tbGateway127001 -e host=localhost -e port=1883 -e accessToken=qTe5oDBHPJf0KCSKO8J3 --restart always thingsboard/tb-gateway\"}\n" +
"}")}))})
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/device-connectivity/gateway-launch/{deviceId}", method = RequestMethod.GET)
@ResponseBody
public JsonNode getGatewayLaunchCommands(@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_ID) String strDeviceId, HttpServletRequest request) throws ThingsboardException, URISyntaxException {
checkParameter(DEVICE_ID, strDeviceId);
DeviceId deviceId = new DeviceId(toUUID(strDeviceId));
Device device = checkDeviceId(deviceId, Operation.READ_CREDENTIALS);
if (!checkIsGateway(device)) {
throw new ThingsboardException("The device must be a gateway!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
String baseUrl = systemSecurityService.getBaseUrl(getTenantId(), getCurrentUser().getCustomerId(), request);
return deviceConnectivityService.findGatewayLaunchCommands(baseUrl, device);
}
@ApiOperation(value = "Download server certificate using file path defined in device.connectivity properties (downloadServerCertificate)", notes = "Download server certificate.")
@RequestMapping(value = "/device-connectivity/{protocol}/certificate/download", method = RequestMethod.GET)
@ResponseBody
@ -104,4 +136,8 @@ public class DeviceConnectivityController extends BaseController {
.body(pemCert);
}
private static boolean checkIsGateway(Device device) {
return device.getAdditionalInfo().has(DataConstants.GATEWAY_PARAMETER) &&
device.getAdditionalInfo().get(DataConstants.GATEWAY_PARAMETER).asBoolean();
}
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityService.java

@ -25,5 +25,7 @@ public interface DeviceConnectivityService {
JsonNode findDevicePublishTelemetryCommands(String baseUrl, Device device) throws URISyntaxException;
JsonNode findGatewayLaunchCommands(String baseUrl, Device device) throws URISyntaxException;
Resource getPemCertFile(String protocol);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -43,6 +43,7 @@ public class DataConstants {
public static final String RETRIES = "retries";
public static final String EDGE_ID = "edgeId";
public static final String DEVICE_ID = "deviceId";
public static final String GATEWAY_PARAMETER = "gateway";
public static final String COAP_TRANSPORT_NAME = "COAP";
public static final String LWM2M_TRANSPORT_NAME = "LWM2M";
public static final String MQTT_TRANSPORT_NAME = "MQTT";

35
dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java

@ -57,8 +57,10 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.LINUX;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.WINDOWS;
@Service("DeviceConnectivityDaoService")
@Slf4j
@ -138,6 +140,26 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
return commands;
}
@Override
public JsonNode findGatewayLaunchCommands(String baseUrl, Device device) throws URISyntaxException {
DeviceId deviceId = device.getId();
log.trace("Executing findDevicePublishTelemetryCommands [{}]", deviceId);
validateId(deviceId, INCORRECT_DEVICE_ID + deviceId);
DeviceCredentials creds = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), deviceId);
ObjectNode commands = JacksonUtil.newObjectNode();
if (deviceConnectivityConfiguration.isEnabled(MQTT)) {
Optional.ofNullable(getGatewayDockerCommands(baseUrl, creds, MQTT))
.ifPresent(v -> commands.set(MQTT, v));
}
if (deviceConnectivityConfiguration.isEnabled(MQTTS)) {
Optional.ofNullable(getGatewayDockerCommands(baseUrl, creds, MQTTS))
.ifPresent(v -> commands.set(MQTTS, v));
}
return commands;
}
@Override
public Resource getPemCertFile(String protocol) {
return certs.computeIfAbsent(protocol, key -> {
@ -272,6 +294,18 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
return null;
}
private JsonNode getGatewayDockerCommands(String baseUrl, DeviceCredentials deviceCredentials, String mqttType) throws URISyntaxException {
ObjectNode dockerLaunchCommands = JacksonUtil.newObjectNode();
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity().get(mqttType);
String mqttHost = getHost(baseUrl, properties);
String mqttPort = properties.getPort().isEmpty() ? null : properties.getPort();
Optional.ofNullable(DeviceConnectivityUtil.getGatewayLaunchCommand(LINUX, mqttHost, mqttPort, deviceCredentials))
.ifPresent(v -> dockerLaunchCommands.put(LINUX, v));
Optional.ofNullable(DeviceConnectivityUtil.getGatewayLaunchCommand(WINDOWS, mqttHost, mqttPort, deviceCredentials))
.ifPresent(v -> dockerLaunchCommands.put(WINDOWS, v));
return dockerLaunchCommands.isEmpty() ? null : dockerLaunchCommands;
}
private String getDockerMqttPublishCommand(String protocol, String baseUrl, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) throws URISyntaxException {
DeviceConnectivityInfo properties = deviceConnectivityConfiguration.getConnectivity(protocol);
String mqttHost = getHost(baseUrl, properties);
@ -329,4 +363,5 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
private String getHost(String baseUrl, DeviceConnectivityInfo properties) throws URISyntaxException {
return properties.getHost().isEmpty() ? new URI(baseUrl).getHost() : properties.getHost();
}
}

49
dao/src/main/java/org/thingsboard/server/dao/util/DeviceConnectivityUtil.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.util;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -34,6 +35,7 @@ public class DeviceConnectivityUtil {
public static final String CHECK_DOCUMENTATION = "Check documentation";
public static final String JSON_EXAMPLE_PAYLOAD = "\"{temperature:25}\"";
public static final String DOCKER_RUN = "docker run --rm -it ";
public static final String GATEWAY_DOCKER_RUN = "docker run -it ";
public static final String MQTT_IMAGE = "thingsboard/mosquitto-clients ";
public static final String COAP_IMAGE = "thingsboard/coap-clients ";
@ -78,6 +80,53 @@ public class DeviceConnectivityUtil {
return command.toString();
}
public static String getGatewayLaunchCommand(String os, String host, String port, DeviceCredentials deviceCredentials) {
String gatewayVolumePathPrefix = "~/.tb-gateway";
if (WINDOWS.equals(os)) {
gatewayVolumePathPrefix = "%HOMEPATH%/tb-gateway";
}
String gatewayContainerName = "tbGateway" + StringUtils.capitalize(host.replace(".", ""));
StringBuilder command = new StringBuilder(GATEWAY_DOCKER_RUN);
command.append("-v {gatewayVolumePathPrefix}/logs:/thingsboard_gateway/logs".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix));
command.append(" -v {gatewayVolumePathPrefix}/extensions:/thingsboard_gateway/extensions".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix));
command.append(" -v {gatewayVolumePathPrefix}/config:/thingsboard_gateway/config".replace("{gatewayVolumePathPrefix}", gatewayVolumePathPrefix));
command.append(" --name ").append(gatewayContainerName);
command.append(" -e host=").append(host);
command.append(" -e port=").append(port);
switch(deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN:
command.append(" -e accessToken=").append(deviceCredentials.getCredentialsId());
break;
case MQTT_BASIC:
BasicMqttCredentials credentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(),
BasicMqttCredentials.class);
if (credentials != null) {
if (credentials.getClientId() != null) {
command.append(" -e clientId=").append(credentials.getClientId());
}
if (credentials.getUserName() != null) {
command.append(" -e username=").append(credentials.getUserName());
}
if (credentials.getPassword() != null) {
command.append(" -e password=").append(credentials.getPassword());
}
} else {
return null;
}
break;
default:
return null;
}
command.append(" --restart always");
command.append(" thingsboard/tb-gateway");
return command.toString();
}
public static String getDockerMqttPublishCommand(String protocol, String baseUrl, String host, String port, String deviceTelemetryTopic, DeviceCredentials deviceCredentials) {
String mqttCommand = getMqttPublishCommand(protocol, host, port, deviceTelemetryTopic, deviceCredentials);

6
ui-ngx/src/app/core/http/device.service.ts

@ -27,7 +27,7 @@ import {
DeviceCredentials,
DeviceInfo,
DeviceInfoQuery,
DeviceSearchQuery,
DeviceSearchQuery, PublishLaunchCommand,
PublishTelemetryCommand
} from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models';
@ -214,4 +214,8 @@ export class DeviceService {
return this.http.get<PublishTelemetryCommand>(`/api/device-connectivity/${deviceId}`, defaultHttpOptionsFromConfig(config));
}
public getDevicePublishLaunchCommands(deviceId: string, config?: RequestConfig): Observable<PublishLaunchCommand> {
return this.http.get<PublishLaunchCommand>(`/api/device-connectivity/gateway-launch/${deviceId}`, defaultHttpOptionsFromConfig(config));
}
}

7
ui-ngx/src/app/core/utils.ts

@ -202,6 +202,13 @@ export function base64toObj(b64Encoded: string): any {
return JSON.parse(json);
}
export function stringToBase64(value: string): string {
return btoa(encodeURIComponent(value).replace(/%([0-9A-F]{2})/g,
function toSolidBytes(match, p1) {
return String.fromCharCode(Number('0x' + p1));
}));
}
const scrollRegex = /(auto|scroll)/;
function parentNodes(node: Node, nodes: Node[]): Node[] {

75
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html

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

37
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.scss

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

98
ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts

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

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

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

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

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

721
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts

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

198
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -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"
required
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>

114
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss

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

517
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

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

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-form.component.html

@ -187,7 +187,7 @@
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div class="tb-panel-title">{{ 'gateway.connectors' | translate | uppercase }}</div>
<div class="tb-panel-title">{{ 'gateway.connectors-config' | translate | uppercase }}</div>
</mat-panel-title>
</mat-expansion-panel-header>

61
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html

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

56
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.scss

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

237
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts

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

57
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.html

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

61
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.ts

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

53
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html

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

55
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss

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

128
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts

@ -0,0 +1,128 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { AfterViewInit, Component, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { PageComponent } from "@shared/components/page.component";
import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models';
import { ContentType } from '@shared/models/constants';
import {
JsonObjectEditDialogComponent,
JsonObjectEditDialogData
} from '@shared/components/dialog/json-object-edit-dialog.component';
@Component({
selector: 'tb-gateway-service-rpc',
templateUrl: './gateway-service-rpc.component.html',
styleUrls: ['./gateway-service-rpc.component.scss']
})
export class GatewayServiceRPCComponent extends PageComponent implements AfterViewInit {
@Input()
ctx: WidgetContext;
contentTypes = ContentType;
@Input()
dialogRef: MatDialogRef<any>;
commandForm: FormGroup;
isConnector: boolean;
connectorType: string;
RPCCommands: Array<string> = [
"Ping",
"Stats",
"Devices",
"Update",
"Version",
"Restart",
"Reboot"
]
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
this.commandForm = this.fb.group({
command: [null,[Validators.required]],
time: [60, [Validators.required, Validators.min(1)]],
params: ["{}", [Validators.required]],
result: [null]
})
}
ngAfterViewInit() {
this.isConnector = this.ctx.settings.isConnector;
if (!this.isConnector) {
this.commandForm.get('command').setValue(this.RPCCommands[0]);
} else {
this.connectorType = this.ctx.stateController.getStateParams().connector_rpc.value.type;
}
}
sendCommand() {
const formValues = this.commandForm.value;
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_';
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe(resp=>{
this.commandForm.get('result').setValue(JSON.stringify(resp));
},error => {
console.log(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error));
})
}
openEditJSONDialog($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<JsonObjectEditDialogComponent, JsonObjectEditDialogData, object>(JsonObjectEditDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
jsonValue: JSON.parse(this.commandForm.get('params').value)
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.commandForm.get('params').setValue(JSON.stringify(res));
}
}
);
}
}

81
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html

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

86
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.scss

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

299
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts

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

26
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html

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

54
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts

@ -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', []]
});
}
}

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html

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

52
ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts

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

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -257,6 +257,12 @@ import {
import {
TripAnimationPointSettingsComponent
} from '@home/components/widget/lib/settings/map/trip-animation-point-settings.component';
import {
GatewayLogsSettingsComponent
} from '@home/components/widget/lib/settings/gateway/gateway-logs-settings.component';
import {
GatewayServiceRPCSettingsComponent
} from '@home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component';
import {
DocLinksWidgetSettingsComponent
} from '@home/components/widget/lib/settings/home-page/doc-links-widget-settings.component';
@ -369,6 +375,8 @@ import {
TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent,
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent,
@ -476,6 +484,8 @@ import {
TripAnimationPointSettingsComponent,
MapWidgetSettingsComponent,
RouteMapWidgetSettingsComponent,
GatewayLogsSettingsComponent,
GatewayServiceRPCSettingsComponent,
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent,
@ -549,6 +559,8 @@ export const widgetSettingsComponentsMap: {[key: string]: Type<IWidgetSettingsCo
'tb-map-widget-settings': MapWidgetSettingsComponent,
'tb-route-map-widget-settings': RouteMapWidgetSettingsComponent,
'tb-trip-animation-widget-settings': TripAnimationWidgetSettingsComponent,
'tb-gateway-logs-settings': GatewayLogsSettingsComponent,
'tb-gateway-service-rpc-settings':GatewayServiceRPCSettingsComponent,
'tb-doc-links-widget-settings': DocLinksWidgetSettingsComponent,
'tb-quick-links-widget-settings': QuickLinksWidgetSettingsComponent,
'tb-value-card-widget-settings': ValueCardWidgetSettingsComponent,

23
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -43,6 +43,15 @@ import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/hom
import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component';
import { LegendComponent } from '@home/components/widget/lib/legend.component';
import { GatewayConnectorComponent } from '@home/components/widget/lib/gateway/gateway-connectors.component';
import { GatewayLogsComponent } from '@home/components/widget/lib/gateway/gateway-logs.component';
import { GatewayStatisticsComponent } from '@home/components/widget/lib/gateway/gateway-statistics.component';
import { GatewayServiceRPCComponent } from '@home/components/widget/lib/gateway/gateway-service-rpc.component';
import { DeviceGatewayCommandComponent } from '@home/components/widget/lib/gateway/device-gateway-command.component';
import { GatewayConfigurationComponent } from '@home/components/widget/lib/gateway/gateway-configuration.component';
import {
GatewayRemoteConfigurationDialogComponent
} from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog';
import { ValueCardWidgetComponent } from '@home/components/widget/lib/cards/value-card-widget.component';
import {
AggregatedValueCardWidgetComponent
@ -71,6 +80,13 @@ import {
SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent
],
@ -101,6 +117,13 @@ import {
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent,
GatewayConnectorComponent,
GatewayLogsComponent,
GatewayStatisticsComponent,
GatewayServiceRPCComponent,
DeviceGatewayCommandComponent,
GatewayConfigurationComponent,
GatewayRemoteConfigurationDialogComponent,
ValueCardWidgetComponent,
AggregatedValueCardWidgetComponent
],

13
ui-ngx/src/app/shared/components/file-input.component.html

@ -16,7 +16,12 @@
-->
<div class="tb-container">
<label class="tb-title" [ngClass]="{'tb-required': !disabled && required}">{{ label }}</label>
<label class="tb-title" [ngClass]="{'tb-required': !disabled && required}">{{ label }}
<mat-icon *ngIf="hint" class="material-icons-outlined pointer-event"
style="cursor:pointer;"
matTooltip="{{hint}}">info_outlined
</mat-icon>
</label>
<ng-container #flow="flow"
[flowConfig]="{allowDuplicateUploads: true}">
<div class="tb-file-select-container">
@ -37,9 +42,11 @@
<mat-icon>cloud_upload</mat-icon>
<span>{{ dropLabel }}</span>
<button type="button" mat-button color="primary" class="browse-file">
<label for="{{inputId}}">{{ (multipleFile ? 'file-input.browse-files' : 'file-input.browse-file') | translate}}</label>
<label
for="{{inputId}}">{{ (multipleFile ? 'file-input.browse-files' : 'file-input.browse-file') | translate}}</label>
</button>
<input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
<input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs"
[flowAttributes]="{accept: accept}" id="{{inputId}}">
</div>
</div>
</div>

4
ui-ngx/src/app/shared/components/file-input.component.scss

@ -93,4 +93,8 @@ $previewSize: 100px !default;
padding: 0 16px;
}
}
.pointer-event {
pointer-events: auto;
}
}

3
ui-ngx/src/app/shared/components/file-input.component.ts

@ -54,6 +54,9 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
@Input()
label: string;
@Input()
hint: string;
@Input()
accept = '*/*';

7
ui-ngx/src/app/shared/models/device.models.ts

@ -865,6 +865,13 @@ export interface PublishTelemetryCommand {
snmp?: string;
}
export interface PublishLaunchCommand {
mqtt: {
linux: string;
windows: string;
};
}
export const dayOfWeekTranslations = new Array<string>(
'device-profile.schedule-day.monday',
'device-profile.schedule-day.tuesday',

247
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -1291,6 +1291,7 @@
"device-details": "Device details",
"add-device-text": "Add new device",
"credentials": "Credentials",
"commands": "Commands",
"manage-credentials": "Manage credentials",
"delete": "Delete device",
"assign-devices": "Assign devices",
@ -1392,6 +1393,11 @@
"copyId": "Copy device Id",
"copyAccessToken": "Copy access token",
"copy-mqtt-authentication": "Copy MQTT credentials",
"transport-command-copied-message": "Transport Command has been copied to clipboard",
"telemetry-commands-help-link": "In order to publish telemetry data to (<a href='{{helpLink}}' target='_blank'>ThingsBoard device</a>) with credentials of the current device you can use the following commands.",
"telemetry-command-setup-step": "1. Setup:",
"telemetry-command-send-step": "2. Send command:",
"telemetry-command-send-step-coap": "2. Send command: (based on CoAP cli)",
"idCopiedMessage": "Device Id has been copied to clipboard",
"accessTokenCopiedMessage": "Device access token has been copied to clipboard",
"mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard",
@ -2628,38 +2634,181 @@
},
"gateway": {
"add-entry": "Add configuration",
"advanced": "Advanced",
"checking-device-activity": "Checking Device Activity:",
"command": "Docker commands",
"command-copied-message": "Docker command has been copied to clipboard",
"configuration": "Configuration",
"connector-json": "Connector JSON",
"connector-add": "Add new connector",
"connector-enabled": "Enable connector",
"connector-name": "Connector name",
"connector-name-required": "Connector name is required.",
"connector-key": "Connector key",
"connector-key-required": "Connector key is required.",
"connector-configuration": "Configuration file name",
"connector-configuration-required": "Configuration file name is required.",
"connector-type": "Connector type",
"connector-type-required": "Connector type is required.",
"connectors": "Connectors configuration",
"connector-types": {
"mqtt": "MQTT Broker Connector",
"modbus": "Modbus Connector",
"modbus_serial": "Modbus Connector (serial)",
"opcua": "OPC-UA Connector",
"opcua_asyncio": "OPC-UA Connector (asyncio)",
"ble": "BLE Connector",
"request": "REQUEST Connector",
"can": "CAN Connector",
"bacnet": "BACnet Connector",
"odbc": "ODBC Connector",
"rest": "REST Connector",
"snmp": "SNMP Connector",
"ftp": "FTP Connector",
"socket": "Socket TCP/UDP Connector",
"xmpp": "XMPP Connector",
"ocpp": "OCPP Connector"
},
"connectors": "Connectors",
"connectors-config": "Connectors configuration",
"connectors-active": "Connector active",
"connectors-inactive": "Connector inactive",
"connectors-table-enabled": "Enabled",
"connectors-table-name": "Name",
"connectors-table-type": "Type",
"connectors-table-status": "Status",
"connectors-table-actions": "Actions",
"connectors-table-key": "Key",
"connectors-table-class": "Class",
"rpc-command-send": "Send",
"rpc-command-result": "Result",
"rpc-command-edit-params": "Edit parameters",
"select-connector": "Select connector",
"gateway-configuration": "Gateway Configuration",
"docker-label": "In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.",
"copy-command": "Copy docker command",
"create-new-gateway": "Create a new gateway",
"create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?",
"created-time": "Created time",
"configuration-delete-dialog-header": "Configurations will be deleted",
"configuration-delete-dialog-body": "Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.<br><br> \nTo turn off configuration, enter gateway name below",
"configuration-delete-dialog-input": "Gateway name",
"configuration-delete-dialog-input-required": "Gateway name is mandatory",
"configuration-delete-dialog-confirm": "Turn Off",
"delete": "Delete configuration",
"download-tip": "Download configuration file",
"drop-file": "Drop file here or",
"gateway": "Gateway",
"gateway-exists": "Device with same name is already exists.",
"gateway-name": "Gateway name",
"gateway-name-required": "Gateway name is required.",
"gateway-saved": "Gateway configuration successfully saved.",
"gateway-search": "Gateway search",
"grpc": "GRPC",
"grpc-keep-alive-timeout": "Keep alive timeout (in ms)",
"grpc-keep-alive-timeout-required": "Keep alive timeout is required",
"grpc-keep-alive-timeout-min": "Keep alive timeout can not be less then 1",
"grpc-keep-alive-timeout-pattern": "Keep alive timeout is not valid",
"grpc-keep-alive": "Keep alive (in ms)",
"grpc-keep-alive-required": "Keep alive is required",
"grpc-keep-alive-min": "Keep alive can not be less then 1",
"grpc-keep-alive-pattern": "Keep alive is not valid",
"grpc-min-time-between-pings": "Min time between pings (in ms)",
"grpc-min-time-between-pings-required": "Min time between pings is required",
"grpc-min-time-between-pings-min": "Min time between pings can not be less then 1",
"grpc-min-time-between-pings-pattern": "Min time between pings is not valid",
"grpc-min-ping-interval-without-data": "Min ping interval without data (in ms)",
"grpc-min-ping-interval-without-data-required": "Min ping interval without data is required",
"grpc-min-ping-interval-without-data-min": "Min ping interval without data can not be less then 1",
"grpc-min-ping-interval-without-data-pattern": "Min ping interval without data is not valid",
"grpc-max-pings-without-data": "Max pings without data",
"grpc-max-pings-without-data-required": "Max pings without data is required",
"grpc-max-pings-without-data-min": "Max pings without data can not be less then 1",
"grpc-max-pings-without-data-pattern": "Max pings without data is not valid",
"handle-device-renaming": "Handle device renaming",
"inactivity-check-period-seconds": "Inactivity check period (in sec)",
"inactivity-check-period-seconds-required": "Inactivity check period is required",
"inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1",
"inactivity-timeout-seconds": "Inactivity timeout (in sec)",
"inactivity-timeout-seconds-required": "Inactivity timeout is required",
"inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.",
"linux-macos": "Linux/MacOS",
"logs": {
"logs": "Logs",
"days": "days",
"hours": "hours",
"minutes": "minutes",
"seconds": "seconds",
"date-format": "Date format",
"date-format-required": "Date format required",
"log-format": "Log format",
"log-type": "Log type",
"log-format-required": "Log format required",
"remote": "Remote logging",
"remote-logs": "Remote logs",
"local": "Local logging",
"level": "Log level",
"file-path": "File path",
"file-path-required": "File path required",
"saving-period": "Log saving period",
"saving-period-min": "Log saving period can not be less then 1",
"saving-period-required": "Log saving period required",
"backup-count": "Backup count",
"backup-count-min": "Backup count can not be less then 1",
"backup-count-required": "Backup count required"
},
"min-pack-send-delay": "Min pack send delay (in ms)",
"min-pack-send-delay-required": "Min pack send delay is required",
"min-pack-send-delay-min": "Min pack send delay can not be less then 0",
"no-connectors": "No connectors",
"no-data": "No configurations",
"no-gateway-found": "No gateway found.",
"no-gateway-matching": " '{{item}}' not found.",
"path-logs": "Path to log files",
"path-logs-required": "Path is required.",
"permit-without-calls": "Keep alive permit without calls",
"remote": "Remote configuration",
"remote-logging-level": "Logging level",
"remove-entry": "Remove configuration",
"remote-shell": "Remote shell",
"remote-configuration": "Remote Configuration",
"other": "Other",
"save-tip": "Save configuration file",
"security-type": "Security type",
"security-types": {
"access-token": "Access Token",
"tls": "TLS"
"username-password": "Username and Password",
"tls": "TLS",
"tls-access-token": "TLS + Access Token",
"tls-private-key": "TLS + Private Key"
},
"server-port": "Server port",
"stats-send-period-in-sec": "Stats send period in seconds",
"statistics": {
"statistic": "Statistic",
"statistics": "Statistics",
"statistic-commands-empty": "No statistics available",
"commands": "Commands",
"send-period": "Statistic send period (in sec)",
"send-period-required": "Statistic send period is required",
"send-period-min": "Statistic send period can not be less then 60",
"send-period-pattern": "Statistic send period is not valid",
"check-connectors-configuration": "Check connectors configuration (in sec)",
"check-connectors-configuration-required": "Check connectors configuration is required",
"check-connectors-configuration-min": "Check connectors configuration can not be less then 1",
"check-connectors-configuration-pattern": "Check connectors configuration is not valid",
"add": "Add command",
"timeout": "Timeout",
"timeout-ms": "Timeout (in ms)",
"timeout-required": "Timeout is required",
"timeout-min": "Timeout can not be less then 1",
"timeout-pattern": "Timeout is not valid",
"attribute-name": "Attribute name",
"attribute-name-required": "Attribute name is required",
"command": "Command",
"command-required": "Command is required",
"remove": "Remove command"
},
"storage": "Storage",
"storage-max-file-records": "Maximum records in file",
@ -2671,6 +2820,16 @@
"storage-max-records-min": "Minimum number of records is 1.",
"storage-max-records-pattern": "Number is not valid.",
"storage-max-records-required": "Maximum records is required.",
"storage-read-record-count": "Read record count in storage",
"storage-read-record-count-min": "Minimum number of records is 1.",
"storage-read-record-count-pattern": "Number is not valid.",
"storage-read-record-count-required": "Read record count is required.",
"storage-max-read-record-count": "Max read record count in storage",
"storage-max-read-record-count-min": "Minimum number of records is 1.",
"storage-max-read-record-count-pattern": "Number is not valid.",
"storage-max-read-record-count-required": "Max Read record count is required.",
"storage-data-folder-path": "Data folder path",
"storage-data-folder-path-required": "Data folder path is required.",
"storage-pack-size": "Maximum event pack size",
"storage-pack-size-min": "Minimum number is 1.",
"storage-pack-size-pattern": "Number is not valid.",
@ -2680,9 +2839,11 @@
"storage-type": "Storage type",
"storage-types": {
"file-storage": "File storage",
"memory-storage": "Memory storage"
"memory-storage": "Memory storage",
"sqlite": "SQLITE"
},
"thingsboard": "ThingsBoard",
"thingsboard-general": "General",
"thingsboard-host": "ThingsBoard host",
"thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port",
@ -2695,10 +2856,67 @@
"title-connectors-json": "Connector {{typeName}} configuration",
"tls-path-ca-certificate": "Path to CA certificate on gateway",
"tls-path-client-certificate": "Path to client certificate on gateway",
"messages-ttl-check-in-hours": "Messages TTL check in hours",
"messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.",
"messages-ttl-check-in-hours-min": "Min number is 1.",
"messages-ttl-check-in-hours-pattern": "Number is not valid.",
"messages-ttl-in-days": "Messages TTL in days",
"messages-ttl-in-days-required": "Messages TTL in days is required.",
"messages-ttl-in-days-min": "Min number is 1.",
"messages-ttl-in-days-pattern": "Number is not valid.",
"mqtt-qos": "QoS",
"mqtt-qos-required": "QoS is required",
"mqtt-qos-range": "QoS values range is from 0 to 1",
"tls-path-private-key": "Path to private key on gateway",
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON"
"update-config": "Add/update configuration JSON",
"windows": "Windows",
"hints": {
"remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"host": "Hostname or IP address of ThingsBoard server",
"port": "Port of MQTT service on ThingsBoard server",
"token": "Access token for the gateway from ThingsBoard server",
"client-id": "MQTT client id for the gateway form ThingsBoard server",
"username": "MQTT username for the gateway form ThingsBoard server",
"password": "MQTT password for the gateway form ThingsBoard server",
"ca-cert": "Path to CA certificate file",
"cert": "Path to certificate file",
"private-key": "Path to private key file",
"date-form": "Date format in log message",
"log-format": "Log message format",
"remote-log": "Enables remote logging and logs reading from the gateway",
"backup-count": "If backup count is > 0, when a rollover is done, no more than backup count files are kept - the oldest ones are deleted",
"storage": "Provides configuration for saving incoming data before it is sent to the ThingsBoard platform",
"file": "Received data saving to the hard drive",
"memory": "Received data saving to the RAM memory",
"sqlite": "Received data saving to the .db file",
"data-folder": "Path to folder, that will contains data (Relative or Absolute)",
"max-file-count": "Maximum count of file that will be created",
"max-read-count": "Count of messages to get from storage and send to ThingsBoard",
"max-records": "Maximum count of records that will be stored in one file",
"read-record-count": "Count of messages to get from storage and send to ThingsBoard",
"max-records-count": "Maximum count of data in storage before send to ThingsBoard",
"ttl-check-hour": "How often will Gateway check data for obsolescence",
"ttl-messages-day": "Maximum days that storage will save data",
"commands": "Commands for collecting additional statistic",
"attribute": "Statistic telemetry key",
"timeout": "Timeout for command executing",
"command": "The result of the command execution, will be used as the value for telemetry",
"check-device-activity": "Enables monitor the activity of each connected device",
"inactivity-timeout": "Time after whose the gateway will disconnect device",
"inactivity-period": "Periodicity of device activity check",
"minimal-pack-delay": "Delay between sending packs of messages (Decreasing this setting results in increased CPU usage)",
"qos": "Quality of Service in MQTT messaging (0 - at most once, 1 - at least once)",
"server-port": "Network port on which GRPC server will listen for incoming connections.",
"grpc-keep-alive-timeout": "Maximum time the server should wait for a keepalive ping response before considering the connection dead.",
"grpc-keep-alive": "Duration between two successive keepalive ping messages when there is no active RPC call.",
"grpc-min-time-between-pings": "Minimum amount of time the server should wait between sending keepalive ping messages",
"grpc-max-pings-without-data": "Maximum number of keepalive ping messages that the server can send without receiving any data before it considers the connection dead.",
"grpc-min-ping-interval-without-data": "Minimum amount of time the server should wait between sending keepalive ping messages when there is no data being sent or received.",
"permit-without-calls": "Allow server to keep the GRPC connection alive even when there are no active RPC calls."
}
},
"grid": {
"delete-item-title": "Are you sure you want to delete this item?",
@ -3273,6 +3491,20 @@
"security": {
"security": "Security",
"general-settings": "General security settings",
"access-token": "Access token",
"access-token-required": "Access token is required",
"clientId": "Client ID",
"clientId-required": "Client ID is required",
"username": "Username",
"username-required": "Username is required",
"password": "Password",
"password-required": "Password is required",
"ca-cert": "CA certificate",
"ca-cert-required": "CA certificate is required",
"cert": "Certificate",
"cert-required": "Certificate is required",
"private-key": "Private Key",
"private-key-required": "Private Key is required",
"2fa": {
"2fa": "Two-factor authentication",
"2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.",
@ -5102,7 +5334,9 @@
"read-only": "Read only",
"events-title": "Gateway events form title",
"events-filter": "Events filter",
"event-key-contains": "Event key contains..."
"event-key-contains": "Event key contains...",
"is-connector": "Is Connector",
"state-param-name": "State parameter connector name"
},
"gauge": {
"default-color": "Default color",
@ -6160,7 +6394,8 @@
"node-selected": "On node selected",
"element-click": "On HTML element click",
"pie-slice-click": "On slice click",
"row-double-click": "On row double click"
"row-double-click": "On row double click",
"action-button-click": "Action button click"
}
},
"paginator" : {

Loading…
Cancel
Save