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": [ "widgetTypeFqns": [
"gateway_widgets.gateway_configuration", "gateway_widgets.gateway_configuration",
"gateway_widgets.attributes_card", "gateway_widgets.attributes_card",
"gateway_widgets.gateway_configuration2", "gateway_widgets.gateway_general_configuration",
"gateway_widgets.config_form_latest", "gateway_widgets.config_form_latest",
"gateway_widgets.gateway_events", "gateway_widgets.gateway_connectors",
"gateway_widgets.gateway_connector",
"gateway_widgets.gateway_logs", "gateway_widgets.gateway_logs",
"gateway_widgets.gateway_statistics", "gateway_widgets.gateway_custom_statistics",
"gateway_widgets.gateway_general_statistics", "gateway_widgets.gateway_general_chart_statistics",
"gateway_widgets.service_rpc" "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, DeviceCredentials,
DeviceInfo, DeviceInfo,
DeviceInfoQuery, DeviceInfoQuery,
DeviceSearchQuery, PublishLaunchCommand, DeviceSearchQuery,
PublishLaunchCommand,
PublishTelemetryCommand PublishTelemetryCommand
} from '@app/shared/models/device.models'; } from '@app/shared/models/device.models';
import { EntitySubtype } from '@app/shared/models/entity-type.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; 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 { Inject, Injectable, Type } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; 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 { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { mergeMap, tap } from 'rxjs/operators'; import { mergeMap, tap } from 'rxjs/operators';
@ -28,7 +26,11 @@ import {
CustomDialogContainerData CustomDialogContainerData
} from '@home/components/widget/dialog/custom-dialog-container.component'; } from '@home/components/widget/dialog/custom-dialog-container.component';
import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; 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() @Injectable()
export class CustomDialogService { export class CustomDialogService {
@ -36,12 +38,11 @@ export class CustomDialogService {
private customModules: Array<Type<any>>; private customModules: Array<Type<any>>;
constructor( constructor(
private translate: TranslateService,
private authService: AuthService,
private dynamicComponentFactoryService: DynamicComponentFactoryService, private dynamicComponentFactoryService: DynamicComponentFactoryService,
@Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>, @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type<any>,
@Inject(SHARED_HOME_COMPONENTS_MODULE_TOKEN) private sharedHomeComponentsModule: Type<any>, @Inject(SHARED_HOME_COMPONENTS_MODULE_TOKEN) private sharedHomeComponentsModule: Type<any>,
@Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type<any>, @Inject(HOME_COMPONENTS_MODULE_TOKEN) private homeComponentsModule: Type<any>,
@Inject(WIDGET_COMPONENTS_MODULE_TOKEN) private widgetComponentsModule: Type<any>,
public dialog: MatDialog public dialog: MatDialog
) { ) {
} }
@ -52,7 +53,8 @@ export class CustomDialogService {
customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any, customDialog(template: string, controller: (instance: CustomDialogComponent) => void, data?: any,
config?: MatDialogConfig): Observable<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)) { if (Array.isArray(this.customModules)) {
modules.push(...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. 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"> <div fxLayout="row" fxLayoutAlign="space-between center">
<span class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</span> <span class="tb-no-data-text">{{ 'gateway.docker-label' | translate }}</span>
<div fxFlexAlign="end" class="tb-help" [tb-help]="helpLink"></div> <div fxFlexAlign="end" class="tb-help" [tb-help]="helpLink"></div>
@ -29,14 +29,9 @@
Windows Windows
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body"> <ng-container
<tb-markdown usePlainMarkdown containerClass="start-code" data=" *ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.windows }">
```bash </ng-container>
{{windowsCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
@ -45,14 +40,9 @@
Linux Linux
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body"> <ng-container
<tb-markdown usePlainMarkdown containerClass="start-code" data=" *ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
```bash </ng-container>
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
@ -61,15 +51,23 @@
MacOS MacOS
</ng-template> </ng-template>
<ng-template matTabContent> <ng-template matTabContent>
<div class="tb-form-panel no-padding no-border tb-tab-body"> <ng-container
<tb-markdown usePlainMarkdown containerClass="start-code" data=" *ngTemplateOutlet="commandsExample; context: {command: commands?.mqtt?.linux }">
```bash </ng-container>
{{linuxCode}}
{:copy-code}
```
"></tb-markdown>
</div>
</ng-template> </ng-template>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </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 { :host ::ng-deep {
.tb-markdown-view { .tb-markdown-view {
.start-code { .start-code {
code[class*="language-"], pre[class*="language-"] { code[class*="language-"] {
overflow: hidden;
white-space: break-spaces; white-space: break-spaces;
word-break: break-all; word-break: break-all;
} }
pre[class*="language-"] {
overflow: hidden;
background: #F3F6FA;
border-color: #305680;
}
.code-wrapper { .code-wrapper {
padding: 0; 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 { 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 { DeviceService } from '@core/http/device.service';
import { helpBaseUrl } from '@shared/models/constants'; import { helpBaseUrl } from '@shared/models/constants';
import { getOS } from '@core/utils'; import { getOS } from '@core/utils';
import { PublishLaunchCommand } from '@shared/models/device.models';
@Component({ @Component({
selector: 'tb-gateway-command', selector: 'tb-gateway-command',
@ -39,17 +34,13 @@ export class DeviceGatewayCommandComponent implements OnInit {
@Input() @Input()
deviceId: string; deviceId: string;
linuxCode: string; commands: PublishLaunchCommand;
windowsCode: string;
helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/'; helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/';
tabIndex = 0; tabIndex = 0;
constructor(protected router: Router, constructor(private cd: ChangeDetectorRef,
protected store: Store<AppState>,
private translate: TranslateService,
private cd: ChangeDetectorRef,
private deviceService: DeviceService) { private deviceService: DeviceService) {
} }
@ -57,7 +48,7 @@ export class DeviceGatewayCommandComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
if (this.deviceId) { if (this.deviceId) {
this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => { this.deviceService.getDevicePublishLaunchCommands(this.deviceId).subscribe(commands => {
this.createRunCode(commands.mqtt); this.commands = commands;
this.cd.detectChanges(); this.cd.detectChanges();
}); });
} }
@ -78,21 +69,4 @@ export class DeviceGatewayCommandComponent implements OnInit {
this.tabIndex = 1; 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 { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: block; display: grid;
grid-template-rows: min-content minmax(auto, 1fr) min-content;
.mat-icon { .configuration-block {
color: rgba(0, 0, 0, .12); display: flex;
} flex-direction: column;
gap: 16px;
.tb-form-panel {
margin-bottom: 20px;
} }
.mat-toolbar { .mat-toolbar {
grid-row: 1;
background: transparent; background: transparent;
color: rgba(0, 0, 0, .87) !important; color: rgba(0, 0, 0, .87) !important;
} }
.mat-content { .tab-group-block {
.expansion-panel-header { min-width: 0;
font-weight: 600; height: 100%;
color: rgba(0, 0, 0, .87) !important; min-height: 0;
} grid-row: 2;
mat-slide-toggle {
margin-bottom: 16px;
}
mat-form-field {
margin-right: 15px;
}
.slider-icon {
position: absolute;
transform: translateY(-3px);
}
.block-title {
font-size: 20px;
font-weight: 400;
padding-top: 16px;
}
.hover-cursor {
cursor: pointer;
}
} }
.security-toggle-group { .toggle-group {
margin-bottom: 15px;
margin-right: auto; margin-right: auto;
} }
.logs-label {
font-weight: 500;
margin-bottom: 10px;
}
.statistics-block {
margin-bottom: 15px;
padding-left: 15px;
padding-top: 15px;
}
.first-capital { .first-capital {
text-transform: capitalize; text-transform: capitalize;
} }
mat-panel-title { textarea {
display: block; resize: none;
padding-top: 20px;
} }
mat-panel-title span { .saving-period {
display: block; flex: 1;
padding-left: 0;
padding-top: 5px;
} }
.tb-hint { .statistics-container {
font-size: 13px; width: 50%;
color: rgba(0, 0, 0, .54);
width: fit-content;
cursor: pointer;
text-transform: none !important;
}
.line-break { .command-container {
width: 100%; width: 100%;
}
} }
textarea { .actions {
resize: none; grid-row: 3;
padding: 8px;
display: flex;
gap: 8px;
justify-content: flex-end;
flex: 1;
} }
} }
:host ::ng-deep { :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-event {
pointer-events: all; pointer-events: all;
} }
.mat-mdc-form-field-icon-suffix { .toggle-group span {
z-index: 100; padding: 0 25px;
} }
.security-toggle-group span { .mat-mdc-form-field-icon-suffix {
padding: 0 25px; 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 { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { import {
FormArray, FormArray,
FormBuilder, FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
UntypedFormGroup,
ValidationErrors, ValidationErrors,
ValidatorFn, ValidatorFn,
Validators Validators
@ -41,79 +37,18 @@ import { Observable, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators'; import { mergeMap } from 'rxjs/operators';
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import {
export enum StorageTypes { GatewayLogLevel,
MEMORY = 'memory', GecurityTypesTranslationsMap,
FILE = 'file', LocalLogsConfigTranslateMap,
SQLITE = 'sqlite' LocalLogsConfigs,
} LogSavingPeriod,
LogSavingPeriodTranslations,
export enum GatewayLogLevel { SecurityTypes,
none = 'NONE', StorageTypes,
critical = 'CRITICAL', StorageTypesTranslationMap
error = 'ERROR', } from './gateway-widget.models';
warning = 'WARNING', import { deepTrim } from '@core/utils';
info = 'INFO',
debug = 'DEBUG'
}
export enum LogSavingPeriod {
days = 'D',
hours = 'H',
minutes = 'M',
seconds = 'S'
}
export enum LocalLogsConfigs {
service = 'service',
connector = 'connector',
converter = 'converter',
tb_connection = 'tb_connection',
storage = 'storage',
extension = 'extension'
}
export const localLogsConfigLabels = new Map<LocalLogsConfigs, string>([
[LocalLogsConfigs.service, 'Service'],
[LocalLogsConfigs.connector, 'Connector'],
[LocalLogsConfigs.converter, 'Converter'],
[LocalLogsConfigs.tb_connection, 'TB Connection'],
[LocalLogsConfigs.storage, 'Storage'],
[LocalLogsConfigs.extension, 'Extension']
]);
export const logSavingPeriodTranslations = new Map<LogSavingPeriod, string>(
[
[LogSavingPeriod.days, 'gateway.logs.days'],
[LogSavingPeriod.hours, 'gateway.logs.hours'],
[LogSavingPeriod.minutes, 'gateway.logs.minutes'],
[LogSavingPeriod.seconds, 'gateway.logs.seconds']
]
);
export const storageTypesTranslations = new Map<StorageTypes, string>(
[
[StorageTypes.MEMORY, 'gateway.storage-types.memory-storage'],
[StorageTypes.FILE, 'gateway.storage-types.file-storage'],
[StorageTypes.SQLITE, 'gateway.storage-types.sqlite']
]
);
export enum SecurityTypes {
ACCESS_TOKEN = 'accessToken',
USERNAME_PASSWORD = 'usernamePassword',
TLS_ACCESS_TOKEN = 'tlsAccessToken',
TLS_PRIVATE_KEY = 'tlsPrivateKey'
}
export const securityTypesTranslationsMap = new Map<SecurityTypes, string>(
[
[SecurityTypes.ACCESS_TOKEN, 'gateway.security-types.access-token'],
[SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'],
[SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'],
// [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'],
]
);
@Component({ @Component({
selector: 'tb-gateway-configuration', selector: 'tb-gateway-configuration',
@ -124,13 +59,16 @@ export class GatewayConfigurationComponent implements OnInit {
gatewayConfigGroup: FormGroup; 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); gatewayLogLevel = Object.values(GatewayLogLevel);
@ -142,24 +80,19 @@ export class GatewayConfigurationComponent implements OnInit {
logSelector: FormControl; logSelector: FormControl;
securityType: SecurityTypes; private initialCredentials: DeviceCredentials;
initialCredentials: DeviceCredentials; constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private deviceService: DeviceService,
constructor(protected router: Router,
protected store: Store<AppState>,
protected fb: FormBuilder,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
public dialog: MatDialog) { private dialog: MatDialog) {
} }
ngOnInit() { ngOnInit() {
this.gatewayConfigGroup = this.fb.group({ this.gatewayConfigGroup = this.fb.group({
thingsboard: 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]+$/)]], port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false, []], remoteShell: [false, []],
remoteConfiguration: [true, []], remoteConfiguration: [true, []],
@ -175,30 +108,30 @@ export class GatewayConfigurationComponent implements OnInit {
handleDeviceRenaming: [true, []], handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({ checkingDeviceActivity: this.fb.group({
checkDeviceInactivity: [false, []], checkDeviceInactivity: [false, []],
inactivityTimeoutSeconds: [200, [Validators.min(1)]], inactivityTimeoutSeconds: [200, [Validators.min(1), Validators.pattern(/^[^.\s]+$/)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1)]] inactivityCheckPeriodSeconds: [500, [Validators.min(1), Validators.pattern(/^[^.\s]+$/)]]
}), }),
security: this.fb.group({ security: this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]], type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required]], accessToken: [null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
clientId: [null, []], clientId: [null, [Validators.pattern(/^[^.\s]+$/)]],
username: [null, []], username: [null, [Validators.pattern(/^[^.\s]+$/)]],
password: [null, []], password: [null, [Validators.pattern(/^[^.\s]+$/)]],
caCert: [null, []], caCert: [null, []],
cert: [null, []], cert: [null, []],
privateKey: [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({ storage: this.fb.group({
type: [StorageTypes.MEMORY, [Validators.required]], type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]], 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]], max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]],
data_folder_path: ['./data/', []], data_folder_path: ['./data/', [Validators.pattern(/^[^\s]+$/)]],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_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]+$/)]], 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_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [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([]), connectors: this.fb.array([]),
logs: this.fb.group({ 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', 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]], type: ['remote', [Validators.required]],
remote: this.fb.group({ remote: this.fb.group({
enabled: [false], enabled: [false],
logLevel: [GatewayLogLevel.info, [Validators.required]], logLevel: [GatewayLogLevel.INFO, [Validators.required]],
}), }),
local: this.fb.group({}) local: this.fb.group({})
}) })
@ -231,7 +164,7 @@ export class GatewayConfigurationComponent implements OnInit {
if (password && password !== '') { if (password && password !== '') {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]); this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([Validators.required]);
} else { } else {
this.gatewayConfigGroup.get('thingsboard.security.username').setValidators([]); this.gatewayConfigGroup.get('thingsboard.security.username').clearValidators();
} }
this.gatewayConfigGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false}); 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]); checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]);
} else { } else {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators(); checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setErrors(null);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators(); 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 => { this.gatewayConfigGroup.get('grpc.enabled').valueChanges.subscribe(value => {
@ -272,7 +205,7 @@ export class GatewayConfigurationComponent implements OnInit {
securityGroup.get('type').valueChanges.subscribe(type => { securityGroup.get('type').valueChanges.subscribe(type => {
this.removeAllSecurityValidators(); this.removeAllSecurityValidators();
if (type === SecurityTypes.ACCESS_TOKEN) { if (type === SecurityTypes.ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required]); securityGroup.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
securityGroup.get('accessToken').updateValueAndValidity(); securityGroup.get('accessToken').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_PRIVATE_KEY) { } else if (type === SecurityTypes.TLS_PRIVATE_KEY) {
securityGroup.get('caCert').addValidators([Validators.required]); securityGroup.get('caCert').addValidators([Validators.required]);
@ -282,21 +215,19 @@ export class GatewayConfigurationComponent implements OnInit {
securityGroup.get('cert').addValidators([Validators.required]); securityGroup.get('cert').addValidators([Validators.required]);
securityGroup.get('cert').updateValueAndValidity(); securityGroup.get('cert').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_ACCESS_TOKEN) { } 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('accessToken').updateValueAndValidity();
securityGroup.get('caCert').addValidators([Validators.required]); securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity(); securityGroup.get('caCert').updateValueAndValidity();
} else if (type === SecurityTypes.USERNAME_PASSWORD) { } else if (type === SecurityTypes.USERNAME_PASSWORD) {
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]) securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
// securityGroup.get('password').addValidators([Validators.required]);
// securityGroup.get('password').updateValueAndValidity();
} }
securityGroup.updateValueAndValidity(); securityGroup.updateValueAndValidity();
}); });
securityGroup.get('caCert').valueChanges.subscribe(_ => this.cd.detectChanges()); securityGroup.get('caCert').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('privateKey').valueChanges.subscribe(_ => this.cd.detectChanges()); securityGroup.get('privateKey').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('cert').valueChanges.subscribe(_ => this.cd.detectChanges()); securityGroup.get('cert').valueChanges.subscribe(() => this.cd.detectChanges());
const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.subscribe(type => { storageGroup.get('type').valueChanges.subscribe(type => {
@ -306,28 +237,37 @@ export class GatewayConfigurationComponent implements OnInit {
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]); [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
storageGroup.get('max_records_count').addValidators( storageGroup.get('max_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [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) { } 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( storageGroup.get('max_file_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_read_records_count').addValidators( storageGroup.get('max_read_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_records_per_file').addValidators( storageGroup.get('max_records_per_file').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [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) { } 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( storageGroup.get('messages_ttl_check_in_hours').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('messages_ttl_in_days').addValidators( storageGroup.get('messages_ttl_in_days').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); [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); this.fetchConfigAttribute(this.device);
} }
atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) { private atLeastOneRequired(validator: ValidatorFn, controls: string[] = null) {
return (group: UntypedFormGroup): ValidationErrors | null => { return (group: FormGroup): ValidationErrors | null => {
if (!controls) { if (!controls) {
controls = Object.keys(group.controls); controls = Object.keys(group.controls);
} }
@ -337,22 +277,10 @@ export class GatewayConfigurationComponent implements OnInit {
}; };
} }
updateSecurityValidators(value: SecurityTypes) { private fetchConfigAttribute(entityId: EntityId) {
this.gatewayConfigGroup.get('thingsboard.security.type').setValue(value, {emitEvent: true}); if (entityId.id === NULL_UUID) {
this.gatewayConfigGroup.get('thingsboard.security.type').markAsDirty(); return;
} }
updateLogType(value: LocalLogsConfigs) {
this.logSelector.setValue(value);
}
updateStorageType(value: StorageTypes) {
this.gatewayConfigGroup.get('storage.type').setValue(value, {emitEvent: true});
this.gatewayConfigGroup.get('storage.type').markAsDirty();
}
fetchConfigAttribute(entityId: EntityId) {
if (entityId.id === NULL_UUID) return;
this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE, this.attributeService.getEntityAttributes(entityId, AttributeScope.CLIENT_SCOPE,
['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe( ['general_configuration', 'grpc_configuration', 'logs_configuration', 'storage_configuration', 'RemoteLoggingLevel']).pipe(
mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes( mergeMap(attributes => attributes.length ? of(attributes) : this.attributeService.getEntityAttributes(
@ -397,24 +325,26 @@ export class GatewayConfigurationComponent implements OnInit {
if (remoteLoggingLevel) { if (remoteLoggingLevel) {
const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote');
remoteLogsFormGroup.patchValue({ remoteLogsFormGroup.patchValue({
enabled: remoteLoggingLevel !== GatewayLogLevel.none, enabled: remoteLoggingLevel !== GatewayLogLevel.NONE,
logLevel: remoteLoggingLevel logLevel: remoteLoggingLevel
}, {emitEvent: false}); }, {emitEvent: false});
remoteLogsFormGroup.markAsPristine(); remoteLogsFormGroup.markAsPristine();
} }
this.cd.detectChanges(); this.cd.detectChanges();
} else { } else {
this.checkAndFetchCredentials({}); this.checkAndFetchCredentials();
} }
}); });
} }
checkAndFetchCredentials(security): void { private checkAndFetchCredentials(security: any = {}): void {
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) { if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) {
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => { this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
this.initialCredentials = credentials; this.initialCredentials = credentials;
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) { 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); this.gatewayConfigGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId);
if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) { if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.gatewayConfigGroup.get('thingsboard.security.caCert').setValue(security.caCert); 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 = { const logsObject = {
local: {} local: {}
}; };
@ -453,43 +383,42 @@ export class GatewayConfigurationComponent implements OnInit {
return {local: logsObject, logFormat, dateFormat}; return {local: logsObject, logFormat, dateFormat};
} }
toggleRpcFields(enable: boolean) { private toggleRpcFields(enable: boolean) {
const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup; const grpcGroup = this.gatewayConfigGroup.get('grpc') as FormGroup;
if (enable) { if (enable) {
grpcGroup.get('serverPort').enable(); grpcGroup.get('serverPort').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').enable(); grpcGroup.get('keepAliveTimeMs').enable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').enable(); grpcGroup.get('keepAliveTimeoutMs').enable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').enable(); grpcGroup.get('keepalivePermitWithoutCalls').enable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').enable(); grpcGroup.get('maxPingsWithoutData').enable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').enable(); grpcGroup.get('minTimeBetweenPingsMs').enable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').enable(); grpcGroup.get('minPingIntervalWithoutDataMs').enable({emitEvent: false});
} else { } else {
grpcGroup.get('serverPort').disable(); grpcGroup.get('serverPort').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeMs').disable(); grpcGroup.get('keepAliveTimeMs').disable({emitEvent: false});
grpcGroup.get('keepAliveTimeoutMs').disable(); grpcGroup.get('keepAliveTimeoutMs').disable({emitEvent: false});
grpcGroup.get('keepalivePermitWithoutCalls').disable(); grpcGroup.get('keepalivePermitWithoutCalls').disable({emitEvent: false});
grpcGroup.get('maxPingsWithoutData').disable(); grpcGroup.get('maxPingsWithoutData').disable({emitEvent: false});
grpcGroup.get('minTimeBetweenPingsMs').disable(); grpcGroup.get('minTimeBetweenPingsMs').disable({emitEvent: false});
grpcGroup.get('minPingIntervalWithoutDataMs').disable(); grpcGroup.get('minPingIntervalWithoutDataMs').disable({emitEvent: false});
} }
} }
addCommand(command?): void { addCommand(command: any = {}): void {
const data = command || {};
const commandsFormArray = this.commandFormArray(); const commandsFormArray = this.commandFormArray();
const commandFormGroup = this.fb.group({ const commandFormGroup = this.fb.group({
attributeOnGateway: [data.attributeOnGateway || null, [Validators.required]], attributeOnGateway: [command.attributeOnGateway || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
command: [data.command || null, [Validators.required]], command: [command.command || null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
timeout: [data.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], timeout: [command.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.pattern(/^[^.\s]+$/)]],
}); });
commandsFormArray.push(commandFormGroup); commandsFormArray.push(commandFormGroup);
} }
addLocalLogConfig(name, config): void { private addLocalLogConfig(name: string, config: any): void {
const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup; const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup;
const configGroup = this.fb.group({ 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]], filePath: [config.filePath || './logs', [Validators.required]],
backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]], backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]],
savingTime: [config.savingTime || 3, [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 { removeCommandControl(index: number, event: any): void {
if (event.pointerType === '') return; if (event.pointerType === '') {
return;
}
this.commandFormArray().removeAt(index); this.commandFormArray().removeAt(index);
this.gatewayConfigGroup.markAsDirty(); this.gatewayConfigGroup.markAsDirty();
} }
removeAllSecurityValidators(): void { private removeAllSecurityValidators(): void {
const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup;
securityGroup.clearValidators(); securityGroup.clearValidators();
for (const controlsKey in securityGroup.controls) { 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; const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup;
for (const storageKey in storageGroup.controls) { for (const storageKey in storageGroup.controls) {
if (storageKey !== 'type') { if (storageKey !== 'type') {
@ -535,7 +466,7 @@ export class GatewayConfigurationComponent implements OnInit {
} }
} }
removeEmpty(obj) { private removeEmpty(obj: any) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(obj) Object.entries(obj)
.filter(([_, v]) => v != null) .filter(([_, v]) => v != null)
@ -543,7 +474,7 @@ export class GatewayConfigurationComponent implements OnInit {
); );
} }
generateLogsFile(logsObj) { private generateLogsFile(logsObj: any) {
const logAttrObj = { const logAttrObj = {
version: 1, version: 1,
disable_existing_loggers: false, disable_existing_loggers: false,
@ -591,7 +522,7 @@ export class GatewayConfigurationComponent implements OnInit {
return logAttrObj; return logAttrObj;
} }
createHandlerObj(logObj, key) { private createHandlerObj(logObj: any, key: string) {
return { return {
class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler', class: 'thingsboard_gateway.tb_utility.tb_handler.TimedRotatingFileHandler',
formatter: 'LogFormatter', formatter: 'LogFormatter',
@ -603,7 +534,7 @@ export class GatewayConfigurationComponent implements OnInit {
}; };
} }
createLoggerObj(logObj, key) { private createLoggerObj(logObj: any, key: string) {
return { return {
handlers: [`${key}Handler`, 'consoleHandler'], handlers: [`${key}Handler`, 'consoleHandler'],
level: logObj.logLevel, level: logObj.logLevel,
@ -612,12 +543,12 @@ export class GatewayConfigurationComponent implements OnInit {
} }
saveConfig(): void { 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); value.thingsboard.statistics.commands = Object.values(value.thingsboard.statistics.commands);
const attributes = []; const attributes = [];
attributes.push({ attributes.push({
key: 'RemoteLoggingLevel', 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; delete value.connectors;
attributes.push({ attributes.push({
@ -642,18 +573,18 @@ export class GatewayConfigurationComponent implements OnInit {
this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { 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) { if (this.dialogRef) {
this.dialogRef.close(); this.dialogRef.close();
} else { } else {
this.gatewayConfigGroup.markAsPristine(); this.gatewayConfigGroup.markAsPristine();
this.cd.detectChanges(); this.cd.detectChanges();
} }
}) });
}); });
} }
updateCredentials(securityConfig): Observable<any> { private updateCredentials(securityConfig: any): Observable<any> {
let updateCredentials = false; let updateCredentials = false;
let newCredentials = {}; let newCredentials = {};
if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) { if (securityConfig.type === SecurityTypes.USERNAME_PASSWORD) {
@ -661,17 +592,26 @@ export class GatewayConfigurationComponent implements OnInit {
updateCredentials = true; updateCredentials = true;
} else { } else {
const parsedCredentials = JSON.parse(this.initialCredentials.credentialsValue); 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) { if (updateCredentials) {
let credentialsValue: { clientId?, userName?, password? } = {}; const credentialsValue: { clientId?: string; userName?: string; password?: string } = {};
const credentialsType = DeviceCredentialsType.MQTT_BASIC; const credentialsType = DeviceCredentialsType.MQTT_BASIC;
if (securityConfig.clientId) credentialsValue.clientId = securityConfig.clientId; if (securityConfig.clientId) {
if (securityConfig.username) credentialsValue.userName = securityConfig.username; credentialsValue.clientId = securityConfig.clientId;
if (securityConfig.password) credentialsValue.password = securityConfig.password; }
if (securityConfig.username) {
credentialsValue.userName = securityConfig.username;
}
if (securityConfig.password) {
credentialsValue.password = securityConfig.password;
}
newCredentials = { newCredentials = {
credentialsType, credentialsType,
credentialsValue: JSON.stringify(credentialsValue) credentialsValue: JSON.stringify(credentialsValue)
}; };
} }
} else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) { } else if (securityConfig.type === SecurityTypes.ACCESS_TOKEN || securityConfig.type === SecurityTypes.TLS_ACCESS_TOKEN) {
@ -684,12 +624,12 @@ export class GatewayConfigurationComponent implements OnInit {
newCredentials = { newCredentials = {
credentialsType: DeviceCredentialsType.ACCESS_TOKEN, credentialsType: DeviceCredentialsType.ACCESS_TOKEN,
credentialsId: securityConfig.accessToken credentialsId: securityConfig.accessToken
} };
} }
} }
if (updateCredentials) { if (updateCredentials) {
return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials}) return this.deviceService.saveDeviceCredentials({...this.initialCredentials,...newCredentials});
} }
return of(null); 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. limitations under the License.
--> -->
<form style="height: calc(100% - 7px)" fxLayout="column"> <div class="connector-container tb-form-panel no-border">
<div fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxFlex class="connector-container"> <section class="table-section tb-form-panel no-padding flex section-container">
<mat-card fxLayout="column" fxFlex.lt-lg style="overflow: auto; min-height: 35vh"> <mat-toolbar class="mat-mdc-table-toolbar">
<mat-toolbar class="mat-mdc-table-toolbar"> <h2>{{ 'gateway.connectors' | translate }}</h2>
<h2>{{ 'gateway.connectors' | translate }}</h2> <span fxFlex></span>
<span fxFlex></span> <button mat-icon-button
<button mat-icon-button [disabled]="isLoading$ | async"
[disabled]="isLoading$ | async" (click)="addAttribute()"
(click)="addAttribute()" matTooltip="{{ 'action.add' | translate }}"
matTooltip="{{ 'action.add' | translate }}" matTooltipPosition="above">
matTooltipPosition="above"> <mat-icon>add</mat-icon>
<mat-icon>add</mat-icon> </button>
</button> </mat-toolbar>
</mat-toolbar> <div class="table-container">
<table mat-table [dataSource]="dataSource" <table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="enabled" sticky> <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 }} {{ 'gateway.connectors-table-enabled' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let attribute"> <mat-cell *matCellDef="let attribute">
@ -61,12 +61,9 @@
{{ 'gateway.configuration' | translate }} {{ 'gateway.configuration' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let attribute" style="text-transform: uppercase"> <mat-cell *matCellDef="let attribute" style="text-transform: uppercase">
<div <div class="status" [class]="isConnectorSynced(attribute) ? 'status-sync' : 'status-unsync'">
[ngClass]="{ {{ isConnectorSynced(attribute) ? 'sync' : 'out of sync' }}
'status-block': true, </div>
'status-sync':isConnectorSynced(attribute),
'status-unsync':!isConnectorSynced(attribute)
}">{{isConnectorSynced(attribute)?'sync' : 'out of sync'}}</div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="errors"> <ng-container matColumnDef="errors">
@ -77,10 +74,8 @@
<span class="dot" <span class="dot"
matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}" matTooltip="{{ 'Errors: '+ getErrorsCount(attribute)}}"
matTooltipPosition="above" matTooltipPosition="above"
[ngClass]="{ [class]="{'hasErrors': +getErrorsCount(attribute) > 0,
'hasErrors': +getErrorsCount(attribute) > 0, 'noErrors': +getErrorsCount(attribute) === 0 || getErrorsCount(attribute) === ''}"></span>
'noErrors': +getErrorsCount(attribute) == 0 || getErrorsCount(attribute) == ''
}"></span>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
@ -139,60 +134,60 @@
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" <mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [ngClass]="{'mat-row-select': true, <mat-row class="mat-row-select" [class]="{'tb-current-entity': isSameConnector(attribute)}"
'tb-current-entity': isSameConnector(attribute)}"
*matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row> *matRowDef="let attribute; columns: displayedColumns;" (click)="selectConnector(attribute)"></mat-row>
</table> </table>
<mat-divider></mat-divider> </div>
</mat-card> </section>
<div [formGroup]="connectorForm" fxLayout="column"> <section [formGroup]="connectorForm" class="tb-form-panel no-border no-padding flex">
<mat-card fxLayout="row" fxLayout.lt-lg="column" fxLayoutGap="15px" fxLayoutGap.lt-lg="5px"> <section class="tb-form-panel input-container section-container">
<mat-form-field class="mat-block tb-value-type"> <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> <mat-label>{{ 'gateway.connectors-table-name' | translate }}</mat-label>
<input matInput formControlName="name" #nameInput/> <input matInput formControlName="name" #nameInput/>
</mat-form-field> </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-label>{{ 'gateway.connectors-table-type' | translate }}</mat-label>
<mat-select formControlName="type"> <mat-select formControlName="type">
<mat-option style="text-transform: uppercase" <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-select>
</mat-form-field> </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> <mat-label>{{ 'gateway.connectors-table-key' | translate }}</mat-label>
<input matInput formControlName="key"/> <input matInput formControlName="key"/>
</mat-form-field> </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> <mat-label>{{ 'gateway.connectors-table-class' | translate }}</mat-label>
<input matInput formControlName="class"/> <input matInput formControlName="class"/>
</mat-form-field> </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-label translate>gateway.remote-logging-level</mat-label>
<mat-select formControlName="logLevel"> <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-select>
</mat-form-field> </mat-form-field>
</mat-card> </section>
<mat-card fxLayout="column" fxFlex> </section>
<tb-json-object-edit <section class="tb-form-panel flex section-container">
fxFlex <tb-json-object-edit
fxLayout="column" fillHeight="true"
jsonRequired class="flex"
label="{{ 'gateway.configuration' | translate }}" fxLayout="column"
formControlName="configurationJson"> jsonRequired
</tb-json-object-edit> label="{{ 'gateway.configuration' | translate }}"
<div fxLayoutAlign="start center"> formControlName="configurationJson">
<button mat-raised-button color="primary" </tb-json-object-edit>
class="action-btns" <div fxLayoutAlign="end center">
type="button" <button mat-raised-button color="primary"
[disabled]="!connectorForm.dirty || connectorForm.invalid" type="button"
(click)="saveConnector()"> [disabled]="!connectorForm.dirty || connectorForm.invalid"
{{ 'action.save' | translate }} (click)="saveConnector()">
</button> {{ 'action.save' | translate }}
</div> </button>
</mat-card> </div>
</div> </section>
</div> </section>
</form> </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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../../../../../../../scss/constants';
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -21,25 +23,32 @@
padding: 0; padding: 0;
.connector-container { .connector-container {
height: 100%;
width: 100%; width: 100%;
flex-direction: row;
@media #{$mat-lt-lg} {
flex-direction: column;
}
.table-section {
min-height: 35vh;
overflow: hidden;
& > mat-card, & > div { .table-container {
min-width: calc(50% - 15px); overflow: auto;
}
} }
mat-card { .flex {
margin: 10px; flex: 1;
padding: 10px;
max-width: 100%;
} }
}
.tb-entity-table { .input-container {
.tb-entity-table-content { height: auto;
width: 100%; }
height: 100%;
background: #fff; .section-container {
overflow: hidden; background-color: #fff;
} }
} }
@ -48,43 +57,24 @@
color: rgba(0, 0, 0, .87) !important; 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 { .mat-mdc-slide-toggle {
margin: 15px; margin: 0 8px;
} }
.status-block { .status {
text-align: center; text-align: center;
border-radius: 16px; border-radius: 16px;
font-weight: 500; font-weight: 500;
width: fit-content; width: fit-content;
padding: 5px 15px; padding: 5px 15px;
} &-sync {
background: rgba(25, 128, 56, .06);
.status-sync { color: rgb(25, 128, 56);
background: rgba(25, 128, 56, .06); }
color: rgb(25, 128, 56); &-unsync {
} background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
.status-unsync { }
background: rgba(203, 37, 48, .06);
color: rgb(203, 37, 48);
}
.action-btns {
margin: 10px 10px 0;
} }
mat-row { 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 { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, NgZone, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, UntypedFormControl, ValidatorFn, Validators } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { MatDialog } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core'; 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 { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link'; 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 { MatSort } from '@angular/material/sort';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service'; import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { MatTableDataSource } from '@angular/material/table'; 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 { ActionNotificationShow } from '@core/notification/notification.actions';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { WidgetContext } from '@home/models/widget-component.models'; 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 { NULL_UUID } from '@shared/models/id/has-uuid';
import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { IWidgetSubscription, WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { DatasourceType, widgetType } from '@shared/models/widget.models'; import { DatasourceType, widgetType } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import {
GatewayConnector,
export interface gatewayConnector { GatewayConnectorDefaultTypesTranslates,
name: string; GatewayLogLevel
type: string; } from './gateway-widget.models';
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export const GatewayConnectorDefaultTypesTranslates = new Map<string, string>([
['mqtt', 'MQTT'],
['modbus', 'MODBUS'],
['grpc', 'GRPC'],
['opcua', 'OPCUA'],
['opcua_asyncio', 'OPCUA ASYNCIO'],
['ble', 'BLE'],
['request', 'REQUEST'],
['can', 'CAN'],
['bacnet', 'BACNET'],
['odbc', 'ODBC'],
['rest', 'REST'],
['snmp', 'SNMP'],
['ftp', 'FTP'],
['socket', 'SOCKET'],
['xmpp', 'XMPP'],
['ocpp', 'OCCP'],
['custom', 'CUSTOM']
]);
@Component({ @Component({
selector: 'tb-gateway-connector', selector: 'tb-gateway-connector',
@ -84,12 +54,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
pageLink: PageLink; pageLink: PageLink;
attributeDataSource: AttributeDatasource;
inactiveConnectorsDataSource: AttributeDatasource;
serverDataSource: AttributeDatasource;
dataSource: MatTableDataSource<AttributeData>; dataSource: MatTableDataSource<AttributeData>;
displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions']; displayedColumns = ['enabled', 'key', 'type', 'syncStatus', 'errors', 'actions'];
@ -107,27 +71,29 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
connectorForm: FormGroup; connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean; textSearchMode: boolean;
activeConnectors: Array<string>; 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: { callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => { onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated(); this.onDataUpdated();
@ -138,20 +104,17 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
}; };
subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
constructor(protected router: Router, constructor(protected store: Store<AppState>,
protected store: Store<AppState>, private fb: FormBuilder,
protected fb: FormBuilder, private translate: TranslateService,
protected translate: TranslateService, private attributeService: AttributeService,
protected attributeService: AttributeService, private dialogService: DialogService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private telemetryWsService: TelemetryWebsocketService, private telemetryWsService: TelemetryWebsocketService,
private zone: NgZone, private zone: NgZone,
private utils: UtilsService, private utils: UtilsService,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef) {
public dialog: MatDialog) {
super(store); super(store);
const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC}; const sortOrder: SortOrder = {property: 'key', direction: Direction.ASC};
this.pageLink = new PageLink(1000, 0, null, sortOrder); 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]; return data[sortHeaderId] || data.value[sortHeaderId];
}; };
this.viewsInited = true;
if (this.device) { if (this.device) {
if (this.device.id === NULL_UUID) return; if (this.device.id === NULL_UUID) {
forkJoin(this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, ['active_connectors']), return;
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, ['inactive_connectors'])).subscribe(attributes => { }
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) { if (attributes.length) {
this.activeConnectors = attributes[0].length ? attributes[0][0].value : []; 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 = 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); this.updateData(true);
} else { } else {
this.activeConnectors = []; this.activeConnectors = [];
@ -206,7 +174,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
} }
uniqNameRequired(): ValidatorFn { private uniqNameRequired(): ValidatorFn {
return (c: UntypedFormControl) => { return (c: UntypedFormControl) => {
const newName = c.value.trim().toLowerCase(); const newName = c.value.trim().toLowerCase();
const found = this.dataSource.data.find((connectorAttr) => { const found = this.dataSource.data.find((connectorAttr) => {
@ -229,7 +197,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
saveConnector(): void { saveConnector(): void {
const value = this.connectorForm.value; const value = this.connectorForm.value;
value.configuration = this.camelize(value.name) + '.json'; value.configuration = camelCase(value.name) + '.json';
if (value.type !== 'grpc') { if (value.type !== 'grpc') {
delete value.key; delete value.key;
} }
@ -242,7 +210,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
value value
}]; }];
const attributesToDelete = []; 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; let updateActiveConnectors = false;
if (this.initialConnector && this.initialConnector.name !== value.name) { if (this.initialConnector && this.initialConnector.name !== value.name) {
attributesToDelete.push({key: this.initialConnector.name}); attributesToDelete.push({key: this.initialConnector.name});
@ -256,19 +226,19 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.inactiveConnectors.splice(inactiveIndex, 1); 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); this.activeConnectors.push(value.name);
updateActiveConnectors = true; 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); this.inactiveConnectors.push(value.name);
updateActiveConnectors = true; updateActiveConnectors = true;
} }
const tasks = [this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)]; const tasks = [this.attributeService.saveEntityAttributes(this.device, scope, attributesToSave)];
if (updateActiveConnectors) { if (updateActiveConnectors) {
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors 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.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
this.attributeDataSource.loadAttributes(this.device, AttributeScope.CLIENT_SCOPE, this.pageLink, reload).subscribe(data => { 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) { isConnectorSynced(attribute: AttributeData) {
const connectorData = attribute.value; const connectorData = attribute.value;
if (!connectorData.ts) return false; if (!connectorData.ts) {
return false;
}
const clientIndex = this.activeData.findIndex(data => { const clientIndex = this.activeData.findIndex(data => {
const sharedData = data.value; const sharedData = data.value;
return sharedData.name === connectorData.name; return sharedData.name === connectorData.name;
}) });
if (clientIndex == -1) return false; if (clientIndex === -1) {
return false;
}
const sharedIndex = this.sharedAttributeData.findIndex(data => { 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 sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts;
}) });
return sharedIndex !== -1; return sharedIndex !== -1;
} }
combineData() { private combineData() {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key) index === self.findIndex((t) => t.key === item.key)
).map(attribute=>{ ).map(attribute => {
attribute.value = typeof attribute.value === 'string' ? JSON.parse(attribute.value) : attribute.value; 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.nameInput.nativeElement.focus();
this.clearOutConnectorForm(); this.clearOutConnectorForm();
} }
clearOutConnectorForm(): void { private clearOutConnectorForm(): void {
this.connectorForm.setValue({ this.connectorForm.setValue({
name: '', name: '',
type: 'mqtt', type: 'mqtt',
logLevel: GatewayLogLevel.info, logLevel: GatewayLogLevel.INFO,
key: 'auto', key: 'auto',
class: '', class: '',
configuration: '', configuration: '',
@ -347,7 +320,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
} }
selectConnector(attribute): void { selectConnector(attribute: AttributeData): void {
if (this.connectorForm.disabled) { if (this.connectorForm.disabled) {
this.connectorForm.enable(); this.connectorForm.enable();
} }
@ -363,8 +336,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
} }
isSameConnector(attribute): boolean { isSameConnector(attribute: AttributeData): boolean {
if (!this.initialConnector) return false; if (!this.initialConnector) {
return false;
}
const connector = attribute.value; const connector = attribute.value;
return this.initialConnector.name === connector.name; return this.initialConnector.name === connector.name;
} }
@ -378,12 +353,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
verticalPosition: 'top', verticalPosition: 'top',
horizontalPosition: 'right', horizontalPosition: 'right',
target: 'dashboardRoot', target: 'dashboardRoot',
// panelClass: this.widgetNamespace,
forceDismiss: true forceDismiss: true
})); }));
} }
returnType(attribute) { returnType(attribute: AttributeData): string {
const value = attribute.value; const value = attribute.value;
return this.gatewayConnectorDefaultTypes.get(value.type); 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.`; const content = `All connector data will be deleted.`;
this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => { this.dialogService.confirm(title, content, 'Cancel', 'Delete').subscribe(result => {
if (result) { if (result) {
const tasks = []; const tasks: Array<Observable<any>> = [];
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;
tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute])); tasks.push(this.attributeService.deleteEntityAttributes(this.device, scope, [attribute]));
const activeIndex = this.activeConnectors.indexOf(attribute.key); const activeIndex = this.activeConnectors.indexOf(attribute.key);
const inactiveIndex = this.inactiveConnectors.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); this.inactiveConnectors.splice(inactiveIndex, 1);
} }
tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{ tasks.push(this.attributeService.saveEntityAttributes(this.device, scope, [{
key: scope == AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors', key: scope === AttributeScope.SHARED_SCOPE ? 'active_connectors' : 'inactive_connectors',
value: scope == AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors value: scope === AttributeScope.SHARED_SCOPE ? this.activeConnectors : this.inactiveConnectors
}])); }]));
forkJoin(tasks).subscribe(_ => { forkJoin(tasks).subscribe(() => {
if (this.initialConnector ? this.initialConnector.name === attribute.key : true) { if (this.initialConnector ? this.initialConnector.name === attribute.key : true) {
this.clearOutConnectorForm(); this.clearOutConnectorForm();
this.cd.detectChanges(); 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 { connectorLogs(attribute: AttributeData, $event: Event): void {
if ($event) { if ($event) {
$event.stopPropagation(); $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 wasEnabled = this.activeConnectors.includes(attribute.key);
const scopeOld = wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE; const scopeOld = wasEnabled ? AttributeScope.SHARED_SCOPE : AttributeScope.SERVER_SCOPE;
const scopeNew = !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); const exceptionData = this.utils.parseException(e);
let errorText = exceptionData.name; let errorText = exceptionData.name;
if (exceptionData.message) { if (exceptionData.message) {
@ -484,11 +456,11 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
console.error(errorText); console.error(errorText);
} }
onDataUpdated() { private onDataUpdated() {
this.cd.detectChanges(); this.cd.detectChanges();
} }
generateSubscription() { private generateSubscription() {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
@ -497,21 +469,23 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
type: DatasourceType.entity, type: DatasourceType.entity,
entityType: EntityType.DEVICE, entityType: EntityType.DEVICE,
entityId: this.device.id, entityId: this.device.id,
entityName: "Gateway", entityName: 'Gateway',
timeseries: [] timeseries: []
}]; }];
this.dataSource.data.forEach(value => { this.dataSource.data.forEach(value => {
subscriptionInfo[0].timeseries.push({name: `${value.key}_ERRORS_COUNT`, label: `${value.key}_ERRORS_COUNT`}) 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.ctx.subscriptionApi.createSubscriptionFromInfo(widgetType.latest, subscriptionInfo, this.subscriptionOptions,
false, true).subscribe(subscription => {
this.subscription = subscription; this.subscription = subscription;
}); });
} }
} }
getErrorsCount(attribute) { getErrorsCount(attribute: AttributeData): string {
const connectorName = attribute.key; const connectorName = attribute.key;
const connector = this.subscription && this.subscription.data.find(data=>data && data.dataKey.name === `${connectorName}_ERRORS_COUNT`); const connector = this.subscription && this.subscription.data
return (connector && this.activeConnectors.includes(connectorName))? connector.data[0][1]: 'Inactive'; .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"> <nav mat-tab-nav-bar [tabPanel]="tabPanel">
<a mat-tab-link *ngFor="let link of logLinks" <a mat-tab-link *ngFor="let link of logLinks"
(click)="onTabChanged(link)" (click)="onTabChanged(link)"
[active]="activeLink.name == link.name"> {{link.name}} </a> [active]="activeLink.name === link.name"> {{ link.name }} </a>
</nav> </nav>
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel> <mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
<table mat-table [dataSource]="dataSource" <table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="ts"> <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"> <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> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="status"> <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"> <mat-cell *matCellDef="let attribute">
<span [ngClass]="statusClass(attribute.status )">{{ attribute.status }}</span> <span [class]="statusClass(attribute.status)">{{ attribute.status }}</span>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="message"> <ng-container matColumnDef="message">
<mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">Message</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header style="width: 70%">{{ 'widgets.gateway.message' | translate }}</mat-header-cell>
<mat-cell *matCellDef="let attribute" [ngClass]="statusClassMsg(attribute.status )"> <mat-cell *matCellDef="let attribute" [class]="statusClassMsg(attribute.status)">
{{ attribute.message }} {{ attribute.message }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" <mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> <mat-row class="mat-row-select" *matRowDef="let attribute; columns: displayedColumns;"></mat-row>
<mat-row [ngClass]="{'mat-row-select': true}"
*matRowDef="let attribute; columns: displayedColumns;"></mat-row>
</table> </table>
<span [fxShow]="dataSource.data.length === 0" <span [fxShow]="dataSource.data.length === 0"
fxFlex fxFlex fxLayoutAlign="center center"
fxLayoutAlign="center center" class="no-data-found">{{ 'attribute.no-telemetry-text' | translate }}</span>
class="no-data-found">{{'attribute.no-telemetry-text' | translate}}</span>
<span fxFlex [fxShow]="dataSource.data.length !== 0"></span> <span fxFlex [fxShow]="dataSource.data.length !== 0"></span>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<mat-paginator #paginator <mat-paginator [length]="dataSource.data.length"
[length]="dataSource.data.length"
[pageIndex]="pageLink.page" [pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator> [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. /// limitations under the License.
/// ///
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core'; import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store'; import { MatDialogRef } from '@angular/material/dialog';
import { AppState } from '@core/core.state'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service';
import { DeviceService } from '@core/http/device.service';
import { TranslateService } from '@ngx-translate/core';
import { AttributeData, DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link'; import { 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 { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; 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 { WidgetContext } from '@home/models/widget-component.models';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { GatewayLogData, GatewayStatus, LogLink } from './gateway-widget.models';
export interface GatewayConnector {
name: string;
type: string;
configuration?: string;
configurationJson: string;
logLevel: string;
key?: string;
}
export interface LogLink {
name: string;
key: string;
filterFn?: Function;
}
@Component({ @Component({
selector: 'tb-gateway-logs', selector: 'tb-gateway-logs',
templateUrl: './gateway-logs.component.html', templateUrl: './gateway-logs.component.html',
styleUrls: ['./gateway-logs.component.scss'] styleUrls: ['./gateway-logs.component.scss']
}) })
export class GatewayLogsComponent extends PageComponent implements AfterViewInit { export class GatewayLogsComponent implements AfterViewInit {
pageLink: PageLink; pageLink: PageLink;
attributeDataSource: AttributeDatasource; dataSource: MatTableDataSource<GatewayLogData>;
dataSource: MatTableDataSource<any>
displayedColumns = ['ts', 'status', 'message']; displayedColumns = ['ts', 'status', 'message'];
@ -76,63 +48,39 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
connectorForm: FormGroup;
viewsInited = false;
textSearchMode: boolean; textSearchMode: boolean;
activeConnectors: Array<string>;
inactiveConnectors: Array<string>;
InitialActiveConnectors: Array<string>;
gatewayLogLevel = Object.values(GatewayLogLevel);
logLinks: Array<LogLink>; logLinks: Array<LogLink>;
initialConnector: GatewayConnector;
activeLink: LogLink; activeLink: LogLink;
gatewayLogLinks: Array<LogLink> = [ gatewayLogLinks: Array<LogLink> = [
{ {
name: "General", name: 'General',
key: "LOGS" key: 'LOGS'
}, { }, {
name: "Service", name: 'Service',
key: "SERVICE_LOGS" key: 'SERVICE_LOGS'
}, },
{ {
name: "Connection", name: 'Connection',
key: "CONNECTION_LOGS" key: 'CONNECTION_LOGS'
}, { }, {
name: "Storage", name: 'Storage',
key: "STORAGE_LOGS" key: 'STORAGE_LOGS'
}, },
{ {
key: 'EXTENSIONS_LOGS', key: 'EXTENSIONS_LOGS',
name: "Extension" name: 'Extension'
}] }];
constructor(protected router: Router, constructor() {
protected store: Store<AppState>,
protected fb: FormBuilder,
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC}; const sortOrder: SortOrder = {property: 'ts', direction: Direction.DESC};
this.pageLink = new PageLink(10, 0, null, sortOrder); this.pageLink = new PageLink(10, 0, null, sortOrder);
this.dataSource = new MatTableDataSource<AttributeData>([]); this.dataSource = new MatTableDataSource<GatewayLogData>([]);
} }
ngAfterViewInit() { ngAfterViewInit() {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator; 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.options.timeWindowConfig = timewindow;
this.ctx.defaultSubscription.updateDataSubscriptions(); this.ctx.defaultSubscription.updateDataSubscriptions();
return timewindow; return timewindow;
} };
if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) { if (this.ctx.settings.isConnectorLog && this.ctx.settings.connectorLogState) {
const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState]; const connector = this.ctx.stateController.getStateParams()[this.ctx.settings.connectorLogState];
this.logLinks = [{ this.logLinks = [{
key: `${connector.key}_LOGS`, key: `${connector.key}_LOGS`,
name: "Connector", name: 'Connector',
filterFn: (attrData)=>{ filterFn: (attrData) => !attrData.message.includes(`_converter.py`)
return !attrData.message.includes(`_converter.py`) }, {
}
},{
key: `${connector.key}_LOGS`, key: `${connector.key}_LOGS`,
name: "Converter", name: 'Converter',
filterFn: (attrData)=>{ filterFn: (attrData) => attrData.message.includes(`_converter.py`)
return attrData.message.includes(`_converter.py`) }];
}
}]
} else { } else {
this.logLinks = this.gatewayLogLinks; 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]) { if (this.ctx.defaultSubscription.data.length && this.ctx.defaultSubscription.data[0]) {
let attrData = this.ctx.defaultSubscription.data[0].data.map(data => { let attrData = this.ctx.defaultSubscription.data[0].data.map(data => {
let result = { const result = {
ts: data[0], ts: data[0],
key: this.activeLink.key, key: this.activeLink.key,
message: /\[(.*)/.exec(data[1])[0], message: /\[(.*)/.exec(data[1])[0],
status: 'INVALID LOG FORMAT' status: 'INVALID LOG FORMAT' as GatewayStatus
}; };
try { try {
result.status= data[1].match(/\|(\w+)\|/)[1]; result.status = data[1].match(/\|(\w+)\|/)[1];
} catch (e) { } catch (e) {
result.status = 'INVALID LOG FORMAT' result.status = 'INVALID LOG FORMAT' as GatewayStatus;
} }
return result; return result;
@ -186,39 +130,35 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
attrData = attrData.filter(data => this.activeLink.filterFn(data)); attrData = attrData.filter(data => this.activeLink.filterFn(data));
} }
this.dataSource.data = attrData; this.dataSource.data = attrData;
if (sort) {
this.dataSource.sortData(this.dataSource.data, this.sort);
}
} }
} }
onTabChanged(link) { onTabChanged(link: LogLink) {
this.activeLink = link; this.activeLink = link;
this.changeSubscription(); this.changeSubscription();
} }
statusClass(status) { statusClass(status: GatewayStatus): string {
switch (status) { switch (status) {
case GatewayLogLevel.debug: case GatewayStatus.DEBUG:
return "status status-debug"; return 'status status-debug';
case GatewayLogLevel.warning: case GatewayStatus.WARNING:
return "status status-warning"; return 'status status-warning';
case GatewayLogLevel.error: case GatewayStatus.ERROR:
case "EXCEPTION": case GatewayStatus.EXCEPTION:
return "status status-error"; return 'status status-error';
case GatewayLogLevel.info:
default: default:
return "status status-info"; return 'status status-info';
} }
} }
statusClassMsg(status) { statusClassMsg(status?: GatewayStatus): string {
if (status === "EXCEPTION") { if (status === GatewayStatus.EXCEPTION) {
return 'msg-status-exception'; return 'msg-status-exception';
} }
} }
changeSubscription() { private changeSubscription() {
if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) { if (this.ctx.datasources && this.ctx.datasources[0].entity && this.ctx.defaultSubscription.options.datasources) {
this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{ this.ctx.defaultSubscription.options.datasources[0].dataKeys = [{
name: this.activeLink.key, name: this.activeLink.key,
@ -229,8 +169,7 @@ export class GatewayLogsComponent extends PageComponent implements AfterViewInit
this.ctx.defaultSubscription.updateDataSubscriptions(); this.ctx.defaultSubscription.updateDataSubscriptions();
this.ctx.defaultSubscription.callbacks.onDataUpdated = () => { this.ctx.defaultSubscription.callbacks.onDataUpdated = () => {
this.updateData(); 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. limitations under the License.
--> -->
<form style="width: 600px; position: relative;"> <mat-toolbar color="warn">
<mat-toolbar color="warn"> <mat-icon>warning</mat-icon>
<mat-icon>warning</mat-icon> <h2 translate>gateway.configuration-delete-dialog-header</h2>
<h2 translate>gateway.configuration-delete-dialog-header</h2> <span fxFlex></span>
<span fxFlex></span> <button mat-icon-button
<button mat-icon-button (click)="close()"
(click)="close()" type="button">
type="button"> <mat-icon class="material-icons">close</mat-icon>
<mat-icon class="material-icons">close</mat-icon> </button>
</button> </mat-toolbar>
</mat-toolbar> <div mat-dialog-content style="max-width: 600px" class="mat-content" fxLayout="column">
<div mat-dialog-content tb-toast toastTarget="activationLinkDialogContent"> <span innerHTML="{{ 'gateway.configuration-delete-dialog-body' | translate }} <b>{{ gatewayName }}</b>" ></span>
<div class="mat-content" fxLayout="column"> <mat-form-field class="mat-block tb-value-type" style="flex-grow: 0">
<span innerHTML="{{'gateway.configuration-delete-dialog-body' | translate}} <b>{{gatewayName}}</b>" > </span> <mat-label translate>gateway.configuration-delete-dialog-input</mat-label>
<mat-form-field class="mat-block tb-value-type" style="flex-grow: 0"> <input matInput [formControl]="gatewayControl" required/>
<mat-label translate>gateway.configuration-delete-dialog-input</mat-label> <mat-error
<input matInput [formControl]="gatewayControl" required/> *ngIf="gatewayControl.hasError('required')">
<mat-error {{ 'gateway.configuration-delete-dialog-input-required' | translate }}
*ngIf="gatewayControl.hasError('required')"> </mat-error>
{{'gateway.configuration-delete-dialog-input-required' | translate }} </mat-form-field>
</mat-error> </div>
</mat-form-field> <div mat-dialog-actions fxLayoutAlign="end center">
</div> <button mat-button color="warn"
</div> type="button"
<div mat-dialog-actions fxLayoutAlign="end center"> cdkFocusInitial
<button mat-button color="warn" (click)="close()">
type="button" {{ 'action.cancel' | translate }}
cdkFocusInitial </button>
(click)="close()"> <button mat-button color="warn"
{{ 'action.cancel' | translate }} type="button"
</button> [disabled]="gatewayControl.value !== gatewayName"
<button mat-button color="warn" (click)="turnOff()">
type="button" {{ 'gateway.configuration-delete-dialog-confirm' | translate }}
cdkFocusInitial </button>
[disabled]="gatewayControl.value !== gatewayName" </div>
(click)="turnOff()">
{{ 'gateway.configuration-delete-dialog-confirm' | translate }}
</button>
</div>
</form>

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

@ -14,7 +14,7 @@
/// limitations under the License. /// 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 { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -31,11 +31,10 @@ export interface GatewayRemoteConfigurationDialogData {
templateUrl: './gateway-remote-configuration-dialog.html' templateUrl: './gateway-remote-configuration-dialog.html'
}) })
export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<GatewayRemoteConfigurationDialogComponent, export class GatewayRemoteConfigurationDialogComponent extends
boolean> implements OnInit { DialogComponent<GatewayRemoteConfigurationDialogComponent, boolean>{
gatewayName: string; gatewayName: string;
gatewayControl: FormControl; gatewayControl: FormControl;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -45,10 +44,7 @@ export class GatewayRemoteConfigurationDialogComponent extends DialogComponent<G
private fb: FormBuilder) { private fb: FormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.gatewayName = this.data.gatewayName; this.gatewayName = this.data.gatewayName;
this.gatewayControl = this.fb.control(null); this.gatewayControl = this.fb.control('');
}
ngOnInit(): void {
} }
close(): void { 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"> <div fxLayout="row" fxLayout.lt-sm="column" class="command-form" fxLayoutGap="10px" [formGroup]="commandForm">
<mat-form-field class="mat-block tb-value-type"> <ng-container *ngIf="!isConnector; else connectorForm">
<mat-label>{{'gateway.statistics.command' | translate}}</mat-label> <mat-form-field>
<mat-select formControlName="command" *ngIf="!isConnector"> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<mat-option *ngFor="let command of RPCCommands" [value]="command"> <mat-select formControlName="command">
{{command}} <mat-option *ngFor="let command of RPCCommands" [value]="command">
</mat-option> {{ command }}
</mat-select> </mat-option>
<input matInput formControlName="command" *ngIf="isConnector"/> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="!isConnector"> <mat-form-field fxFlex>
<mat-label>{{'gateway.statistics.timeout-ms' | translate}}</mat-label> <mat-label>{{ 'gateway.statistics.timeout-ms' | translate }}</mat-label>
<input matInput formControlName="time" type="number" min="1"/> <input matInput formControlName="time" type="number" min="1"/>
<mat-error <mat-error *ngIf="commandForm.get('time').hasError('min')">
*ngIf="commandForm.get('time').hasError('min')"> {{ 'gateway.statistics.timeout-min' | translate }}
{{'gateway.statistics.timeout-min' | translate }} </mat-error>
</mat-error> </mat-form-field>
</mat-form-field> </ng-container>
<mat-form-field class="mat-block tb-value-type" fxFlex *ngIf="isConnector"> <ng-template #connectorForm>
<mat-label>{{'widget-config.datasource-parameters' | translate}}</mat-label> <mat-form-field>
<input matInput formControlName="params" type="JSON"/> <mat-label>{{ 'gateway.statistics.command' | translate }}</mat-label>
<mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon" <input matInput formControlName="command" />
matSuffix style="cursor:pointer;" </mat-form-field>
(click)="openEditJSONDialog($event)" <mat-form-field fxFlex>
matTooltip="{{'gateway.rpc-command-edit-params' | translate}}">edit <mat-label>{{ 'widget-config.datasource-parameters' | translate }}</mat-label>
</mat-icon> <input matInput formControlName="params" type="JSON"/>
</mat-form-field> <mat-icon class="material-icons-outlined" aria-hidden="false" aria-label="help-icon"
<button mat-raised-button color="primary" (click)="sendCommand()" matIconSuffix style="cursor:pointer;"
[disabled]="commandForm.invalid">{{'gateway.rpc-command-send' | translate}}</button> (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> </div>
<mat-card class="result-block" [formGroup]="commandForm" fxFlex> <section class="result-block" [formGroup]="commandForm">
<span>{{'gateway.rpc-command-result' | translate}}</span> <span>{{ 'gateway.rpc-command-result' | translate }}</span>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result" fxFlex></tb-json-content> <tb-json-content [contentType]="contentTypes.JSON" readonly="true" formControlName="result"></tb-json-content>
</mat-card> </section>

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

@ -22,24 +22,24 @@
padding: 0; padding: 0;
.command-form { .command-form {
width: 100%;
flex-wrap: nowrap; flex-wrap: nowrap;
padding: 0 15px 5px; padding: 0 5px 5px;
margin-bottom: 5px;
& > button { & > button {
margin-top: 10px; margin-top: 10px;
} }
} }
.result-block { .result-block {
padding: 0 15px; padding: 0 5px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1;
& > span { & > span {
font-weight: 600; font-weight: 600;
} }
tb-json-content {
flex: 1;
}
} }
} }
@ -47,9 +47,5 @@
.tb-json-content { .tb-json-content {
height: 100%; 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. /// limitations under the License.
/// ///
import { AfterViewInit, Component, Input } from '@angular/core'; import { AfterViewInit, Component, Input } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog, MatDialogRef } from '@angular/material/dialog'; 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 { WidgetContext } from '@home/models/widget-component.models';
import { ContentType } from '@shared/models/constants'; import { ContentType } from '@shared/models/constants';
import { import {
@ -33,13 +25,12 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component'; } from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component'; import { jsonRequired } from '@shared/components/json-object-edit.component';
@Component({ @Component({
selector: 'tb-gateway-service-rpc', selector: 'tb-gateway-service-rpc',
templateUrl: './gateway-service-rpc.component.html', templateUrl: './gateway-service-rpc.component.html',
styleUrls: ['./gateway-service-rpc.component.scss'] styleUrls: ['./gateway-service-rpc.component.scss']
}) })
export class GatewayServiceRPCComponent extends PageComponent implements AfterViewInit { export class GatewayServiceRPCComponent implements AfterViewInit {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -53,39 +44,28 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
isConnector: boolean; isConnector: boolean;
connectorType: string;
RPCCommands: Array<string> = [ RPCCommands: Array<string> = [
"Ping", 'Ping',
"Stats", 'Stats',
"Devices", 'Devices',
"Update", 'Update',
"Version", 'Version',
"Restart", 'Restart',
"Reboot" 'Reboot'
] ];
private connectorType: string;
constructor(protected router: Router,
protected store: Store<AppState>, constructor(private fb: FormBuilder,
protected fb: FormBuilder, private dialog: MatDialog) {
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
public dialog: MatDialog) {
super(store);
this.commandForm = this.fb.group({ this.commandForm = this.fb.group({
command: [null,[Validators.required]], command: [null,[Validators.required]],
time: [60, [Validators.required, Validators.min(1)]], time: [60, [Validators.required, Validators.min(1)]],
params: [{}, [jsonRequired]], params: [{}, [jsonRequired]],
result: [null] result: [null]
}) });
} }
ngAfterViewInit() { ngAfterViewInit() {
this.isConnector = this.ctx.settings.isConnector; this.isConnector = this.ctx.settings.isConnector;
if (!this.isConnector) { if (!this.isConnector) {
@ -95,16 +75,16 @@ export class GatewayServiceRPCComponent extends PageComponent implements AfterVi
} }
} }
sendCommand() { sendCommand() {
const formValues = this.commandForm.value; const formValues = this.commandForm.value;
const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_'; const commandPrefix = this.isConnector ? `${this.connectorType}_` : 'gateway_';
this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe(resp=>{ this.ctx.controlApi.sendTwoWayCommand(commandPrefix+formValues.command.toLowerCase(), {},formValues.time).subscribe({
this.commandForm.get('result').setValue(JSON.stringify(resp)); next: resp => this.commandForm.get('result').setValue(JSON.stringify(resp)),
},error => { error: error => {
console.log(error); console.log(error);
this.commandForm.get('result').setValue(JSON.stringify(error.error)); this.commandForm.get('result').setValue(JSON.stringify(error.error));
}) }
});
} }
openEditJSONDialog($event: Event) { 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"> <div class="statistics-container" fxLayout="row" fxLayout.lt-md="column">
<mat-card [formGroup]="statisticForm" *ngIf="!general"> <mat-card [formGroup]="statisticForm" *ngIf="!general">
<mat-form-field class="mat-block"> <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-select formControlName="statisticKey">
<mat-option *ngFor="let key of statisticsKeys" [value]="key"> <mat-option *ngFor="let key of statisticsKeys" [value]="key">
{{key}} {{ key }}
</mat-option> </mat-option>
<mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway"> <mat-option *ngFor="let command of commands" [value]="command.attributeOnGateway">
{{command.attributeOnGateway}} {{ command.attributeOnGateway }}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -34,44 +34,42 @@
{{'gateway.statistics.statistic-commands-empty' | translate }} {{'gateway.statistics.statistic-commands-empty' | translate }}
</mat-error> </mat-error>
<mat-form-field class="mat-block" *ngIf="commandObj"> <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> <input matInput [value]="commandObj.command" disabled>
</mat-form-field> </mat-form-field>
</mat-card> </mat-card>
<div class="chart-box" fxLayout="column"> <div class="chart-box" fxLayout="column">
<div class="chart-container" #statisticChart [fxShow]="isNumericData"> <div class="chart-container" #statisticChart [fxShow]="isNumericData"></div>
</div>
<table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource" <table [fxShow]="!isNumericData" mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()"
matSortDisableClear> matSortDisableClear>
<ng-container matColumnDef="0"> <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"> <mat-cell *matCellDef="let row; let rowIndex = index">
{{row[0]| date:'yyyy-MM-dd HH:mm:ss' }} {{row[0]| date:'yyyy-MM-dd HH:mm:ss' }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="1"> <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"> <mat-cell *matCellDef="let row">
{{ row[1] }} {{ row[1] }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" <mat-header-row class="mat-row-select"
*matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> *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> *matRowDef="let row; columns: displayedColumns;"></mat-row>
</table> </table>
<span [fxShow]="dataSource.data.length === 0 && !isNumericData" <span [fxShow]="dataSource.data.length === 0 && !isNumericData"
fxLayoutAlign="center center" 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 fxFlex class="legend" fxLayout="row" fxLayoutAlign="center center" [fxShow]="isNumericData">
<div class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row" <div class="legend-keys" *ngFor="let legendKey of legendData?.keys" fxLayout="row"
fxLayoutAlign="center center"> 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" <div class="legend-label"
(click)="onLegendKeyHiddenChange(legendKey.dataIndex)" (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"> [innerHTML]="legendKey.dataKey.label">
</div> </div>
</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. /// limitations under the License.
/// ///
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewChild } from '@angular/core'; 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 { FormBuilder, FormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { AttributeService } from '@core/http/attribute.service'; 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 { 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 { WidgetContext } from '@home/models/widget-component.models';
import { TbFlot } from '@home/components/widget/lib/flot-widget'; import { TbFlot } from '@home/components/widget/lib/flot-widget';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
@ -41,13 +33,12 @@ import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
@Component({ @Component({
selector: 'tb-gateway-statistics', selector: 'tb-gateway-statistics',
templateUrl: './gateway-statistics.component.html', templateUrl: './gateway-statistics.component.html',
styleUrls: ['./gateway-statistics.component.scss'] styleUrls: ['./gateway-statistics.component.scss']
}) })
export class GatewayStatisticsComponent extends PageComponent implements AfterViewInit { export class GatewayStatisticsComponent implements AfterViewInit {
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
@ViewChild('statisticChart') statisticChart: ElementRef; @ViewChild('statisticChart') statisticChart: ElementRef;
@ -58,10 +49,11 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
@Input() @Input()
public general: boolean; public general: boolean;
public isNumericData: boolean = false; public isNumericData = false;
public dataTypeDefined: boolean = false;
public chartInited: boolean; public chartInited: boolean;
private flot: TbFlot; private flot: TbFlot;
private flotCtx; private flotCtx: WidgetContext;
public statisticForm: FormGroup; public statisticForm: FormGroup;
public statisticsKeys = []; public statisticsKeys = [];
public commands = []; public commands = [];
@ -89,70 +81,61 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
}; };
constructor(protected router: Router, constructor(private fb: FormBuilder,
protected store: Store<AppState>, private attributeService: AttributeService,
protected fb: FormBuilder, private utils: UtilsService) {
protected translate: TranslateService,
protected attributeService: AttributeService,
protected deviceService: DeviceService,
protected dialogService: DialogService,
private cd: ChangeDetectorRef,
private utils: UtilsService,
public dialog: MatDialog) {
super(store);
const sortOrder: SortOrder = {property: '0', direction: Direction.DESC}; const sortOrder: SortOrder = {property: '0', direction: Direction.DESC};
this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder); this.pageLink = new PageLink(Number.POSITIVE_INFINITY, 0, null, sortOrder);
this.displayedColumns = ['0', '1']; this.displayedColumns = ['0', '1'];
this.dataSource = new MatTableDataSource<any>([]); this.dataSource = new MatTableDataSource<any>([]);
this.statisticForm = this.fb.group({ this.statisticForm = this.fb.group({
statisticKey: [null, []] statisticKey: [null, []]
}) });
this.statisticForm.get('statisticKey').valueChanges.subscribe(value => { this.statisticForm.get('statisticKey').valueChanges.subscribe(value => {
this.commandObj = null; this.commandObj = null;
if (this.commands.length) { if (this.commands.length) {
this.commandObj = this.commands.find(command => command.attributeOnGateway === value); 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() { ngAfterViewInit() {
this.dataSource.sort = this.sort; this.dataSource.sort = this.sort;
this.sort.sortChange.subscribe(_=>{ this.sort.sortChange.subscribe(() => this.sortData());
this.sortData();
})
this.init(); this.init();
if (this.ctx.defaultSubscription.datasources.length) { if (this.ctx.defaultSubscription.datasources.length) {
const gateway = this.ctx.defaultSubscription.datasources[0].entity; 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) { if (!this.general) {
this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ["general_configuration"]).subscribe((resp: AttributeData[]) => { this.attributeService.getEntityAttributes(gateway.id, AttributeScope.SHARED_SCOPE, ['general_configuration'])
if (resp && resp.length) { .subscribe((resp: AttributeData[]) => {
this.commands = resp[0].value.statistics.commands; if (resp && resp.length) {
if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) { this.commands = resp[0].value.statistics.commands;
this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway); if (!this.statisticForm.get('statisticKey').value && this.commands && this.commands.length) {
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway); this.statisticForm.get('statisticKey').setValue(this.commands[0].attributeOnGateway);
this.createChartsSubscription(gateway, this.commands[0].attributeOnGateway);
}
} }
} });
})
} else { } else {
let connectorsTs;
this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe( this.attributeService.getEntityTimeseriesLatest(gateway.id).subscribe(
data => { data => {
connectorsTs = Object.keys(data) const connectorsTs = Object.keys(data)
.filter(el => el.includes( .filter(el => el.includes('ConnectorEventsProduced') || el.includes('ConnectorEventsSent'));
'ConnectorEventsProduced'
) || el.includes(
'ConnectorEventsSent'))
this.createGeneralChartsSubscription(gateway, connectorsTs); this.createGeneralChartsSubscription(gateway, connectorsTs);
}) });
} }
} }
} }
public sortData () { public sortData() {
this.dataSource.sortData(this.dataSource.data, this.sort); 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) { private createChartsSubscription(gateway: BaseData<EntityId>, attr: string) {
let subscriptionInfo = [{ const subscriptionInfo = [{
type: DatasourceType.entity, type: DatasourceType.entity,
entityType: EntityType.DEVICE, entityType: EntityType.DEVICE,
entityId: gateway.id.id, entityId: gateway.id.id,
@ -173,10 +156,11 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
subscriptionInfo[0].timeseries = [{name: attr, label: attr}]; subscriptionInfo[0].timeseries = [{name: attr, label: attr}];
this.subscriptionInfo = subscriptionInfo; this.subscriptionInfo = subscriptionInfo;
this.changeSubscription(subscriptionInfo); this.changeSubscription(subscriptionInfo);
this.ctx.defaultSubscription.unsubscribe();
} }
private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: [string]) { private createGeneralChartsSubscription(gateway: BaseData<EntityId>, attrData: string[]) {
let subscriptionInfo = [{ const subscriptionInfo = [{
type: DatasourceType.entity, type: DatasourceType.entity,
entityType: EntityType.DEVICE, entityType: EntityType.DEVICE,
entityId: gateway.id.id, entityId: gateway.id.id,
@ -184,20 +168,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
timeseries: [] timeseries: []
}]; }];
subscriptionInfo[0].timeseries = []; subscriptionInfo[0].timeseries = [];
if (attrData && attrData.length) { if (attrData?.length) {
attrData.forEach(attr => { 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 => { 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.changeSubscription(subscriptionInfo);
this.ctx.defaultSubscription.unsubscribe();
} }
init = () => { private init = () => {
this.flotCtx = { this.flotCtx = {
$scope: this.ctx.$scope, $scope: this.ctx.$scope,
$injector: this.ctx.$injector, $injector: this.ctx.$injector,
@ -207,20 +191,20 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
subscriptionApi: this.ctx.subscriptionApi, subscriptionApi: this.ctx.subscriptionApi,
detectChanges: this.ctx.detectChanges, detectChanges: this.ctx.detectChanges,
settings: this.ctx.settings settings: this.ctx.settings
}; } as WidgetContext;
} };
updateChart = () => { private updateChart = () => {
if (this.flot && this.ctx.defaultSubscription.data.length) { if (this.flot && this.ctx.defaultSubscription.data.length) {
this.flot.update(); this.flot.update();
} }
} };
resize = () => { private resize = () => {
if (this.flot) { if (this.flot) {
this.flot.resize(); this.flot.resize();
} }
} };
private reset() { private reset() {
if (this.resize$) { if (this.resize$) {
@ -260,7 +244,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
this.chartInited = true; this.chartInited = true;
this.flotCtx.$container = $(this.statisticChart.nativeElement); this.flotCtx.$container = $(this.statisticChart.nativeElement);
this.resize$.observe(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(); this.flot.update();
} }
@ -270,16 +254,21 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
return; return;
} }
this.dataSource.data = this.subscription.data.length ? this.subscription.data[0].data : []; 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) { if (this.subscription) {
this.reset(); this.reset();
} }
if (this.ctx.datasources[0].entity) { 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.subscription = subscription;
this.isDataOnlyNumbers(); this.isDataOnlyNumbers();
this.legendData = this.subscription.legendData; this.legendData = this.subscription.legendData;
@ -291,9 +280,7 @@ export class GatewayStatisticsComponent extends PageComponent implements AfterVi
if (this.isNumericData) { if (this.isNumericData) {
this.initChart(); 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. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column"> <div class="tb-form-panel" [formGroup]="gatewayLogSettingForm">
<mat-slide-toggle class="mat-slide" formControlName="isConnectorLog"> <div class="tb-form-row column-xs">
{{"widgets.gateway.is-connector" | translate}} <mat-slide-toggle class="mat-slide fixed-title-width" formControlName="isConnectorLog">
</mat-slide-toggle> {{ "widgets.gateway.show-connector" | translate }}
<mat-form-field fxFlex class="mat-block" *ngIf="GatewayLogSettingForm.get('isConnectorLog').value"> </mat-slide-toggle>
<mat-label>{{"widgets.gateway.state-param-name" | translate}}</mat-label> <mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="connectorLogState"> <input matInput formControlName="connectorLogState" placeholder="{{ 'widgets.gateway.connector-state-param-key' | translate }}">
</mat-form-field> </mat-form-field>
</section> </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 { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -27,28 +27,41 @@ import { AppState } from '@core/core.state';
}) })
export class GatewayLogsSettingsComponent extends WidgetSettingsComponent { export class GatewayLogsSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup; gatewayLogSettingForm: FormGroup;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
super(store); super(store);
} }
protected settingsForm(): UntypedFormGroup { protected settingsForm(): FormGroup {
return this.GatewayLogSettingForm; return this.gatewayLogSettingForm;
} }
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
return { return {
isConnectorLog: false, isConnectorLog: false,
connectorLogState: 'default' connectorLogState: ''
}; };
} }
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({ this.gatewayLogSettingForm = this.fb.group({
isConnectorLog: [false, []], 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. limitations under the License.
--> -->
<section class="tb-widget-settings" [formGroup]="GatewayLogSettingForm" fxLayout="column"> <div class="tb-form-panel" [formGroup]="gatewayServiceRPCSettingForm">
<mat-slide-toggle class="mat-slide" formControlName="isConnector"> <div class="tb-form-row column-xs">
{{"widgets.gateway.is-connector" | translate}} <mat-slide-toggle class="mat-slide margin" formControlName="isConnector">
</mat-slide-toggle> {{ "widgets.gateway.show-connector" | translate }}
</section> </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 { Component } from '@angular/core';
import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; 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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -27,15 +27,15 @@ import { AppState } from '@core/core.state';
}) })
export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent { export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent {
GatewayLogSettingForm: UntypedFormGroup; gatewayServiceRPCSettingForm: FormGroup;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder) { private fb: FormBuilder) {
super(store); super(store);
} }
protected settingsForm(): UntypedFormGroup { protected settingsForm(): FormGroup {
return this.GatewayLogSettingForm; return this.gatewayServiceRPCSettingForm;
} }
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
@ -45,7 +45,7 @@ export class GatewayServiceRPCSettingsComponent extends WidgetSettingsComponent
} }
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.GatewayLogSettingForm = this.fb.group({ this.gatewayServiceRPCSettingForm = this.fb.group({
isConnector: [false, []] isConnector: [false, []]
}); });
} }

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

@ -16,11 +16,10 @@
--> -->
<div class="tb-container"> <div class="tb-container">
<label class="tb-title" [ngClass]="{'tb-required': !disabled && required}">{{ label }} <label class="tb-title"
<mat-icon *ngIf="hint" class="material-icons-outlined pointer-event" [class.tb-required]="!disabled && required"
style="cursor:pointer;" [class.pointer-event]="hint"
matTooltip="{{hint}}">info_outlined tb-hint-tooltip-icon="{{ hint }}">{{ label }}
</mat-icon>
</label> </label>
<ng-container #flow="flow" <ng-container #flow="flow"
[flowConfig]="{allowDuplicateUploads: true}"> [flowConfig]="{allowDuplicateUploads: true}">

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

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

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

@ -17,5 +17,6 @@
--> -->
<ng-content></ng-content> <ng-content></ng-content>
<tb-icon class="tb-hint-tooltip-icon tb-mat-18" <tb-icon class="tb-hint-tooltip-icon tb-mat-18"
matTooltip="{{ tooltipText }}" *ngIf="tooltipText"
[matTooltipPosition]="tooltipPosition">{{ hintIcon }}</tb-icon> 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host { :host(.tb-hint-tooltip) {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
}
:host {
.tb-hint-tooltip-icon { .tb-hint-tooltip-icon {
color: #E0E0E0; color: #E0E0E0;
overflow: visible; overflow: visible;

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

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

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

@ -1293,7 +1293,6 @@
"device-details": "Device details", "device-details": "Device details",
"add-device-text": "Add new device", "add-device-text": "Add new device",
"credentials": "Credentials", "credentials": "Credentials",
"commands": "Commands",
"manage-credentials": "Manage credentials", "manage-credentials": "Manage credentials",
"delete": "Delete device", "delete": "Delete device",
"assign-devices": "Assign devices", "assign-devices": "Assign devices",
@ -1395,11 +1394,6 @@
"copyId": "Copy device Id", "copyId": "Copy device Id",
"copyAccessToken": "Copy access token", "copyAccessToken": "Copy access token",
"copy-mqtt-authentication": "Copy MQTT credentials", "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", "idCopiedMessage": "Device Id has been copied to clipboard",
"accessTokenCopiedMessage": "Device access token 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", "mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard",
@ -2643,43 +2637,18 @@
"gateway": { "gateway": {
"add-entry": "Add configuration", "add-entry": "Add configuration",
"advanced": "Advanced", "advanced": "Advanced",
"checking-device-activity": "Checking Device Activity:", "checking-device-activity": "Checking device activity",
"command": "Docker commands", "command": "Docker commands",
"command-copied-message": "Docker command has been copied to clipboard", "command-copied-message": "Docker command has been copied to clipboard",
"configuration": "Configuration", "configuration": "Configuration",
"connector-json": "Connector JSON",
"connector-add": "Add new connector", "connector-add": "Add new connector",
"connector-enabled": "Enable connector", "connector-enabled": "Enable connector",
"connector-name": "Connector name", "connector-name": "Connector name",
"connector-name-required": "Connector name is required.", "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": "Connector type",
"connector-type-required": "Connector type is required.", "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": "Connectors",
"connectors-config": "Connectors configuration", "connectors-config": "Connectors configuration",
"connectors-active": "Connector active",
"connectors-inactive": "Connector inactive",
"connectors-table-enabled": "Enabled", "connectors-table-enabled": "Enabled",
"connectors-table-name": "Name", "connectors-table-name": "Name",
"connectors-table-type": "Type", "connectors-table-type": "Type",
@ -2690,10 +2659,8 @@
"rpc-command-send": "Send", "rpc-command-send": "Send",
"rpc-command-result": "Result", "rpc-command-result": "Result",
"rpc-command-edit-params": "Edit parameters", "rpc-command-edit-params": "Edit parameters",
"select-connector": "Select connector", "gateway-configuration": "General Configuration",
"gateway-configuration": "Gateway Configuration",
"docker-label": "In order to run ThingsBoard IoT gateway in docker with credentials for this device you can use the following commands.", "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": "Create a new gateway",
"create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?",
"created-time": "Created time", "created-time": "Created time",
@ -2710,7 +2677,6 @@
"gateway-name": "Gateway name", "gateway-name": "Gateway name",
"gateway-name-required": "Gateway name is required.", "gateway-name-required": "Gateway name is required.",
"gateway-saved": "Gateway configuration successfully saved.", "gateway-saved": "Gateway configuration successfully saved.",
"gateway-search": "Gateway search",
"grpc": "GRPC", "grpc": "GRPC",
"grpc-keep-alive-timeout": "Keep alive timeout (in ms)", "grpc-keep-alive-timeout": "Keep alive timeout (in ms)",
"grpc-keep-alive-timeout-required": "Keep alive timeout is required", "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-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-min": "Max pings without data can not be less then 1",
"grpc-max-pings-without-data-pattern": "Max pings without data is not valid", "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": "Inactivity check period (in sec)",
"inactivity-check-period-seconds-required": "Inactivity check period is required", "inactivity-check-period-seconds-required": "Inactivity check period is required",
"inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1", "inactivity-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", "inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1",
"json-parse": "Not valid JSON.", "json-parse": "Not valid JSON.",
"json-required": "Field cannot be empty.", "json-required": "Field cannot be empty.",
"linux-macos": "Linux/MacOS",
"logs": { "logs": {
"logs": "Logs", "logs": "Logs",
"days": "days", "days": "days",
@ -2792,7 +2756,6 @@
"tls-private-key": "TLS + Private Key" "tls-private-key": "TLS + Private Key"
}, },
"server-port": "Server port", "server-port": "Server port",
"stats-send-period-in-sec": "Stats send period in seconds",
"statistics": { "statistics": {
"statistic": "Statistic", "statistic": "Statistic",
"statistics": "Statistics", "statistics": "Statistics",
@ -2851,7 +2814,7 @@
"sqlite": "SQLITE" "sqlite": "SQLITE"
}, },
"thingsboard": "ThingsBoard", "thingsboard": "ThingsBoard",
"thingsboard-general": "General", "general": "General",
"thingsboard-host": "ThingsBoard host", "thingsboard-host": "ThingsBoard host",
"thingsboard-host-required": "Host is required.", "thingsboard-host-required": "Host is required.",
"thingsboard-port": "ThingsBoard port", "thingsboard-port": "ThingsBoard port",
@ -2879,7 +2842,6 @@
"toggle-fullscreen": "Toggle fullscreen", "toggle-fullscreen": "Toggle fullscreen",
"transformer-json-config": "Configuration JSON*", "transformer-json-config": "Configuration JSON*",
"update-config": "Add/update configuration JSON", "update-config": "Add/update configuration JSON",
"windows": "Windows",
"hints": { "hints": {
"remote-configuration": "Enables remote configuration and management of the gateway", "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", "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", "username": "MQTT username for the gateway form ThingsBoard server",
"password": "MQTT password for the gateway form ThingsBoard server", "password": "MQTT password for the gateway form ThingsBoard server",
"ca-cert": "Path to CA certificate file", "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", "date-form": "Date format in log message",
"log-format": "Log message format", "log-format": "Log message format",
"remote-log": "Enables remote logging and logs reading from the gateway", "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", "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", "storage": "Provides configuration for saving incoming data before it is sent to the platform",
"file": "Received data saving to the hard drive",
"memory": "Received data saving to the RAM memory",
"sqlite": "Received data saving to the .db file",
"data-folder": "Path to folder, that will contains data (Relative or Absolute)",
"max-file-count": "Maximum count of file that will be created", "max-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-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", "max-records": "Maximum count of records that will be stored in one file",
@ -3505,14 +3461,7 @@
"clientId-required": "Client ID is required", "clientId-required": "Client ID is required",
"username": "Username", "username": "Username",
"username-required": "Username is required", "username-required": "Username is required",
"password": "Password",
"password-required": "Password is required",
"ca-cert": "CA certificate", "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": {
"2fa": "Two-factor authentication", "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.", "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-title": "Gateway events form title",
"events-filter": "Events filter", "events-filter": "Events filter",
"event-key-contains": "Event key contains...", "event-key-contains": "Event key contains...",
"is-connector": "Is Connector", "show-connector": "Show for the connector",
"state-param-name": "State parameter connector name" "connector-state-param-key": "Connector state parameter key",
"status": "Status",
"message": "Message",
"created-time": "Created time"
}, },
"gauge": { "gauge": {
"default-color": "Default color", "default-color": "Default color",
@ -6402,8 +6354,7 @@
"node-selected": "On node selected", "node-selected": "On node selected",
"element-click": "On HTML element click", "element-click": "On HTML element click",
"pie-slice-click": "On slice click", "pie-slice-click": "On slice click",
"row-double-click": "On row double click", "row-double-click": "On row double click"
"action-button-click": "Action button click"
} }
}, },
"paginator" : { "paginator" : {

5
ui-ngx/src/form.scss

@ -65,6 +65,9 @@
box-shadow: none; box-shadow: none;
border-radius: 0; border-radius: 0;
} }
&.no-gap {
gap: 0;
}
&.tb-slide-toggle { &.tb-slide-toggle {
padding: 0; padding: 0;
gap: 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 { &.mat-form-field-appearance-fill {
.mdc-text-field--filled:not(.mdc-text-field--disabled) { .mdc-text-field--filled:not(.mdc-text-field--disabled) {
&:before { &:before {

Loading…
Cancel
Save