From 9497f2853507c9448944e2768deed2c60e4c492d Mon Sep 17 00:00:00 2001 From: Maksym Dudnik Date: Sat, 8 Apr 2023 00:57:57 +0300 Subject: [PATCH] CE pre PR comit from PE --- .../widget_bundles/gateway_widgets.json | 45 +- ui-ngx/src/app/core/utils.ts | 7 + ui-ngx/src/app/modules/common/modules-map.ts | 9 + .../device/device-credentials.module.ts | 41 +- .../device-example-command.component.html | 101 +++ .../device-example-command.component.scss | 56 ++ .../device-example-command.component.ts | 113 +++ .../device-gateway-command.component.html | 55 -- .../device-gateway-command.component.ts | 116 --- .../gateway-command-dialog.component.html | 41 -- .../gateway-command-dialog.component.ts | 58 -- .../gateway/gateway-list-table-config.ts | 130 ---- .../gateway/gateway-list.component.html | 18 - .../gateway/gateway-list.component.scss | 40 - .../gateway/gateway-list.component.ts | 63 -- .../home/components/home-components.module.ts | 3 - .../shared-home-components.module.ts | 3 - .../device-wizard-dialog.component.html | 15 +- .../device-wizard-dialog.component.scss | 4 + .../wizard/device-wizard-dialog.component.ts | 41 +- .../device-gateway-command.component.html | 70 ++ .../device-gateway-command.component.ts | 125 ++++ .../gateway-configuration.component.html | 683 ++++++++++++++++++ .../gateway-configuration.component.scss | 166 +++++ .../device/gateway-configuration.component.ts | 521 +++++++++++++ .../device/gateway-connectors.component.html | 143 ++++ .../device/gateway-connectors.component.scss | 73 ++ .../device/gateway-connectors.component.ts | 195 +++++ .../gateway-remote-configuration-dialog.html | 72 ++ .../gateway-remote-configuration-dialog.ts | 76 ++ ui-ngx/src/app/shared/shared.module.ts | 17 +- .../assets/locale/locale.constant-en_US.json | 163 ++++- 32 files changed, 2689 insertions(+), 574 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/device/device-example-command.component.html create mode 100644 ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts delete mode 100644 ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html delete mode 100644 ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts delete mode 100644 ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html delete mode 100644 ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts delete mode 100644 ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts delete mode 100644 ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html delete mode 100644 ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss delete mode 100644 ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts create mode 100644 ui-ngx/src/app/shared/components/device/device-gateway-command.component.html create mode 100644 ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts create mode 100644 ui-ngx/src/app/shared/components/device/gateway-configuration.component.html create mode 100644 ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss create mode 100644 ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts create mode 100644 ui-ngx/src/app/shared/components/device/gateway-connectors.component.html create mode 100644 ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss create mode 100644 ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts create mode 100644 ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html create mode 100644 ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts diff --git a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json index bf94b2c5b5..bd6e15ed3d 100644 --- a/application/src/main/data/json/system/widget_bundles/gateway_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/gateway_widgets.json @@ -3,7 +3,9 @@ "alias": "gateway_widgets", "title": "Gateway widgets", "image": "data:image/png;base64,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", - "description": "Widgets to manage ThingsBoard IoT Gateway." + "description": "Widgets to manage ThingsBoard IoT Gateway.", + "externalId": null, + "name": "Gateway widgets" }, "widgetTypes": [ { @@ -64,22 +66,39 @@ } }, { - "alias": "gateway_list", - "name": "Gateway list", + "alias": "gateway_configuration2", + "name": "Gateway configuration", + "image": "data:image/jpeg;base64,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ii3bMj5ytRKUXRfXI+nX7lP95c2MS6LR7nIvE14C9kW6GU4nLsXkYCIm5VnZTleH1fVnWyF/wX95ejZo6ShCqymG/tu8Elrxk1meelEtkETXy0kFCPJR7A2kGdGclcdgy1UpJ+5Qj2Tt8dxWPimMdaz1OQgGDgn7u6M7W56sYqWBxeHmuuY9s3ibzTRlJCXt9vfTRgsrPMoG5qCPa1Ys0Wtf78EanZWpf77hs3eInbO2sdGsG1WbBGGhm7ZVk2M1afGWc0ThYVu3XZNncbGv8c71GPYN6t6tv3kladrTLrNl3FvQLvBM6vKyFbsWT6Hta312UYwfUpSlOn7xZFFwkdBdrFxbBudowVg4RVq3ZK4jb46LkGhojJM9DmzaO51KVdHjYxR8nJqfvHTRo+bqNXxIqLSZ0j0lGjNdX46ySKaCREUDxMSd6WTP+8ctmrxBRq8TjItBiaNQXhYRcqBVTx7A6rVYy8NDOmabBwvHx61WlVlIyMVarMVFomKcsyRzhJFJBIiKH7xCLimjld2zUiopVRwserZp8RwqVGGhm7ZVk2/YQRqdlal/9Tp19m4I1OytSgyhymqUZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScGdH6V6eLj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/Hj4/jx8fx4+P48fH8ePj+PHx/FsKXkVitufxzeHpSjg9KdBknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScZJxknGScJnMf7x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am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq5xJ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AXzHXy1t/wDd1/s/vv8A1rb/AO7r/Z/ff+tbf/d15AX3/rW3/wB3XkDfP+vbP/d15A3z96vWzG6VUVmysEqaT/LjJloPSgAgq2JL7yqSJ1U2otCqkqqqkZmS5IzJdZGS61Vdar5tS9GKwqdFNb0prelNb0prelNb0prelNb0prelNb0prelYSv4r5ualRf2dNDvSmt6U1vSmt6U1vSmt6U1vSmt6U1vSmt6VhOvzpWCToprelNb0prelNb0prelNb0prelNb0prelNb0prelNb0prelBjHSq0uVTo8+r99N/Cmt6U1vSmt6U1vSmt6U1vSmt6UO9KVETp8+pejFYVOimt6U1vSmt6U1vSmt6U1vSmt6U1vSmt6UO9KVETp/xb0o5+KepzqTv1S8/W5+dfvI6Uc/FPU51J36pefrc/Ov3kdKOfinqc6k79UvP1ufnXz6sU5upzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3JwpzcnCnNycKc3Uuscfxz5lxqzTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFObk4U5uThTm5OFOb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qb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41frdtTfGr9btqb41LZkNo4oKbLguIioiZTI5wuvzXaGw+GjpNuvgBjkUVMiq9VX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NX63bU3xq/W7am+NT48lG9DT5B0HNHSzjOiq4zjzXOJHcVtDQHngAsKqpnBLzaqv1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV+t21N8av1u2pvjV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", + "description": "Allows to create or edit gateway configuration.", + "descriptor": { + "type": "latest", + "sizeX": 11, + "sizeY": 8, + "resources": [], + "templateHtml": "", + "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 configuration\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + } + }, + { + "alias": "gateway_connector", + "name": "Gateway connector", "image": null, - "description": null, + "description": "Allows to create or edit gateway connectors.", "descriptor": { - "type": "static", - "sizeX": 7.5, - "sizeY": 3, + "type": "latest", + "sizeX": 11, + "sizeY": 8, "resources": [], - "templateHtml": "", - "templateCss": " .tb-entity-table {\r\n padding: 0 !important;\r\n }", - "controllerScript": "self.onInit = function() {\n}\n", + "templateHtml": "", + "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": "{}", - "settingsDirective": "", - "defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"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\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"cardHtml\":\"
HTML code here
\",\"cardCss\":\".card {\\n font-weight: bold;\\n font-size: 32px;\\n color: #999;\\n width: 100%;\\n height: 100%;\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n}\"},\"title\":\"Gateway list\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":true,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" + "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 connector\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}" } } ] diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 4018fd491f..4bde4ab41d 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -205,6 +205,13 @@ export function base64toObj(b64Encoded: string): any { return JSON.parse(json); } +export function stringToBase64(value: string): string { + return btoa(encodeURIComponent(value).replace(/%([0-9A-F]{2})/g, + function toSolidBytes(match, p1) { + return String.fromCharCode(Number('0x' + p1)); + })); +} + const scrollRegex = /(auto|scroll)/; function parentNodes(node: Node, nodes: Node[]): Node[] { diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index f7ef860562..f71adb36fa 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -296,6 +296,11 @@ import * as QueueFormComponent from '@home/components/queue/queue-form.component import * as AssetProfileComponent from '@home/components/profile/asset-profile.component'; import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component'; import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component'; +import * as DeviceGatewayCommandComponent from '@shared/components/device/device-gateway-command.component'; +import * as GatewayRemoteConfigurationDialogComponent from '@shared/components/dialog/gateway-remote-configuration-dialog'; +import * as GatewayConfigurationComponent from '@shared/components/device/gateway-configuration.component'; +import * as GatewayConnectorComponent from '@shared/components/device/gateway-connectors.component'; + import { IModulesMap } from '@modules/common/modules-map.models'; @@ -398,6 +403,10 @@ class ModulesMap implements IModulesMap { '@shared/components/help.component': HelpComponent, '@shared/components/help-markdown.component': HelpMarkdownComponent, '@shared/components/help-popup.component': HelpPopupComponent, + '@shared/components/device/device-gateway-command.component': DeviceGatewayCommandComponent, + '@shared/components/device/gateway-configuration.component': GatewayConfigurationComponent, + '@shared/components/device/gateway-connectors.component': GatewayConnectorComponent, + '@shared/components/device/gateway-remote-configuration-dialog': GatewayRemoteConfigurationDialogComponent, '@shared/components/tb-checkbox.component': TbCheckboxComponent, '@shared/components/toast.directive': TbToast, '@shared/components/tb-error.component': TbErrorComponent, diff --git a/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts b/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts index 7023f09a40..58c39a3699 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts +++ b/ui-ngx/src/app/modules/home/components/device/device-credentials.module.ts @@ -1,17 +1,32 @@ /// -/// Copyright © 2016-2023 The Thingsboard Authors +/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL /// -/// 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 +/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved. /// -/// http://www.apache.org/licenses/LICENSE-2.0 +/// NOTICE: All information contained herein is, and remains +/// the property of ThingsBoard, Inc. and its suppliers, +/// if any. The intellectual and technical concepts contained +/// herein are proprietary to ThingsBoard, Inc. +/// and its suppliers and may be covered by U.S. and Foreign Patents, +/// patents in process, and are protected by trade secret or copyright law. /// -/// 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. +/// Dissemination of this information or reproduction of this material is strictly forbidden +/// unless prior written permission is obtained from COMPANY. +/// +/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, +/// managers or contractors who have executed Confidentiality and Non-disclosure agreements +/// explicitly covering such access. +/// +/// The copyright notice above does not evidence any actual or intended publication +/// or disclosure of this source code, which includes +/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. +/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, +/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT +/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, +/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. +/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION +/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, +/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. /// import { NgModule } from '@angular/core'; @@ -22,7 +37,7 @@ import { DeviceCredentialsComponent } from '@home/components/device/device-crede import { DeviceCredentialsLwm2mComponent } from '@home/components/device/device-credentials-lwm2m.component'; import { DeviceCredentialsLwm2mServerComponent } from '@home/components/device/device-credentials-lwm2m-server.component'; import { DeviceCredentialsMqttBasicComponent } from '@home/components/device/device-credentials-mqtt-basic.component'; -import {DeviceGatewayCommandComponent} from "@home/components/device/device-gateway-command.component"; +import { DeviceExampleCommandComponent } from '@home/components/device/device-example-command.component'; @NgModule({ declarations: [ @@ -31,7 +46,7 @@ import {DeviceGatewayCommandComponent} from "@home/components/device/device-gate DeviceCredentialsLwm2mComponent, DeviceCredentialsLwm2mServerComponent, DeviceCredentialsMqttBasicComponent, - DeviceGatewayCommandComponent + DeviceExampleCommandComponent ], imports: [ CommonModule, @@ -43,7 +58,7 @@ import {DeviceGatewayCommandComponent} from "@home/components/device/device-gate DeviceCredentialsLwm2mComponent, DeviceCredentialsLwm2mServerComponent, DeviceCredentialsMqttBasicComponent, - DeviceGatewayCommandComponent + DeviceExampleCommandComponent ] }) export class DeviceCredentialsModule { } diff --git a/ui-ngx/src/app/modules/home/components/device/device-example-command.component.html b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.html new file mode 100644 index 0000000000..0663d12a14 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.html @@ -0,0 +1,101 @@ + +
+ + + MQTT + + + COAP + + + HTTP + + + In order to publish telemetry data to (ThingsBoard device) with credentials of the current device you can use the following commands. +
+
+ MQTT + 1. Setup: + 2. Send command: +
+
{{ mqttCode }}
+ +
+
+
+ COAP + 1. Setup: + 2. Send command: (based on CoAP cli) +
+
{{ coapCode }}
+ +
JSON example: {{'[{"temperature": 40}, {"isRainy": true}]'}}
+
+
+
+ HTTP + 1. Setup: + 2. Send command: +
+
{{ httpCode }}
+ +
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss new file mode 100644 index 0000000000..e3fc24d4dc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.scss @@ -0,0 +1,56 @@ +/** + * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * + * Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of ThingsBoard, Inc. and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to ThingsBoard, Inc. + * and its suppliers and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * + * Dissemination of this information or reproduction of this material is strictly forbidden + * unless prior written permission is obtained from COMPANY. + * + * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, + * managers or contractors who have executed Confidentiality and Non-disclosure agreements + * explicitly covering such access. + * + * The copyright notice above does not evidence any actual or intended publication + * or disclosure of this source code, which includes + * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. + * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, + * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT + * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, + * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. + * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION + * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, + * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + */ +:host { + + mat-button-toggle-group, .protocol-title { + margin-bottom: 20px; + } + + span { + margin-bottom: 10px; + } + + .protocol-toggle { + width: fit-content; + } + + .help-title { + padding-bottom: 5px; + } + + .protocol-send-command { + width: 100%; + } + + .pre-wrap { + white-space: pre-wrap; + } +} diff --git a/ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts new file mode 100644 index 0000000000..853084ae32 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/device/device-example-command.component.ts @@ -0,0 +1,113 @@ +/// +/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// +/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved. +/// +/// NOTICE: All information contained herein is, and remains +/// the property of ThingsBoard, Inc. and its suppliers, +/// if any. The intellectual and technical concepts contained +/// herein are proprietary to ThingsBoard, Inc. +/// and its suppliers and may be covered by U.S. and Foreign Patents, +/// patents in process, and are protected by trade secret or copyright law. +/// +/// Dissemination of this information or reproduction of this material is strictly forbidden +/// unless prior written permission is obtained from COMPANY. +/// +/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, +/// managers or contractors who have executed Confidentiality and Non-disclosure agreements +/// explicitly covering such access. +/// +/// The copyright notice above does not evidence any actual or intended publication +/// or disclosure of this source code, which includes +/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. +/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, +/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT +/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, +/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. +/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION +/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, +/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { helpBaseUrl } from '@shared/models/constants'; + +@Component({ + selector: 'tb-device-example-command', + templateUrl: './device-example-command.component.html', + styleUrls: ['./device-example-command.component.scss'] +}) + +export class DeviceExampleCommandComponent implements OnInit { + + @Input() + token: string; + + helpLink: string = helpBaseUrl + '/docs/reference/protocols/'; + + mqttCode: string; + + coapCode: string; + + httpCode: string; + + mqttSetup: string; + + coapSetup: string; + + httpSetup: string; + + protocolCtrl: FormControl; + + constructor(protected router: Router, + protected store: Store, + private translate: TranslateService) { + } + + + ngOnInit(): void { + const HOST = window.location.hostname; + this.mqttCode = `mosquitto_pub -d -q 1 -h ${HOST} -t "v1/devices/me/telemetry" -u "${this.token}" -m "{"temperature":42}"`; + this.coapCode = `cat telemetry-data.json | coap post coap://${HOST}/api/v1/${this.token}/telemetry`; + this.httpCode = `curl -v -X POST --data "{"temperature":42,"humidity":73}" http://${HOST}/api/v1/${this.token}/telemetry --header "Content-Type:application/json"`; + + this.protocolCtrl = new FormControl('mqtt'); + const mqttLink = ' Mqtt Guide'; + + // @ts-ignore + const platform = window.navigator?.userAgentData?.platform || window.navigator.platform, + macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], + windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']; + if (macosPlatforms.indexOf(platform) !== -1) { + this.mqttSetup = 'brew install mosquitto-clients'; + this.coapSetup = 'npm install coap-cli -g'; + this.httpSetup = 'brew install curl'; + } else if (windowsPlatforms.indexOf(platform) !== -1) { + this.mqttSetup = mqttLink; + this.coapSetup = 'npm install coap-cli -g'; + this.httpSetup = 'not required, available by default in windows 10+'; + } else if (/Linux/.test(platform)) { + this.mqttSetup = 'sudo apt-get install mosquitto-clients'; + this.coapSetup = 'npm install coap-cli -g'; + this.httpSetup = 'sudo apt-get install curl'; + } + } + + onDockerCodeCopied() { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('gateway.command-copied-message'), + type: 'success', + target: 'dockerCommandDialogContent', + duration: 1200, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + } +} diff --git a/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html b/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html deleted file mode 100644 index 64c60ba8fa..0000000000 --- a/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.html +++ /dev/null @@ -1,55 +0,0 @@ - -
- In order to run ThingsBoard IoT gateway (ThingsBoard IoT gateway) in docker with credentials for this device you can use the following commands. -
- - gateway.windows - gateway.linux-macos - -
-
-
-
-
{{ linuxCode }}
- -
-
-
-
-
{{ windowsCode }}
- -
-
-
diff --git a/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts b/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts deleted file mode 100644 index 40c7375571..0000000000 --- a/ui-ngx/src/app/modules/home/components/device/device-gateway-command.component.ts +++ /dev/null @@ -1,116 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import {Component, Input, OnInit} from '@angular/core'; -import { - FormControl, -} from '@angular/forms'; -import {Router} from "@angular/router"; -import {Store} from "@ngrx/store"; -import {AppState} from "@core/core.state"; -import {TranslateService} from "@ngx-translate/core"; -import {ActionNotificationShow} from "@core/notification/notification.actions"; -import {DeviceService} from "@core/http/device.service"; -import {helpBaseUrl} from "@shared/models/constants"; - -enum OsType { - linux = 'linux', - macos = 'macos', - windows = 'win' -} - -@Component({ - selector: 'tb-gateway-command', - templateUrl: './device-gateway-command.component.html', - styleUrls: [] -}) - -export class DeviceGatewayCommandComponent implements OnInit { - - @Input() - token: string; - - @Input() - deviceId: string; - - linuxCode: string; - windowsCode: string; - selectedOSControll: FormControl; - osTypes = OsType; - helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/'; - - constructor(protected router: Router, - protected store: Store, - private translate: TranslateService, - private deviceService: DeviceService) { - } - - - ngOnInit(): void { - const HOST = window.location.hostname; - if (this.deviceId) { - this.deviceService.getDeviceCredentials(this.deviceId).subscribe(credentials=>{ - this.token = credentials.credentialsId; - this.createRunCode(HOST); - }) - } - this.selectedOSControll = new FormControl(''); - // @ts-ignore - const platform = window.navigator?.userAgentData?.platform || window.navigator.platform, - macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], - windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']; - if (macosPlatforms.indexOf(platform) !== -1) { - this.selectedOSControll.setValue(OsType.macos); - } else if (windowsPlatforms.indexOf(platform) !== -1) { - this.selectedOSControll.setValue(OsType.windows); - } else if (/Linux/.test(platform)) { - this.selectedOSControll.setValue(OsType.linux); - } - this.createRunCode(HOST); - } - - createRunCode(HOST) { - this.linuxCode = "docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v " + - "~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name tb-gateway -e host=" + - HOST + - " -e port=1883 -e accessToken=" + - this.token + - " --restart always thingsboard/tb-gateway"; - this.windowsCode = "docker run -it -v %HOMEPATH%/tb-gateway/config:/thingsboard_gateway/config -v " + - "%HOMEPATH%/tb-gateway/extensions:/thingsboard_gateway/extensions -v %HOMEPATH%/tb-gateway/logs:/thingsboard_gateway/logs " + - "--name tb-gateway -e host=" + - HOST + - " -e port=1883 -e accessToken=" + - this.token + - " --restart always thingsboard/tb-gateway"; - } - - goToDocumentationPage() { - window.open(this.helpLink, '_blank'); - } - - onDockerCodeCopied() { - this.store.dispatch(new ActionNotificationShow( - { - message: this.translate.instant('gateway.command-copied-message'), - type: 'success', - target: 'dockerCommandDialogContent', - duration: 1200, - verticalPosition: 'bottom', - horizontalPosition: 'left' - })); - } -} diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html b/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html deleted file mode 100644 index 6318b74537..0000000000 --- a/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.html +++ /dev/null @@ -1,41 +0,0 @@ - -
- -

gateway.command

- - -
- - -
- -
- -
-
diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts b/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts deleted file mode 100644 index 60bf9c1e21..0000000000 --- a/ui-ngx/src/app/modules/home/components/gateway/gateway-command-dialog.component.ts +++ /dev/null @@ -1,58 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import {Component, Inject, OnInit} from '@angular/core'; -import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; -import {Store} from '@ngrx/store'; -import {AppState} from '@core/core.state'; -import {Router} from '@angular/router'; -import {DialogComponent} from '@app/shared/components/dialog.component'; -import {TranslateService} from '@ngx-translate/core'; -import {Device, DeviceCredentials} from "@shared/models/device.models"; - -export interface GatewayCommandDialogData { - device: Device, - credentials: DeviceCredentials -} - -enum OsType { - linux = 'linux', - macos = 'macos', - windows = 'win' -} - -@Component({ - selector: 'tb-gateway-command-dialog', - templateUrl: './gateway-command-dialog.component.html', - styleUrls: [] -}) -export class GatewayCommandDialogComponent extends DialogComponent implements OnInit { - - constructor(protected router: Router, - protected store: Store, - @Inject(MAT_DIALOG_DATA) public data: GatewayCommandDialogData, - public dialogRef: MatDialogRef,) { - super(store, router, dialogRef); - } - - ngOnInit() { - } - - close(): void { - this.dialogRef.close(); - } - -} diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts b/ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts deleted file mode 100644 index 00ff31343e..0000000000 --- a/ui-ngx/src/app/modules/home/components/gateway/gateway-list-table-config.ts +++ /dev/null @@ -1,130 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import { - DateEntityTableColumn, - EntityTableColumn, - EntityTableConfig -} from '@home/models/entity/entities-table-config.models'; -import {EntityType, EntityTypeResource} from '@shared/models/entity-type.models'; -import {TranslateService} from '@ngx-translate/core'; -import {DatePipe} from '@angular/common'; -import {Direction} from '@shared/models/page/sort-order'; -import {MatDialog} from '@angular/material/dialog'; -import {TimePageLink} from '@shared/models/page/page-link'; -import {Observable} from 'rxjs'; -import {emptyPageData, PageData} from '@shared/models/page/page-data'; -import {UtilsService} from '@core/services/utils.service'; -import {DeviceService} from "@core/http/device.service"; -import {AttributeService} from "@core/http/attribute.service"; -import {Device} from "@shared/models/device.models"; -import { - GatewayCommandDialogComponent, - GatewayCommandDialogData -} from "@home/components/gateway/gateway-command-dialog.component"; -import {ActionNotificationShow} from "@core/notification/notification.actions"; -import {Store} from "@ngrx/store"; -import {AppState} from "@core/core.state"; -import {map} from "rxjs/operators"; -import {EntityService} from "@core/http/entity.service"; - -export class GatewayListTableConfig extends EntityTableConfig { - - constructor(protected store: Store, - private deviceService: DeviceService, - private attributeService: AttributeService, - private entityService: EntityService, - private datePipe: DatePipe, - private translate: TranslateService, - private utils: UtilsService, - private dialog: MatDialog, - updateOnInit = true) { - super(); - this.loadDataOnInit = updateOnInit; - this.tableTitle = 'Gateway list'; - this.useTimePageLink = false; - this.pageMode = false; - this.displayPagination = false; - this.detailsPanelEnabled = false; - this.selectionEnabled = false; - this.searchEnabled = false; - this.addEnabled = false; - this.entitiesDeleteEnabled = false; - this.actionsColumnTitle = ''; - this.entityTranslations = { - noEntities: 'gateway.no-gateway-found', - search: 'gateway.gateway-search' - }; - this.entityResources = {} as EntityTypeResource; - - this.entitiesFetchFunction = () => this.fetchGateways(); - - this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; - - this.columns.push( - new DateEntityTableColumn('createdTime', 'gateway.created-time', this.datePipe, '150px')); - - this.columns.push( - new EntityTableColumn('entityName', 'gateway.gateway-name', '20%', - (entity => this.utils.customTranslation(entity.name, entity.name)) - ) - ); - - this.cellActionDescriptors.push( - { - name: this.translate.instant('gateway.command'), - icon: 'vpn_key', - isEnabled: () => true, - onAction: ($event, entity) => this.showGatewayDockerCommand(entity) - } - ); - } - - showGatewayDockerCommand(entity: Device) { - this.deviceService.getDeviceCredentials(entity.id.id).subscribe(credentials => { - this.dialog.open - (GatewayCommandDialogComponent, - { - disableClose: true, - panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], - data: { - credentials: credentials, - device: entity - } - }).afterClosed().subscribe( - (res) => { - if (res) { - this.updateData(); - } - } - ); - }, error => { - const messageToShow = `
${error}
`; - this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'})); - }) - } - - fetchGateways(): Observable> { - return this.entityService.getEntitiesByNameFilter(EntityType.DEVICE, "", -1).pipe( - map((array: Array) => { - const pageData = emptyPageData(); - pageData.data = array.filter(device => device.additionalInfo?.gateway); - pageData.totalPages = 1; - pageData.totalElements = array.length; - return pageData; - })) - } -} diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html b/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html deleted file mode 100644 index 7190c87ead..0000000000 --- a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.html +++ /dev/null @@ -1,18 +0,0 @@ - - diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss b/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss deleted file mode 100644 index 09f06ae6b5..0000000000 --- a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.scss +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright © 2016-2022 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -:host { - width: 100%; - height: 100%; - display: block; - .tb-table-widget { - .table-container { - position: relative; - } - .mat-table { - .mat-row { - &.invisible { - visibility: hidden; - } - } - } - span.no-data-found { - position: absolute; - top: 60px; - bottom: 0; - left: 0; - right: 0; - } - } -} - diff --git a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts b/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts deleted file mode 100644 index 14e95234a2..0000000000 --- a/ui-ngx/src/app/modules/home/components/gateway/gateway-list.component.ts +++ /dev/null @@ -1,63 +0,0 @@ -/// -/// Copyright © 2016-2022 The Thingsboard Authors -/// -/// Licensed under the Apache License, Version 2.0 (the "License"); -/// you may not use this file except in compliance with the License. -/// You may obtain a copy of the License at -/// -/// http://www.apache.org/licenses/LICENSE-2.0 -/// -/// Unless required by applicable law or agreed to in writing, software -/// distributed under the License is distributed on an "AS IS" BASIS, -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -/// See the License for the specific language governing permissions and -/// limitations under the License. -/// - -import {Component, OnInit} from '@angular/core'; -import {UtilsService} from '@core/services/utils.service'; -import {TranslateService} from '@ngx-translate/core'; -import {DeviceService} from '@core/http/device.service'; -import {AttributeService} from '@core/http/attribute.service'; -import {GatewayListTableConfig} from "@home/components/gateway/gateway-list-table-config"; -import {DatePipe} from "@angular/common"; -import {MatDialog} from "@angular/material/dialog"; -import {Store} from "@ngrx/store"; -import {AppState} from "@core/core.state"; -import {EntityService} from "@core/http/entity.service"; - -@Component({ - selector: 'tb-gateway-list', - templateUrl: './gateway-list.component.html', - styleUrls: ['./gateway-list.component.scss'] -}) - - -export class GatewayListComponent implements OnInit { - gatewayListTableConfig: GatewayListTableConfig; - - constructor( - protected store: Store, - private utils: UtilsService, - private translate: TranslateService, - private datePipe: DatePipe, - private deviceService: DeviceService, - private entityService: EntityService, - private attributeService: AttributeService, - private dialog: MatDialog, - ) { - } - - ngOnInit(): void { - this.gatewayListTableConfig = new GatewayListTableConfig( - this.store, - this.deviceService, - this.attributeService, - this.entityService, - this.datePipe, - this.translate, - this.utils, - this.dialog - ); - } -} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 2b9a446e20..7a91645e31 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -178,7 +178,6 @@ import { modulesMap } from '@modules/common/modules-map'; import { AlarmAssigneePanelComponent } from '@home/components/alarm/alarm-assignee-panel.component'; import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component'; -import { GatewayListComponent } from "@home/components/gateway/gateway-list.component"; @NgModule({ declarations: @@ -203,7 +202,6 @@ import { GatewayListComponent } from "@home/components/gateway/gateway-list.comp AlarmTableHeaderComponent, AlarmTableComponent, AlarmAssigneePanelComponent, - GatewayListComponent, AttributeTableComponent, AddAttributeDialogComponent, EditAttributeValuePanelComponent, @@ -354,7 +352,6 @@ import { GatewayListComponent } from "@home/components/gateway/gateway-list.comp RelationFiltersComponent, AlarmTableComponent, AlarmAssigneePanelComponent, - GatewayListComponent, AttributeTableComponent, AliasesEntitySelectComponent, AliasesEntityAutocompleteComponent, diff --git a/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts index 912fcfb52e..45ee713d10 100644 --- a/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/shared-home-components.module.ts @@ -18,7 +18,6 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from '@app/shared/shared.module'; import { AlarmDetailsDialogComponent } from '@home/components/alarm/alarm-details-dialog.component'; -import { GatewayCommandDialogComponent } from "@home/components/gateway/gateway-command-dialog.component"; import { SHARED_HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { DeviceCredentialsModule } from "@home/components/device/device-credentials.module"; import { AlarmCommentComponent } from '@home/components/alarm/alarm-comment.component'; @@ -35,7 +34,6 @@ import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.co AlarmCommentComponent, AlarmCommentDialogComponent, AlarmAssigneeComponent - GatewayCommandDialogComponent ], imports: [ CommonModule, @@ -47,7 +45,6 @@ import { AlarmAssigneeComponent } from '@home/components/alarm/alarm-assignee.co AlarmCommentComponent, AlarmCommentDialogComponent, AlarmAssigneeComponent - GatewayCommandDialogComponent ] }) export class SharedHomeComponentsModule { } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html index c1485cf450..52a31314e2 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -29,7 +29,7 @@
- + check @@ -178,9 +178,17 @@ +
+ + + + +
-
+
@@ -195,10 +203,11 @@
+ (click)="add()">{{ (deviceCredentials ? 'action.close': 'action.add') | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss index 0fe18467fd..e3fbd3c7c6 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss @@ -37,6 +37,10 @@ } :host ::ng-deep { + .commands-container { + padding: 24px; + } + .mat-mdc-dialog-content { display: flex; flex-direction: column; diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts index 77916d379c..80aa438e2d 100644 --- a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -24,6 +24,8 @@ import { Router } from '@angular/router'; import { createDeviceProfileConfiguration, createDeviceProfileTransportConfiguration, + Device, + DeviceCredentials, DeviceProfile, DeviceProfileInfo, DeviceProfileType, @@ -57,9 +59,9 @@ import { deepTrim } from '@core/utils'; styleUrls: ['./device-wizard-dialog.component.scss'] }) export class DeviceWizardDialogComponent extends - DialogComponent implements OnDestroy, ErrorStateMatcher { + DialogComponent implements OnDestroy, ErrorStateMatcher { - @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatStepper; + @ViewChild('addDeviceWizardStepper') addDeviceWizardStepper: MatStepper; selectedIndex = 0; @@ -69,7 +71,7 @@ export class DeviceWizardDialogComponent extends entityType = EntityType; - deviceTransportTypes = Object.values(DeviceTransportType); + deviceTransportTypes = Object.values(DeviceTransportType) as DeviceTransportType[]; deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; @@ -91,6 +93,10 @@ export class DeviceWizardDialogComponent extends serviceType = ServiceType.TB_RULE_ENGINE; + device: Device; + + deviceCredentials: DeviceCredentials; + private subscriptions: Subscription[] = []; private currentDeviceProfileTransportType = DeviceTransportType.DEFAULT; @@ -98,7 +104,7 @@ export class DeviceWizardDialogComponent extends protected router: Router, @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData>, @SkipSelf() private errorStateMatcher: ErrorStateMatcher, - public dialogRef: MatDialogRef, + public dialogRef: MatDialogRef, private deviceProfileService: DeviceProfileService, private deviceService: DeviceService, private breakpointObserver: BreakpointObserver, @@ -263,13 +269,15 @@ export class DeviceWizardDialogComponent extends } add(): void { - if (this.allValid()) { + if (this.deviceCredentials) { + this.dialogRef.close(this.device); + } else if (this.allValid()) { this.createDeviceProfile().pipe( mergeMap(profileId => this.createDevice(profileId)), - mergeMap(device => this.saveCredentials(device)) + mergeMap(device => this.saveCredentials(device)), ).subscribe( - (created) => { - this.dialogRef.close(created); + (device) => { + this.device = device; } ); } @@ -326,7 +334,7 @@ export class DeviceWizardDialogComponent extends } } - private createDevice(profileId): Observable> { + private createDevice(profileId): Observable { const device = { name: this.deviceWizardFormGroup.get('name').value, label: this.deviceWizardFormGroup.get('label').value, @@ -349,10 +357,10 @@ export class DeviceWizardDialogComponent extends this.addDeviceWizardStepper.selectedIndex = 0; return throwError(e); }) - ); + ) as Observable; } - private saveCredentials(device: BaseData): Observable { + private saveCredentials(device: Device): Observable { if (this.credentialsFormGroup.get('setCredential').value) { return this.deviceService.getDeviceCredentials(device.id.id).pipe( mergeMap( @@ -370,9 +378,16 @@ export class DeviceWizardDialogComponent extends ); } ), - map(() => true)); + map(() => device)); + } else { + return this.deviceService.getDeviceCredentials(device.id.id).pipe( + map((deviceCredentials) => { + this.deviceCredentials = deviceCredentials; + return device; + }) + ); } - return of(true); + return of(device); } allValid(): boolean { diff --git a/ui-ngx/src/app/shared/components/device/device-gateway-command.component.html b/ui-ngx/src/app/shared/components/device/device-gateway-command.component.html new file mode 100644 index 0000000000..711897ce4d --- /dev/null +++ b/ui-ngx/src/app/shared/components/device/device-gateway-command.component.html @@ -0,0 +1,70 @@ + +
+ In order to run ThingsBoard IoT gateway (ThingsBoard IoT gateway) in docker with credentials for this device you can use the following commands. +
+ + gateway.windows + gateway.linux-macos + +
+
+
+
+
{{ linuxCode }}
+ +
+
+
+
+
{{ windowsCode }}
+ +
+
+
diff --git a/ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts b/ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts new file mode 100644 index 0000000000..ab0548e273 --- /dev/null +++ b/ui-ngx/src/app/shared/components/device/device-gateway-command.component.ts @@ -0,0 +1,125 @@ +/// +/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// +/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved. +/// +/// NOTICE: All information contained herein is, and remains +/// the property of ThingsBoard, Inc. and its suppliers, +/// if any. The intellectual and technical concepts contained +/// herein are proprietary to ThingsBoard, Inc. +/// and its suppliers and may be covered by U.S. and Foreign Patents, +/// patents in process, and are protected by trade secret or copyright law. +/// +/// Dissemination of this information or reproduction of this material is strictly forbidden +/// unless prior written permission is obtained from COMPANY. +/// +/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, +/// managers or contractors who have executed Confidentiality and Non-disclosure agreements +/// explicitly covering such access. +/// +/// The copyright notice above does not evidence any actual or intended publication +/// or disclosure of this source code, which includes +/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. +/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, +/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT +/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, +/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. +/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION +/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, +/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { DeviceService } from '@core/http/device.service'; +import { helpBaseUrl } from '@shared/models/constants'; + +enum OsType { + linux = 'linux', + macos = 'macos', + windows = 'win' +} + +@Component({ + selector: 'tb-gateway-command', + templateUrl: './device-gateway-command.component.html', + styleUrls: [] +}) + +export class DeviceGatewayCommandComponent implements OnInit { + + @Input() + token: string; + + @Input() + deviceId: string; + + linuxCode: string; + windowsCode: string; + selectedOSCControl: FormControl; + osTypes = OsType; + helpLink: string = helpBaseUrl + '/docs/iot-gateway/install/docker-installation/'; + + constructor(protected router: Router, + protected store: Store, + private translate: TranslateService, + private deviceService: DeviceService) { + } + + + ngOnInit(): void { + const HOST = window.location.hostname; + if (this.deviceId) { + this.deviceService.getDeviceCredentials(this.deviceId).subscribe(credentials => { + this.token = credentials.credentialsId; + this.createRunCode(HOST); + }); + } + this.selectedOSCControl = new FormControl(''); + // @ts-ignore + const platform = window.navigator?.userAgentData?.platform || window.navigator.platform, + macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], + windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']; + if (macosPlatforms.indexOf(platform) !== -1) { + this.selectedOSCControl.setValue(OsType.macos); + } else if (windowsPlatforms.indexOf(platform) !== -1) { + this.selectedOSCControl.setValue(OsType.windows); + } else if (/Linux/.test(platform)) { + this.selectedOSCControl.setValue(OsType.linux); + } + this.createRunCode(HOST); + } + + createRunCode(HOST) { + this.linuxCode = 'docker run -it -v ~/.tb-gateway/logs:/thingsboard_gateway/logs -v ' + + '~/.tb-gateway/extensions:/thingsboard_gateway/extensions -v ~/.tb-gateway/config:/thingsboard_gateway/config --name tb-gateway -e host=' + + HOST + + ' -e port=1883 -e accessToken=' + + this.token + + ' --restart always thingsboard/tb-gateway'; + this.windowsCode = 'docker run -it -v %HOMEPATH%/tb-gateway/config:/thingsboard_gateway/config -v ' + + '%HOMEPATH%/tb-gateway/extensions:/thingsboard_gateway/extensions -v %HOMEPATH%/tb-gateway/logs:/thingsboard_gateway/logs ' + + '--name tb-gateway -e host=' + + HOST + + ' -e port=1883 -e accessToken=' + + this.token + + ' --restart always thingsboard/tb-gateway'; + } + + onDockerCodeCopied() { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('gateway.command-copied-message'), + type: 'success', + target: 'dockerCommandDialogContent', + duration: 1200, + verticalPosition: 'bottom', + horizontalPosition: 'left' + })); + } +} diff --git a/ui-ngx/src/app/shared/components/device/gateway-configuration.component.html b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.html new file mode 100644 index 0000000000..6c1300ece4 --- /dev/null +++ b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.html @@ -0,0 +1,683 @@ + +
+ +

gateway.gateway-configuration

+ + +
+ + +
+ + +
+ + {{ 'gateway.remote-configuration' | translate }} + + + {{ 'gateway.remote-shell' | translate }} + + + gateway.thingsboard-host + + + {{ 'gateway.thingsboard-host-required' | translate }} + + + + gateway.thingsboard-port + + + {{'gateway.thingsboard-port-required' | translate }} + + + {{'gateway.thingsboard-port-min' | translate }} + + + {{'gateway.thingsboard-port-max' | translate }} + + + {{'gateway.thingsboard-port-pattern' | translate }} + + + +
+
+ + + security.security + +
+ + + {{ securityType.value | translate }} + + + + security.access-token + + + {{'security.access-token-required' | translate}} + + + + security.clientId + + + {{'security.clientId-required' | translate}} + + + + security.username + + + {{'security.username-required' | translate}} + + + + security.password + + + {{'security.password-required' | translate}} + + + + + + + + +
+
+
+
+
+ +
+ + +
+ + gateway.logs.date-format + + + {{'gateway.logs.date-format-required' | translate }} + + + + gateway.logs.log-format + + + {{'gateway.logs.log-format-required' | translate }} + + + + + {{'gateway.logs.remote' | translate}} + {{'gateway.logs.local' | translate}} + +
+ + {{ 'gateway.logs.remote-logs' | translate }} + + + gateway.logs.level + + {{logLevel}} + + +
+
+ + + {{ logConfig }} + + +
+ + gateway.logs.level + + {{logLevel}} + + + + gateway.logs.file-path + + + {{'gateway.logs.file-path-required' | translate }} + + + + gateway.logs.saving-period + + + {{'gateway.logs.saving-period-required' | translate }} + + + {{'gateway.logs.saving-period-min' | translate }} + + + + + + + {{period.value | translate}} + + + + + gateway.logs.backup-count + + + {{'gateway.logs.backup-count-required' | translate }} + + + {{'gateway.logs.backup-count-min' | translate }} + + +
+
+
+
+
+
+
+ +
+ + + + gateway.storage + +
+ + + {{ storageType.value | translate }} + + + + gateway.storage-read-record-count + + + {{'gateway.storage-read-record-count-required' | translate}} + + + {{'gateway.storage-read-record-count-min' | translate}} + + + {{'gateway.storage-read-record-count-pattern' | translate}} + + + + gateway.storage-max-records + + + {{'gateway.storage-max-records-required' | translate}} + + + {{'gateway.storage-max-records-min' | translate}} + + + {{'gateway.storage-max-records-pattern' | translate}} + + + + gateway.storage-data-folder-path + + + {{'gateway.storage-data-folder-path-required' | translate}} + + + + gateway.storage-max-files + + + {{'gateway.storage-max-files-required' | translate}} + + + {{'gateway.storage-max-files-min' | translate}} + + + {{'gateway.storage-max-files-pattern' | translate}} + + + + gateway.storage-max-read-record-count + + + {{'gateway.storage-max-read-record-count-required' | translate}} + + + {{'gateway.storage-max-read-record-count-min' | translate}} + + + {{'gateway.storage-max-read-record-count-pattern' | translate}} + + + + gateway.storage-max-file-records + + + {{'gateway.storage-max-records-required' | translate}} + + + {{'gateway.storage-max-records-min' | translate}} + + + {{'gateway.storage-max-records-pattern' | translate}} + + + + gateway.storage-path + + + {{'gateway.storage-path-required' | translate}} + + + + gateway.messages-ttl-check-in-hours + + + {{'gateway.messages-ttl-check-in-hours-required' | translate}} + + + {{'gateway.messages-ttl-check-in-hours-min' | translate}} + + + {{'gateway.messages-ttl-check-in-hours-pattern' | translate}} + + + + gateway.messages-ttl-in-days + + + {{'gateway.messages-ttl-in-days-required' | translate}} + + + {{'gateway.messages-ttl-in-days-min' | translate}} + + + {{'gateway.messages-ttl-in-days-pattern' | translate}} + + +
+
+
+
+
+ +
+ + +
+ + {{ 'gateway.grpc' | translate }} + + + {{ 'gateway.permit-without-calls' | translate }} + + + gateway.server-port + + + {{'gateway.thingsboard-port-required' | translate }} + + + {{'gateway.thingsboard-port-min' | translate }} + + + {{'gateway.thingsboard-port-max' | translate }} + + + {{'gateway.thingsboard-port-pattern' | translate }} + + + + gateway.grpc-keep-alive-timeout + + + {{'gateway.grpc-keep-alive-timeout-required' | translate }} + + + {{'gateway.grpc-keep-alive-timeout-min' | translate }} + + + {{'gateway.grpc-keep-alive-timeout-pattern' | translate }} + + + + gateway.grpc-keep-alive + + + {{'gateway.grpc-keep-alive-required' | translate }} + + + {{'gateway.grpc-keep-alive-min' | translate }} + + + {{'gateway.grpc-keep-alive-pattern' | translate }} + + + + gateway.grpc-min-time-between-pings + + + {{'gateway.grpc-min-time-between-pings-required' | translate }} + + + {{'gateway.grpc-min-time-between-pings-min' | translate }} + + + {{'gateway.grpc-min-time-between-pings-pattern' | translate }} + + + + gateway.grpc-max-pings-without-data + + + {{'gateway.grpc-max-pings-without-data-required' | translate }} + + + {{'gateway.grpc-max-pings-without-data-min' | translate }} + + + {{'gateway.grpc-max-pings-without-data-pattern' | translate }} + + + + gateway.grpc-min-ping-interval-without-data + + + {{'gateway.grpc-min-ping-interval-without-data-required' | translate }} + + + {{'gateway.grpc-min-ping-interval-without-data-min' | translate }} + + + {{'gateway.grpc-min-ping-interval-without-data-pattern' | translate }} + + +
+
+
+
+
+ +
+ + +
+ + {{ 'gateway.statistics.statistics' | translate }} + + + gateway.statistics.send-period + + + {{'gateway.statistics.send-period-required' | translate }} + + + {{'gateway.statistics.send-period-min' | translate }} + + + {{'gateway.statistics.send-period-pattern' | translate }} + + + + gateway.statistics.check-connectors-configuration + + + {{'gateway.statistics.check-connectors-configuration-required' | translate }} + + + {{'gateway.statistics.check-connectors-configuration-min' | translate }} + + + {{'gateway.statistics.check-connectors-configuration-pattern' | translate }} + + +
+
+ + + gateway.statistics.statistics + +
+
+ + + gateway.statistics.attribute-name + + + {{'gateway.statistics.attribute-name-required' | translate }} + + + + gateway.statistics.timeout + + + {{'gateway.statistics.timeout-required' | translate }} + + + {{'gateway.statistics.timeout-min' | translate }} + + + {{'gateway.statistics.timeout-pattern' | translate }} + + + + gateway.statistics.command + + + {{'gateway.statistics.command-required' | translate }} + + + + +
+ +
+
+
+
+
+ +
+ + + + + + {{ 'gateway.checking-device-activity' | translate }} + + + +
+ + gateway.inactivity-timeout-seconds + + + {{'gateway.inactivity-timeout-seconds-required' | translate }} + + + {{'gateway.inactivity-timeout-seconds-min' | translate }} + + + + gateway.inactivity-check-period-seconds + + + {{'gateway.inactivity-check-period-seconds-required' | translate }} + + + {{'gateway.inactivity-check-period-seconds-min' | translate }} + + +
+
+ + + gateway.advanced + +
+ + gateway.min-pack-send-delay + + + {{ 'gateway.min-pack-send-delay-required' | translate }} + + + {{ 'gateway.min-pack-send-delay-min' | translate }} + + + + gateway.mqtt-qos + + + {{ 'gateway.mqtt-qos-required' | translate}} + + + {{ 'gateway.mqtt-qos-range' | translate}} + + + {{ 'gateway.mqtt-qos-range' | translate}} + + +
+
+ +
+
+
+
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss new file mode 100644 index 0000000000..d6bcdf6e8a --- /dev/null +++ b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.scss @@ -0,0 +1,166 @@ +/** + * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * + * Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of ThingsBoard, Inc. and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to ThingsBoard, Inc. + * and its suppliers and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * + * Dissemination of this information or reproduction of this material is strictly forbidden + * unless prior written permission is obtained from COMPANY. + * + * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, + * managers or contractors who have executed Confidentiality and Non-disclosure agreements + * explicitly covering such access. + * + * The copyright notice above does not evidence any actual or intended publication + * or disclosure of this source code, which includes + * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. + * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, + * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT + * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, + * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. + * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION + * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, + * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + */ +:host { + width: 100%; + height: 100%; + display: block; + + .mat-toolbar { + background: transparent; + color: rgba(0, 0, 0, .87) !important; + } + + .mat-content { + .expansion-panel-header { + font-weight: 600; + color: rgba(0, 0, 0, .87) !important; + } + + mat-slide-toggle { + margin-bottom: 16px; + } + + mat-form-field { + margin-right: 15px; + } + + .block-title { + font-size: 20px; + font-weight: 400; + padding-top: 16px; + } + + .hover-cursor { + cursor: pointer; + } + } + + .security-toggle-group { + padding-bottom: 0; + background: transparent !important; + margin-bottom: 15px; + border: none; + + .mat-button-toggle { + border-radius: 15px; + border: 1px solid rgba(0, 0, 0, 0); + background: rgba(0, 0, 0, .05); + margin: 5px; + color: rgba(0, 0, 0, .2); + font-weight: 500; + } + + .mat-button-toggle-checked { + border-radius: 15px; + border: 1px solid #305680; + background: white; + color: #305680; + } + + ::ng-deep span { + text-transform: capitalize; + line-height: 20px; + font-size: 13px; + padding: 5px 25px; + } + + ::ng-deep .mat-button-toggle-ripple { + border-radius: 15px; + } + + ::ng-deep .mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { + opacity: 0 !important; + } + } + + .logs-label { + font-weight: 500; + margin-bottom: 10px; + } + + .logs-toggle-group { + border: none; + border-radius: 15px; + background: rgba(0, 0, 0, .05); + color: rgba(0, 0, 0, .2); + font-weight: 500; + + .mat-button-toggle { + border: none; + background: rgba(0, 0, 0, .0); + color: rgba(0, 0, 0, .2); + font-weight: 500; + } + + .mat-button-toggle-checked { + border-radius: 15px; + color: white; + background: #305680; + } + + ::ng-deep span { + line-height: 20px; + font-size: 13px; + padding: 5px 25px; + } + + ::ng-deep .mat-button-toggle-ripple { + border-radius: 15px; + } + + ::ng-deep .mat-button-toggle-appearance-standard:not(.mat-button-toggle-disabled):hover .mat-button-toggle-focus-overlay { + opacity: 0 !important; + } + } + + .first-capital { + text-transform: capitalize; + } + + ::ng-deep.mat-tab-label-active { + color: white; + opacity: 1; + } + + ::ng-deep.mat-tab-label, ::ng-deep.mat-tab-label-active { + min-width: 50px !important; + padding: 3px !important; + margin: 3px !important; + flex-grow: 1; + } + + ::ng-deep.mat-ink-bar { + height: 100%; + z-index: -10; + border-radius: 5px; + } +} + diff --git a/ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts new file mode 100644 index 0000000000..2fd02f60b7 --- /dev/null +++ b/ui-ngx/src/app/shared/components/device/gateway-configuration.component.ts @@ -0,0 +1,521 @@ +/// +/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// +/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved. +/// +/// NOTICE: All information contained herein is, and remains +/// the property of ThingsBoard, Inc. and its suppliers, +/// if any. The intellectual and technical concepts contained +/// herein are proprietary to ThingsBoard, Inc. +/// and its suppliers and may be covered by U.S. and Foreign Patents, +/// patents in process, and are protected by trade secret or copyright law. +/// +/// Dissemination of this information or reproduction of this material is strictly forbidden +/// unless prior written permission is obtained from COMPANY. +/// +/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, +/// managers or contractors who have executed Confidentiality and Non-disclosure agreements +/// explicitly covering such access. +/// +/// The copyright notice above does not evidence any actual or intended publication +/// or disclosure of this source code, which includes +/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. +/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, +/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT +/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, +/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. +/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION +/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, +/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { base64toObj, base64toString, objToBase64, stringToBase64 } from '@core/utils'; +import { AttributeService } from '@core/http/attribute.service'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { + GatewayRemoteConfigurationDialogComponent, + GatewayRemoteConfigurationDialogData +} from '@shared/components/dialog/gateway-remote-configuration-dialog'; +import { DeviceService } from '@core/http/device.service'; + +export enum StorageTypes { + MEMORY = 'memory', + FILE = 'file', + SQLITE = 'sqlite' +} + +export enum GatewayLogLevel { + none = 'NONE', + critical = 'CRITICAL', + error = 'ERROR', + warning = 'WARNING', + info = 'INFO', + debug = 'DEBUG' +} + +export enum LogSavingPeriod { + days = 'd', + hours = 'h', + minutes = 'm', + seconds = 's' +} + +export enum LocalLogsConfigs { + service = 'service', + connector = 'connector', + converter = 'converter', + tb_connection = 'tb_connection', + storage = 'storage', + extension = 'extension' +} + +export const logSavingPeriodTranslations = new Map( + [ + [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.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.ACCESS_TOKEN, 'gateway.security-types.access-token'], + [SecurityTypes.USERNAME_PASSWORD, 'gateway.security-types.username-password'], + [SecurityTypes.TLS_ACCESS_TOKEN, 'gateway.security-types.tls-access-token'], + [SecurityTypes.TLS_PRIVATE_KEY, 'gateway.security-types.tls-private-key'], + ] +); + +@Component({ + selector: 'tb-gateway-configuration', + templateUrl: './gateway-configuration.component.html', + styleUrls: ['./gateway-configuration.component.scss'] +}) +export class GatewayConfigurationComponent implements OnInit { + + gatewayConfigGroup: FormGroup; + + storageTypes = storageTypesTranslations; + + logSavingPeriods = logSavingPeriodTranslations; + + securityTypes = securityTypesTranslationsMap; + + gatewayLogLevel = Object.values(GatewayLogLevel); + + localLogsConfigs = Object.values(LocalLogsConfigs); + + @Input() + device: EntityId; + + @Input() + dialogRef: MatDialogRef; + + logSelector: FormControl; + + + constructor(protected router: Router, + protected store: Store, + protected fb: FormBuilder, + protected attributeService: AttributeService, + protected deviceService: DeviceService, + public dialog: MatDialog) { + } + + ngOnInit() { + this.gatewayConfigGroup = this.fb.group({ + thingsboard: this.fb.group({ + host: ['thingsboard.cloud', [Validators.required]], + port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], + remoteShell: [false, []], + remoteConfiguration: [true, []], + statistics: this.fb.group({ + enable: [true, []], + statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]], + checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + handleDeviceRenaming: [false, []], + commands: this.fb.array([], []) + }), + maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + minPackSendDelayMS: [200, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]], + minPackSizeToSend: [500, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + handleDeviceRenaming: [true, []], + checkingDeviceActivity: this.fb.group({ + checkDeviceInactivity: [false, []], + inactivityTimeoutSeconds: [200, [Validators.min(1)]], + inactivityCheckPeriodSeconds: [500, [Validators.min(1)]] + }), + security: this.fb.group({ + type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]], + accessToken: [null, [Validators.required]], + clientId: [null, []], + username: [null, []], + password: [null, []], + caCert: [null, []], + cert: [null, []], + privateKey: [null, []], + }), + qos: [1, [Validators.min(0), Validators.max(2), Validators.required]] + }), + storage: this.fb.group({ + type: [StorageTypes.MEMORY, [Validators.required]], + read_records_count: [100, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]], + max_records_count: [100000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]], + data_folder_path: ['./data/', []], + max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + data_file_path: ['./data/data.db', []], + messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + + }), + grpc: this.fb.group({ + enabled: [false, []], + serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]], + keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + keepalivePermitWithoutCalls: [true, []], + maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]], + minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + }), + connectors: this.fb.array([]), + logs: this.fb.group({ + dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required]], + logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s', + [Validators.required]], + type: ['remote', [Validators.required]], + remote: this.fb.group({ + enabled: [false], + logLevel: [GatewayLogLevel.info, [Validators.required]], + }), + local: this.fb.group({}) + }) + }); + + this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => { + if (!enabled) { + this.openConfigurationConfirmDialog(); + } + }); + + this.logSelector = this.fb.control(LocalLogsConfigs.service); + + for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) { + this.addLocalLogConfig(localLogsConfigsKey, {}); + } + + const checkingDeviceActivityGroup = this.gatewayConfigGroup.get('thingsboard.checkingDeviceActivity') as FormGroup; + checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => { + checkingDeviceActivityGroup.updateValueAndValidity(); + if (enabled) { + checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators([Validators.min(1), Validators.required]); + checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators([Validators.min(1), Validators.required]); + } else { + checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators(); + checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setErrors(null); + checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators(); + checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setErrors(null); + } + }); + + const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; + securityGroup.get('type').valueChanges.subscribe(type => { + this.removeAllSecurityValidators(); + if (type === SecurityTypes.ACCESS_TOKEN) { + securityGroup.get('accessToken').addValidators([Validators.required]); + securityGroup.get('accessToken').updateValueAndValidity(); + } else if (type === SecurityTypes.TLS_PRIVATE_KEY) { + securityGroup.get('caCert').addValidators([Validators.required]); + securityGroup.get('caCert').updateValueAndValidity(); + securityGroup.get('privateKey').addValidators([Validators.required]); + securityGroup.get('privateKey').updateValueAndValidity(); + securityGroup.get('cert').addValidators([Validators.required]); + securityGroup.get('cert').updateValueAndValidity(); + } else if (type === SecurityTypes.TLS_ACCESS_TOKEN) { + securityGroup.get('accessToken').addValidators([Validators.required]); + securityGroup.get('accessToken').updateValueAndValidity(); + securityGroup.get('caCert').addValidators([Validators.required]); + securityGroup.get('caCert').updateValueAndValidity(); + } else if (type === SecurityTypes.USERNAME_PASSWORD) { + securityGroup.get('clientId').addValidators([Validators.required]); + securityGroup.get('clientId').updateValueAndValidity(); + securityGroup.get('username').addValidators([Validators.required]); + securityGroup.get('username').updateValueAndValidity(); + securityGroup.get('password').addValidators([Validators.required]); + securityGroup.get('password').updateValueAndValidity(); + } + securityGroup.updateValueAndValidity(); + }); + + const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; + storageGroup.get('type').valueChanges.subscribe(type => { + this.removeAllStorageValidators(); + if (type === StorageTypes.MEMORY) { + storageGroup.get('read_records_count').addValidators( + [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]); + storageGroup.get('max_records_count').addValidators( + [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); + } else if (type === StorageTypes.FILE) { + storageGroup.get('data_folder_path').addValidators([Validators.required]); + storageGroup.get('max_file_count').addValidators( + [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); + storageGroup.get('max_read_records_count').addValidators( + [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); + storageGroup.get('max_records_per_file').addValidators( + [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); + } else if (type === StorageTypes.SQLITE) { + storageGroup.get('data_file_path').addValidators([Validators.required]); + storageGroup.get('messages_ttl_check_in_hours').addValidators( + [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); + storageGroup.get('messages_ttl_in_days').addValidators( + [Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]); + } + }); + + this.fetchConfigAttribute(this.device); + } + + fetchConfigAttribute(entityId: EntityId) { + this.attributeService.getEntityAttributes(entityId, AttributeScope.SHARED_SCOPE, + ['configuration', 'RemoteLoggingLevel']).subscribe(attributes => { + if (attributes.length) { + const configuration = attributes.find(attribute => attribute.key === 'configuration').value; + const remoteLoggingLevel = attributes.find(attribute => attribute.key === 'RemoteLoggingLevel').value; + if (configuration) { + const configObj = base64toObj(configuration).thingsboard; + configObj.logs = this.logsBase64ToObj(configObj.logs); + if (configObj.thingsboard.statistics && configObj.thingsboard.statistics.commands) { + for (const command of Object.values(configObj.thingsboard.statistics.commands)) { + this.addCommand(command); + } + delete configObj.thingsboard.statistics.commands; + } + this.gatewayConfigGroup.patchValue(configObj, {emitEvent: false}); + this.gatewayConfigGroup.markAsDirty(); + if (!configObj.thingsboard.remoteConfiguration) { + this.gatewayConfigGroup.disable({emitEvent: false}); + } + } + if (remoteLoggingLevel) { + const remoteLogsFormGroup = this.gatewayConfigGroup.get('logs.remote'); + remoteLogsFormGroup.patchValue({ + enabled: remoteLoggingLevel !== GatewayLogLevel.none, + logLevel: remoteLoggingLevel + }, {emitEvent: false}); + remoteLogsFormGroup.markAsDirty(); + } + } + }); + } + + logsBase64ToObj(logs64: string) { + const logsString = base64toString(logs64).split('"').join(''); + const logsObject = {}; + logsString.split('logger_').forEach((split, index) => { + if (index > 0) { + const key = split.split(']')[0]; + if (LocalLogsConfigs[key]) { + const logLevel = split.split('=')[1].replace('}}handlers', ''); + logsObject[key] = { + logLevel + }; + } + } + }); + logsString.split('handler_').forEach((split, index) => { + if (index > 0) { + const key = split.split('Handler]')[0]; + if (LocalLogsConfigs[key]) { + const args = split.split('args=(')[1].split(','); + logsObject[key].filePath = args[0].replace(`/${key}.log`, ''); + logsObject[key].savingPeriod = args[1].replace(' ', ''); + logsObject[key].savingTime = +args[2]; + logsObject[key].backupCount = +args[3]; + } + } + }); + const formatArgs = logsString.split('format=')[1].split('}}datefmt='); + const logFormat = formatArgs[0]; + const dateFormat = formatArgs[1]; + + return {local: logsObject, logFormat, dateFormat}; + } + + addCommand(command?): void { + const data = command || {}; + const commandsFormArray = this.commandFormArray(); + const commandFormGroup = this.fb.group({ + attributeName: [data.attributeName || null, [Validators.required]], + command: [data.command || null, [Validators.required]], + timeout: [data.timeout || null, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], + }); + commandsFormArray.push(commandFormGroup); + } + + addLocalLogConfig(name, config): void { + const localLogsFormGroup = this.gatewayConfigGroup.get('logs.local') as FormGroup; + const configGroup = this.fb.group({ + logLevel: [config.logLevel || GatewayLogLevel.info, [Validators.required]], + filePath: [config.filePath || './data', [Validators.required]], + backupCount: [config.backupCount || 7, [Validators.required, Validators.min(0)]], + savingTime: [config.savingTime || 3, [Validators.required, Validators.min(0)]], + savingPeriod: [config.savingPeriod || LogSavingPeriod.days, [Validators.required]] + }); + localLogsFormGroup.addControl(name, configGroup); + } + + getLogFormGroup(value: string): FormGroup { + return this.gatewayConfigGroup.get(`logs.local.${value}`) as FormGroup; + } + + commandFormArray(): FormArray { + return this.gatewayConfigGroup.get('thingsboard.statistics.commands') as FormArray; + } + + removeCommandControl(index: number): void { + this.commandFormArray().removeAt(index); + } + + removeAllSecurityValidators(): void { + const securityGroup = this.gatewayConfigGroup.get('thingsboard.security') as FormGroup; + for (const controlsKey in securityGroup.controls) { + if (controlsKey !== 'type') { + securityGroup.controls[controlsKey].clearValidators(); + securityGroup.controls[controlsKey].setErrors(null); + securityGroup.controls[controlsKey].updateValueAndValidity(); + } + } + } + + removeAllStorageValidators(): void { + const storageGroup = this.gatewayConfigGroup.get('storage') as FormGroup; + for (const storageKey in storageGroup.controls) { + if (storageKey !== 'type') { + storageGroup.controls[storageKey].clearValidators(); + storageGroup.controls[storageKey].setErrors(null); + storageGroup.controls[storageKey].updateValueAndValidity(); + } + } + } + + removeEmpty(obj) { + return Object.fromEntries( + Object.entries(obj) + .filter(([_, v]) => v != null) + .map(([k, v]) => [k, v === Object(v) ? this.removeEmpty(v) : v]) + ); + } + + generateLogsFile(logsObj): string { + const line = '[loggers]}}' + + 'keys=root, service, connector, converter, tb_connection, storage, extension}}' + + '[handlers]}}' + + 'keys=consoleHandler, serviceHandler, connectorHandler, converterHandler, tb_connectionHandler, storageHandler, extensionHandler}}' + + '[formatters]}}' + + 'keys=LogFormatter}}' + + '[logger_root]}}' + + 'level=ERROR}}' + + 'handlers=consoleHandler}}' + + '[logger_database]}}' + + 'level=INFO}}' + + 'handlers=databaseHandler}}' + + 'formatter=LogFormatter}}' + + 'qualname=database}}'; + + let loggerLine = ''; + let handlerLine = ''; + + for (const key of Object.keys(logsObj.local)) { + loggerLine += `[logger_${key}]}}` + + `level=${logsObj.local[key].logLevel}}}` + + `handlers=${key}Handler}}` + + `formatter=LogFormatter}}` + + `qualname=${key}}}`; + handlerLine += `[handler_${key}Handler]}}` + + `level=${logsObj.local[key].logLevel}}}` + + `class=thingsboard_gateway.tb_utility.tb_logger.TimedRotatingFileHandler}}` + + `formatter=LogFormatter}}` + + `args=("${logsObj.local[key].filePath}/${key}.log", ${logsObj.local[key].savingPeriod},` + + ` ${logsObj.local[key].savingTime}, ${logsObj.local[key].backupCount},)}}`; + } + + const logerEnding = `[formatter_LogFormatter]}}` + + `format="${logsObj.logFormat}"}}` + + `datefmt="${logsObj.dateFormat}"`; + + return line + loggerLine + handlerLine + logerEnding; + } + + saveConfig(): void { + const value = this.removeEmpty(this.gatewayConfigGroup.value); + const attributes = []; + attributes.push({ + key: 'RemoteLoggingLevel', + value: value.logs.remote.logLevel + }); + delete value.connectors; + value.logs = stringToBase64(this.generateLogsFile(value.logs)); + const configuration = objToBase64({thingsboard: value}); + attributes.push({ + key: 'configuration', + value: configuration + }); + this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, attributes).subscribe(_ => { + if (this.dialogRef) { + this.dialogRef.close(); + } + }); + } + + cancel(): void { + if (this.dialogRef) { + this.dialogRef.close(); + } + } + + private openConfigurationConfirmDialog(): void { + this.deviceService.getDevice(this.device.id).subscribe(gateway => { + this.dialog.open + (GatewayRemoteConfigurationDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + gatewayName: gateway.name + } + }).afterClosed().subscribe( + (res) => { + if (!res) { + this.gatewayConfigGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false}); + } + } + ); + }); + + + } +} diff --git a/ui-ngx/src/app/shared/components/device/gateway-connectors.component.html b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.html new file mode 100644 index 0000000000..f14ae4294d --- /dev/null +++ b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.html @@ -0,0 +1,143 @@ + +
+ +

gateway.connectors

+ + +
+
+
+ + + + + {{generatePanelTitle(connectorControl)}} + + +
+ + + {{ (connectorControl.get('active').value ? 'gateway.connectors-active' : 'gateway.connectors-inactive') | translate }} + + + gateway.connector-name + + + {{'gateway.connector-name-required' | translate }} + + + + gateway.connector-configuration + + + {{'gateway.connector-configuration-required' | translate }} + + + + gateway.connector-type + + {{type}} + + + {{'gateway.connector-type-required' | translate }} + + + + gateway.connector-key + + + {{'gateway.connector-key-required' | translate }} + + + +
+ + +
+
+
+
+ +
+ + +
+
+ + + +
+
diff --git a/ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss new file mode 100644 index 0000000000..9f683170c2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.scss @@ -0,0 +1,73 @@ +/** + * ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL + * + * Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of ThingsBoard, Inc. and its suppliers, + * if any. The intellectual and technical concepts contained + * herein are proprietary to ThingsBoard, Inc. + * and its suppliers and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * + * Dissemination of this information or reproduction of this material is strictly forbidden + * unless prior written permission is obtained from COMPANY. + * + * Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, + * managers or contractors who have executed Confidentiality and Non-disclosure agreements + * explicitly covering such access. + * + * The copyright notice above does not evidence any actual or intended publication + * or disclosure of this source code, which includes + * information that is confidential and/or proprietary, and is a trade secret, of COMPANY. + * ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, + * OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT + * THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, + * AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. + * THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION + * DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, + * OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. + */ +:host { + width: 100%; + height: 100%; + display: block; + + .mat-toolbar { + background: transparent; + color: rgba(0, 0, 0, .87) !important; + } + + .mat-mdc-form-field { + margin-right: 15px; + } + + mat-expansion-panel { + border: 1px solid #305680; + } + + mat-card { + padding-left: 10px; + } + + .mat-card-selected { + background-color: white; + } + + .mat-mdc-slide-toggle { + margin: 15px; + } + + ::ng-deep tb-json-object-edit > div { + flex-grow: 1; + } + + .mat-accordion .mat-expansion-panel { + border-radius: 5px; + } + + .action-btns { + margin: 10px 10px 0; + } +} + diff --git a/ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts new file mode 100644 index 0000000000..0abf9ede4a --- /dev/null +++ b/ui-ngx/src/app/shared/components/device/gateway-connectors.component.ts @@ -0,0 +1,195 @@ +/// +/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// +/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved. +/// +/// NOTICE: All information contained herein is, and remains +/// the property of ThingsBoard, Inc. and its suppliers, +/// if any. The intellectual and technical concepts contained +/// herein are proprietary to ThingsBoard, Inc. +/// and its suppliers and may be covered by U.S. and Foreign Patents, +/// patents in process, and are protected by trade secret or copyright law. +/// +/// Dissemination of this information or reproduction of this material is strictly forbidden +/// unless prior written permission is obtained from COMPANY. +/// +/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, +/// managers or contractors who have executed Confidentiality and Non-disclosure agreements +/// explicitly covering such access. +/// +/// The copyright notice above does not evidence any actual or intended publication +/// or disclosure of this source code, which includes +/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. +/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, +/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT +/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, +/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. +/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION +/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, +/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// + +import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { EntityId } from '@shared/models/id/entity-id'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { AttributeService } from '@core/http/attribute.service'; +import { DeviceService } from '@core/http/device.service'; +import { TranslateService } from '@ngx-translate/core'; +import { forkJoin } from 'rxjs'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; + + +export interface gatewayConnector { + name: string; + type: string; + configuration: string; + jsonConfiguration: string; + key: string; +} + +@Component({ + selector: 'tb-gateway-connector', + templateUrl: './gateway-connectors.component.html', + styleUrls: ['./gateway-connectors.component.scss'] +}) +export class GatewayConnectorComponent implements OnInit { + + gatewayConnectorsGroup: FormGroup; + + gatewayConnectorDefaultTypes: Array = + ['mqtt', + 'modbus', + 'grpc', + 'opcua', + 'opcua_asyncio', + 'ble', + 'request', + 'can', + 'bacnet', + 'odbc', + 'rest', + 'snmp', + 'ftp', + 'socket', + 'xmpp', + 'ocpp' + ]; + + selectedConnector: number; + + @Input() + device: EntityId; + + @Input() + dialogRef: MatDialogRef; + + logSelector: FormControl; + + + constructor(protected router: Router, + protected store: Store, + protected fb: FormBuilder, + protected translate: TranslateService, + protected attributeService: AttributeService, + protected deviceService: DeviceService, + private cd: ChangeDetectorRef, + public dialog: MatDialog) { + } + + ngOnInit() { + this.gatewayConnectorsGroup = this.fb.group({ + connectors: this.fb.array([], [Validators.required]) + }); + this.getConnectorsData(); + } + + cancel(): void { + if (this.dialogRef) { + this.dialogRef.close(); + } + } + + getConnectorsData(): void { + forkJoin([ + this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['implementedConnectors']), + this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE,['implementedConnectors']), + this.attributeService.getEntityAttributes(this.device, AttributeScope.SHARED_SCOPE,['connectorTypes']) + ]).subscribe(attributes=>{ + if (attributes[0].length) { + attributes[0][0].value.forEach(connector=>this.addConnector(true, connector)); + } + if (attributes[1].length) { + attributes[1][0].value.forEach(connector=>this.addConnector(false, connector)); + } + if(attributes[2].length) { + attributes[1][0].value.forEach(type=> { + if (this.gatewayConnectorDefaultTypes.indexOf(type) === -1) { + this.gatewayConnectorDefaultTypes.push(type); + } + }); + } + this.cd.detectChanges(); + }); + } + + saveConnectors(): void { + const connectors = this.gatewayConnectorsGroup.value.connectors; + const activeConnectors = connectors.filter(connector=>connector.active); + const inactiveConnectors = connectors.filter(connector=>!connector.active); + forkJoin([ + this.attributeService.saveEntityAttributes(this.device, AttributeScope.SERVER_SCOPE, + [{key: 'implementedConnectors', value: inactiveConnectors}]), + this.attributeService.saveEntityAttributes(this.device, AttributeScope.SHARED_SCOPE, + [{key: 'implementedConnectors', value: activeConnectors}]) + ]).subscribe(_=> { + if (this.dialogRef) { + this.dialogRef.close(); + } + }); + } + + connectorsFormArray(): FormArray { + return this.gatewayConnectorsGroup.get('connectors') as FormArray; + } + + addConnector(active?: boolean,connector?: gatewayConnector) { + const newConnector = this.fb.group({ + active: [!!active], + name: [connector?.name || '', [Validators.required]], + type: [connector?.type || '', [Validators.required]], + configuration: [connector?.configuration || '', [Validators.required]], + jsonConfiguration: [connector?.jsonConfiguration || {}, [Validators.required]], + key: [connector?.key || 'auto', [Validators.required]], + }); + const connectorsFormArray = this.connectorsFormArray(); + connectorsFormArray.push(newConnector); + this.selectConnector(connectorsFormArray.length-1); + } + + removeConnector(index: number): void { + this.connectorsFormArray().removeAt(index); + } + + selectConnector(index: number): void { + this.selectedConnector = index; + } + + getJsonControl(selectedConnector: number): FormControl { + return this.connectorsFormArray().at(selectedConnector).get('jsonConfiguration') as FormControl; + } + + generatePanelTitle(connectorControl): string{ + const connectorValues = connectorControl.value; + const activeTxt = this.translate.instant(connectorValues.active ? 'gateway.connectors-active' : 'gateway.connectors-inactive'); + let title = `${connectorValues.name} | ${connectorValues.type} `; + if (connectorValues.type === 'grpc') { + title += `| ${connectorValues.key} `; + } + title += `| ${activeTxt}`; + return title; + } +} diff --git a/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html b/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html new file mode 100644 index 0000000000..cc7bb69f6b --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.html @@ -0,0 +1,72 @@ + +
+ + warning +

gateway.configuration-delete-dialog-header

+ + +
+
+
+ + + gateway.configuration-delete-dialog-input + + + {{'gateway.configuration-delete-dialog-input-required' | translate }} + + +
+
+
+ + +
+
diff --git a/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts b/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts new file mode 100644 index 0000000000..086e12a68a --- /dev/null +++ b/ui-ngx/src/app/shared/components/dialog/gateway-remote-configuration-dialog.ts @@ -0,0 +1,76 @@ +/// +/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// +/// Copyright © 2016-2023 ThingsBoard, Inc. All Rights Reserved. +/// +/// NOTICE: All information contained herein is, and remains +/// the property of ThingsBoard, Inc. and its suppliers, +/// if any. The intellectual and technical concepts contained +/// herein are proprietary to ThingsBoard, Inc. +/// and its suppliers and may be covered by U.S. and Foreign Patents, +/// patents in process, and are protected by trade secret or copyright law. +/// +/// Dissemination of this information or reproduction of this material is strictly forbidden +/// unless prior written permission is obtained from COMPANY. +/// +/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, +/// managers or contractors who have executed Confidentiality and Non-disclosure agreements +/// explicitly covering such access. +/// +/// The copyright notice above does not evidence any actual or intended publication +/// or disclosure of this source code, which includes +/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. +/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, +/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT +/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, +/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. +/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION +/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, +/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// + +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { FormBuilder, FormControl } from '@angular/forms'; + +export interface GatewayRemoteConfigurationDialogData { + gatewayName: string; +} + +@Component({ + selector: 'tb-activation-link-dialog', + templateUrl: './gateway-remote-configuration-dialog.html' +}) + +export class GatewayRemoteConfigurationDialogComponent extends DialogComponent implements OnInit { + + gatewayName: string; + + gatewayForm: FormControl; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: GatewayRemoteConfigurationDialogData, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.gatewayName = this.data.gatewayName; + this.gatewayForm = this.fb.control(null); + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(); + } + + turnOff(): void { + this.dialogRef.close(true); + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index c82ae61644..7b053007f1 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -176,10 +176,13 @@ import { NotificationComponent } from '@shared/components/notification/notificat import { TemplateAutocompleteComponent } from '@shared/components/notification/template-autocomplete.component'; import { SlackConversationAutocompleteComponent } from '@shared/components/slack-conversation-autocomplete.component'; import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; +import { DeviceGatewayCommandComponent } from '@shared/components/device/device-gateway-command.component'; +import { GatewayConfigurationComponent } from '@shared/components/device/gateway-configuration.component'; +import { GatewayRemoteConfigurationDialogComponent } from '@shared/components/dialog/gateway-remote-configuration-dialog'; +import { GatewayConnectorComponent } from '@shared/components/device/gateway-connectors.component'; + +export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService) => markedOptionsService; -export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { - return markedOptionsService; -} @NgModule({ providers: [ @@ -241,6 +244,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) HelpComponent, HelpMarkdownComponent, HelpPopupComponent, + DeviceGatewayCommandComponent, + GatewayConfigurationComponent, + GatewayConnectorComponent, + GatewayRemoteConfigurationDialogComponent, TbCheckboxComponent, TbSnackBarComponent, TbErrorComponent, @@ -407,6 +414,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) HelpComponent, HelpMarkdownComponent, HelpPopupComponent, + DeviceGatewayCommandComponent, + GatewayConfigurationComponent, + GatewayConnectorComponent, + GatewayRemoteConfigurationDialogComponent, TbCheckboxComponent, TbErrorComponent, TbCheatSheetComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d810da019c..e25d2667e6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1186,6 +1186,7 @@ "device-details": "Device details", "add-device-text": "Add new device", "credentials": "Credentials", + "commands": "Commands", "manage-credentials": "Manage credentials", "delete": "Delete device", "assign-devices": "Assign devices", @@ -1278,6 +1279,7 @@ "copyId": "Copy device Id", "copyAccessToken": "Copy access token", "copy-mqtt-authentication": "Copy MQTT credentials", + "transportCommandCopiedMessage": "Transport Command has been copied to clipboard", "idCopiedMessage": "Device Id has been copied to clipboard", "accessTokenCopiedMessage": "Device access token has been copied to clipboard", "mqtt-authentication-copied-message": "Device MQTT authentication has been copied to clipboard", @@ -2466,44 +2468,164 @@ }, "gateway": { "add-entry": "Add configuration", + "advanced": "Advanced", + "checking-device-activity": "Checking Device Activity:", "command": "Docker commands", "command-copied-message": "Docker command has been copied to clipboard", + "configuration": "Configuration", + "connector-json": "Connector JSON", "connector-add": "Add new connector", "connector-enabled": "Enable connector", "connector-name": "Connector name", "connector-name-required": "Connector name is required.", + "connector-key": "Connector key", + "connector-key-required": "Connector key is required.", + "connector-configuration": "Configuration file name", + "connector-configuration-required": "Configuration file name is required.", "connector-type": "Connector type", "connector-type-required": "Connector type is required.", + "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 configuration", + "connectors-active": "Connector active", + "connectors-inactive": "Connector inactive", + "select-connector": "Select connector", + "gateway-configuration": "Gateway Configuration", "copy-command": "Copy docker command", "create-new-gateway": "Create a new gateway", "create-new-gateway-text": "Are you sure you want create a new gateway with name: '{{gatewayName}}'?", "created-time": "Created time", + "configuration-delete-dialog-header": "Configurations will be deleted", + "configuration-delete-dialog-body": "Turning off Remote Configuration is possible only if there is physical access to the Gateway. All previous configurations will be deleted.

\nTo turn off configuration, enter gateway name below.", + "configuration-delete-dialog-input": "Gateway name", + "configuration-delete-dialog-input-required": "Gateway name is mandatory", + "configuration-delete-dialog-confirm": "Turn Off", "delete": "Delete configuration", "download-tip": "Download configuration file", + "drop-file": "Drop file here or", "gateway": "Gateway", "gateway-exists": "Device with same name is already exists.", "gateway-name": "Gateway name", "gateway-name-required": "Gateway name is required.", "gateway-saved": "Gateway configuration successfully saved.", "gateway-search": "Gateway search", + "grpc": "GRPC", + "grpc-keep-alive-timeout": "Keep alive timeout (in ms)", + "grpc-keep-alive-timeout-required": "Keep alive timeout is required", + "grpc-keep-alive-timeout-min": "Keep alive timeout can not be less then 1", + "grpc-keep-alive-timeout-pattern": "Keep alive timeout is not valid", + "grpc-keep-alive": "Keep alive (in ms)", + "grpc-keep-alive-required": "Keep alive is required", + "grpc-keep-alive-min": "Keep alive can not be less then 1", + "grpc-keep-alive-pattern": "Keep alive is not valid", + "grpc-min-time-between-pings": "Min time between pings (in ms)", + "grpc-min-time-between-pings-required": "Min time between pings is required", + "grpc-min-time-between-pings-min": "Min time between pings can not be less then 1", + "grpc-min-time-between-pings-pattern": "Min time between pings is not valid", + "grpc-min-ping-interval-without-data": "Min ping interval without data (in ms)", + "grpc-min-ping-interval-without-data-required": "Min ping interval without data is required", + "grpc-min-ping-interval-without-data-min": "Min ping interval without data can not be less then 1", + "grpc-min-ping-interval-without-data-pattern": "Min ping interval without data is not valid", + "grpc-max-pings-without-data": "Max pings without data", + "grpc-max-pings-without-data-required": "Max pings without data is required", + "grpc-max-pings-without-data-min": "Max pings without data can not be less then 1", + "grpc-max-pings-without-data-pattern": "Max pings without data is not valid", + "handle-device-renaming": "Handle device renaming", + "inactivity-check-period-seconds": "Inactivity check period (in sec)", + "inactivity-check-period-seconds-required": "Inactivity check period is required", + "inactivity-check-period-seconds-min": "Inactivity check period can not be less then 1", + "inactivity-timeout-seconds": "Inactivity timeout (in sec)", + "inactivity-timeout-seconds-required": "Inactivity timeout is required", + "inactivity-timeout-seconds-min": "Inactivity timeout can not be less then 1", "json-parse": "Not valid JSON.", "json-required": "Field cannot be empty.", "linux-macos": "Linux/MacOS", + "logs": { + "logs": "Logs", + "days": "days", + "hours": "hours", + "minutes": "minutes", + "seconds": "seconds", + "date-format": "Date format", + "date-format-required": "Date format required", + "log-format": "Log format", + "log-type": "Log type", + "log-format-required": "Log format required", + "remote": "Remote logging", + "remote-logs": "Remote logs", + "local": "Local logging", + "level": "Log level", + "file-path": "File path", + "file-path-required": "File path required", + "saving-period": "Log saving period", + "saving-period-min": "Log saving period can not be less then 0", + "saving-period-required": "Log saving period required", + "backup-count": "Backup count", + "backup-count-min": "Backup count can not be less then 0", + "backup-count-required": "Backup count required" + }, + "min-pack-send-delay": "Min pack send delay (in ms)", + "min-pack-send-delay-required": "Min pack send delay is required", + "min-pack-send-delay-min": "Min pack send delay can not be less then 1", "no-connectors": "No connectors", "no-data": "No configurations", "no-gateway-found": "No gateway found.", "no-gateway-matching": " '{{item}}' not found.", "path-logs": "Path to log files", "path-logs-required": "Path is required.", + "permit-without-calls": "Keep alive permit without calls", "remote": "Remote configuration", "remote-logging-level": "Logging level", "remove-entry": "Remove configuration", + "remote-shell": "Remote shell", + "remote-configuration": "Remote Configuration", + "other": "Other", "save-tip": "Save configuration file", "security-type": "Security type", "security-types": { "access-token": "Access Token", - "tls": "TLS" + "tls": "TLS", + "tls-access-token": "TLS + Access Token", + "tls-private-key": "TLS + Private Key" + }, + "server-port": "Server port", + "stats-send-period-in-sec": "Stats send period in seconds", + "statistics": { + "statistics": "Statistics", + "send-period": "Statistic send period (in ms)", + "send-period-required": "Statistic send period is required", + "send-period-min": "Statistic send period can not be less then 0", + "send-period-pattern": "Statistic send period is not valid", + "check-connectors-configuration": "Check connectors configuration (in ms)", + "check-connectors-configuration-required": "Check connectors configuration is required", + "check-connectors-configuration-min": "Check connectors configuration can not be less then 0", + "check-connectors-configuration-pattern": "Check connectors configuration is not valid", + "add": "Add command", + "timeout": "Timeout", + "timeout-required": "Timeout is required", + "timeout-min": "Timeout can not be less then 0", + "timeout-pattern": "Timeout is not valid", + "attribute-name": "Attribute name", + "attribute-name-required": "Attribute name is required", + "command": "Command", + "command-required": "Command is required", + "remove": "Remove command" }, "storage": "Storage", "storage-max-file-records": "Maximum records in file", @@ -2515,6 +2637,16 @@ "storage-max-records-min": "Minimum number of records is 1.", "storage-max-records-pattern": "Number is not valid.", "storage-max-records-required": "Maximum records is required.", + "storage-read-record-count": "Read record count in storage", + "storage-read-record-count-min": "Minimum number of records is 1.", + "storage-read-record-count-pattern": "Number is not valid.", + "storage-read-record-count-required": "Read record count is required.", + "storage-max-read-record-count": "Max read record count in storage", + "storage-max-read-record-count-min": "Minimum number of records is 1.", + "storage-max-read-record-count-pattern": "Number is not valid.", + "storage-max-read-record-count-required": "Max Read record count is required.", + "storage-data-folder-path": "Data folder path", + "storage-data-folder-path-required": "Data folder path is required.", "storage-pack-size": "Maximum event pack size", "storage-pack-size-min": "Minimum number is 1.", "storage-pack-size-pattern": "Number is not valid.", @@ -2524,9 +2656,11 @@ "storage-type": "Storage type", "storage-types": { "file-storage": "File storage", - "memory-storage": "Memory storage" + "memory-storage": "Memory storage", + "sqlite": "SQLITE" }, "thingsboard": "ThingsBoard", + "thingsboard-general": "General", "thingsboard-host": "ThingsBoard host", "thingsboard-host-required": "Host is required.", "thingsboard-port": "ThingsBoard port", @@ -2539,6 +2673,17 @@ "title-connectors-json": "Connector {{typeName}} configuration", "tls-path-ca-certificate": "Path to CA certificate on gateway", "tls-path-client-certificate": "Path to client certificate on gateway", + "messages-ttl-check-in-hours": "Messages TTL check in hours", + "messages-ttl-check-in-hours-required": "Messages TTL check in hours is required.", + "messages-ttl-check-in-hours-min": "Min number is 1.", + "messages-ttl-check-in-hours-pattern": "Number is not valid.", + "messages-ttl-in-days": "Messages TTL in days", + "messages-ttl-in-days-required": "Messages TTL in days is required.", + "messages-ttl-in-days-min": "Min number is 1.", + "messages-ttl-in-days-pattern": "Number is not valid.", + "mqtt-qos": "QoS", + "mqtt-qos-required": "QoS is required", + "mqtt-qos-range": "QoS values range is from 0 to 2", "tls-path-private-key": "Path to private key on gateway", "toggle-fullscreen": "Toggle fullscreen", "transformer-json-config": "Configuration JSON*", @@ -3065,6 +3210,20 @@ }, "security": { "security": "Security", + "access-token": "Access token", + "access-token-required": "Access token is required", + "clientId": "Client ID", + "clientId-required": "Client ID is required", + "username": "Username", + "username-required": "Username is required", + "password": "Password", + "password-required": "Password is required", + "ca-cert": "CA certificate", + "ca-cert-required": "CA certificate is required", + "cert": "Certificate", + "cert-required": "Certificate is required", + "private-key": "Private Key", + "private-key-required": "Private Key is required", "2fa": { "2fa": "Two-factor authentication", "2fa-description": "Two-factor authentication protects your account from unauthorized access. All you have to do is enter a security code when you log in.",