Browse Source

Merge pull request #9242 from vvlladd28/improvement/gateway-dashboard/change

Improvement gateway dashboard after merge
pull/9248/head
Igor Kulikov 3 years ago
committed by GitHub
parent
commit
51d09e0d1a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 703
      application/src/main/data/json/demo/dashboards/gateway.json
  2. 11
      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. 19
      application/src/main/data/json/system/widget_types/gateway_connectors.json
  6. 23
      application/src/main/data/json/system/widget_types/gateway_custom_statistics.json
  7. 10
      application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json
  8. 19
      application/src/main/data/json/system/widget_types/gateway_general_configuration.json
  9. 10
      application/src/main/data/json/system/widget_types/gateway_logs.json
  10. 23
      application/src/main/data/json/system/widget_types/gateway_statistics.json
  11. 6
      application/src/main/data/json/system/widget_types/service_rpc.json
  12. 3
      ui-ngx/src/app/core/http/device.service.ts
  13. 5
      ui-ngx/src/app/core/utils.ts
  14. 14
      ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts
  15. 48
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.html
  16. 37
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.scss
  17. 34
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/device-gateway-command.component.ts
  18. 1437
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  19. 124
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.scss
  20. 310
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts
  21. 117
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  22. 74
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.scss
  23. 204
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  24. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.html
  25. 155
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-logs.component.ts
  26. 75
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.html
  27. 12
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-remote-configuration-dialog.ts
  28. 74
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html
  29. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss
  30. 67
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  31. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.html
  32. 127
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-statistics.component.ts
  33. 142
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  34. 19
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.html
  35. 29
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-logs-settings.component.ts
  36. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.html
  37. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/gateway/gateway-service-rpc-settings.component.ts
  38. 9
      ui-ngx/src/app/shared/components/file-input.component.html
  39. 2
      ui-ngx/src/app/shared/components/file-input.component.scss
  40. 5
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html
  41. 4
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss
  42. 3
      ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts
  43. 69
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  44. 5
      ui-ngx/src/form.scss

703
application/src/main/data/json/demo/dashboards/gateway_list.json → application/src/main/data/json/demo/dashboards/gateway.json

File diff suppressed because it is too large

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

@ -10,13 +10,12 @@
"widgetTypeFqns": [
"gateway_widgets.gateway_configuration",
"gateway_widgets.attributes_card",
"gateway_widgets.gateway_configuration2",
"gateway_widgets.gateway_general_configuration",
"gateway_widgets.config_form_latest",
"gateway_widgets.gateway_events",
"gateway_widgets.gateway_connector",
"gateway_widgets.gateway_connectors",
"gateway_widgets.gateway_logs",
"gateway_widgets.gateway_statistics",
"gateway_widgets.gateway_general_statistics",
"gateway_widgets.gateway_custom_statistics",
"gateway_widgets.gateway_general_chart_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

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

@ -0,0 +1,19 @@
{
"fqn": "gateway_widgets.gateway_connectors",
"name": "Gateway connectors",
"deprecated": false,
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAA7VBMVEXg4ODf39/g4ODg4OAAAAD////88vPg4OD39/fo6Ojv7+9hYWHt7e2GhobLy8upqan99fb9+Pn++/zDw8OXl5fb29vnjJHspao9PT1VVVXPz8/l5eX65efd3d1tbW2enp7hc3nz8/O2tra6urrU1NT32dvcWmHxv8KPj4+YmJixsbGgoKBzc3P+/f332Nuqqqp6enrXQEj0zM7Z2dlJSUnZTVR9fX3pmZ2wsLDusraSkpLr6+tpaWmRkZGNjY3eZ23k5OTkf4Xjf4XkgIWBgYHeZm3aTVXvsrbfZ23UMTnFxcXusrWzs7PkgIbeZmxYj4YaAAAABXRSTlPvIMCwAL23wXkAAAxzSURBVHja7NbBquMgGIbhdM75KLbILILw/b+g6L4YqKvCuf/bmtjNTLMJswm2+BIU1M2DWTh9n35N53fv6/Q9nabLDW/e7bIyvi74gC7rb/X299G6TdMZH9F5QDprQHprQHprQHprC1FrM96xDSQyLAzYTWVGX71CDC1QqEi5GEDVlRnITqMBoFHbWLJJkdYhlXZo1nl+LuKw9iEzFSvCG6mBBbZWYQblKjXB1sCIzKuIu1KyabsKqbSZQeQ4yT4k0qD1oIetaf1QLbhAmWdmxJpkgWdWZgRJCAEiq1GQrOKo9iGFd7SCAJl3K4BY0MIwR4YgdIxYaxCxTQwJgIosGYe1D1FmwAVdBCgbSGFWVfABmLSBwM+WPd0IRFSv1WcWI4IXiPLhtCCIK5wdHylWp2KfkBCMYcZh7UP8lZQ78NOmVwiKsP6GF/IHEMa0kME3yHMxJBzWPgRI/u+0zad/xnYobbb+v/FEGZDeG5DeGpDeGpDeGpDeGpDeGpDeGpDe+lDIH/LqoMVtGAjD8OmDDO4kBzUgjWAGDXtY6EEC+STw//9btZO27CYBey+l0PdgsBnLPAgsEpHy58YBeMe9mvEhugJS8KLTCTtdReSKnYrQ06NwHGKJueNXogDUcS8JPmRzQaRXjvf3PcnSItvOTJg4CT5HDKWjEAagQ4maBkmNizo17aXp/ABpiFRVqbKOdn/h5vjxfVdCEcSwoKyEoY7njCFh+7Kt1zLUi7bQamwsxEcgc1LEzBbEJ0lFq3q79Mk5P0BYZYV0rrz0mFvlS543x3ltT0IRmCnl2Je2rVzwVG6pgi89VXbeZq6zSEIkG8ZHdyTCzJWTKMzU06RshvQAkRRJWY2NIth0Ul0hbzif8XYAMpoikign1ReQK2W1aVItqYVt5ppwg1Ca+hGIupcN0sZIMksK6mMED1N43BEskWZpvyHGwbG2QYB9iERHrIN9EnsBsSbNbASHJmwzd8gctvsDEDGzbBDJS61UF4f34kuH2+e/lgQUK2HxEKQYgmCdOgopBmDOiLYUOAte5EvFbc3u2GZKBVW4wcZfOke+nXCkxkAkfLmQ8r91IHYAGV8vl//rZP+pIQxGPTLYwKhHBhsYRh7xYh8WwGv4xMioRwYXALBftr1twkAA/nRXNIwdAgFpwEaFwSZvG0jNixpt7bq264f9/7+zI2xL46Rj+dAITXtELOQAukfH+cx/kdfCHWA3A7fvIi7+Yp7e5nWI6vEuTVPEOEhzpNMcf+L2WmTngQ95XagUwyBf3lL06XJexVWex7gz6Ra5zFbeNZwV02NeIFZbkeZAvCIXVYUh7nA7RbKNbdtTPmyYwfmY4Y6YIn96JlLFeK8qpfAZXSLcbmlb/vURlUyXDI5iJbAjOXKV1BaAluDRIzyH2A5mQohi+f02flCfw1ZkGawLrHAPt0PEsVv0RWsCJlzAqISIM5AJZyzhEiz6QSIZd6LmlOYAosw5FOELCXzKm0doAD+B7dCyt17FzWs0XyoaaiTWyxjXxtrVIWJvdMNmetECJmNOA3MoaD8Tk5HmCybGwuPvs5LrGzFejUc6A+KYiCjBF1xw+tcyRPBUOkQ2m68N9seXREQjkmWQeT4blSNBoUzFZEIBMVbCFGBBcy+JZH5UZmcRMV8tBiaegEhzAUL+FlkwYEKCxzQsGNN/EvEc0uCCUgodIiGtuQHOg8emnWCQhieKXLQe07dbjzdwyEr7N5YofRAsmkRjEIn0fcl8x7OcTPqfJM01+EeK3bN8GEtYOVQsdCPQYNTIbuGKqcJVGgZPeRhQyRf5aTUC/F3j8dqNkk2sgzayT5HWOL8qKBffwqCo0WTY3RC9D94lnB8jI7EqFN6HGIRf0oCk8ng/If3dogz3RYqmi6SKROKnILxbX+X7CemviGGy7SIqx5qOmhqJ4dHnTSPM8C8ZzHq9+yWG7qAbd9j375F/6AvxR3vn2ps2DIXh7nJSbzj2WifOh4QtUy7kAmikChd1gra7a5f//3N2nKwrkGyQSWMh6ys4GCuK+sousR/OCfdGTi5IJ3Rxwp50Qqw7U+veSLvUQiOj8506AiPnz/ZboZy33MidDStfZYM3uFR08xwZ13i1kloYhtlxkMa14QgdJ1zjWmGAPZONQdnDyAWmylzAAbWbNE4K0ui6jUjji57S/GWh8z9POCH0F32UqCe2UUWoJ415tjYiSBqzSbOtruiVEqdKV1UnJOa6gFqRtX4apXGVBwwJ0CEjsaEzZkyXRhFq4IN0XenkMgxKI4v82m1KGqe9Um9PCz2pI42Ug+AMPI/btsc9fJ8Q4nHKpz42KfYBYkjQSZU0CmB6gYOmvyWN2jjAoLbpDj4xBviuKWmcK70e/hbQIdMy+gZjqWnQJV2a84QLOzVjsjT92NR9QPkpVIwkKeg/SCP926SxV+iWNF7VItO+Io3eGmnkifqDNkgjjWpJo+AJU6eY/m3SyHuleGmkyoXMyBaxGRPjjjQufWonCeU0Ikbfx77CR7/OiDnsc1acYhdp/PouHIfa2J1JvJyMV+4brdnUejksSePpE6U6vsWMhAJLBQhCPcpA2JRzSnh6BXOmmrSYfJxzG7Zk+sCB9YGlugcgbFChnjSOc+3zQH5wB/LTwJI3lnYdNPtnh8uoh0T+bxM64ldh0KZkaCHccjWJDRkGf0AayeXF6TkcWNWPrYF2MysB3ey9xFcrOBbSONoijQh8SyNaKLP31/kW2WqvkU0nt1eR20e25aPNi0YYnWn76az1pBFR426dj1q/serSVve/N+I/7YT87hjpzNS6N9Iutc/IXhfE9hs5W19oOeoRoG737KpV6qzdRkbrFGU1K7jWTZgjzfoQ4gYRuVb2c9HYZiOjTa412SKNWVPS+HL+ujecw+H1rAlpRCf77dmnl0q1TJGwPuyhvk+rjNLH4NPiFDZDFaFmqztxxpPsoza7HZEMB6UhaXzRK5X+iqLQSKQCakX5OqIQUQ1ptBVppIZIPdPTeRlqdrpWmGfazcdVgARFUzQ+nGlhM/gw3CSNNmzLEwqNpgaHlEfCVGXGfhRTmsaCTxk2bTPVAQghU1IFMB5wnSUCiF4ljbvUkDQO9yGNjOOrYSPDSmFp633GOSMJjUGnNMI+lKnzGtKog+EdhDTuHpFEjYjYzGn0PJZukUZCYEmrgC5JFGkUeBTsMhI4jlNETT2CpkbmvVLz0giBbdkRSxLEuzr9acTwGBNcRHSKzYQXRkTqG6SONFLOaCSSZJeR/DocTBzLlaFmyXEu3zs7plY9jk+fFhpBVVT4KphgEuITfKHAGAFfEGCmamIfyhc1n1pXUBxPBedENQBDfSqgMwnGwQzJnJxYluJaC6chaTz/8ur5Ae7oae8ijYPwo6NNkDTO8pl8V51Zo91X9pdPCRxY1SEZW5r8QRoHE3mz0GS2OSDtXaJsfgCXV5GbgTZbqKqLVXh9PKTx95eSYyKNcNaRhIH9MjhGo/ZvrDq01b030plUwM4kZ3Zmat0baZdaZ6QjSf6jZ3d1iAv1Tfs4W1hFYetgEWjYdG7XKO02srYjWcnZm4JrLdwVGpHX1nsNN1hBs0KYy4tLOKSqPhxEP/lW0XE2CYJGpUmnqhJR/wdWtF2kUcompPGyTNgamlco+2kdQuQJhXqRzaJD2FaBJXhfnYL4HFWEmq1uHoxDdCOt0siN5eROJRWwEWmsOrF1KuIizZJiIORHrqXqMSOCTdWnTOl1OY19MIfM1k0vpuZSmEWo2VWN1d0ELBdNDD6rqXbtInhsRlGQYCv11gBdlWvZdMkjYrwwPMRwOvEM3feN1OO67xmGMKdLdVxSY8RIgMcsEQfIoOu9TpV2kEaRoCHDLrmWPWWJKjr2aQxTIHpBGik3a4x4EdUPRBr1Xin9V/XswgPKCyN3Rce+aac+9JURgka48ruMpv2qES9CG8Lbo+h4bA2cBU6tLFA17QOrAoQa3WHArsn71dPII1ES3ZFGL+a+r/OE6EyksYd9SrUjQhRpLE6xA2I7OZYch4ElJeJGaa0GeVPSWDLTL6QQ1MlU3TYBfKEY8ChKsWnjs2wSKERhW+pQFcAWafGuCPWkMbccZ6BIYy4t+S7TsqakUeXGxyYcXpWppb7eUUXHoawlja1domwOSeaihZI0jj9J+cbJg6MhjZtOLFc6mpVh0qxmfdbkN2vTR5sXjehkl46jEAZgtOdNkEZQMXLSjR8IPj152I2fbH7UnR/Rfvzg0emx6+Thg8ffAbOE0ZCNlb+aAAAAAElFTkSuQmCC",
"description": "Allow to create and manage gateway connectors.",
"descriptor": {
"type": "latest",
"sizeX": 11,
"sizeY": 8,
"resources": [],
"templateHtml": "<tb-gateway-connector [device]=\"entityId\" *ngIf=\"entityId\" [ctx]=\"ctx\"></tb-gateway-connector>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway connectors\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
}
}

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

File diff suppressed because one or more lines are too long

10
application/src/main/data/json/system/widget_types/gateway_general_statistics.json → application/src/main/data/json/system/widget_types/gateway_general_chart_statistics.json

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

10
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

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

File diff suppressed because one or more lines are too long

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

@ -27,7 +27,8 @@ import {
DeviceCredentials,
DeviceInfo,
DeviceInfoQuery,
DeviceSearchQuery, PublishLaunchCommand,
DeviceSearchQuery,
PublishLaunchCommand,
PublishTelemetryCommand
} from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.models';

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

@ -812,3 +812,8 @@ export const getOS = (): string => {
return os;
};
export const camelCase = (str: string): string => {
return _.camelCase(str);
};

14
ui-ngx/src/app/modules/home/components/widget/dialog/custom-dialog.service.ts

@ -17,8 +17,6 @@
import { Inject, Injectable, Type } from '@angular/core';
import { Observable } from 'rxjs';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { AuthService } from '@core/auth/auth.service';
import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
import { CommonModule } from '@angular/common';
import { mergeMap, tap } from 'rxjs/operators';
@ -28,7 +26,11 @@ import {
CustomDialogContainerData
} from '@home/components/widget/dialog/custom-dialog-container.component';
import { SHARED_MODULE_TOKEN } from '@shared/components/tokens';
import { HOME_COMPONENTS_MODULE_TOKEN, SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import {
HOME_COMPONENTS_MODULE_TOKEN,
SHARED_HOME_COMPONENTS_MODULE_TOKEN,
WIDGET_COMPONENTS_MODULE_TOKEN
} from '@home/components/tokens';
@Injectable()
export class CustomDialogService {
@ -36,12 +38,11 @@ export class CustomDialogService {
private customModules: Array<Type<any>>;
constructor(
private translate: TranslateService,
private authService: AuthService,
private dynamicComponentFactoryService: DynamicComponentFactoryService,
@Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>,
@Inject(SHARED_HOME_COMPONENTS_MODULE_TOKEN) private sharedHomeComponentsModule: Type<any>,
@Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type<any>,
@Inject(WIDGET_COMPONENTS_MODULE_TOKEN) private widgetComponentsModule: Type<any>,
public dialog: MatDialog
) {
}
@ -52,7 +53,8 @@ export class CustomDialogService {
customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any,
config?: MatDialogConfig): Observable<any> {
const modules = [this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule];
const modules = [this.sharedModule, CommonModule, this.sharedHomeComponentsModule, this.homeComponentsModule,
this.widgetComponentsModule];
if (Array.isArray(this.customModules)) {
modules.push(...this.customModules);
}

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

@ -15,7 +15,7 @@
limitations under the License.
-->
<div mat-dialog-content tb-toast fxLayout="column" toastTarget="dockerCommandDialogContent" style="padding-top: 5px">
<div mat-dialog-content fxLayout="column" style="padding: 0 8px 8px">
<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>
@ -29,14 +29,9 @@
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-container
*ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.windows }">
</ng-container>
</ng-template>
</mat-tab>
<mat-tab>
@ -45,14 +40,9 @@
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-container
*ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
</ng-container>
</ng-template>
</mat-tab>
<mat-tab>
@ -61,15 +51,23 @@
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-container
*ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
</ng-container>
</ng-template>
</mat-tab>
</mat-tab-group>
</div>
<ng-template #commandsExample let-command="command">
<div class="tb-form-panel stroked tb-tab-body">
<div class="tb-form-panel-title" translate>device.connectivity.execute-following-command</div>
<tb-markdown usePlainMarkdown containerClass="start-code"
data="
```bash
{{ command }}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template>

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

@ -22,16 +22,47 @@
:host ::ng-deep {
.tb-markdown-view {
.start-code {
code[class*="language-"], pre[class*="language-"] {
overflow: hidden;
code[class*="language-"] {
white-space: break-spaces;
word-break: break-all;
}
pre[class*="language-"] {
overflow: hidden;
background: #F3F6FA;
border-color: #305680;
}
.code-wrapper {
padding: 0;
}
button.clipboard-btn {
right: 0;
p {
color: #305680;
}
p, div {
background-color: #F3F6FA;
}
div {
img {
display: none;
}
&:after {
content: "";
position: initial;
display: block;
width: 18px;
height: 18px;
background: #305680;
mask-image: url(/assets/copy-code-icon.svg);
mask-repeat: no-repeat;
}
}
}
}
}
.tb-form-panel.tb-tab-body {
margin-top: 16px;
}
}

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

@ -15,15 +15,10 @@
///
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';
import { PublishLaunchCommand } from '@shared/models/device.models';
@Component({
selector: 'tb-gateway-command',
@ -39,17 +34,13 @@ export class DeviceGatewayCommandComponent implements OnInit {
@Input()
deviceId: string;
linuxCode: string;
windowsCode: string;
commands: PublishLaunchCommand;
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,
constructor(private cd: ChangeDetectorRef,
private deviceService: DeviceService) {
}
@ -57,7 +48,7 @@ export class DeviceGatewayCommandComponent implements OnInit {
ngOnInit(): void {
if (this.deviceId) {
this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => {
this.createRunCode(commands.mqtt);
this.commands = commands;
this.cd.detectChanges();
});
}
@ -78,21 +69,4 @@ export class DeviceGatewayCommandComponent implements OnInit {
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'
}));
}
}

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

File diff suppressed because it is too large

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

@ -16,128 +16,76 @@
:host {
width: 100%;
height: 100%;
display: block;
display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
.mat-icon {
color: rgba(0, 0, 0, .12);
}
.tb-form-panel {
margin-bottom: 20px;
.configuration-block {
display: flex;
flex-direction: column;
gap: 16px;
}
.mat-toolbar {
grid-row: 1;
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;
}
.tab-group-block {
min-width: 0;
height: 100%;
min-height: 0;
grid-row: 2;
}
.security-toggle-group {
margin-bottom: 15px;
.toggle-group {
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;
textarea {
resize: none;
}
mat-panel-title span {
display: block;
padding-left: 0;
padding-top: 5px;
.saving-period {
flex: 1;
}
.tb-hint {
font-size: 13px;
color: rgba(0, 0, 0, .54);
width: fit-content;
cursor: pointer;
text-transform: none !important;
}
.statistics-container {
width: 50%;
.line-break {
width: 100%;
.command-container {
width: 100%;
}
}
textarea {
resize: none;
.actions {
grid-row: 3;
padding: 8px;
display: flex;
gap: 8px;
justify-content: flex-end;
flex: 1;
}
}
: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;
.toggle-group span {
padding: 0 25px;
}
.security-toggle-group span {
padding: 0 25px;
.mat-mdc-form-field-icon-suffix {
color: #E0E0E0;
&:hover {
color: #9E9E9E;
}
}
}

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

@ -15,15 +15,11 @@
///
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
@ -41,79 +37,18 @@ 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'],
]
);
import {
GatewayLogLevel,
GecurityTypesTranslationsMap,
LocalLogsConfigTranslateMap,
LocalLogsConfigs,
LogSavingPeriod,
LogSavingPeriodTranslations,
SecurityTypes,
StorageTypes,
StorageTypesTranslationMap
} from './gateway-widget.models';
import { deepTrim } from '@core/utils';
@Component({
selector: 'tb-gateway-configuration',
@ -124,13 +59,16 @@ export class GatewayConfigurationComponent implements OnInit {
gatewayConfigGroup: FormGroup;
storageTypes = storageTypesTranslations;
StorageTypes = StorageTypes;
storageTypes = Object.values(StorageTypes) as StorageTypes[];
storageTypesTranslationMap = StorageTypesTranslationMap;
logSavingPeriods = logSavingPeriodTranslations;
logSavingPeriods = LogSavingPeriodTranslations;
localLogsConfigLabels = localLogsConfigLabels;
localLogsConfigs = Object.keys(LocalLogsConfigs) as LocalLogsConfigs[];
localLogsConfigTranslateMap = LocalLogsConfigTranslateMap;
securityTypes = securityTypesTranslationsMap;
securityTypes = GecurityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
@ -142,24 +80,19 @@ export class GatewayConfigurationComponent implements OnInit {
logSelector: FormControl;
securityType: SecurityTypes;
private initialCredentials: DeviceCredentials;
initialCredentials: DeviceCredentials;
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private deviceService: DeviceService,
private cd: ChangeDetectorRef,
public dialog: MatDialog) {
private dialog: MatDialog) {
}
ngOnInit() {
this.gatewayConfigGroup = this.fb.group({
thingsboard: this.fb.group({
host: [window.location.hostname, [Validators.required]],
host: [window.location.hostname, [Validators.required, Validators.pattern(/^[^\s]+$/)]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false, []],
remoteConfiguration: [true, []],
@ -175,30 +108,30 @@ export class GatewayConfigurationComponent implements OnInit {
handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({
checkDeviceInactivity: [false, []],
inactivityTimeoutSeconds: [200, [Validators.min(1)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1)]]
inactivityTimeoutSeconds: [200, [Validators.min(1), Validators.pattern(/^[^.\s]+$/)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1), Validators.pattern(/^[^.\s]+$/)]]
}),
security: this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required]],
clientId: [null, []],
username: [null, []],
password: [null, []],
accessToken: [null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
clientId: [null, [Validators.pattern(/^[^.\s]+$/)]],
username: [null, [Validators.pattern(/^[^.\s]+$/)]],
password: [null, [Validators.pattern(/^[^.\s]+$/)]],
caCert: [null, []],
cert: [null, []],
privateKey: [null, []],
}),
qos: [1, [Validators.min(0), Validators.max(1), Validators.required]]
qos: [1, [Validators.min(0), Validators.max(1), Validators.required, Validators.pattern(/^[^.\s]+$/)]]
}),
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/', []],
read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]],
max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]],
data_folder_path: ['./data/', [Validators.pattern(/^[^\s]+$/)]],
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', []],
data_file_path: ['./data/data.db', [Validators.pattern(/^[^\s]+$/)]],
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]+$/)]],
@ -215,13 +148,13 @@ export class GatewayConfigurationComponent implements OnInit {
}),
connectors: this.fb.array([]),
logs: this.fb.group({
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required]],
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
[Validators.required]],
[Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
type: ['remote', [Validators.required]],
remote: this.fb.group({
enabled: [false],
logLevel: [GatewayLogLevel.info, [Validators.required]],
logLevel: [GatewayLogLevel.INFO, [Validators.required]],
}),
local: this.fb.group({})
})
@ -231,7 +164,7 @@ export class GatewayConfigurationComponent implements OnInit {
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').clearValidators();
}
this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false});
});
@ -258,10 +191,10 @@ export class GatewayConfigurationComponent implements OnInit {
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);
}
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').updateValueAndValidity({emitEvent: false});
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({emitEvent: false});
});
this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => {
@ -272,7 +205,7 @@ export class GatewayConfigurationComponent implements OnInit {
securityGroup.get('type').valueChanges.subscribe(type => {
this.removeAllSecurityValidators();
if (type === SecurityTypes.ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required]);
securityGroup.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
securityGroup.get('accessToken').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_PRIVATE_KEY) {
securityGroup.get('caCert').addValidators([Validators.required]);
@ -282,21 +215,19 @@ export class GatewayConfigurationComponent implements OnInit {
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').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
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.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
}
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());
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 => {
@ -306,28 +237,37 @@ export class GatewayConfigurationComponent implements OnInit {
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
storageGroup.get('max_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_count').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.FILE) {
storageGroup.get('data_folder_path').addValidators([Validators.required]);
storageGroup.get('data_folder_path').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
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]);
storageGroup.get('data_folder_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_file_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_per_file').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.SQLITE) {
storageGroup.get('data_file_path').addValidators([Validators.required]);
storageGroup.get('data_file_path').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
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]);
storageGroup.get('data_file_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_check_in_hours').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_in_days').updateValueAndValidity({emitEvent: false});
}
});
this.fetchConfigAttribute(this.device);
}
atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) {
return (group: UntypedFormGroup): ValidationErrors | null => {
private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) {
return (group: FormGroup): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
@ -337,22 +277,10 @@ export class GatewayConfigurationComponent implements OnInit {
};
}
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;
private 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(
@ -397,24 +325,26 @@ export class GatewayConfigurationComponent implements OnInit {
if (remoteLoggingLevel) {
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
remoteLogsFormGroup.patchValue({
enabled: remoteLoggingLevel !== GatewayLogLevel.none,
enabled: remoteLoggingLevel !== GatewayLogLevel.NONE,
logLevel: remoteLoggingLevel
}, {emitEvent: false});
remoteLogsFormGroup.markAsPristine();
}
this.cd.detectChanges();
} else {
this.checkAndFetchCredentials({});
this.checkAndFetchCredentials();
}
});
}
checkAndFetchCredentials(security): void {
private checkAndFetchCredentials(security: any = {}): 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.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);
@ -432,7 +362,7 @@ export class GatewayConfigurationComponent implements OnInit {
}
}
logsToObj(logsConfig) {
private logsToObj(logsConfig: any) {
const logsObject = {
local: {}
};
@ -453,43 +383,42 @@ export class GatewayConfigurationComponent implements OnInit {
return {local: logsObject, logFormat, dateFormat};
}
toggleRpcFields(enable: boolean) {
private 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();
grpcGroup.get('serverPort').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false});
} 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();
grpcGroup.get('serverPort').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false});
}
}
addCommand(command?): void {
const data = command || {};
addCommand(command: any = {}): void {
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]+$/)]],
attributeOnGateway: [command.attributeOnGateway || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
command: [command.command || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
timeout: [command.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]],
});
commandsFormArray.push(commandFormGroup);
}
addLocalLogConfig(name, config): void {
private addLocalLogConfig(name: string, config: any): void {
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
const configGroup = this.fb.group({
logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]],
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)]],
@ -507,12 +436,14 @@ export class GatewayConfigurationComponent implements OnInit {
}
removeCommandControl(index: number, event: any): void {
if (event.pointerType === '') return;
if (event.pointerType === '') {
return;
}
this.commandFormArray().removeAt(index);
this.gatewayConfigGroup.markAsDirty();
}
removeAllSecurityValidators(): void {
private removeAllSecurityValidators(): void {
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
securityGroup.clearValidators();
for (const controlsKey in securityGroup.controls) {
@ -524,7 +455,7 @@ export class GatewayConfigurationComponent implements OnInit {
}
}
removeAllStorageValidators(): void {
private removeAllStorageValidators(): void {
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
for (const storageKey in storageGroup.controls) {
if (storageKey !== 'type') {
@ -535,7 +466,7 @@ export class GatewayConfigurationComponent implements OnInit {
}
}
removeEmpty(obj) {
private removeEmpty(obj: any) {
return Object.fromEntries(
Object.entries(obj)
.filter(([_, v]) => v != null)
@ -543,7 +474,7 @@ export class GatewayConfigurationComponent implements OnInit {
);
}
generateLogsFile(logsObj) {
private generateLogsFile(logsObj: any) {
const logAttrObj = {
version: 1,
disable_existing_loggers: false,
@ -591,7 +522,7 @@ export class GatewayConfigurationComponent implements OnInit {
return logAttrObj;
}
createHandlerObj(logObj, key) {
private createHandlerObj(logObj: any, key: string) {
return {
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler',
formatter: 'LogFormatter',
@ -603,7 +534,7 @@ export class GatewayConfigurationComponent implements OnInit {
};
}
createLoggerObj(logObj, key) {
private createLoggerObj(logObj: any, key: string) {
return {
handlers: [`${key}Handler`, 'consoleHandler'],
level: logObj.logLevel,
@ -612,12 +543,12 @@ export class GatewayConfigurationComponent implements OnInit {
}
saveConfig(): void {
const value = this.removeEmpty(this.gatewayConfigGroup.value);
const value = deepTrim(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
value: value.logs.remote.enabled ? value.logs.remote.logLevel : GatewayLogLevel.NONE
});
delete value.connectors;
attributes.push({
@ -642,18 +573,18 @@ export class GatewayConfigurationComponent implements OnInit {
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => {
this.updateCredentials(value.thingsboard.security).subscribe(_ => {
this.updateCredentials(value.thingsboard.security).subscribe(() => {
if (this.dialogRef) {
this.dialogRef.close();
} else {
this.gatewayConfigGroup.markAsPristine();
this.cd.detectChanges();
}
})
});
});
}
updateCredentials(securityConfig): Observable<any> {
private updateCredentials(securityConfig: any): Observable<any> {
let updateCredentials = false;
let newCredentials = {};
if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) {
@ -661,17 +592,26 @@ export class GatewayConfigurationComponent implements OnInit {
updateCredentials = true;
} else {
const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue);
updateCredentials = !(parsedCredentials.clientId === securityConfig.clientId && parsedCredentials.userName === securityConfig.username && parsedCredentials.password === securityConfig.password);
updateCredentials = !(
parsedCredentials.clientId === securityConfig.clientId &&
parsedCredentials.userName === securityConfig.username &&
parsedCredentials.password === securityConfig.password);
}
if (updateCredentials) {
let credentialsValue: { clientId?, userName?, password? } = {};
const credentialsValue: { clientId?: string; userName?: string; password?: string } = {};
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;
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)
credentialsType,
credentialsValue: JSON.stringify(credentialsValue)
};
}
} else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) {
@ -684,12 +624,12 @@ export class GatewayConfigurationComponent implements OnInit {
newCredentials = {
credentialsType: DeviceCredentialsType.ACCESS_TOKEN,
credentialsId: securityConfig.accessToken
}
};
}
}
if (updateCredentials) {
return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials})
return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials});
}
return of(null);
}

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

@ -15,25 +15,25 @@
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>
<div class="connector-container tb-form-panel no-border">
<section class="table-section tb-form-panel no-padding flex section-container">
<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>
<div class="table-container">
<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;">
<mat-header-cell *matHeaderCellDef style="width: 60px;min-width: 60px;">
{{ 'gateway.connectors-table-enabled' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let attribute">
@ -61,12 +61,9 @@
{{ '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>
<div class="status" [class]="isConnectorSynced(attribute) ? 'status-sync' : 'status-unsync'">
{{ isConnectorSynced(attribute) ? 'sync' : 'out of sync' }}
</div>
</mat-cell>
</ng-container>
<ng-container matColumnDef="errors">
@ -77,10 +74,8 @@
<span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above"
[ngClass]="{
'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) == 0 || getErrorsCount(attribute) == ''
}"></span>
[class]="{'hasErrors': +getErrorsCount(attribute) > 0,
'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
@ -139,60 +134,60 @@
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
<mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true,
'tb-current-entity': isSameConnector(attribute)}"
<mat-row class="mat-row-select" [class]="{'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">
</div>
</section>
<section [formGroup]="connectorForm" class="tb-form-panel no-border no-padding flex">
<section class="tb-form-panel input-container section-container">
<section class="tb-form-row tb-standard-fields no-padding no-border column-lt-md input-container">
<mat-form-field class="flex" subscriptSizing="dynamic">
<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-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
<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>
*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-form-field class="flex" *ngIf="connectorForm.get('type').value === 'grpc'" subscriptSizing="dynamic">
<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-form-field class="flex" *ngIf="connectorForm.get('type').value === 'custom'" subscriptSizing="dynamic">
<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-form-field class="flex" subscriptSizing="dynamic" hideRequiredMarker>
<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-option *ngFor="let logLevel of gatewayLogLevel" [value]="logLevel">{{ logLevel }}</mat-option>
</mat-select>
</mat-form-field>
</mat-card>
<mat-card fxLayout="column" fxFlex>
<tb-json-object-edit
fxFlex
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
<div fxLayoutAlign="start center">
<button mat-raised-button color="primary"
class="action-btns"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector()">
{{ 'action.save' | translate }}
</button>
</div>
</mat-card>
</div>
</div>
</form>
</section>
</section>
<section class="tb-form-panel flex section-container">
<tb-json-object-edit
fillHeight="true"
class="flex"
fxLayout="column"
jsonRequired
label="{{ 'gateway.configuration' | translate }}"
formControlName="configurationJson">
</tb-json-object-edit>
<div fxLayoutAlign="end center">
<button mat-raised-button color="primary"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector()">
{{ 'action.save' | translate }}
</button>
</div>
</section>
</section>
</div>

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

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../scss/constants';
:host {
width: 100%;
height: 100%;
@ -21,25 +23,32 @@
padding: 0;
.connector-container {
height: 100%;
width: 100%;
flex-direction: row;
@media #{$mat-lt-lg} {
flex-direction: column;
}
.table-section {
min-height: 35vh;
overflow: hidden;
& > mat-card, & > div {
min-width: calc(50% - 15px);
.table-container {
overflow: auto;
}
}
mat-card {
margin: 10px;
padding: 10px;
max-width: 100%;
.flex {
flex: 1;
}
}
.tb-entity-table {
.tb-entity-table-content {
width: 100%;
height: 100%;
background: #fff;
overflow: hidden;
.input-container {
height: auto;
}
.section-container {
background-color: #fff;
}
}
@ -48,43 +57,24 @@
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;
margin: 0 8px;
}
.status-block {
.status {
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;
&-sync {
background: rgba(25, 128, 56, .06);
color: rgb(25, 128, 56);
}
&-unsync {
background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
}
}
mat-row {

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

@ -17,14 +17,11 @@
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 { forkJoin, Observable } 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';
@ -33,47 +30,20 @@ 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 { camelCase, deepClone, isString } 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']
]);
import {
GatewayConnector,
GatewayConnectorDefaultTypesTranslates,
GatewayLogLevel
} from './gateway-widget.models';
@Component({
selector: 'tb-gateway-connector',
@ -84,12 +54,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
pageLink: PageLink;
attributeDataSource: AttributeDatasource;
inactiveConnectorsDataSource: AttributeDatasource;
serverDataSource: AttributeDatasource;
dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
@ -107,27 +71,29 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
InitialActiveConnectors: Array<string>;
private inactiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
private attributeDataSource: AttributeDatasource;
private inactiveConnectorsDataSource: AttributeDatasource;
activeData: Array<any> = [];
private serverDataSource: AttributeDatasource;
inactiveData: Array<any> = [];
private activeData: Array<any> = [];
sharedAttributeData: Array<AttributeData> = [];
private inactiveData: Array<any> = [];
initialConnector: gatewayConnector;
private sharedAttributeData: Array<AttributeData> = [];
subscriptionOptions: WidgetSubscriptionOptions = {
private initialConnector: GatewayConnector;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated();
@ -138,20 +104,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
};
subscription: IWidgetSubscription;
private 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,
constructor(protected store: Store<AppState>,
private fb: FormBuilder,
private translate: TranslateService,
private attributeService: AttributeService,
private dialogService: DialogService,
private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone,
private utils: UtilsService,
private cd: ChangeDetectorRef,
public dialog: MatDialog) {
private cd: ChangeDetectorRef) {
super(store);
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC};
this.pageLink = new PageLink(1000, 0, null, sortOrder);
@ -186,16 +149,21 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
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 (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.activeConnectors = isString(this.activeConnectors) ? JSON.parse(this.activeConnectors as any) : this.activeConnectors;
this.inactiveConnectors = attributes[1].length ? attributes[1][0].value : [];
this.inactiveConnectors = typeof this.inactiveConnectors === 'string' ? JSON.parse(this.inactiveConnectors) : this.inactiveConnectors;
this.inactiveConnectors = isString(this.inactiveConnectors)
? JSON.parse(this.inactiveConnectors as any)
: this.inactiveConnectors;
this.updateData(true);
} else {
this.activeConnectors = [];
@ -206,7 +174,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
}
uniqNameRequired(): ValidatorFn {
private uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase();
const found = this.dataSource.data.find((connectorAttr) => {
@ -229,7 +197,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
saveConnector(): void {
const value = this.connectorForm.value;
value.configuration = this.camelize(value.name) + '.json';
value.configuration = camelCase(value.name) + '.json';
if (value.type !== 'grpc') {
delete value.key;
}
@ -242,7 +210,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
value
}];
const attributesToDelete = [];
const scope = (this.initialConnector && this.activeConnectors.includes(this.initialConnector.name)) ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
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});
@ -256,19 +226,19 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.inactiveConnectors.splice(inactiveIndex, 1);
}
}
if (!this.activeConnectors.includes(value.name) && scope == AttributeScope.SHARED_SCOPE) {
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) {
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
key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}]));
}
@ -282,7 +252,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
updateData(reload: boolean = false) {
private 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 => {
@ -301,26 +271,30 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
isConnectorSynced(attribute: AttributeData) {
const connectorData = attribute.value;
if (!connectorData.ts) return false;
const connectorData = attribute.value;
if (!connectorData.ts) {
return false;
}
const clientIndex = this.activeData.findIndex(data => {
const sharedData = data.value;
const sharedData = data.value;
return sharedData.name === connectorData.name;
})
if (clientIndex == -1) return false;
});
if (clientIndex === -1) {
return false;
}
const sharedIndex = this.sharedAttributeData.findIndex(data => {
const sharedData = data.value;
const sharedData = data.value;
return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts;
})
});
return sharedIndex !== -1;
}
combineData() {
private combineData() {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key)
).map(attribute=>{
).map(attribute => {
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value;
return attribute
return attribute;
});
}
@ -330,14 +304,13 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
this.nameInput.nativeElement.focus();
this.clearOutConnectorForm();
}
clearOutConnectorForm(): void {
private clearOutConnectorForm(): void {
this.connectorForm.setValue({
name: '',
type: 'mqtt',
logLevel: GatewayLogLevel.info,
logLevel: GatewayLogLevel.INFO,
key: 'auto',
class: '',
configuration: '',
@ -347,7 +320,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine();
}
selectConnector(attribute): void {
selectConnector(attribute: AttributeData): void {
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
@ -363,8 +336,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine();
}
isSameConnector(attribute): boolean {
if (!this.initialConnector) return false;
isSameConnector(attribute: AttributeData): boolean {
if (!this.initialConnector) {
return false;
}
const connector = attribute.value;
return this.initialConnector.name === connector.name;
}
@ -378,12 +353,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
verticalPosition: 'top',
horizontalPosition: 'right',
target: 'dashboardRoot',
// panelClass: this.widgetNamespace,
forceDismiss: true
}));
}
returnType(attribute) {
returnType(attribute: AttributeData): string {
const value = attribute.value;
return this.gatewayConnectorDefaultTypes.get(value.type);
}
@ -396,8 +370,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
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;
const tasks: Array<Observable<any>> = [];
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);
@ -408,10 +384,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
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
key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}]));
forkJoin(tasks).subscribe(_ => {
forkJoin(tasks).subscribe(() => {
if (this.initialConnector ? this.initialConnector.name === attribute.key : true) {
this.clearOutConnectorForm();
this.cd.detectChanges();
@ -423,10 +399,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
camelize(str): string {
return str.toLowerCase().replace(/\s+/g, '_');
}
connectorLogs(attribute: AttributeData, $event: Event): void {
if ($event) {
$event.stopPropagation();
@ -448,7 +420,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
enableConnector(attribute): void {
enableConnector(attribute: AttributeData): 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;
@ -475,7 +447,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
onDataUpdateError(e: any) {
private onDataUpdateError(e: any) {
const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name;
if (exceptionData.message) {
@ -484,11 +456,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
console.error(errorText);
}
onDataUpdated() {
private onDataUpdated() {
this.cd.detectChanges();
}
generateSubscription() {
private generateSubscription() {
if (this.subscription) {
this.subscription.unsubscribe();
}
@ -497,21 +469,23 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: this.device.id,
entityName: "Gateway",
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 => {
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) {
getErrorsCount(attribute: AttributeData): string {
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';
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';
}
}

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

@ -18,43 +18,39 @@
<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>
[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-header-cell *matHeaderCellDef mat-sort-header style="width: 20%">{{ 'widgets.gateway.created-time' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute">
{{ attribute.ts | date:'yyyy-MM-dd HH:mm:ss'}}
{{ 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-header-cell *matHeaderCellDef mat-sort-header style="width: 10%">{{ 'widgets.gateway.status' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute">
<span [ngClass]="statusClass(attribute.status )">{{ attribute.status }}</span>
<span [class]="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 )">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute" [class]="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>
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row class="mat-row-select" *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>
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"
<mat-paginator [length]="dataSource.data.length"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator>

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

@ -14,55 +14,27 @@
/// 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 { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
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 { 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;
}
import { GatewayLogData, GatewayStatus, LogLink } from './gateway-widget.models';
@Component({
selector: 'tb-gateway-logs',
templateUrl: './gateway-logs.component.html',
styleUrls: ['./gateway-logs.component.scss']
})
export class GatewayLogsComponent extends PageComponent implements AfterViewInit {
export class GatewayLogsComponent implements AfterViewInit {
pageLink: PageLink;
attributeDataSource: AttributeDatasource;
dataSource: MatTableDataSource<any>
dataSource: MatTableDataSource<GatewayLogData>;
displayedColumns = ['ts', 'status', 'message'];
@ -76,63 +48,39 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
@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: 'General',
key: 'LOGS'
}, {
name: "Service",
key: "SERVICE_LOGS"
name: 'Service',
key: 'SERVICE_LOGS'
},
{
name: "Connection",
key: "CONNECTION_LOGS"
name: 'Connection',
key: 'CONNECTION_LOGS'
}, {
name: "Storage",
key: "STORAGE_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);
name: 'Extension'
}];
constructor() {
const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC};
this.pageLink = new PageLink(10, 0, null, sortOrder);
this.dataSource = new MatTableDataSource<AttributeData>([]);
this.dataSource = new MatTableDataSource<GatewayLogData>([]);
}
ngAfterViewInit() {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
@ -140,22 +88,18 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
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`)
}
},{
name: 'Connector',
filterFn: (attrData) => !attrData.message.includes(`_converter.py`)
}, {
key: `${connector.key}_LOGS`,
name: "Converter",
filterFn: (attrData)=>{
return attrData.message.includes(`_converter.py`)
}
}]
name: 'Converter',
filterFn: (attrData) => attrData.message.includes(`_converter.py`)
}];
} else {
this.logLinks = this.gatewayLogLinks;
}
@ -164,20 +108,20 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
}
updateData(sort?) {
private updateData() {
if (this.ctx.defaultSubscription.data.length && this.ctx.defaultSubscription.data[0]) {
let attrData = this.ctx.defaultSubscription.data[0].data.map(data => {
let result = {
const result = {
ts: data[0],
key: this.activeLink.key,
message: /\[(.*)/.exec(data[1])[0],
status: 'INVALID LOG FORMAT'
status: 'INVALID LOG FORMAT' as GatewayStatus
};
try {
result.status= data[1].match(/\|(\w+)\|/)[1];
result.status = data[1].match(/\|(\w+)\|/)[1];
} catch (e) {
result.status = 'INVALID LOG FORMAT'
result.status = 'INVALID LOG FORMAT' as GatewayStatus;
}
return result;
@ -186,39 +130,35 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
attrData = attrData.filter(data => this.activeLink.filterFn(data));
}
this.dataSource.data = attrData;
if (sort) {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
}
}
onTabChanged(link) {
onTabChanged(link: LogLink) {
this.activeLink = link;
this.changeSubscription();
}
statusClass(status) {
statusClass(status: GatewayStatus): string {
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:
case GatewayStatus.DEBUG:
return 'status status-debug';
case GatewayStatus.WARNING:
return 'status status-warning';
case GatewayStatus.ERROR:
case GatewayStatus.EXCEPTION:
return 'status status-error';
default:
return "status status-info";
return 'status status-info';
}
}
statusClassMsg(status) {
if (status === "EXCEPTION") {
statusClassMsg(status?: GatewayStatus): string {
if (status === GatewayStatus.EXCEPTION) {
return 'msg-status-exception';
}
}
changeSubscription() {
private 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,
@ -229,8 +169,7 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
this.ctx.defaultSubscription.updateDataSubscriptions();
this.ctx.defaultSubscription.callbacks.onDataUpdated = () => {
this.updateData();
}
};
}
}

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

@ -15,43 +15,38 @@
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>
<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 style="max-width: 600px" 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 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"
[disabled]="gatewayControl.value !== gatewayName"
(click)="turnOff()">
{{ 'gateway.configuration-delete-dialog-confirm' | translate }}
</button>
</div>

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Inject, OnInit } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -31,11 +31,10 @@ export interface GatewayRemoteConfigurationDialogData {
templateUrl: './gateway-remote-configuration-dialog.html'
})
export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<GatewayRemoteConfigurationDialogComponent,
boolean> implements OnInit {
export class GatewayRemoteConfigurationDialogComponent extends
DialogComponent<GatewayRemoteConfigurationDialogComponent, boolean>{
gatewayName: string;
gatewayControl: FormControl;
constructor(protected store: Store<AppState>,
@ -45,10 +44,7 @@ export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<G
private fb: FormBuilder) {
super(store, router, dialogRef);
this.gatewayName = this.data.gatewayName;
this.gatewayControl = this.fb.control(null);
}
ngOnInit(): void {
this.gatewayControl = this.fb.control('');
}
close(): void {

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

@ -16,38 +16,48 @@
-->
<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>
<ng-container *ngIf="!isConnector; else connectorForm">
<mat-form-field>
<mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<mat-select formControlName="command">
<mat-option *ngFor="let command of RPCCommands" [value]="command">
{{ command }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<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>
</ng-container>
<ng-template #connectorForm>
<mat-form-field>
<mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<input matInput formControlName="command" />
</mat-form-field>
<mat-form-field fxFlex>
<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"
matIconSuffix style="cursor:pointer;"
(click)="openEditJSONDialog($event)"
matTooltip="{{ 'gateway.rpc-command-edit-params' | translate }}">edit
</mat-icon>
</mat-form-field>
</ng-template>
<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>
<section class="result-block" [formGroup]="commandForm">
<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>
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
</section>

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

@ -22,24 +22,24 @@
padding: 0;
.command-form {
width: 100%;
flex-wrap: nowrap;
padding: 0 15px 5px;
margin-bottom: 5px;
padding: 0 5px 5px;
& > button {
margin-top: 10px;
}
}
.result-block {
padding: 0 15px;
padding: 0 5px;
display: flex;
flex-direction: column;
flex: 1;
& > span {
font-weight: 600;
}
tb-json-content {
flex: 1;
}
}
}
@ -47,9 +47,5 @@
.tb-json-content {
height: 100%;
}
.mat-mdc-form-field-icon-suffix {
z-index: 100;
}
}

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

@ -14,17 +14,9 @@
/// 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 { AfterViewInit, Component, Input } from '@angular/core';
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 {
@ -33,13 +25,12 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
@Component({
selector: 'tb-gateway-service-rpc',
templateUrl: './gateway-service-rpc.component.html',
styleUrls: ['./gateway-service-rpc.component.scss']
})
export class GatewayServiceRPCComponent extends PageComponent implements AfterViewInit {
export class GatewayServiceRPCComponent implements AfterViewInit {
@Input()
ctx: WidgetContext;
@ -53,39 +44,28 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
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);
'Ping',
'Stats',
'Devices',
'Update',
'Version',
'Restart',
'Reboot'
];
private connectorType: string;
constructor(private fb: FormBuilder,
private dialog: MatDialog) {
this.commandForm = this.fb.group({
command: [null,[Validators.required]],
time: [60, [Validators.required, Validators.min(1)]],
params: [{}, [jsonRequired]],
result: [null]
})
});
}
ngAfterViewInit() {
this.isConnector = this.ctx.settings.isConnector;
if (!this.isConnector) {
@ -95,16 +75,16 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
}
}
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));
})
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe({
next: resp => this.commandForm.get('result').setValue(JSON.stringify(resp)),
error: error => {
console.log(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error));
}
});
}
openEditJSONDialog($event: Event) {
@ -126,5 +106,4 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
}
);
}
}

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

@ -19,13 +19,13 @@
<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-label>{{ 'gateway.statistics.statistic' | translate }}</mat-label>
<mat-select formControlName="statisticKey">
<mat-option *ngFor="let key of statisticsKeys" [value]="key">
{{key}}
{{ key }}
</mat-option>
<mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway">
{{command.attributeOnGateway}}
{{ command.attributeOnGateway }}
</mat-option>
</mat-select>
</mat-form-field>
@ -34,44 +34,42 @@
{{'gateway.statistics.statistic-commands-empty' | translate }}
</mat-error>
<mat-form-field class="mat-block" *ngIf="commandObj">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label>
<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>
<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-header-cell *matHeaderCellDef mat-sort-header>{{ 'widgets.gateway.created-time' | 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-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row[1] }}
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}"
<mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true}"
<mat-row class="mat-row-select"
*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>
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>
<span class="legend-line" [style.background-color]="legendKey.dataKey.color"></span>
<div class="legend-label"
(click)="onLegendKeyHiddenChange(legendKey.dataIndex)"
[ngClass]="{ 'hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden }"
[class]="{ 'hidden-label': legendData.keys[legendKey.dataIndex].dataKey.hidden }"
[innerHTML]="legendKey.dataKey.label">
</div>
</div>

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

@ -14,18 +14,10 @@
/// 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 { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
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';
@ -41,13 +33,12 @@ 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 {
export class GatewayStatisticsComponent implements AfterViewInit {
@ViewChild(MatSort) sort: MatSort;
@ViewChild('statisticChart') statisticChart: ElementRef;
@ -58,10 +49,11 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
@Input()
public general: boolean;
public isNumericData: boolean = false;
public isNumericData = false;
public dataTypeDefined: boolean = false;
public chartInited: boolean;
private flot: TbFlot;
private flotCtx;
private flotCtx: WidgetContext;
public statisticForm: FormGroup;
public statisticsKeys = [];
public commands = [];
@ -89,70 +81,61 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
};
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);
constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private utils: UtilsService) {
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);
})
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.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 (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);
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'))
const connectorsTs = Object.keys(data)
.filter(el => el.includes('ConnectorEventsProduced') || el.includes('ConnectorEventsSent'));
this.createGeneralChartsSubscription(gateway, connectorsTs);
})
});
}
}
}
public sortData () {
public sortData() {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
@ -162,7 +145,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
}
private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) {
let subscriptionInfo = [{
const subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: gateway.id.id,
@ -173,10 +156,11 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
subscriptionInfo[0].timeseries = [{name: attr, label: attr}];
this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo);
this.ctx.defaultSubscription.unsubscribe();
}
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: [string]) {
let subscriptionInfo = [{
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: string[]) {
const subscriptionInfo = [{
type: DatasourceType.entity,
entityType: EntityType.DEVICE,
entityId: gateway.id.id,
@ -184,20 +168,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
timeseries: []
}];
subscriptionInfo[0].timeseries = [];
if (attrData && attrData.length) {
if (attrData?.length) {
attrData.forEach(attr => {
subscriptionInfo[0].timeseries.push({name: attr, label: 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})
})
subscriptionInfo[0].timeseries.push({name: dataKey.name, label: dataKey.label});
});
this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo);
this.ctx.defaultSubscription.unsubscribe();
}
init = () => {
private init = () => {
this.flotCtx = {
$scope: this.ctx.$scope,
$injector: this.ctx.$injector,
@ -207,20 +191,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
subscriptionApi: this.ctx.subscriptionApi,
detectChanges: this.ctx.detectChanges,
settings: this.ctx.settings
};
}
} as WidgetContext;
};
updateChart = () => {
private updateChart = () => {
if (this.flot && this.ctx.defaultSubscription.data.length) {
this.flot.update();
}
}
};
resize = () => {
private resize = () => {
if (this.flot) {
this.flot.resize();
}
}
};
private reset() {
if (this.resize$) {
@ -260,7 +244,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
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 = new TbFlot(this.flotCtx as WidgetContext, 'line');
this.flot.update();
}
@ -270,16 +254,21 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
return;
}
this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : [];
this.isNumericData = this.dataSource.data.every(data => !isNaN(+data[1]) );
if (this.dataSource.data.length && !this.dataTypeDefined) {
this.dataTypeDefined = true;
this.isNumericData = this.dataSource.data.every(data => !isNaN(+data[1]));
}
}
changeSubscription(subscriptionInfo: SubscriptionInfo[]) {
private 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.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.timeseries, subscriptionInfo, this.subscriptionOptions,
false, true).subscribe(subscription => {
this.dataTypeDefined = false;
this.subscription = subscription;
this.isDataOnlyNumbers();
this.legendData = this.subscription.legendData;
@ -291,9 +280,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
if (this.isNumericData) {
this.initChart();
}
})
});
}
}
}

142
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -0,0 +1,142 @@
///
/// 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.
///
export enum StorageTypes {
MEMORY = 'memory',
FILE = 'file',
SQLITE = 'sqlite'
}
export enum DeviceGatewayStatus {
EXCEPTION = 'EXCEPTION'
}
export enum GatewayLogLevel {
NONE = 'NONE',
CRITICAL = 'CRITICAL',
ERROR = 'ERROR',
WARNING = 'WARNING',
INFO = 'INFO',
DEBUG = 'DEBUG'
}
export const GatewayStatus = {
...GatewayLogLevel,
...DeviceGatewayStatus
};
export type GatewayStatus = DeviceGatewayStatus | GatewayLogLevel;
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 LocalLogsConfigTranslateMap = 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 StorageTypesTranslationMap = 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 GecurityTypesTranslationsMap = 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'],
]
);
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']
]);
export interface LogLink {
name: string;
key: string;
filterFn?: (arg: any) => boolean;
}
export interface GatewayLogData {
ts: number;
key: string;
message: string;
status: GatewayStatus;
}

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

@ -15,12 +15,13 @@
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>
<div class="tb-form-panel" [formGroup]="gatewayLogSettingForm">
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="isConnectorLog">
{{ "widgets.gateway.show-connector" | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="connectorLogState" placeholder="{{ 'widgets.gateway.connector-state-param-key' | translate }}">
</mat-form-field>
</div>
</div>

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

@ -16,7 +16,7 @@
import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -27,28 +27,41 @@ import { AppState } from '@core/core.state';
})
export class GatewayLogsSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup;
gatewayLogSettingForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.GatewayLogSettingForm;
protected settingsForm(): FormGroup {
return this.gatewayLogSettingForm;
}
protected defaultSettings(): WidgetSettings {
return {
isConnectorLog: false,
connectorLogState: 'default'
connectorLogState: ''
};
}
protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({
this.gatewayLogSettingForm = this.fb.group({
isConnectorLog: [false, []],
connectorLogState: ['default', []]
connectorLogState: ['', Validators.required]
});
}
protected validatorTriggers(): string[] {
return ['isConnectorLog'];
}
protected updateValidators(emitEvent: boolean) {
const isConnectorLog: boolean = this.gatewayLogSettingForm.get('isConnectorLog').value;
if (isConnectorLog) {
this.gatewayLogSettingForm.get('connectorLogState').enable({emitEvent});
} else {
this.gatewayLogSettingForm.get('connectorLogState').disable({emitEvent});
}
}
}

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

@ -15,8 +15,10 @@
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>
<div class="tb-form-panel" [formGroup]="gatewayServiceRPCSettingForm">
<div class="tb-form-row column-xs">
<mat-slide-toggle class="mat-slide margin" formControlName="isConnector">
{{ "widgets.gateway.show-connector" | translate }}
</mat-slide-toggle>
</div>
</div>

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

@ -16,7 +16,7 @@
import { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -27,15 +27,15 @@ import { AppState } from '@core/core.state';
})
export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup;
gatewayServiceRPCSettingForm: FormGroup;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) {
private fb: FormBuilder) {
super(store);
}
protected settingsForm(): UntypedFormGroup {
return this.GatewayLogSettingForm;
protected settingsForm(): FormGroup {
return this.gatewayServiceRPCSettingForm;
}
protected defaultSettings(): WidgetSettings {
@ -45,7 +45,7 @@ export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent
}
protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({
this.gatewayServiceRPCSettingForm = this.fb.group({
isConnector: [false, []]
});
}

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

@ -16,11 +16,10 @@
-->
<div class="tb-container">
<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 class="tb-title"
[class.tb-required]="!disabled && required"
[class.pointer-event]="hint"
tb-hint-tooltip-icon="{{ hint }}">{{ label }}
</label>
<ng-container #flow="flow"
[flowConfig]="{allowDuplicateUploads: true}">

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

@ -22,7 +22,7 @@ $previewSize: 100px !default;
.tb-container {
margin-top: 0;
label.tb-title {
display: block;
display: flex;
padding-bottom: 8px;
}
}

5
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.html

@ -17,5 +17,6 @@
-->
<ng-content></ng-content>
<tb-icon class="tb-hint-tooltip-icon tb-mat-18"
matTooltip="{{ tooltipText }}"
[matTooltipPosition]="tooltipPosition">{{ hintIcon }}</tb-icon>
*ngIf="tooltipText"
matTooltip="{{ tooltipText }}"
[matTooltipPosition]="tooltipPosition">{{ hintIcon }}</tb-icon>

4
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.scss

@ -13,12 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
:host(.tb-hint-tooltip) {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
:host {
.tb-hint-tooltip-icon {
color: #E0E0E0;
overflow: visible;

3
ui-ngx/src/app/shared/components/hint-tooltip-icon.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Input } from '@angular/core';
import { Component, HostBinding, Input } from '@angular/core';
import { TooltipPosition } from '@angular/material/tooltip';
@Component({
@ -24,6 +24,7 @@ import { TooltipPosition } from '@angular/material/tooltip';
})
export class HintTooltipIconComponent {
@HostBinding('class.tb-hint-tooltip')
@Input('tb-hint-tooltip-icon') tooltipText: string;
@Input()

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

@ -1293,7 +1293,6 @@
"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",
@ -1395,11 +1394,6 @@
"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",
@ -2643,43 +2637,18 @@
"gateway": {
"add-entry": "Add configuration",
"advanced": "Advanced",
"checking-device-activity": "Checking Device Activity:",
"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.",
"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",
@ -2690,10 +2659,8 @@
"rpc-command-send": "Send",
"rpc-command-result": "Result",
"rpc-command-edit-params": "Edit parameters",
"select-connector": "Select connector",
"gateway-configuration": "Gateway Configuration",
"gateway-configuration": "General 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",
@ -2710,7 +2677,6 @@
"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",
@ -2732,7 +2698,6 @@
"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",
@ -2741,7 +2706,6 @@
"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",
@ -2792,7 +2756,6 @@
"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",
@ -2851,7 +2814,7 @@
"sqlite": "SQLITE"
},
"thingsboard": "ThingsBoard",
"thingsboard-general": "General",
"general": "General",
"thingsboard-host": "ThingsBoard host",
"thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port",
@ -2879,7 +2842,6 @@
"toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "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",
@ -2890,17 +2852,11 @@
"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)",
"storage": "Provides configuration for saving incoming data before it is sent to the platform",
"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",
@ -3505,14 +3461,7 @@
"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.",
@ -5343,8 +5292,11 @@
"events-title": "Gateway events form title",
"events-filter": "Events filter",
"event-key-contains": "Event key contains...",
"is-connector": "Is Connector",
"state-param-name": "State parameter connector name"
"show-connector": "Show for the connector",
"connector-state-param-key": "Connector state parameter key",
"status": "Status",
"message": "Message",
"created-time": "Created time"
},
"gauge": {
"default-color": "Default color",
@ -6402,8 +6354,7 @@
"node-selected": "On node selected",
"element-click": "On HTML element click",
"pie-slice-click": "On slice click",
"row-double-click": "On row double click",
"action-button-click": "Action button click"
"row-double-click": "On row double click"
}
},
"paginator" : {

5
ui-ngx/src/form.scss

@ -65,6 +65,9 @@
box-shadow: none;
border-radius: 0;
}
&.no-gap {
gap: 0;
}
&.tb-slide-toggle {
padding: 0;
gap: 0;
@ -218,7 +221,7 @@
}
}
.tb-form-row .mat-mdc-form-field, .mat-mdc-form-field.tb-inline-field {
.tb-form-row:not(.tb-standard-fields) .mat-mdc-form-field:not(.tb-not-inline-field), .mat-mdc-form-field.tb-inline-field {
&.mat-form-field-appearance-fill {
.mdc-text-field--filled:not(.mdc-text-field--disabled) {
&:before {

Loading…
Cancel
Save