From 5a7673a118787834f1ec06d5b742af3cc777383b Mon Sep 17 00:00:00 2001 From: hanyuepeng Date: Wed, 10 Nov 2021 17:16:17 +0800 Subject: [PATCH 01/72] fix(chinese translate): fix some display problem In Api Usage module,when use chinese ,there are some display problem because of the lose of the translate items --- ui-ngx/src/assets/locale/locale.constant-zh_CN.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 270fd6ad4e..39f4063b49 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -388,7 +388,14 @@ "transport-messages": "传输消息", "transport-monthly-activity": "Transport monthly activity", "view-details": "查看详细信息", - "view-statistics": "查看统计信息" + "view-statistics": "查看统计信息", + "alarm":"警告", + "alarms-created": "创建警告数", + "alarms-created-hourly-activity":"Alarms created hourly activity", + "alarms-created-daily-activity": "Alarms created daily activity", + "alarms-created-monthly-activity": "Alarms created monthly activity", + "notifications": "通知", + "notifications-hourly-activity": "Notifications hourly activity" }, "asset": { "add": "添加资产", From 524ff2f980cf4a5baa963dc29aaf2455e1e7c4d7 Mon Sep 17 00:00:00 2001 From: Vladyslav Date: Wed, 10 Nov 2021 14:36:28 +0200 Subject: [PATCH 02/72] Update locale.constant-zh_CN.json --- ui-ngx/src/assets/locale/locale.constant-zh_CN.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 39f4063b49..f8e7e810fe 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -391,11 +391,7 @@ "view-statistics": "查看统计信息", "alarm":"警告", "alarms-created": "创建警告数", - "alarms-created-hourly-activity":"Alarms created hourly activity", - "alarms-created-daily-activity": "Alarms created daily activity", - "alarms-created-monthly-activity": "Alarms created monthly activity", - "notifications": "通知", - "notifications-hourly-activity": "Notifications hourly activity" + "notifications": "通知" }, "asset": { "add": "添加资产", From 79f3d3826a1c916288c8a89b15c9be64d068d9ef Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 12 Nov 2021 15:52:58 +0200 Subject: [PATCH 03/72] UI: Added clearing selectedWidgetsBundleAlias when calling resetSortAndFilter function in attribute table --- .../home/components/attribute/attribute-table.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index bcb9edd07e..9e9bbceb6f 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -263,6 +263,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI this.attributeScopeSelectionReadonly = true; } this.mode = 'default'; + this.selectedWidgetsBundleAlias = null; this.attributeScope = this.defaultAttributeScope; this.pageLink.textSearch = null; if (this.viewsInited) { From 5d4579ce288bcd6750b9a8a82e80f94d757c26ed Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 26 Nov 2021 17:00:51 +0200 Subject: [PATCH 04/72] UI: New control widget Persistent table --- .../widget_bundles/control_widgets.json | 18 + .../server/controller/RpcV2Controller.java | 11 +- .../server/dao/rpc/RpcService.java | 2 + .../server/dao/rpc/BaseRpcService.java | 10 +- .../thingsboard/server/dao/rpc/RpcDao.java | 4 +- .../server/dao/sql/rpc/JpaRpcDao.java | 7 +- .../server/dao/sql/rpc/RpcRepository.java | 2 + ui-ngx/src/app/core/api/widget-api.models.ts | 14 +- .../src/app/core/api/widget-subscription.ts | 23 +- ui-ngx/src/app/core/http/device.service.ts | 13 +- .../rpc/persistent-add-dialog.component.html | 92 ++++ .../rpc/persistent-add-dialog.component.scss | 30 ++ .../rpc/persistent-add-dialog.component.ts | 77 +++ .../persistent-details-dialog.component.html | 118 +++++ .../persistent-details-dialog.component.scss | 33 ++ .../persistent-details-dialog.component.ts | 151 ++++++ .../persistent-filter-panel.component.html | 44 ++ .../persistent-filter-panel.component.scss | 36 ++ .../rpc/persistent-filter-panel.component.ts | 71 +++ .../lib/rpc/persistent-table.component.html | 125 +++++ .../lib/rpc/persistent-table.component.scss | 52 ++ .../lib/rpc/persistent-table.component.ts | 476 ++++++++++++++++++ .../widget/lib/rpc/rpc-widgets.module.ts | 15 +- .../home/models/widget-component.models.ts | 10 +- .../json-object-view.component.html | 22 + .../json-object-view.component.scss | 27 + .../components/json-object-view.component.ts | 166 ++++++ ui-ngx/src/app/shared/models/rpc.models.ts | 51 +- ui-ngx/src/app/shared/shared.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 44 +- .../assets/locale/locale.constant-ru_RU.json | 41 ++ .../assets/locale/locale.constant-uk_UA.json | 41 ++ 32 files changed, 1805 insertions(+), 24 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts create mode 100644 ui-ngx/src/app/shared/components/json-object-view.component.html create mode 100644 ui-ngx/src/app/shared/components/json-object-view.component.scss create mode 100644 ui-ngx/src/app/shared/components/json-object-view.component.ts diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index f1e3eaa1fe..73a1ef633d 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -149,6 +149,24 @@ "dataKeySettingsSchema": "{}\n", "defaultConfig": "{\"showTitle\":false,\"backgroundColor\":\"#e6e7e8\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"styleButton\":{\"isRaised\":true,\"isPrimary\":false},\"entityParameters\":\"{}\",\"entityAttributeType\":\"SERVER_SCOPE\",\"buttonText\":\"Update device attribute\"},\"title\":\"Update device attribute\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{},\"targetDeviceAliases\":[]}" } + }, + { + "alias": "persistent_table", + "name": "Persistent table", + "image": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgdmlld0JveD0iMCAwIDIwMCAxNjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0yMDAgMEgwVjE2MEgyMDBWMFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0yMDAgMTIwSDBWMTIxSDIwMFYxMjBaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0yMDAgODBIMFY4MUgyMDBWODBaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0yMDAgMzlIMFY0MEgyMDBWMzlaIiBmaWxsPSIjRTBFMEUwIi8+CjxwYXRoIGQ9Ik0xNS42Njg1IDE5Ljk4NjhIMTQuMTUzOFYyM0gxMi43OTQ5VjE1LjE3OTdIMTUuNTQ0OUMxNi40NDczIDE1LjE3OTcgMTcuMTQzNyAxNS4zODIgMTcuNjM0MyAxNS43ODY2QzE4LjEyNDggMTYuMTkxMiAxOC4zNzAxIDE2Ljc3NjcgMTguMzcwMSAxNy41NDNDMTguMzcwMSAxOC4wNjU4IDE4LjI0MyAxOC41MDQ0IDE3Ljk4ODggMTguODU4OUMxNy43MzgxIDE5LjIwOTggMTcuMzg3MiAxOS40ODAxIDE2LjkzNiAxOS42Njk5TDE4LjY5MjQgMjIuOTMwMlYyM0gxNy4yMzY4TDE1LjY2ODUgMTkuOTg2OFpNMTQuMTUzOCAxOC44OTY1SDE1LjU1MDNDMTYuMDA4NiAxOC44OTY1IDE2LjM2NjcgMTguNzgxOSAxNi42MjQ1IDE4LjU1MjdDMTYuODgyMyAxOC4zMiAxNy4wMTEyIDE4LjAwMzEgMTcuMDExMiAxNy42MDIxQzE3LjAxMTIgMTcuMTgzMSAxNi44OTEzIDE2Ljg1OSAxNi42NTE0IDE2LjYyOTlDMTYuNDE1IDE2LjQwMDcgMTYuMDYwNSAxNi4yODI2IDE1LjU4NzkgMTYuMjc1NEgxNC4xNTM4VjE4Ljg5NjVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0yMS4wMTgxIDIwLjA5NDJWMjNIMTkuNjU5MlYxNS4xNzk3SDIyLjY1MDlDMjMuNTI0NiAxNS4xNzk3IDI0LjIxNzQgMTUuNDA3MSAyNC43Mjk1IDE1Ljg2MThDMjUuMjQ1MSAxNi4zMTY2IDI1LjUwMjkgMTYuOTE4MSAyNS41MDI5IDE3LjY2NjVDMjUuNTAyOSAxOC40MzI4IDI1LjI1MDUgMTkuMDI5IDI0Ljc0NTYgMTkuNDU1MUMyNC4yNDQzIDE5Ljg4MTIgMjMuNTQwNyAyMC4wOTQyIDIyLjYzNDggMjAuMDk0MkgyMS4wMTgxWk0yMS4wMTgxIDE5LjAwMzlIMjIuNjUwOUMyMy4xMzQzIDE5LjAwMzkgMjMuNTAzMSAxOC44OTExIDIzLjc1NzMgMTguNjY1NUMyNC4wMTE2IDE4LjQzNjQgMjQuMTM4NyAxOC4xMDY5IDI0LjEzODcgMTcuNjc3MkMyNC4xMzg3IDE3LjI1NDcgMjQuMDA5OCAxNi45MTgxIDIzLjc1MiAxNi42Njc1QzIzLjQ5NDEgMTYuNDEzMiAyMy4xMzk2IDE2LjI4MjYgMjIuNjg4NSAxNi4yNzU0SDIxLjAxODFWMTkuMDAzOVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTMyLjY2MjYgMjAuNDU0MUMzMi41ODM4IDIxLjI4ODQgMzIuMjc1OSAyMS45NDAxIDMxLjczODggMjIuNDA5MkMzMS4yMDE3IDIyLjg3NDcgMzAuNDg3MyAyMy4xMDc0IDI5LjU5NTcgMjMuMTA3NEMyOC45NzI3IDIzLjEwNzQgMjguNDIzIDIyLjk2MDYgMjcuOTQ2OCAyMi42NjdDMjcuNDc0MSAyMi4zNjk4IDI3LjEwODkgMjEuOTQ5MSAyNi44NTExIDIxLjQwNDhDMjYuNTkzMyAyMC44NjA1IDI2LjQ1OSAyMC4yMjg1IDI2LjQ0ODIgMTkuNTA4OFYxOC43NzgzQzI2LjQ0ODIgMTguMDQwNyAyNi41Nzg5IDE3LjM5MDggMjYuODQwMyAxNi44Mjg2QzI3LjEwMTcgMTYuMjY2NCAyNy40NzU5IDE1LjgzMzIgMjcuOTYyOSAxNS41Mjg4QzI4LjQ1MzUgMTUuMjI0NCAyOS4wMTkyIDE1LjA3MjMgMjkuNjYwMiAxNS4wNzIzQzMwLjUyMzEgMTUuMDcyMyAzMS4yMTc4IDE1LjMwNjggMzEuNzQ0MSAxNS43NzU5QzMyLjI3MDUgMTYuMjQ1IDMyLjU3NjcgMTYuOTA3NCAzMi42NjI2IDE3Ljc2MzJIMzEuMzA5MUMzMS4yNDQ2IDE3LjIwMSAzMS4wNzk5IDE2Ljc5NjQgMzAuODE0OSAxNi41NDkzQzMwLjU1MzUgMTYuMjk4NyAzMC4xNjg2IDE2LjE3MzMgMjkuNjYwMiAxNi4xNzMzQzI5LjA2OTMgMTYuMTczMyAyOC42MTQ2IDE2LjM5IDI4LjI5NTkgMTYuODIzMkMyNy45ODA4IDE3LjI1MjkgMjcuODE5NyAxNy44ODQ5IDI3LjgxMjUgMTguNzE5MlYxOS40MTIxQzI3LjgxMjUgMjAuMjU3MiAyNy45NjI5IDIwLjkwMTcgMjguMjYzNyAyMS4zNDU3QzI4LjU2OCAyMS43ODk3IDI5LjAxMiAyMi4wMTE3IDI5LjU5NTcgMjIuMDExN0MzMC4xMjkyIDIyLjAxMTcgMzAuNTMwMyAyMS44OTE4IDMwLjc5ODggMjEuNjUxOUMzMS4wNjc0IDIxLjQxMTkgMzEuMjM3NSAyMS4wMTI3IDMxLjMwOTEgMjAuNDU0MUgzMi42NjI2WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMzguMDU1MiAyM0gzNi43MDE3VjE1LjE3OTdIMzguMDU1MlYyM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTM5LjcyNTYgMjNWMTUuMTc5N0g0Mi4wMzUyQzQyLjcyNjIgMTUuMTc5NyA0My4zMzg1IDE1LjMzMzcgNDMuODcyMSAxNS42NDE2QzQ0LjQwOTIgMTUuOTQ5NSA0NC44MjQ1IDE2LjM4NjQgNDUuMTE4MiAxNi45NTIxQzQ1LjQxMTggMTcuNTE3OSA0NS41NTg2IDE4LjE2NiA0NS41NTg2IDE4Ljg5NjVWMTkuMjg4NkM0NS41NTg2IDIwLjAyOTggNDUuNDEgMjAuNjgxNSA0NS4xMTI4IDIxLjI0MzdDNDQuODE5MiAyMS44MDU4IDQ0LjM5ODQgMjIuMjM5MSA0My44NTA2IDIyLjU0MzVDNDMuMzA2MyAyMi44NDc4IDQyLjY4MTUgMjMgNDEuOTc2MSAyM0gzOS43MjU2Wk00MS4wODQ1IDE2LjI3NTRWMjEuOTE1SDQxLjk3MDdDNDIuNjgzMyAyMS45MTUgNDMuMjI5MyAyMS42OTMgNDMuNjA4OSAyMS4yNDlDNDMuOTkyIDIwLjgwMTQgNDQuMTg3MiAyMC4xNjA1IDQ0LjE5NDMgMTkuMzI2MlYxOC44OTExQzQ0LjE5NDMgMTguMDQyNSA0NC4wMDk5IDE3LjM5NDQgNDMuNjQxMSAxNi45NDY4QzQzLjI3MjMgMTYuNDk5MiA0Mi43MzcgMTYuMjc1NCA0Mi4wMzUyIDE2LjI3NTRINDEuMDg0NVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTYxLjU1NjYgMTUuMTc5N0w2My44MTI1IDIxLjE3MzhMNjYuMDYzIDE1LjE3OTdINjcuODE5M1YyM0g2Ni40NjU4VjIwLjQyMTlMNjYuNjAwMSAxNi45NzM2TDY0LjI5MDUgMjNINjMuMzE4NEw2MS4wMTQyIDE2Ljk3OUw2MS4xNDg0IDIwLjQyMTlWMjNINTkuNzk0OVYxNS4xNzk3SDYxLjU1NjZaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik03MS44NjM4IDIzLjEwNzRDNzEuMDM2NiAyMy4xMDc0IDcwLjM2NTIgMjIuODQ3OCA2OS44NDk2IDIyLjMyODZDNjkuMzM3NiAyMS44MDU4IDY5LjA4MTUgMjEuMTExMiA2OS4wODE1IDIwLjI0NDZWMjAuMDgzNUM2OS4wODE1IDE5LjUwMzQgNjkuMTkyNSAxOC45ODYgNjkuNDE0NiAxOC41MzEyQzY5LjY0MDEgMTguMDcyOSA2OS45NTUyIDE3LjcxNjYgNzAuMzU5OSAxNy40NjI0QzcwLjc2NDUgMTcuMjA4MiA3MS4yMTU3IDE3LjA4MTEgNzEuNzEzNCAxNy4wODExQzcyLjUwNDcgMTcuMDgxMSA3My4xMTUyIDE3LjMzMzUgNzMuNTQ0OSAxNy44Mzg0QzczLjk3ODIgMTguMzQzMyA3NC4xOTQ4IDE5LjA1NzYgNzQuMTk0OCAxOS45ODE0VjIwLjUwNzhINzAuMzk3NUM3MC40MzY4IDIwLjk4NzYgNzAuNTk2MiAyMS4zNjcyIDcwLjg3NTUgMjEuNjQ2NUM3MS4xNTg0IDIxLjkyNTggNzEuNTEyOSAyMi4wNjU0IDcxLjkzOSAyMi4wNjU0QzcyLjUzNjkgMjIuMDY1NCA3My4wMjM5IDIxLjgyMzcgNzMuMzk5OSAyMS4zNDAzTDc0LjEwMzUgMjIuMDExN0M3My44NzA4IDIyLjM1OSA3My41NTkyIDIyLjYyOTQgNzMuMTY4OSAyMi44MjI4QzcyLjc4MjIgMjMuMDEyNSA3Mi4zNDcyIDIzLjEwNzQgNzEuODYzOCAyMy4xMDc0Wk03MS43MDggMTguMTI4NEM3MS4zNDk5IDE4LjEyODQgNzEuMDU5OSAxOC4yNTM3IDcwLjgzNzkgMTguNTA0NEM3MC42MTk1IDE4Ljc1NSA3MC40Nzk4IDE5LjEwNDIgNzAuNDE4OSAxOS41NTE4SDcyLjkwNThWMTkuNDU1MUM3Mi44NzcxIDE5LjAxODIgNzIuNzYwNyAxOC42ODg4IDcyLjU1NjYgMTguNDY2OEM3Mi4zNTI1IDE4LjI0MTIgNzIuMDY5NyAxOC4xMjg0IDcxLjcwOCAxOC4xMjg0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNzguNDcwMiAyMS40MjA5Qzc4LjQ3MDIgMjEuMTg4MiA3OC4zNzM1IDIxLjAxMDkgNzguMTgwMiAyMC44ODkyQzc3Ljk5MDQgMjAuNzY3NCA3Ny42NzM1IDIwLjY2IDc3LjIyOTUgMjAuNTY2OUM3Ni43ODU1IDIwLjQ3MzggNzYuNDE0OSAyMC4zNTU2IDc2LjExNzcgMjAuMjEyNEM3NS40NjYgMTkuODk3MyA3NS4xNDAxIDE5LjQ0MDggNzUuMTQwMSAxOC44NDI4Qzc1LjE0MDEgMTguMzQxNSA3NS4zNTE0IDE3LjkyMjUgNzUuNzczOSAxNy41ODU5Qzc2LjE5NjUgMTcuMjQ5MyA3Ni43MzM2IDE3LjA4MTEgNzcuMzg1MyAxNy4wODExQzc4LjA3OTkgMTcuMDgxMSA3OC42NDAzIDE3LjI1MjkgNzkuMDY2NCAxNy41OTY3Qzc5LjQ5NjEgMTcuOTQwNCA3OS43MTA5IDE4LjM4NjIgNzkuNzEwOSAxOC45MzQxSDc4LjQwNThDNzguNDA1OCAxOC42ODM0IDc4LjMxMjcgMTguNDc1NyA3OC4xMjY1IDE4LjMxMUM3Ny45NDAzIDE4LjE0MjcgNzcuNjkzMiAxOC4wNTg2IDc3LjM4NTMgMTguMDU4NkM3Ny4wOTg4IDE4LjA1ODYgNzYuODY0MyAxOC4xMjQ4IDc2LjY4MTYgMTguMjU3M0M3Ni41MDI2IDE4LjM4OTggNzYuNDEzMSAxOC41NjcxIDc2LjQxMzEgMTguNzg5MUM3Ni40MTMxIDE4Ljk4OTYgNzYuNDk3MiAxOS4xNDUzIDc2LjY2NTUgMTkuMjU2M0M3Ni44MzM4IDE5LjM2NzQgNzcuMTc0IDE5LjQ4MDEgNzcuNjg2IDE5LjU5NDdDNzguMTk4MSAxOS43MDU3IDc4LjU5OTEgMTkuODQgNzguODg5MiAxOS45OTc2Qzc5LjE4MjggMjAuMTUxNSA3OS4zOTk0IDIwLjMzNzcgNzkuNTM5MSAyMC41NTYyQzc5LjY4MjMgMjAuNzc0NiA3OS43NTM5IDIxLjAzOTYgNzkuNzUzOSAyMS4zNTExQzc5Ljc1MzkgMjEuODczOSA3OS41MzczIDIyLjI5ODIgNzkuMTA0IDIyLjYyNEM3OC42NzA3IDIyLjk0NjMgNzguMTAzMiAyMy4xMDc0IDc3LjQwMTQgMjMuMTA3NEM3Ni45MjUxIDIzLjEwNzQgNzYuNTAwOCAyMy4wMjE1IDc2LjEyODQgMjIuODQ5NkM3NS43NTYgMjIuNjc3NyA3NS40NjYgMjIuNDQxNCA3NS4yNTgzIDIyLjE0MDZDNzUuMDUwNiAyMS44Mzk4IDc0Ljk0NjggMjEuNTE1OCA3NC45NDY4IDIxLjE2ODVINzYuMjE0NEM3Ni4yMzIzIDIxLjQ3NjQgNzYuMzQ4NiAyMS43MTQ1IDc2LjU2MzUgMjEuODgyOEM3Ni43NzgzIDIyLjA0NzUgNzcuMDYzIDIyLjEyOTkgNzcuNDE3NSAyMi4xMjk5Qzc3Ljc2MTIgMjIuMTI5OSA3OC4wMjI2IDIyLjA2NTQgNzguMjAxNyAyMS45MzY1Qzc4LjM4MDcgMjEuODA0IDc4LjQ3MDIgMjEuNjMyMiA3OC40NzAyIDIxLjQyMDlaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik04NC4xNTI4IDIxLjQyMDlDODQuMTUyOCAyMS4xODgyIDg0LjA1NjIgMjEuMDEwOSA4My44NjI4IDIwLjg4OTJDODMuNjczIDIwLjc2NzQgODMuMzU2MSAyMC42NiA4Mi45MTIxIDIwLjU2NjlDODIuNDY4MSAyMC40NzM4IDgyLjA5NzUgMjAuMzU1NiA4MS44MDAzIDIwLjIxMjRDODEuMTQ4NiAxOS44OTczIDgwLjgyMjggMTkuNDQwOCA4MC44MjI4IDE4Ljg0MjhDODAuODIyOCAxOC4zNDE1IDgxLjAzNCAxNy45MjI1IDgxLjQ1NjUgMTcuNTg1OUM4MS44NzkxIDE3LjI0OTMgODIuNDE2MiAxNy4wODExIDgzLjA2NzkgMTcuMDgxMUM4My43NjI1IDE3LjA4MTEgODQuMzIyOSAxNy4yNTI5IDg0Ljc0OSAxNy41OTY3Qzg1LjE3ODcgMTcuOTQwNCA4NS4zOTM2IDE4LjM4NjIgODUuMzkzNiAxOC45MzQxSDg0LjA4ODRDODQuMDg4NCAxOC42ODM0IDgzLjk5NTMgMTguNDc1NyA4My44MDkxIDE4LjMxMUM4My42MjI5IDE4LjE0MjcgODMuMzc1OCAxOC4wNTg2IDgzLjA2NzkgMTguMDU4NkM4Mi43ODE0IDE4LjA1ODYgODIuNTQ2OSAxOC4xMjQ4IDgyLjM2NDMgMTguMjU3M0M4Mi4xODUyIDE4LjM4OTggODIuMDk1NyAxOC41NjcxIDgyLjA5NTcgMTguNzg5MUM4Mi4wOTU3IDE4Ljk4OTYgODIuMTc5OSAxOS4xNDUzIDgyLjM0ODEgMTkuMjU2M0M4Mi41MTY0IDE5LjM2NzQgODIuODU2NiAxOS40ODAxIDgzLjM2ODcgMTkuNTk0N0M4My44ODA3IDE5LjcwNTcgODQuMjgxNyAxOS44NCA4NC41NzE4IDE5Ljk5NzZDODQuODY1NCAyMC4xNTE1IDg1LjA4MiAyMC4zMzc3IDg1LjIyMTcgMjAuNTU2MkM4NS4zNjQ5IDIwLjc3NDYgODUuNDM2NSAyMS4wMzk2IDg1LjQzNjUgMjEuMzUxMUM4NS40MzY1IDIxLjg3MzkgODUuMjE5OSAyMi4yOTgyIDg0Ljc4NjYgMjIuNjI0Qzg0LjM1MzQgMjIuOTQ2MyA4My43ODU4IDIzLjEwNzQgODMuMDg0IDIzLjEwNzRDODIuNjA3NyAyMy4xMDc0IDgyLjE4MzQgMjMuMDIxNSA4MS44MTEgMjIuODQ5NkM4MS40Mzg2IDIyLjY3NzcgODEuMTQ4NiAyMi40NDE0IDgwLjk0MDkgMjIuMTQwNkM4MC43MzMyIDIxLjgzOTggODAuNjI5NCAyMS41MTU4IDgwLjYyOTQgMjEuMTY4NUg4MS44OTdDODEuOTE0OSAyMS40NzY0IDgyLjAzMTIgMjEuNzE0NSA4Mi4yNDYxIDIxLjg4MjhDODIuNDYwOSAyMi4wNDc1IDgyLjc0NTYgMjIuMTI5OSA4My4xMDAxIDIyLjEyOTlDODMuNDQzOCAyMi4xMjk5IDgzLjcwNTIgMjIuMDY1NCA4My44ODQzIDIxLjkzNjVDODQuMDYzMyAyMS44MDQgODQuMTUyOCAyMS42MzIyIDg0LjE1MjggMjEuNDIwOVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTkwLjA1MDMgMjNDODkuOTkzIDIyLjg4OSA4OS45NDI5IDIyLjcwODIgODkuODk5OSAyMi40NTc1Qzg5LjQ4NDUgMjIuODkwOCA4OC45NzYxIDIzLjEwNzQgODguMzc0NSAyMy4xMDc0Qzg3Ljc5MDkgMjMuMTA3NCA4Ny4zMTQ2IDIyLjk0MDkgODYuOTQ1OCAyMi42MDc5Qzg2LjU3NyAyMi4yNzQ5IDg2LjM5MjYgMjEuODYzMSA4Ni4zOTI2IDIxLjM3MjZDODYuMzkyNiAyMC43NTMxIDg2LjYyMTcgMjAuMjc4NiA4Ny4wODAxIDE5Ljk0OTJDODcuNTQyIDE5LjYxNjIgODguMjAwOCAxOS40NDk3IDg5LjA1NjYgMTkuNDQ5N0g4OS44NTY5VjE5LjA2ODRDODkuODU2OSAxOC43Njc2IDg5Ljc3MjggMTguNTI3NyA4OS42MDQ1IDE4LjM0ODZDODkuNDM2MiAxOC4xNjYgODkuMTgwMiAxOC4wNzQ3IDg4LjgzNjQgMTguMDc0N0M4OC41MzkyIDE4LjA3NDcgODguMjk1NyAxOC4xNDk5IDg4LjEwNiAxOC4zMDAzQzg3LjkxNjIgMTguNDQ3MSA4Ny44MjEzIDE4LjYzNTEgODcuODIxMyAxOC44NjQzSDg2LjUxNjFDODYuNTE2MSAxOC41NDU2IDg2LjYyMTcgMTguMjQ4NCA4Ni44MzMgMTcuOTcyN0M4Ny4wNDQzIDE3LjY5MzQgODcuMzMwNyAxNy40NzQ5IDg3LjY5MjQgMTcuMzE3NEM4OC4wNTc2IDE3LjE1OTggODguNDY0IDE3LjA4MTEgODguOTExNiAxNy4wODExQzg5LjU5MiAxNy4wODExIDkwLjEzNDQgMTcuMjUyOSA5MC41MzkxIDE3LjU5NjdDOTAuOTQzNyAxNy45MzY4IDkxLjE1MTQgMTguNDE2NyA5MS4xNjIxIDE5LjAzNjFWMjEuNjU3MkM5MS4xNjIxIDIyLjE4IDkxLjIzNTUgMjIuNTk3MiA5MS4zODIzIDIyLjkwODdWMjNIOTAuMDUwM1pNODguNjE2MiAyMi4wNjAxQzg4Ljg3NCAyMi4wNjAxIDg5LjExNTcgMjEuOTk3NCA4OS4zNDEzIDIxLjg3MjFDODkuNTcwNSAyMS43NDY3IDg5Ljc0MjQgMjEuNTc4NSA4OS44NTY5IDIxLjM2NzJWMjAuMjcxNUg4OS4xNTMzQzg4LjY2OTkgMjAuMjcxNSA4OC4zMDY1IDIwLjM1NTYgODguMDYzIDIwLjUyMzlDODcuODE5NSAyMC42OTIyIDg3LjY5NzggMjAuOTMwMyA4Ny42OTc4IDIxLjIzODNDODcuNjk3OCAyMS40ODg5IDg3Ljc4MDEgMjEuNjg5NSA4Ny45NDQ4IDIxLjgzOThDODguMTEzMSAyMS45ODY3IDg4LjMzNjkgMjIuMDYwMSA4OC42MTYyIDIyLjA2MDFaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05Mi4zMDA4IDIwLjA1MTNDOTIuMzAwOCAxOS4xNDg5IDkyLjUxMiAxOC40MjkyIDkyLjkzNDYgMTcuODkyMUM5My4zNjA3IDE3LjM1MTQgOTMuOTI0NiAxNy4wODExIDk0LjYyNjUgMTcuMDgxMUM5NS4yODg5IDE3LjA4MTEgOTUuODA5OSAxNy4zMTIgOTYuMTg5NSAxNy43NzM5TDk2LjI0ODUgMTcuMTg4NUg5Ny40MjQ4VjIyLjgyMjhDOTcuNDI0OCAyMy41ODU0IDk3LjE4NjcgMjQuMTg3IDk2LjcxMDQgMjQuNjI3NEM5Ni4yMzc4IDI1LjA2NzkgOTUuNTk4NiAyNS4yODgxIDk0Ljc5MyAyNS4yODgxQzk0LjM2NjkgMjUuMjg4MSA5My45NDk3IDI1LjE5ODYgOTMuNTQxNSAyNS4wMTk1QzkzLjEzNjkgMjQuODQ0MSA5Mi44Mjg5IDI0LjYxMzEgOTIuNjE3NyAyNC4zMjY3TDkzLjIzNTQgMjMuNTQyNUM5My42MzY0IDI0LjAxODcgOTQuMTMwNSAyNC4yNTY4IDk0LjcxNzggMjQuMjU2OEM5NS4xNTEgMjQuMjU2OCA5NS40OTMgMjQuMTM4NyA5NS43NDM3IDIzLjkwMjNDOTUuOTk0MyAyMy42Njk2IDk2LjExOTYgMjMuMzI1OCA5Ni4xMTk2IDIyLjg3MTFWMjIuNDc5Qzk1Ljc0MzcgMjIuODk3OSA5NS4yNDI0IDIzLjEwNzQgOTQuNjE1NyAyMy4xMDc0QzkzLjkzNTQgMjMuMTA3NCA5My4zNzg2IDIyLjgzNzEgOTIuOTQ1MyAyMi4yOTY0QzkyLjUxNTYgMjEuNzU1NyA5Mi4zMDA4IDIxLjAwNzMgOTIuMzAwOCAyMC4wNTEzWk05My42MDA2IDIwLjE2NDFDOTMuNjAwNiAyMC43NDc3IDkzLjcxODggMjEuMjA3OCA5My45NTUxIDIxLjU0NDRDOTQuMTk1IDIxLjg3NzQgOTQuNTI2MiAyMi4wNDM5IDk0Ljk0ODcgMjIuMDQzOUM5NS40NzUxIDIyLjA0MzkgOTUuODY1NCAyMS44MTg0IDk2LjExOTYgMjEuMzY3MlYxOC44MTA1Qzk1Ljg3MjYgMTguMzcwMSA5NS40ODU4IDE4LjE0OTkgOTQuOTU5NSAxOC4xNDk5Qzk0LjUyOTggMTguMTQ5OSA5NC4xOTUgMTguMzIgOTMuOTU1MSAxOC42NjAyQzkzLjcxODggMTkuMDAwMyA5My42MDA2IDE5LjUwMTYgOTMuNjAwNiAyMC4xNjQxWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTAxLjMzIDIzLjEwNzRDMTAwLjUwMiAyMy4xMDc0IDk5LjgzMTEgMjIuODQ3OCA5OS4zMTU0IDIyLjMyODZDOTguODAzNCAyMS44MDU4IDk4LjU0NzQgMjEuMTExMiA5OC41NDc0IDIwLjI0NDZWMjAuMDgzNUM5OC41NDc0IDE5LjUwMzQgOTguNjU4NCAxOC45ODYgOTguODgwNCAxOC41MzEyQzk5LjEwNiAxOC4wNzI5IDk5LjQyMTEgMTcuNzE2NiA5OS44MjU3IDE3LjQ2MjRDMTAwLjIzIDE3LjIwODIgMTAwLjY4MSAxNy4wODExIDEwMS4xNzkgMTcuMDgxMUMxMDEuOTcxIDE3LjA4MTEgMTAyLjU4MSAxNy4zMzM1IDEwMy4wMTEgMTcuODM4NEMxMDMuNDQ0IDE4LjM0MzMgMTAzLjY2MSAxOS4wNTc2IDEwMy42NjEgMTkuOTgxNFYyMC41MDc4SDk5Ljg2MzNDOTkuOTAyNyAyMC45ODc2IDEwMC4wNjIgMjEuMzY3MiAxMDAuMzQxIDIxLjY0NjVDMTAwLjYyNCAyMS45MjU4IDEwMC45NzkgMjIuMDY1NCAxMDEuNDA1IDIyLjA2NTRDMTAyLjAwMyAyMi4wNjU0IDEwMi40OSAyMS44MjM3IDEwMi44NjYgMjEuMzQwM0wxMDMuNTY5IDIyLjAxMTdDMTAzLjMzNyAyMi4zNTkgMTAzLjAyNSAyMi42Mjk0IDEwMi42MzUgMjIuODIyOEMxMDIuMjQ4IDIzLjAxMjUgMTAxLjgxMyAyMy4xMDc0IDEwMS4zMyAyMy4xMDc0Wk0xMDEuMTc0IDE4LjEyODRDMTAwLjgxNiAxOC4xMjg0IDEwMC41MjYgMTguMjUzNyAxMDAuMzA0IDE4LjUwNDRDMTAwLjA4NSAxOC43NTUgOTkuOTQ1NiAxOS4xMDQyIDk5Ljg4NDggMTkuNTUxOEgxMDIuMzcyVjE5LjQ1NTFDMTAyLjM0MyAxOS4wMTgyIDEwMi4yMjcgMTguNjg4OCAxMDIuMDIyIDE4LjQ2NjhDMTAxLjgxOCAxOC4yNDEyIDEwMS41MzUgMTguMTI4NCAxMDEuMTc0IDE4LjEyODRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xMDkuMDUzIDE1Ljc3NTlWMTcuMTg4NUgxMTAuMDc5VjE4LjE1NTNIMTA5LjA1M1YyMS4zOTk0QzEwOS4wNTMgMjEuNjIxNCAxMDkuMDk2IDIxLjc4MjYgMTA5LjE4MiAyMS44ODI4QzEwOS4yNzIgMjEuOTc5NSAxMDkuNDI5IDIyLjAyNzggMTA5LjY1NSAyMi4wMjc4QzEwOS44MDUgMjIuMDI3OCAxMDkuOTU3IDIyLjAwOTkgMTEwLjExMSAyMS45NzQxVjIyLjk4MzlDMTA5LjgxNCAyMy4wNjYyIDEwOS41MjggMjMuMTA3NCAxMDkuMjUyIDIzLjEwNzRDMTA4LjI0OSAyMy4xMDc0IDEwNy43NDggMjIuNTU0MiAxMDcuNzQ4IDIxLjQ0NzhWMTguMTU1M0gxMDYuNzkyVjE3LjE4ODVIMTA3Ljc0OFYxNS43NzU5SDEwOS4wNTNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xMTMuMTE0IDIxLjEzMDlMMTE0LjI5NSAxNy4xODg1SDExNS42ODdMMTEzLjM3NyAyMy44ODA5QzExMy4wMjIgMjQuODU4NCAxMTIuNDIxIDI1LjM0NzIgMTExLjU3MiAyNS4zNDcyQzExMS4zODIgMjUuMzQ3MiAxMTEuMTczIDI1LjMxNDkgMTEwLjk0NCAyNS4yNTA1VjI0LjI0MDdMMTExLjE5MSAyNC4yNTY4QzExMS41MiAyNC4yNTY4IDExMS43NjcgMjQuMTk2IDExMS45MzIgMjQuMDc0MkMxMTIuMSAyMy45NTYxIDExMi4yMzMgMjMuNzU1NSAxMTIuMzMgMjMuNDcyN0wxMTIuNTE4IDIyLjk3MzFMMTEwLjQ3NyAxNy4xODg1SDExMS44ODRMMTEzLjExNCAyMS4xMzA5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTIxLjUzIDIwLjE1MzNDMTIxLjUzIDIxLjA1MjEgMTIxLjMyNiAyMS43NyAxMjAuOTE4IDIyLjMwNzFDMTIwLjUxIDIyLjg0MDcgMTE5Ljk2MiAyMy4xMDc0IDExOS4yNzQgMjMuMTA3NEMxMTguNjM3IDIzLjEwNzQgMTE4LjEyNyAyMi44OTc5IDExNy43NDQgMjIuNDc5VjI1LjIzNDRIMTE2LjQzOFYxNy4xODg1SDExNy42NDJMMTE3LjY5NSAxNy43NzkzQzExOC4wNzggMTcuMzEzOCAxMTguNTk5IDE3LjA4MTEgMTE5LjI1OCAxNy4wODExQzExOS45NjcgMTcuMDgxMSAxMjAuNTIyIDE3LjM0NiAxMjAuOTIzIDE3Ljg3NkMxMjEuMzI4IDE4LjQwMjMgMTIxLjUzIDE5LjEzNDYgMTIxLjUzIDIwLjA3MjhWMjAuMTUzM1pNMTIwLjIzIDIwLjA0MDVDMTIwLjIzIDE5LjQ2MDQgMTIwLjExNCAxOS4wMDAzIDExOS44ODEgMTguNjYwMkMxMTkuNjUyIDE4LjMyIDExOS4zMjMgMTguMTQ5OSAxMTguODkzIDE4LjE0OTlDMTE4LjM2IDE4LjE0OTkgMTE3Ljk3NiAxOC4zNzAxIDExNy43NDQgMTguODEwNVYyMS4zODg3QzExNy45OCAyMS44Mzk4IDExOC4zNjcgMjIuMDY1NCAxMTguOTA0IDIyLjA2NTRDMTE5LjMxOSAyMi4wNjU0IDExOS42NDMgMjEuODk4OSAxMTkuODc2IDIxLjU2NTlDMTIwLjExMiAyMS4yMjkzIDEyMC4yMyAyMC43MjA5IDEyMC4yMyAyMC4wNDA1WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTI1LjE5OSAyMy4xMDc0QzEyNC4zNzIgMjMuMTA3NCAxMjMuNyAyMi44NDc4IDEyMy4xODUgMjIuMzI4NkMxMjIuNjczIDIxLjgwNTggMTIyLjQxNyAyMS4xMTEyIDEyMi40MTcgMjAuMjQ0NlYyMC4wODM1QzEyMi40MTcgMTkuNTAzNCAxMjIuNTI4IDE4Ljk4NiAxMjIuNzUgMTguNTMxMkMxMjIuOTc1IDE4LjA3MjkgMTIzLjI5IDE3LjcxNjYgMTIzLjY5NSAxNy40NjI0QzEyNC4wOTkgMTcuMjA4MiAxMjQuNTUxIDE3LjA4MTEgMTI1LjA0OCAxNy4wODExQzEyNS44NCAxNy4wODExIDEyNi40NSAxNy4zMzM1IDEyNi44OCAxNy44Mzg0QzEyNy4zMTMgMTguMzQzMyAxMjcuNTMgMTkuMDU3NiAxMjcuNTMgMTkuOTgxNFYyMC41MDc4SDEyMy43MzJDMTIzLjc3MiAyMC45ODc2IDEyMy45MzEgMjEuMzY3MiAxMjQuMjEgMjEuNjQ2NUMxMjQuNDkzIDIxLjkyNTggMTI0Ljg0OCAyMi4wNjU0IDEyNS4yNzQgMjIuMDY1NEMxMjUuODcyIDIyLjA2NTQgMTI2LjM1OSAyMS44MjM3IDEyNi43MzUgMjEuMzQwM0wxMjcuNDM4IDIyLjAxMTdDMTI3LjIwNiAyMi4zNTkgMTI2Ljg5NCAyMi42Mjk0IDEyNi41MDQgMjIuODIyOEMxMjYuMTE3IDIzLjAxMjUgMTI1LjY4MiAyMy4xMDc0IDEyNS4xOTkgMjMuMTA3NFpNMTI1LjA0MyAxOC4xMjg0QzEyNC42ODUgMTguMTI4NCAxMjQuMzk1IDE4LjI1MzcgMTI0LjE3MyAxOC41MDQ0QzEyMy45NTQgMTguNzU1IDEyMy44MTUgMTkuMTA0MiAxMjMuNzU0IDE5LjU1MThIMTI2LjI0MVYxOS40NTUxQzEyNi4yMTIgMTkuMDE4MiAxMjYuMDk2IDE4LjY4ODggMTI1Ljg5MiAxOC40NjY4QzEyNS42ODggMTguMjQxMiAxMjUuNDA1IDE4LjEyODQgMTI1LjA0MyAxOC4xMjg0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTQ0Ljg4MiAyMC45ODU4QzE0NC44ODIgMjAuNjQyMSAxNDQuNzYxIDIwLjM3NzEgMTQ0LjUxNyAyMC4xOTA5QzE0NC4yNzcgMjAuMDA0NyAxNDMuODQyIDE5LjgxNjcgMTQzLjIxMiAxOS42MjdDMTQyLjU4MiAxOS40MzcyIDE0Mi4wOCAxOS4yMjU5IDE0MS43MDggMTguOTkzMkMxNDAuOTk1IDE4LjU0NTYgMTQwLjYzOSAxNy45NjE5IDE0MC42MzkgMTcuMjQyMkMxNDAuNjM5IDE2LjYxMiAxNDAuODk1IDE2LjA5MjggMTQxLjQwNyAxNS42ODQ2QzE0MS45MjMgMTUuMjc2NCAxNDIuNTkxIDE1LjA3MjMgMTQzLjQxMSAxNS4wNzIzQzE0My45NTUgMTUuMDcyMyAxNDQuNDQgMTUuMTcyNSAxNDQuODY2IDE1LjM3M0MxNDUuMjkyIDE1LjU3MzYgMTQ1LjYyNyAxNS44NiAxNDUuODcxIDE2LjIzMjRDMTQ2LjExNCAxNi42MDEyIDE0Ni4yMzYgMTcuMDExMiAxNDYuMjM2IDE3LjQ2MjRIMTQ0Ljg4MkMxNDQuODgyIDE3LjA1NDIgMTQ0Ljc1MyAxNi43MzU1IDE0NC40OTYgMTYuNTA2M0MxNDQuMjQxIDE2LjI3MzYgMTQzLjg3NiAxNi4xNTcyIDE0My40IDE2LjE1NzJDMTQyLjk1NiAxNi4xNTcyIDE0Mi42MSAxNi4yNTIxIDE0Mi4zNjMgMTYuNDQxOUMxNDIuMTIgMTYuNjMxNyAxNDEuOTk4IDE2Ljg5NjYgMTQxLjk5OCAxNy4yMzY4QzE0MS45OTggMTcuNTIzMyAxNDIuMTMxIDE3Ljc2MzIgMTQyLjM5NiAxNy45NTY1QzE0Mi42NiAxOC4xNDYzIDE0My4wOTcgMTguMzMyNSAxNDMuNzA2IDE4LjUxNTFDMTQ0LjMxNSAxOC42OTQyIDE0NC44MDQgMTguOTAwMSAxNDUuMTcyIDE5LjEzMjhDMTQ1LjU0MSAxOS4zNjIgMTQ1LjgxMiAxOS42MjcgMTQ1Ljk4MyAxOS45Mjc3QzE0Ni4xNTUgMjAuMjI0OSAxNDYuMjQxIDIwLjU3NDEgMTQ2LjI0MSAyMC45NzUxQzE0Ni4yNDEgMjEuNjI2OCAxNDUuOTkxIDIyLjE0NiAxNDUuNDg5IDIyLjUzMjdDMTQ0Ljk5MiAyMi45MTU5IDE0NC4zMTUgMjMuMTA3NCAxNDMuNDU5IDIzLjEwNzRDMTQyLjg5MyAyMy4xMDc0IDE0Mi4zNzIgMjMuMDAzNiAxNDEuODk2IDIyLjc5NTlDMTQxLjQyMyAyMi41ODQ2IDE0MS4wNTUgMjIuMjk0NiAxNDAuNzkgMjEuOTI1OEMxNDAuNTI4IDIxLjU1NyAxNDAuMzk3IDIxLjEyNzMgMTQwLjM5NyAyMC42MzY3SDE0MS43NTZDMTQxLjc1NiAyMS4wODA3IDE0MS45MDMgMjEuNDI0NSAxNDIuMTk3IDIxLjY2OEMxNDIuNDkgMjEuOTExNSAxNDIuOTExIDIyLjAzMzIgMTQzLjQ1OSAyMi4wMzMyQzE0My45MzIgMjIuMDMzMiAxNDQuMjg2IDIxLjkzODMgMTQ0LjUyMiAyMS43NDg1QzE0NC43NjIgMjEuNTU1MiAxNDQuODgyIDIxLjMwMDkgMTQ0Ljg4MiAyMC45ODU4WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTQ4Ljk0MyAxNS43NzU5VjE3LjE4ODVIMTQ5Ljk2OVYxOC4xNTUzSDE0OC45NDNWMjEuMzk5NEMxNDguOTQzIDIxLjYyMTQgMTQ4Ljk4NiAyMS43ODI2IDE0OS4wNzIgMjEuODgyOEMxNDkuMTYxIDIxLjk3OTUgMTQ5LjMxOSAyMi4wMjc4IDE0OS41NDQgMjIuMDI3OEMxNDkuNjk1IDIyLjAyNzggMTQ5Ljg0NyAyMi4wMDk5IDE1MC4wMDEgMjEuOTc0MVYyMi45ODM5QzE0OS43MDQgMjMuMDY2MiAxNDkuNDE3IDIzLjEwNzQgMTQ5LjE0MiAyMy4xMDc0QzE0OC4xMzkgMjMuMTA3NCAxNDcuNjM4IDIyLjU1NDIgMTQ3LjYzOCAyMS40NDc4VjE4LjE1NTNIMTQ2LjY4MlYxNy4xODg1SDE0Ny42MzhWMTUuNzc1OUgxNDguOTQzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTU0LjQ0MyAyM0MxNTQuMzg2IDIyLjg4OSAxNTQuMzM1IDIyLjcwODIgMTU0LjI5MiAyMi40NTc1QzE1My44NzcgMjIuODkwOCAxNTMuMzY5IDIzLjEwNzQgMTUyLjc2NyAyMy4xMDc0QzE1Mi4xODMgMjMuMTA3NCAxNTEuNzA3IDIyLjk0MDkgMTUxLjMzOCAyMi42MDc5QzE1MC45NyAyMi4yNzQ5IDE1MC43ODUgMjEuODYzMSAxNTAuNzg1IDIxLjM3MjZDMTUwLjc4NSAyMC43NTMxIDE1MS4wMTQgMjAuMjc4NiAxNTEuNDczIDE5Ljk0OTJDMTUxLjkzNSAxOS42MTYyIDE1Mi41OTMgMTkuNDQ5NyAxNTMuNDQ5IDE5LjQ0OTdIMTU0LjI1VjE5LjA2ODRDMTU0LjI1IDE4Ljc2NzYgMTU0LjE2NSAxOC41Mjc3IDE1My45OTcgMTguMzQ4NkMxNTMuODI5IDE4LjE2NiAxNTMuNTczIDE4LjA3NDcgMTUzLjIyOSAxOC4wNzQ3QzE1Mi45MzIgMTguMDc0NyAxNTIuNjg4IDE4LjE0OTkgMTUyLjQ5OSAxOC4zMDAzQzE1Mi4zMDkgMTguNDQ3MSAxNTIuMjE0IDE4LjYzNTEgMTUyLjIxNCAxOC44NjQzSDE1MC45MDlDMTUwLjkwOSAxOC41NDU2IDE1MS4wMTQgMTguMjQ4NCAxNTEuMjI2IDE3Ljk3MjdDMTUxLjQzNyAxNy42OTM0IDE1MS43MjMgMTcuNDc0OSAxNTIuMDg1IDE3LjMxNzRDMTUyLjQ1IDE3LjE1OTggMTUyLjg1NyAxNy4wODExIDE1My4zMDQgMTcuMDgxMUMxNTMuOTg1IDE3LjA4MTEgMTU0LjUyNyAxNy4yNTI5IDE1NC45MzIgMTcuNTk2N0MxNTUuMzM2IDE3LjkzNjggMTU1LjU0NCAxOC40MTY3IDE1NS41NTUgMTkuMDM2MVYyMS42NTcyQzE1NS41NTUgMjIuMTggMTU1LjYyOCAyMi41OTcyIDE1NS43NzUgMjIuOTA4N1YyM0gxNTQuNDQzWk0xNTMuMDA5IDIyLjA2MDFDMTUzLjI2NyAyMi4wNjAxIDE1My41MDggMjEuOTk3NCAxNTMuNzM0IDIxLjg3MjFDMTUzLjk2MyAyMS43NDY3IDE1NC4xMzUgMjEuNTc4NSAxNTQuMjUgMjEuMzY3MlYyMC4yNzE1SDE1My41NDZDMTUzLjA2MiAyMC4yNzE1IDE1Mi42OTkgMjAuMzU1NiAxNTIuNDU2IDIwLjUyMzlDMTUyLjIxMiAyMC42OTIyIDE1Mi4wOSAyMC45MzAzIDE1Mi4wOSAyMS4yMzgzQzE1Mi4wOSAyMS40ODg5IDE1Mi4xNzMgMjEuNjg5NSAxNTIuMzM3IDIxLjgzOThDMTUyLjUwNiAyMS45ODY3IDE1Mi43MjkgMjIuMDYwMSAxNTMuMDA5IDIyLjA2MDFaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xNTguNTU3IDE1Ljc3NTlWMTcuMTg4NUgxNTkuNTgzVjE4LjE1NTNIMTU4LjU1N1YyMS4zOTk0QzE1OC41NTcgMjEuNjIxNCAxNTguNiAyMS43ODI2IDE1OC42ODYgMjEuODgyOEMxNTguNzc2IDIxLjk3OTUgMTU4LjkzMyAyMi4wMjc4IDE1OS4xNTkgMjIuMDI3OEMxNTkuMzA5IDIyLjAyNzggMTU5LjQ2MSAyMi4wMDk5IDE1OS42MTUgMjEuOTc0MVYyMi45ODM5QzE1OS4zMTggMjMuMDY2MiAxNTkuMDMyIDIzLjEwNzQgMTU4Ljc1NiAyMy4xMDc0QzE1Ny43NTMgMjMuMTA3NCAxNTcuMjUyIDIyLjU1NDIgMTU3LjI1MiAyMS40NDc4VjE4LjE1NTNIMTU2LjI5NlYxNy4xODg1SDE1Ny4yNTJWMTUuNzc1OUgxNTguNTU3WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTY0LjEwNSAyMi40MzA3QzE2My43MjIgMjIuODgxOCAxNjMuMTc4IDIzLjEwNzQgMTYyLjQ3MyAyMy4xMDc0QzE2MS44NDIgMjMuMTA3NCAxNjEuMzY0IDIyLjkyMyAxNjEuMDM5IDIyLjU1NDJDMTYwLjcxNiAyMi4xODU0IDE2MC41NTUgMjEuNjUxOSAxNjAuNTU1IDIwLjk1MzZWMTcuMTg4NUgxNjEuODZWMjAuOTM3NUMxNjEuODYgMjEuNjc1MSAxNjIuMTY3IDIyLjA0MzkgMTYyLjc3OSAyMi4wNDM5QzE2My40MTMgMjIuMDQzOSAxNjMuODQgMjEuODE2NiAxNjQuMDYyIDIxLjM2MThWMTcuMTg4NUgxNjUuMzY4VjIzSDE2NC4xMzhMMTY0LjEwNSAyMi40MzA3WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTY5Ljk1NSAyMS40MjA5QzE2OS45NTUgMjEuMTg4MiAxNjkuODU4IDIxLjAxMDkgMTY5LjY2NSAyMC44ODkyQzE2OS40NzUgMjAuNzY3NCAxNjkuMTU4IDIwLjY2IDE2OC43MTQgMjAuNTY2OUMxNjguMjcgMjAuNDczOCAxNjcuODk5IDIwLjM1NTYgMTY3LjYwMiAyMC4yMTI0QzE2Ni45NSAxOS44OTczIDE2Ni42MjUgMTkuNDQwOCAxNjYuNjI1IDE4Ljg0MjhDMTY2LjYyNSAxOC4zNDE1IDE2Ni44MzYgMTcuOTIyNSAxNjcuMjU4IDE3LjU4NTlDMTY3LjY4MSAxNy4yNDkzIDE2OC4yMTggMTcuMDgxMSAxNjguODcgMTcuMDgxMUMxNjkuNTY0IDE3LjA4MTEgMTcwLjEyNSAxNy4yNTI5IDE3MC41NTEgMTcuNTk2N0MxNzAuOTggMTcuOTQwNCAxNzEuMTk1IDE4LjM4NjIgMTcxLjE5NSAxOC45MzQxSDE2OS44OUMxNjkuODkgMTguNjgzNCAxNjkuNzk3IDE4LjQ3NTcgMTY5LjYxMSAxOC4zMTFDMTY5LjQyNSAxOC4xNDI3IDE2OS4xNzggMTguMDU4NiAxNjguODcgMTguMDU4NkMxNjguNTgzIDE4LjA1ODYgMTY4LjM0OSAxOC4xMjQ4IDE2OC4xNjYgMTguMjU3M0MxNjcuOTg3IDE4LjM4OTggMTY3Ljg5NyAxOC41NjcxIDE2Ny44OTcgMTguNzg5MUMxNjcuODk3IDE4Ljk4OTYgMTY3Ljk4MiAxOS4xNDUzIDE2OC4xNSAxOS4yNTYzQzE2OC4zMTggMTkuMzY3NCAxNjguNjU4IDE5LjQ4MDEgMTY5LjE3IDE5LjU5NDdDMTY5LjY4MiAxOS43MDU3IDE3MC4wODMgMTkuODQgMTcwLjM3NCAxOS45OTc2QzE3MC42NjcgMjAuMTUxNSAxNzAuODg0IDIwLjMzNzcgMTcxLjAyMyAyMC41NTYyQzE3MS4xNjcgMjAuNzc0NiAxNzEuMjM4IDIxLjAzOTYgMTcxLjIzOCAyMS4zNTExQzE3MS4yMzggMjEuODczOSAxNzEuMDIyIDIyLjI5ODIgMTcwLjU4OCAyMi42MjRDMTcwLjE1NSAyMi45NDYzIDE2OS41ODggMjMuMTA3NCAxNjguODg2IDIzLjEwNzRDMTY4LjQxIDIzLjEwNzQgMTY3Ljk4NSAyMy4wMjE1IDE2Ny42MTMgMjIuODQ5NkMxNjcuMjQgMjIuNjc3NyAxNjYuOTUgMjIuNDQxNCAxNjYuNzQzIDIyLjE0MDZDMTY2LjUzNSAyMS44Mzk4IDE2Ni40MzEgMjEuNTE1OCAxNjYuNDMxIDIxLjE2ODVIMTY3LjY5OUMxNjcuNzE3IDIxLjQ3NjQgMTY3LjgzMyAyMS43MTQ1IDE2OC4wNDggMjEuODgyOEMxNjguMjYzIDIyLjA0NzUgMTY4LjU0NyAyMi4xMjk5IDE2OC45MDIgMjIuMTI5OUMxNjkuMjQ2IDIyLjEyOTkgMTY5LjUwNyAyMi4wNjU0IDE2OS42ODYgMjEuOTM2NUMxNjkuODY1IDIxLjgwNCAxNjkuOTU1IDIxLjYzMjIgMTY5Ljk1NSAyMS40MjA5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTIuNzczNCA2My40NzlDMTIuNzczNCA2My4zMDcxIDEyLjgyMzYgNjMuMTYzOSAxMi45MjM4IDYzLjA0OTNDMTMuMDI3NyA2Mi45MzQ3IDEzLjE4MTYgNjIuODc3NCAxMy4zODU3IDYyLjg3NzRDMTMuNTg5OCA2Mi44Nzc0IDEzLjc0MzggNjIuOTM0NyAxMy44NDc3IDYzLjA0OTNDMTMuOTU1MSA2My4xNjM5IDE0LjAwODggNjMuMzA3MSAxNC4wMDg4IDYzLjQ3OUMxNC4wMDg4IDYzLjY0MzcgMTMuOTU1MSA2My43ODE2IDEzLjg0NzcgNjMuODkyNkMxMy43NDM4IDY0LjAwMzYgMTMuNTg5OCA2NC4wNTkxIDEzLjM4NTcgNjQuMDU5MUMxMy4xODE2IDY0LjA1OTEgMTMuMDI3NyA2NC4wMDM2IDEyLjkyMzggNjMuODkyNkMxMi44MjM2IDYzLjc4MTYgMTIuNzczNCA2My42NDM3IDEyLjc3MzQgNjMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTUuNjczOCA2My40NzlDMTUuNjczOCA2My4zMDcxIDE1LjcyNCA2My4xNjM5IDE1LjgyNDIgNjMuMDQ5M0MxNS45MjgxIDYyLjkzNDcgMTYuMDgyIDYyLjg3NzQgMTYuMjg2MSA2Mi44Nzc0QzE2LjQ5MDIgNjIuODc3NCAxNi42NDQyIDYyLjkzNDcgMTYuNzQ4IDYzLjA0OTNDMTYuODU1NSA2My4xNjM5IDE2LjkwOTIgNjMuMzA3MSAxNi45MDkyIDYzLjQ3OUMxNi45MDkyIDYzLjY0MzcgMTYuODU1NSA2My43ODE2IDE2Ljc0OCA2My44OTI2QzE2LjY0NDIgNjQuMDAzNiAxNi40OTAyIDY0LjA1OTEgMTYuMjg2MSA2NC4wNTkxQzE2LjA4MiA2NC4wNTkxIDE1LjkyODEgNjQuMDAzNiAxNS44MjQyIDYzLjg5MjZDMTUuNzI0IDYzLjc4MTYgMTUuNjczOCA2My42NDM3IDE1LjY3MzggNjMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjAuODgzOCA2My4yOTY0QzIxLjIzODMgNjMuMjk2NCAyMS41NDggNjMuMTg5IDIxLjgxMyA2Mi45NzQxQzIyLjA3OCA2Mi43NTkzIDIyLjIyNDggNjIuNDkwNyAyMi4yNTM0IDYyLjE2ODVIMjMuMTkzNEMyMy4xNzU1IDYyLjUwMTUgMjMuMDYwOSA2Mi44MTg0IDIyLjg0OTYgNjMuMTE5MUMyMi42MzgzIDYzLjQxOTkgMjIuMzU1NSA2My42NTk4IDIyLjAwMSA2My44Mzg5QzIxLjY1MDEgNjQuMDE3OSAyMS4yNzc3IDY0LjEwNzQgMjAuODgzOCA2NC4xMDc0QzIwLjA5MjQgNjQuMTA3NCAxOS40NjIyIDYzLjg0NDIgMTguOTkzMiA2My4zMTc5QzE4LjUyNzcgNjIuNzg3OSAxOC4yOTQ5IDYyLjA2NDYgMTguMjk0OSA2MS4xNDc5VjYwLjk4MTRDMTguMjk0OSA2MC40MTU3IDE4LjM5ODggNTkuOTEyNiAxOC42MDY0IDU5LjQ3MjJDMTguODE0MSA1OS4wMzE3IDE5LjExMTMgNTguNjg5OCAxOS40OTggNTguNDQ2M0MxOS44ODgzIDU4LjIwMjggMjAuMzQ4NSA1OC4wODExIDIwLjg3ODQgNTguMDgxMUMyMS41MzAxIDU4LjA4MTEgMjIuMDcwOCA1OC4yNzYyIDIyLjUwMDUgNTguNjY2NUMyMi45MzM4IDU5LjA1NjggMjMuMTY0NyA1OS41NjM1IDIzLjE5MzQgNjAuMTg2NUgyMi4yNTM0QzIyLjIyNDggNTkuODEwNSAyMi4wODE1IDU5LjUwMjYgMjEuODIzNyA1OS4yNjI3QzIxLjU2OTUgNTkuMDE5MiAyMS4yNTQ0IDU4Ljg5NzUgMjAuODc4NCA1OC44OTc1QzIwLjM3MzUgNTguODk3NSAxOS45ODE0IDU5LjA4MDEgMTkuNzAyMSA1OS40NDUzQzE5LjQyNjQgNTkuODA3IDE5LjI4ODYgNjAuMzMxNSAxOS4yODg2IDYxLjAxOVY2MS4yMDdDMTkuMjg4NiA2MS44NzY2IDE5LjQyNjQgNjIuMzkyMyAxOS43MDIxIDYyLjc1MzlDMTkuOTc3OSA2My4xMTU2IDIwLjM3MTcgNjMuMjk2NCAyMC44ODM4IDYzLjI5NjRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0yNC4wNDc0IDYxLjA0MDVDMjQuMDQ3NCA2MC40NzEyIDI0LjE1ODQgNTkuOTU5MSAyNC4zODA0IDU5LjUwNDRDMjQuNjA2IDU5LjA0OTYgMjQuOTE3NSA1OC42OTg3IDI1LjMxNDkgNTguNDUxN0MyNS43MTYgNTguMjA0NiAyNi4xNzI1IDU4LjA4MTEgMjYuNjg0NiA1OC4wODExQzI3LjQ3NTkgNTguMDgxMSAyOC4xMTUxIDU4LjM1NSAyOC42MDIxIDU4LjkwMjhDMjkuMDkyNiA1OS40NTA3IDI5LjMzNzkgNjAuMTc5NCAyOS4zMzc5IDYxLjA4ODlWNjEuMTU4N0MyOS4zMzc5IDYxLjcyNDQgMjkuMjI4NyA2Mi4yMzI5IDI5LjAxMDMgNjIuNjg0MUMyOC43OTU0IDYzLjEzMTcgMjguNDg1NyA2My40ODA4IDI4LjA4MTEgNjMuNzMxNEMyNy42OCA2My45ODIxIDI3LjIxODEgNjQuMTA3NCAyNi42OTUzIDY0LjEwNzRDMjUuOTA3NiA2NC4xMDc0IDI1LjI2ODQgNjMuODMzNSAyNC43Nzc4IDYzLjI4NTZDMjQuMjkwOSA2Mi43Mzc4IDI0LjA0NzQgNjIuMDEyNyAyNC4wNDc0IDYxLjExMDRWNjEuMDQwNVpNMjUuMDQ2NCA2MS4xNTg3QzI1LjA0NjQgNjEuODAzMiAyNS4xOTUgNjIuMzIwNiAyNS40OTIyIDYyLjcxMDlDMjUuNzkzIDYzLjEwMTIgMjYuMTk0IDYzLjI5NjQgMjYuNjk1MyA2My4yOTY0QzI3LjIwMDIgNjMuMjk2NCAyNy42MDEyIDYzLjA5OTQgMjcuODk4NCA2Mi43MDU2QzI4LjE5NTYgNjIuMzA4MSAyOC4zNDQyIDYxLjc1MzEgMjguMzQ0MiA2MS4wNDA1QzI4LjM0NDIgNjAuNDAzMiAyOC4xOTIxIDU5Ljg4NzUgMjcuODg3NyA1OS40OTM3QzI3LjU4NjkgNTkuMDk2MiAyNy4xODU5IDU4Ljg5NzUgMjYuNjg0NiA1OC44OTc1QzI2LjE5NCA1OC44OTc1IDI1Ljc5ODMgNTkuMDkyNiAyNS40OTc2IDU5LjQ4MjlDMjUuMTk2OCA1OS44NzMyIDI1LjA0NjQgNjAuNDMxOCAyNS4wNDY0IDYxLjE1ODdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zNC42NzY4IDYxLjM3MzVIMzUuNzYxN1Y2Mi4xODQ2SDM0LjY3NjhWNjRIMzMuNjc3N1Y2Mi4xODQ2SDMwLjExNjdWNjEuNTk5MUwzMy42MTg3IDU2LjE3OTdIMzQuNjc2OFY2MS4zNzM1Wk0zMS4yNDQ2IDYxLjM3MzVIMzMuNjc3N1Y1Ny41Mzg2TDMzLjU1OTYgNTcuNzUzNEwzMS4yNDQ2IDYxLjM3MzVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zNy43ODY2IDU4LjE4ODVWNjQuNjcxNEMzNy43ODY2IDY1Ljc4ODYgMzcuMjc5OSA2Ni4zNDcyIDM2LjI2NjYgNjYuMzQ3MkMzNi4wNDgyIDY2LjM0NzIgMzUuODQ1OSA2Ni4zMTQ5IDM1LjY1OTcgNjYuMjUwNVY2NS40NTU2QzM1Ljc3NDMgNjUuNDg0MiAzNS45MjQ2IDY1LjQ5ODUgMzYuMTEwOCA2NS40OTg1QzM2LjMzMjggNjUuNDk4NSAzNi41MDExIDY1LjQzNzcgMzYuNjE1NyA2NS4zMTU5QzM2LjczMzkgNjUuMTk3OCAzNi43OTMgNjQuOTkwMSAzNi43OTMgNjQuNjkyOVY1OC4xODg1SDM3Ljc4NjZaTTM2LjY5MDkgNTYuNjQ3QzM2LjY5MDkgNTYuNDg5NCAzNi43MzkzIDU2LjM1NTEgMzYuODM1OSA1Ni4yNDQxQzM2LjkzNjIgNTYuMTI5NiAzNy4wODEyIDU2LjA3MjMgMzcuMjcxIDU2LjA3MjNDMzcuNDY0NCA1Ni4wNzIzIDM3LjYxMTIgNTYuMTI3OCAzNy43MTE0IDU2LjIzODhDMzcuODExNyA1Ni4zNDk4IDM3Ljg2MTggNTYuNDg1OCAzNy44NjE4IDU2LjY0N0MzNy44NjE4IDU2LjgwODEgMzcuODExNyA1Ni45NDI0IDM3LjcxMTQgNTcuMDQ5OEMzNy42MTEyIDU3LjE1NzIgMzcuNDY0NCA1Ny4yMTA5IDM3LjI3MSA1Ny4yMTA5QzM3LjA3NzYgNTcuMjEwOSAzNi45MzI2IDU3LjE1NzIgMzYuODM1OSA1Ny4wNDk4QzM2LjczOTMgNTYuOTQyNCAzNi42OTA5IDU2LjgwODEgMzYuNjkwOSA1Ni42NDdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik00Mi45ODA1IDYzLjQyNTNDNDIuNTkzOCA2My44OCA0Mi4wMjYyIDY0LjEwNzQgNDEuMjc3OCA2NC4xMDc0QzQwLjY1ODQgNjQuMTA3NCA0MC4xODU3IDYzLjkyODQgMzkuODU5OSA2My41NzAzQzM5LjUzNzYgNjMuMjA4NyAzOS4zNzQ3IDYyLjY3NTEgMzkuMzcxMSA2MS45Njk3VjU4LjE4ODVINDAuMzY0N1Y2MS45NDI5QzQwLjM2NDcgNjIuODIzNyA0MC43MjI4IDYzLjI2NDIgNDEuNDM5IDYzLjI2NDJDNDIuMTk4MSA2My4yNjQyIDQyLjcwMyA2Mi45ODEzIDQyLjk1MzYgNjIuNDE1NVY1OC4xODg1SDQzLjk0NzNWNjRINDMuMDAyTDQyLjk4MDUgNjMuNDI1M1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTY1LjMxMSA1Ny4wMjgzSDYyLjc5NzRWNjRINjEuNzcxNVY1Ny4wMjgzSDU5LjI2MzJWNTYuMTc5N0g2NS4zMTFWNTcuMDI4M1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTcxLjE0OTQgNjIuNjMwNEw3Mi4yNjY2IDU4LjE4ODVINzMuMjYwM0w3MS41Njg0IDY0SDcwLjc2MjdMNjkuMzUwMSA1OS41OTU3TDY3Ljk3NTEgNjRINjcuMTY5NEw2NS40ODI5IDU4LjE4ODVINjYuNDcxMkw2Ny42MTUyIDYyLjUzOTFMNjguOTY4OCA1OC4xODg1SDY5Ljc2OUw3MS4xNDk0IDYyLjYzMDRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik03NC4wMTIyIDYxLjA0MDVDNzQuMDEyMiA2MC40NzEyIDc0LjEyMzIgNTkuOTU5MSA3NC4zNDUyIDU5LjUwNDRDNzQuNTcwOCA1OS4wNDk2IDc0Ljg4MjMgNTguNjk4NyA3NS4yNzk4IDU4LjQ1MTdDNzUuNjgwOCA1OC4yMDQ2IDc2LjEzNzQgNTguMDgxMSA3Ni42NDk0IDU4LjA4MTFDNzcuNDQwOCA1OC4wODExIDc4LjA3OTkgNTguMzU1IDc4LjU2NjkgNTguOTAyOEM3OS4wNTc1IDU5LjQ1MDcgNzkuMzAyNyA2MC4xNzk0IDc5LjMwMjcgNjEuMDg4OVY2MS4xNTg3Qzc5LjMwMjcgNjEuNzI0NCA3OS4xOTM1IDYyLjIzMjkgNzguOTc1MSA2Mi42ODQxQzc4Ljc2MDMgNjMuMTMxNyA3OC40NTA1IDYzLjQ4MDggNzguMDQ1OSA2My43MzE0Qzc3LjY0NDkgNjMuOTgyMSA3Ny4xODI5IDY0LjEwNzQgNzYuNjYwMiA2NC4xMDc0Qzc1Ljg3MjQgNjQuMTA3NCA3NS4yMzMyIDYzLjgzMzUgNzQuNzQyNyA2My4yODU2Qzc0LjI1NTcgNjIuNzM3OCA3NC4wMTIyIDYyLjAxMjcgNzQuMDEyMiA2MS4xMTA0VjYxLjA0MDVaTTc1LjAxMTIgNjEuMTU4N0M3NS4wMTEyIDYxLjgwMzIgNzUuMTU5OCA2Mi4zMjA2IDc1LjQ1NyA2Mi43MTA5Qzc1Ljc1NzggNjMuMTAxMiA3Ni4xNTg5IDYzLjI5NjQgNzYuNjYwMiA2My4yOTY0Qzc3LjE2NSA2My4yOTY0IDc3LjU2NjEgNjMuMDk5NCA3Ny44NjMzIDYyLjcwNTZDNzguMTYwNSA2Mi4zMDgxIDc4LjMwOTEgNjEuNzUzMSA3OC4zMDkxIDYxLjA0MDVDNzguMzA5MSA2MC40MDMyIDc4LjE1NjkgNTkuODg3NSA3Ny44NTI1IDU5LjQ5MzdDNzcuNTUxOCA1OS4wOTYyIDc3LjE1MDcgNTguODk3NSA3Ni42NDk0IDU4Ljg5NzVDNzYuMTU4OSA1OC44OTc1IDc1Ljc2MzIgNTkuMDkyNiA3NS40NjI0IDU5LjQ4MjlDNzUuMTYxNiA1OS44NzMyIDc1LjAxMTIgNjAuNDMxOCA3NS4wMTEyIDYxLjE1ODdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik04Mi42MTY3IDYxLjA4MzVINzkuOTk1NlY2MC4yNzI1SDgyLjYxNjdWNjEuMDgzNVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTg4LjczNDQgNjIuNjMwNEw4OS44NTE2IDU4LjE4ODVIOTAuODQ1Mkw4OS4xNTMzIDY0SDg4LjM0NzdMODYuOTM1MSA1OS41OTU3TDg1LjU2MDEgNjRIODQuNzU0NEw4My4wNjc5IDU4LjE4ODVIODQuMDU2Mkw4NS4yMDAyIDYyLjUzOTFMODYuNTUzNyA1OC4xODg1SDg3LjM1NEw4OC43MzQ0IDYyLjYzMDRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05NS40NDgyIDY0Qzk1LjM5MSA2My44ODU0IDk1LjM0NDQgNjMuNjgxMyA5NS4zMDg2IDYzLjM4NzdDOTQuODQ2NyA2My44Njc1IDk0LjI5NTIgNjQuMTA3NCA5My42NTQzIDY0LjEwNzRDOTMuMDgxNCA2NC4xMDc0IDkyLjYxMDUgNjMuOTQ2MyA5Mi4yNDE3IDYzLjYyNEM5MS44NzY1IDYzLjI5ODIgOTEuNjkzOCA2Mi44ODY0IDkxLjY5MzggNjIuMzg4N0M5MS42OTM4IDYxLjc4MzUgOTEuOTIzIDYxLjMxNDUgOTIuMzgxMyA2MC45ODE0QzkyLjg0MzMgNjAuNjQ0OSA5My40OTE0IDYwLjQ3NjYgOTQuMzI1NyA2MC40NzY2SDk1LjI5MjVWNjAuMDJDOTUuMjkyNSA1OS42NzI3IDk1LjE4ODYgNTkuMzk3IDk0Ljk4MSA1OS4xOTI5Qzk0Ljc3MzMgNTguOTg1MiA5NC40NjcxIDU4Ljg4MTMgOTQuMDYyNSA1OC44ODEzQzkzLjcwOCA1OC44ODEzIDkzLjQxMDggNTguOTcwOSA5My4xNzA5IDU5LjE0OTlDOTIuOTMxIDU5LjMyODkgOTIuODExIDU5LjU0NTYgOTIuODExIDU5Ljc5OThIOTEuODEyQzkxLjgxMiA1OS41MDk4IDkxLjkxNDEgNTkuMjMwNSA5Mi4xMTgyIDU4Ljk2MTlDOTIuMzI1OCA1OC42ODk4IDkyLjYwNTEgNTguNDc0OSA5Mi45NTYxIDU4LjMxNzRDOTMuMzEwNSA1OC4xNTk4IDkzLjY5OTEgNTguMDgxMSA5NC4xMjE2IDU4LjA4MTFDOTQuNzkxMiA1OC4wODExIDk1LjMxNTggNTguMjQ5MyA5NS42OTUzIDU4LjU4NTlDOTYuMDc0OSA1OC45MTg5IDk2LjI3MTggNTkuMzc5MSA5Ni4yODYxIDU5Ljk2NjNWNjIuNjQxMUM5Ni4yODYxIDYzLjE3NDYgOTYuMzU0MiA2My41OTkgOTYuNDkwMiA2My45MTQxVjY0SDk1LjQ0ODJaTTkzLjc5OTMgNjMuMjQyN0M5NC4xMTA4IDYzLjI0MjcgOTQuNDA2MiA2My4xNjIxIDk0LjY4NTUgNjMuMDAxQzk0Ljk2NDggNjIuODM5OCA5NS4xNjcyIDYyLjYzMDQgOTUuMjkyNSA2Mi4zNzI2VjYxLjE4MDJIOTQuNTEzN0M5My4yOTYyIDYxLjE4MDIgOTIuNjg3NSA2MS41MzY1IDkyLjY4NzUgNjIuMjQ5QzkyLjY4NzUgNjIuNTYwNSA5Mi43OTEzIDYyLjgwNCA5Mi45OTkgNjIuOTc5NUM5My4yMDY3IDYzLjE1NDkgOTMuNDczNSA2My4yNDI3IDkzLjc5OTMgNjMuMjQyN1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTk5LjY1OTIgNjIuNTQ0NEwxMDEuMDEzIDU4LjE4ODVIMTAyLjA3Nkw5OS43Mzk3IDY0Ljg5N0M5OS4zNzgxIDY1Ljg2MzggOTguODAzNCA2Ni4zNDcyIDk4LjAxNTYgNjYuMzQ3Mkw5Ny44Mjc2IDY2LjMzMTFMOTcuNDU3IDY2LjI2MTJWNjUuNDU1Nkw5Ny43MjU2IDY1LjQ3NzFDOTguMDYyMiA2NS40NzcxIDk4LjMyMzYgNjUuNDA5IDk4LjUwOTggNjUuMjcyOUM5OC42OTk1IDY1LjEzNjkgOTguODU1MyA2NC44ODggOTguOTc3MSA2NC41MjY0TDk5LjE5NzMgNjMuOTM1NUw5Ny4xMjQgNTguMTg4NUg5OC4yMDlMOTkuNjU5MiA2Mi41NDQ0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNjUuOTIzMyAxMDAuMzQyQzY1LjkyMzMgMTAxLjEwOSA2NS43OTQ0IDEwMS43NzggNjUuNTM2NiAxMDIuMzUxQzY1LjI3ODggMTAyLjkyIDY0LjkxMzYgMTAzLjM1NSA2NC40NDA5IDEwMy42NTZDNjMuOTY4MyAxMDMuOTU3IDYzLjQxNjggMTA0LjEwNyA2Mi43ODY2IDEwNC4xMDdDNjIuMTcwNyAxMDQuMTA3IDYxLjYyNDcgMTAzLjk1NyA2MS4xNDg0IDEwMy42NTZDNjAuNjcyMiAxMDMuMzUyIDYwLjMwMTYgMTAyLjkyIDYwLjAzNjYgMTAyLjM2MkM1OS43NzUyIDEwMS44IDU5LjY0MSAxMDEuMTUgNTkuNjMzOCAxMDAuNDEyVjk5Ljg0ODFDNTkuNjMzOCA5OS4wOTYyIDU5Ljc2NDUgOTguNDMyIDYwLjAyNTkgOTcuODU1NUM2MC4yODczIDk3LjI3OSA2MC42NTYxIDk2LjgzODUgNjEuMTMyMyA5Ni41MzQyQzYxLjYxMjEgOTYuMjI2MiA2Mi4xNiA5Ni4wNzIzIDYyLjc3NTkgOTYuMDcyM0M2My40MDI1IDk2LjA3MjMgNjMuOTUzOSA5Ni4yMjQ0IDY0LjQzMDIgOTYuNTI4OEM2NC45MSA5Ni44Mjk2IDY1LjI3ODggOTcuMjY4MiA2NS41MzY2IDk3Ljg0NDdDNjUuNzk0NCA5OC40MTc2IDY1LjkyMzMgOTkuMDg1NCA2NS45MjMzIDk5Ljg0ODFWMTAwLjM0MlpNNjQuODk3NSA5OS44Mzc0QzY0Ljg5NzUgOTguOTEgNjQuNzExMyA5OC4xOTkyIDY0LjMzODkgOTcuNzA1MUM2My45NjY1IDk3LjIwNzQgNjMuNDQ1NSA5Ni45NTg1IDYyLjc3NTkgOTYuOTU4NUM2Mi4xMjQyIDk2Ljk1ODUgNjEuNjEwNCA5Ny4yMDc0IDYxLjIzNDQgOTcuNzA1MUM2MC44NjIgOTguMTk5MiA2MC42NzA0IDk4Ljg4NjcgNjAuNjU5NyA5OS43Njc2VjEwMC4zNDJDNjAuNjU5NyAxMDEuMjQxIDYwLjg0NzcgMTAxLjk0OCA2MS4yMjM2IDEwMi40NjRDNjEuNjAzMiAxMDIuOTc2IDYyLjEyNDIgMTAzLjIzMiA2Mi43ODY2IDEwMy4yMzJDNjMuNDUyNiAxMDMuMjMyIDYzLjk2ODMgMTAyLjk5IDY0LjMzMzUgMTAyLjUwN0M2NC42OTg3IDEwMi4wMiA2NC44ODY3IDEwMS4zMjMgNjQuODk3NSAxMDAuNDE3Vjk5LjgzNzRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik02OC4yNTQ0IDk4LjE4ODVMNjguMjg2NiA5OC45MTg5QzY4LjczMDYgOTguMzYwNCA2OS4zMTA3IDk4LjA4MTEgNzAuMDI2OSA5OC4wODExQzcxLjI1NSA5OC4wODExIDcxLjg3NDUgOTguNzczOSA3MS44ODUzIDEwMC4xNlYxMDRINzAuODkxNlYxMDAuMTU0QzcwLjg4OCA5OS43MzU0IDcwLjc5MTMgOTkuNDI1NiA3MC42MDE2IDk5LjIyNTFDNzAuNDE1NCA5OS4wMjQ2IDcwLjEyMzUgOTguOTI0MyA2OS43MjYxIDk4LjkyNDNDNjkuNDAzOCA5OC45MjQzIDY5LjEyMDkgOTkuMDEwMyA2OC44Nzc0IDk5LjE4MjFDNjguNjM0IDk5LjM1NCA2OC40NDQyIDk5LjU3OTYgNjguMzA4MSA5OS44NTg5VjEwNEg2Ny4zMTQ1Vjk4LjE4ODVINjguMjU0NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTc1Ljc5NTQgMTA0LjEwN0M3NS4wMDc2IDEwNC4xMDcgNzQuMzY2NyAxMDMuODUgNzMuODcyNiAxMDMuMzM0QzczLjM3ODQgMTAyLjgxNSA3My4xMzEzIDEwMi4xMjIgNzMuMTMxMyAxMDEuMjU1VjEwMS4wNzNDNzMuMTMxMyAxMDAuNDk2IDczLjI0MDYgOTkuOTgyNCA3My40NTkgOTkuNTMxMkM3My42ODEgOTkuMDc2NSA3My45ODg5IDk4LjcyMiA3NC4zODI4IDk4LjQ2NzhDNzQuNzgwMyA5OC4yMSA3NS4yMSA5OC4wODExIDc1LjY3MTkgOTguMDgxMUM3Ni40Mjc0IDk4LjA4MTEgNzcuMDE0NiA5OC4zMjk5IDc3LjQzMzYgOTguODI3NkM3Ny44NTI1IDk5LjMyNTQgNzguMDYyIDEwMC4wMzggNzguMDYyIDEwMC45NjVWMTAxLjM3OUg3NC4xMjVDNzQuMTM5MyAxMDEuOTUyIDc0LjMwNTggMTAyLjQxNiA3NC42MjQ1IDEwMi43N0M3NC45NDY4IDEwMy4xMjEgNzUuMzU1IDEwMy4yOTYgNzUuODQ5MSAxMDMuMjk2Qzc2LjIgMTAzLjI5NiA3Ni40OTcyIDEwMy4yMjUgNzYuNzQwNyAxMDMuMDgyQzc2Ljk4NDIgMTAyLjkzOCA3Ny4xOTczIDEwMi43NDkgNzcuMzc5OSAxMDIuNTEyTDc3Ljk4NjggMTAyLjk4NUM3Ny40OTk4IDEwMy43MzMgNzYuNzY5NCAxMDQuMTA3IDc1Ljc5NTQgMTA0LjEwN1pNNzUuNjcxOSA5OC44OTc1Qzc1LjI3MDggOTguODk3NSA3NC45MzQyIDk5LjA0NDMgNzQuNjYyMSA5OS4zMzc5Qzc0LjM5IDk5LjYyNzkgNzQuMjIxNyAxMDAuMDM2IDc0LjE1NzIgMTAwLjU2Mkg3Ny4wNjg0VjEwMC40ODdDNzcuMDM5NyA5OS45ODI0IDc2LjkwMzYgOTkuNTkyMSA3Ni42NjAyIDk5LjMxNjRDNzYuNDE2NyA5OS4wMzcxIDc2LjA4NzIgOTguODk3NSA3NS42NzE5IDk4Ljg5NzVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik04MS4yODQ3IDEwMS4wODNINzguNjYzNlYxMDAuMjcySDgxLjI4NDdWMTAxLjA4M1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTg3LjQwMjMgMTAyLjYzTDg4LjUxOTUgOTguMTg4NUg4OS41MTMyTDg3LjgyMTMgMTA0SDg3LjAxNTZMODUuNjAzIDk5LjU5NTdMODQuMjI4IDEwNEg4My40MjI0TDgxLjczNTggOTguMTg4NUg4Mi43MjQxTDgzLjg2ODIgMTAyLjUzOUw4NS4yMjE3IDk4LjE4ODVIODYuMDIyTDg3LjQwMjMgMTAyLjYzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNOTQuMTE2MiAxMDRDOTQuMDU4OSAxMDMuODg1IDk0LjAxMjQgMTAzLjY4MSA5My45NzY2IDEwMy4zODhDOTMuNTE0NiAxMDMuODY4IDkyLjk2MzIgMTA0LjEwNyA5Mi4zMjIzIDEwNC4xMDdDOTEuNzQ5MyAxMDQuMTA3IDkxLjI3ODUgMTAzLjk0NiA5MC45MDk3IDEwMy42MjRDOTAuNTQ0NCAxMDMuMjk4IDkwLjM2MTggMTAyLjg4NiA5MC4zNjE4IDEwMi4zODlDOTAuMzYxOCAxMDEuNzg0IDkwLjU5MSAxMDEuMzE0IDkxLjA0OTMgMTAwLjk4MUM5MS41MTEyIDEwMC42NDUgOTIuMTU5MyAxMDAuNDc3IDkyLjk5MzcgMTAwLjQ3N0g5My45NjA0VjEwMC4wMkM5My45NjA0IDk5LjY3MjcgOTMuODU2NiA5OS4zOTcgOTMuNjQ4OSA5OS4xOTI5QzkzLjQ0MTIgOTguOTg1MiA5My4xMzUxIDk4Ljg4MTMgOTIuNzMwNSA5OC44ODEzQzkyLjM3NiA5OC44ODEzIDkyLjA3ODggOTguOTcwOSA5MS44Mzg5IDk5LjE0OTlDOTEuNTk5IDk5LjMyODkgOTEuNDc5IDk5LjU0NTYgOTEuNDc5IDk5Ljc5OThIOTAuNDhDOTAuNDggOTkuNTA5OCA5MC41ODIgOTkuMjMwNSA5MC43ODYxIDk4Ljk2MTlDOTAuOTkzOCA5OC42ODk4IDkxLjI3MzEgOTguNDc0OSA5MS42MjQgOTguMzE3NEM5MS45Nzg1IDk4LjE1OTggOTIuMzY3IDk4LjA4MTEgOTIuNzg5NiA5OC4wODExQzkzLjQ1OTEgOTguMDgxMSA5My45ODM3IDk4LjI0OTMgOTQuMzYzMyA5OC41ODU5Qzk0Ljc0MjggOTguOTE4OSA5NC45Mzk4IDk5LjM3OTEgOTQuOTU0MSA5OS45NjYzVjEwMi42NDFDOTQuOTU0MSAxMDMuMTc1IDk1LjAyMjEgMTAzLjU5OSA5NS4xNTgyIDEwMy45MTRWMTA0SDk0LjExNjJaTTkyLjQ2NzMgMTAzLjI0M0M5Mi43Nzg4IDEwMy4yNDMgOTMuMDc0MiAxMDMuMTYyIDkzLjM1MzUgMTAzLjAwMUM5My42MzI4IDEwMi44NCA5My44MzUxIDEwMi42MyA5My45NjA0IDEwMi4zNzNWMTAxLjE4SDkzLjE4MTZDOTEuOTY0MiAxMDEuMTggOTEuMzU1NSAxMDEuNTM2IDkxLjM1NTUgMTAyLjI0OUM5MS4zNTU1IDEwMi41NjEgOTEuNDU5MyAxMDIuODA0IDkxLjY2NyAxMDIuOTc5QzkxLjg3NDcgMTAzLjE1NSA5Mi4xNDE0IDEwMy4yNDMgOTIuNDY3MyAxMDMuMjQzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNOTguMzI3MSAxMDIuNTQ0TDk5LjY4MDcgOTguMTg4NUgxMDAuNzQ0TDk4LjQwNzcgMTA0Ljg5N0M5OC4wNDYxIDEwNS44NjQgOTcuNDcxNCAxMDYuMzQ3IDk2LjY4MzYgMTA2LjM0N0w5Ni40OTU2IDEwNi4zMzFMOTYuMTI1IDEwNi4yNjFWMTA1LjQ1Nkw5Ni4zOTM2IDEwNS40NzdDOTYuNzMwMSAxMDUuNDc3IDk2Ljk5MTUgMTA1LjQwOSA5Ny4xNzc3IDEwNS4yNzNDOTcuMzY3NSAxMDUuMTM3IDk3LjUyMzMgMTA0Ljg4OCA5Ny42NDUgMTA0LjUyNkw5Ny44NjUyIDEwMy45MzZMOTUuNzkyIDk4LjE4ODVIOTYuODc3TDk4LjMyNzEgMTAyLjU0NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTY1LjkyMzMgMTQwLjM0MkM2NS45MjMzIDE0MS4xMDkgNjUuNzk0NCAxNDEuNzc4IDY1LjUzNjYgMTQyLjM1MUM2NS4yNzg4IDE0Mi45MiA2NC45MTM2IDE0My4zNTUgNjQuNDQwOSAxNDMuNjU2QzYzLjk2ODMgMTQzLjk1NyA2My40MTY4IDE0NC4xMDcgNjIuNzg2NiAxNDQuMTA3QzYyLjE3MDcgMTQ0LjEwNyA2MS42MjQ3IDE0My45NTcgNjEuMTQ4NCAxNDMuNjU2QzYwLjY3MjIgMTQzLjM1MiA2MC4zMDE2IDE0Mi45MiA2MC4wMzY2IDE0Mi4zNjJDNTkuNzc1MiAxNDEuOCA1OS42NDEgMTQxLjE1IDU5LjYzMzggMTQwLjQxMlYxMzkuODQ4QzU5LjYzMzggMTM5LjA5NiA1OS43NjQ1IDEzOC40MzIgNjAuMDI1OSAxMzcuODU1QzYwLjI4NzMgMTM3LjI3OSA2MC42NTYxIDEzNi44MzkgNjEuMTMyMyAxMzYuNTM0QzYxLjYxMjEgMTM2LjIyNiA2Mi4xNiAxMzYuMDcyIDYyLjc3NTkgMTM2LjA3MkM2My40MDI1IDEzNi4wNzIgNjMuOTUzOSAxMzYuMjI0IDY0LjQzMDIgMTM2LjUyOUM2NC45MSAxMzYuODMgNjUuMjc4OCAxMzcuMjY4IDY1LjUzNjYgMTM3Ljg0NUM2NS43OTQ0IDEzOC40MTggNjUuOTIzMyAxMzkuMDg1IDY1LjkyMzMgMTM5Ljg0OFYxNDAuMzQyWk02NC44OTc1IDEzOS44MzdDNjQuODk3NSAxMzguOTEgNjQuNzExMyAxMzguMTk5IDY0LjMzODkgMTM3LjcwNUM2My45NjY1IDEzNy4yMDcgNjMuNDQ1NSAxMzYuOTU4IDYyLjc3NTkgMTM2Ljk1OEM2Mi4xMjQyIDEzNi45NTggNjEuNjEwNCAxMzcuMjA3IDYxLjIzNDQgMTM3LjcwNUM2MC44NjIgMTM4LjE5OSA2MC42NzA0IDEzOC44ODcgNjAuNjU5NyAxMzkuNzY4VjE0MC4zNDJDNjAuNjU5NyAxNDEuMjQxIDYwLjg0NzcgMTQxLjk0OCA2MS4yMjM2IDE0Mi40NjRDNjEuNjAzMiAxNDIuOTc2IDYyLjEyNDIgMTQzLjIzMiA2Mi43ODY2IDE0My4yMzJDNjMuNDUyNiAxNDMuMjMyIDYzLjk2ODMgMTQyLjk5IDY0LjMzMzUgMTQyLjUwN0M2NC42OTg3IDE0Mi4wMiA2NC44ODY3IDE0MS4zMjMgNjQuODk3NSAxNDAuNDE3VjEzOS44MzdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik02OC4yNTQ0IDEzOC4xODhMNjguMjg2NiAxMzguOTE5QzY4LjczMDYgMTM4LjM2IDY5LjMxMDcgMTM4LjA4MSA3MC4wMjY5IDEzOC4wODFDNzEuMjU1IDEzOC4wODEgNzEuODc0NSAxMzguNzc0IDcxLjg4NTMgMTQwLjE2VjE0NEg3MC44OTE2VjE0MC4xNTRDNzAuODg4IDEzOS43MzUgNzAuNzkxMyAxMzkuNDI2IDcwLjYwMTYgMTM5LjIyNUM3MC40MTU0IDEzOS4wMjUgNzAuMTIzNSAxMzguOTI0IDY5LjcyNjEgMTM4LjkyNEM2OS40MDM4IDEzOC45MjQgNjkuMTIwOSAxMzkuMDEgNjguODc3NCAxMzkuMTgyQzY4LjYzNCAxMzkuMzU0IDY4LjQ0NDIgMTM5LjU4IDY4LjMwODEgMTM5Ljg1OVYxNDRINjcuMzE0NVYxMzguMTg4SDY4LjI1NDRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik03NS43OTU0IDE0NC4xMDdDNzUuMDA3NiAxNDQuMTA3IDc0LjM2NjcgMTQzLjg1IDczLjg3MjYgMTQzLjMzNEM3My4zNzg0IDE0Mi44MTUgNzMuMTMxMyAxNDIuMTIyIDczLjEzMTMgMTQxLjI1NVYxNDEuMDczQzczLjEzMTMgMTQwLjQ5NiA3My4yNDA2IDEzOS45ODIgNzMuNDU5IDEzOS41MzFDNzMuNjgxIDEzOS4wNzYgNzMuOTg4OSAxMzguNzIyIDc0LjM4MjggMTM4LjQ2OEM3NC43ODAzIDEzOC4yMSA3NS4yMSAxMzguMDgxIDc1LjY3MTkgMTM4LjA4MUM3Ni40Mjc0IDEzOC4wODEgNzcuMDE0NiAxMzguMzMgNzcuNDMzNiAxMzguODI4Qzc3Ljg1MjUgMTM5LjMyNSA3OC4wNjIgMTQwLjAzOCA3OC4wNjIgMTQwLjk2NVYxNDEuMzc5SDc0LjEyNUM3NC4xMzkzIDE0MS45NTIgNzQuMzA1OCAxNDIuNDE2IDc0LjYyNDUgMTQyLjc3Qzc0Ljk0NjggMTQzLjEyMSA3NS4zNTUgMTQzLjI5NiA3NS44NDkxIDE0My4yOTZDNzYuMiAxNDMuMjk2IDc2LjQ5NzIgMTQzLjIyNSA3Ni43NDA3IDE0My4wODJDNzYuOTg0MiAxNDIuOTM4IDc3LjE5NzMgMTQyLjc0OSA3Ny4zNzk5IDE0Mi41MTJMNzcuOTg2OCAxNDIuOTg1Qzc3LjQ5OTggMTQzLjczMyA3Ni43Njk0IDE0NC4xMDcgNzUuNzk1NCAxNDQuMTA3Wk03NS42NzE5IDEzOC44OTdDNzUuMjcwOCAxMzguODk3IDc0LjkzNDIgMTM5LjA0NCA3NC42NjIxIDEzOS4zMzhDNzQuMzkgMTM5LjYyOCA3NC4yMjE3IDE0MC4wMzYgNzQuMTU3MiAxNDAuNTYySDc3LjA2ODRWMTQwLjQ4N0M3Ny4wMzk3IDEzOS45ODIgNzYuOTAzNiAxMzkuNTkyIDc2LjY2MDIgMTM5LjMxNkM3Ni40MTY3IDEzOS4wMzcgNzYuMDg3MiAxMzguODk3IDc1LjY3MTkgMTM4Ljg5N1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTgxLjI4NDcgMTQxLjA4M0g3OC42NjM2VjE0MC4yNzJIODEuMjg0N1YxNDEuMDgzWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNODcuNDAyMyAxNDIuNjNMODguNTE5NSAxMzguMTg4SDg5LjUxMzJMODcuODIxMyAxNDRIODcuMDE1Nkw4NS42MDMgMTM5LjU5Nkw4NC4yMjggMTQ0SDgzLjQyMjRMODEuNzM1OCAxMzguMTg4SDgyLjcyNDFMODMuODY4MiAxNDIuNTM5TDg1LjIyMTcgMTM4LjE4OEg4Ni4wMjJMODcuNDAyMyAxNDIuNjNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05NC4xMTYyIDE0NEM5NC4wNTg5IDE0My44ODUgOTQuMDEyNCAxNDMuNjgxIDkzLjk3NjYgMTQzLjM4OEM5My41MTQ2IDE0My44NjggOTIuOTYzMiAxNDQuMTA3IDkyLjMyMjMgMTQ0LjEwN0M5MS43NDkzIDE0NC4xMDcgOTEuMjc4NSAxNDMuOTQ2IDkwLjkwOTcgMTQzLjYyNEM5MC41NDQ0IDE0My4yOTggOTAuMzYxOCAxNDIuODg2IDkwLjM2MTggMTQyLjM4OUM5MC4zNjE4IDE0MS43ODQgOTAuNTkxIDE0MS4zMTQgOTEuMDQ5MyAxNDAuOTgxQzkxLjUxMTIgMTQwLjY0NSA5Mi4xNTkzIDE0MC40NzcgOTIuOTkzNyAxNDAuNDc3SDkzLjk2MDRWMTQwLjAyQzkzLjk2MDQgMTM5LjY3MyA5My44NTY2IDEzOS4zOTcgOTMuNjQ4OSAxMzkuMTkzQzkzLjQ0MTIgMTM4Ljk4NSA5My4xMzUxIDEzOC44ODEgOTIuNzMwNSAxMzguODgxQzkyLjM3NiAxMzguODgxIDkyLjA3ODggMTM4Ljk3MSA5MS44Mzg5IDEzOS4xNUM5MS41OTkgMTM5LjMyOSA5MS40NzkgMTM5LjU0NiA5MS40NzkgMTM5LjhIOTAuNDhDOTAuNDggMTM5LjUxIDkwLjU4MiAxMzkuMjMgOTAuNzg2MSAxMzguOTYyQzkwLjk5MzggMTM4LjY5IDkxLjI3MzEgMTM4LjQ3NSA5MS42MjQgMTM4LjMxN0M5MS45Nzg1IDEzOC4xNiA5Mi4zNjcgMTM4LjA4MSA5Mi43ODk2IDEzOC4wODFDOTMuNDU5MSAxMzguMDgxIDkzLjk4MzcgMTM4LjI0OSA5NC4zNjMzIDEzOC41ODZDOTQuNzQyOCAxMzguOTE5IDk0LjkzOTggMTM5LjM3OSA5NC45NTQxIDEzOS45NjZWMTQyLjY0MUM5NC45NTQxIDE0My4xNzUgOTUuMDIyMSAxNDMuNTk5IDk1LjE1ODIgMTQzLjkxNFYxNDRIOTQuMTE2MlpNOTIuNDY3MyAxNDMuMjQzQzkyLjc3ODggMTQzLjI0MyA5My4wNzQyIDE0My4xNjIgOTMuMzUzNSAxNDMuMDAxQzkzLjYzMjggMTQyLjg0IDkzLjgzNTEgMTQyLjYzIDkzLjk2MDQgMTQyLjM3M1YxNDEuMThIOTMuMTgxNkM5MS45NjQyIDE0MS4xOCA5MS4zNTU1IDE0MS41MzYgOTEuMzU1NSAxNDIuMjQ5QzkxLjM1NTUgMTQyLjU2MSA5MS40NTkzIDE0Mi44MDQgOTEuNjY3IDE0Mi45NzlDOTEuODc0NyAxNDMuMTU1IDkyLjE0MTQgMTQzLjI0MyA5Mi40NjczIDE0My4yNDNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik05OC4zMjcxIDE0Mi41NDRMOTkuNjgwNyAxMzguMTg4SDEwMC43NDRMOTguNDA3NyAxNDQuODk3Qzk4LjA0NjEgMTQ1Ljg2NCA5Ny40NzE0IDE0Ni4zNDcgOTYuNjgzNiAxNDYuMzQ3TDk2LjQ5NTYgMTQ2LjMzMUw5Ni4xMjUgMTQ2LjI2MVYxNDUuNDU2TDk2LjM5MzYgMTQ1LjQ3N0M5Ni43MzAxIDE0NS40NzcgOTYuOTkxNSAxNDUuNDA5IDk3LjE3NzcgMTQ1LjI3M0M5Ny4zNjc1IDE0NS4xMzcgOTcuNTIzMyAxNDQuODg4IDk3LjY0NSAxNDQuNTI2TDk3Ljg2NTIgMTQzLjkzNkw5NS43OTIgMTM4LjE4OEg5Ni44NzdMOTguMzI3MSAxNDIuNTQ0WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTIuNzczNCAxMDMuNDc5QzEyLjc3MzQgMTAzLjMwNyAxMi44MjM2IDEwMy4xNjQgMTIuOTIzOCAxMDMuMDQ5QzEzLjAyNzcgMTAyLjkzNSAxMy4xODE2IDEwMi44NzcgMTMuMzg1NyAxMDIuODc3QzEzLjU4OTggMTAyLjg3NyAxMy43NDM4IDEwMi45MzUgMTMuODQ3NyAxMDMuMDQ5QzEzLjk1NTEgMTAzLjE2NCAxNC4wMDg4IDEwMy4zMDcgMTQuMDA4OCAxMDMuNDc5QzE0LjAwODggMTAzLjY0NCAxMy45NTUxIDEwMy43ODIgMTMuODQ3NyAxMDMuODkzQzEzLjc0MzggMTA0LjAwNCAxMy41ODk4IDEwNC4wNTkgMTMuMzg1NyAxMDQuMDU5QzEzLjE4MTYgMTA0LjA1OSAxMy4wMjc3IDEwNC4wMDQgMTIuOTIzOCAxMDMuODkzQzEyLjgyMzYgMTAzLjc4MiAxMi43NzM0IDEwMy42NDQgMTIuNzczNCAxMDMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTUuNjczOCAxMDMuNDc5QzE1LjY3MzggMTAzLjMwNyAxNS43MjQgMTAzLjE2NCAxNS44MjQyIDEwMy4wNDlDMTUuOTI4MSAxMDIuOTM1IDE2LjA4MiAxMDIuODc3IDE2LjI4NjEgMTAyLjg3N0MxNi40OTAyIDEwMi44NzcgMTYuNjQ0MiAxMDIuOTM1IDE2Ljc0OCAxMDMuMDQ5QzE2Ljg1NTUgMTAzLjE2NCAxNi45MDkyIDEwMy4zMDcgMTYuOTA5MiAxMDMuNDc5QzE2LjkwOTIgMTAzLjY0NCAxNi44NTU1IDEwMy43ODIgMTYuNzQ4IDEwMy44OTNDMTYuNjQ0MiAxMDQuMDA0IDE2LjQ5MDIgMTA0LjA1OSAxNi4yODYxIDEwNC4wNTlDMTYuMDgyIDEwNC4wNTkgMTUuOTI4MSAxMDQuMDA0IDE1LjgyNDIgMTAzLjg5M0MxNS43MjQgMTAzLjc4MiAxNS42NzM4IDEwMy42NDQgMTUuNjczOCAxMDMuNDc5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMTguMjg5NiAxMDEuMDQxQzE4LjI4OTYgMTAwLjQ3MSAxOC40MDA2IDk5Ljk1OTEgMTguNjIyNiA5OS41MDQ0QzE4Ljg0ODEgOTkuMDQ5NiAxOS4xNTk3IDk4LjY5ODcgMTkuNTU3MSA5OC40NTE3QzE5Ljk1ODIgOTguMjA0NiAyMC40MTQ3IDk4LjA4MTEgMjAuOTI2OCA5OC4wODExQzIxLjcxODEgOTguMDgxMSAyMi4zNTczIDk4LjM1NSAyMi44NDQyIDk4LjkwMjhDMjMuMzM0OCA5OS40NTA3IDIzLjU4MDEgMTAwLjE3OSAyMy41ODAxIDEwMS4wODlWMTAxLjE1OUMyMy41ODAxIDEwMS43MjQgMjMuNDcwOSAxMDIuMjMzIDIzLjI1MjQgMTAyLjY4NEMyMy4wMzc2IDEwMy4xMzIgMjIuNzI3OSAxMDMuNDgxIDIyLjMyMzIgMTAzLjczMUMyMS45MjIyIDEwMy45ODIgMjEuNDYwMyAxMDQuMTA3IDIwLjkzNzUgMTA0LjEwN0MyMC4xNDk3IDEwNC4xMDcgMTkuNTEwNiAxMDMuODMzIDE5LjAyIDEwMy4yODZDMTguNTMzIDEwMi43MzggMTguMjg5NiAxMDIuMDEzIDE4LjI4OTYgMTAxLjExVjEwMS4wNDFaTTE5LjI4ODYgMTAxLjE1OUMxOS4yODg2IDEwMS44MDMgMTkuNDM3MiAxMDIuMzIxIDE5LjczNDQgMTAyLjcxMUMyMC4wMzUyIDEwMy4xMDEgMjAuNDM2MiAxMDMuMjk2IDIwLjkzNzUgMTAzLjI5NkMyMS40NDI0IDEwMy4yOTYgMjEuODQzNCAxMDMuMDk5IDIyLjE0MDYgMTAyLjcwNkMyMi40Mzc4IDEwMi4zMDggMjIuNTg2NCAxMDEuNzUzIDIyLjU4NjQgMTAxLjA0MUMyMi41ODY0IDEwMC40MDMgMjIuNDM0MiA5OS44ODc1IDIyLjEyOTkgOTkuNDkzN0MyMS44MjkxIDk5LjA5NjIgMjEuNDI4MSA5OC44OTc1IDIwLjkyNjggOTguODk3NUMyMC40MzYyIDk4Ljg5NzUgMjAuMDQwNSA5OS4wOTI2IDE5LjczOTcgOTkuNDgyOUMxOS40MzkgOTkuODczMiAxOS4yODg2IDEwMC40MzIgMTkuMjg4NiAxMDEuMTU5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjYuMTY4OSA5OS42MDY0SDI2LjkxNTVDMjcuMzg0NiA5OS41OTkzIDI3Ljc1MzQgOTkuNDc1NyAyOC4wMjIgOTkuMjM1OEMyOC4yOTA1IDk4Ljk5NTkgMjguNDI0OCA5OC42NzE5IDI4LjQyNDggOTguMjYzN0MyOC40MjQ4IDk3LjM0NyAyNy45NjgzIDk2Ljg4ODcgMjcuMDU1MiA5Ni44ODg3QzI2LjYyNTUgOTYuODg4NyAyNi4yODE3IDk3LjAxMjIgMjYuMDIzOSA5Ny4yNTkzQzI1Ljc2OTcgOTcuNTAyOCAyNS42NDI2IDk3LjgyNjggMjUuNjQyNiA5OC4yMzE0SDI0LjY0ODlDMjQuNjQ4OSA5Ny42MTIgMjQuODc0NSA5Ny4wOTgxIDI1LjMyNTcgOTYuNjg5OUMyNS43ODA0IDk2LjI3ODIgMjYuMzU2OSA5Ni4wNzIzIDI3LjA1NTIgOTYuMDcyM0MyNy43OTI4IDk2LjA3MjMgMjguMzcxMSA5Ni4yNjc0IDI4Ljc5IDk2LjY1NzdDMjkuMjA5IDk3LjA0OCAyOS40MTg1IDk3LjU5MDUgMjkuNDE4NSA5OC4yODUyQzI5LjQxODUgOTguNjI1MyAyOS4zMDc1IDk4Ljk1NDggMjkuMDg1NCA5OS4yNzM0QzI4Ljg2NyA5OS41OTIxIDI4LjU2OCA5OS44MzAyIDI4LjE4ODUgOTkuOTg3OEMyOC42MTgyIDEwMC4xMjQgMjguOTQ5NCAxMDAuMzQ5IDI5LjE4MjEgMTAwLjY2NUMyOS40MTg1IDEwMC45OCAyOS41MzY2IDEwMS4zNjUgMjkuNTM2NiAxMDEuODE5QzI5LjUzNjYgMTAyLjUyMSAyOS4zMDc1IDEwMy4wNzggMjguODQ5MSAxMDMuNDlDMjguMzkwOCAxMDMuOTAyIDI3Ljc5NDYgMTA0LjEwNyAyNy4wNjA1IDEwNC4xMDdDMjYuMzI2NSAxMDQuMTA3IDI1LjcyODUgMTAzLjkwOSAyNS4yNjY2IDEwMy41MTFDMjQuODA4MyAxMDMuMTE0IDI0LjU3OTEgMTAyLjU4OSAyNC41NzkxIDEwMS45MzhIMjUuNTc4MUMyNS41NzgxIDEwMi4zNDkgMjUuNzEyNCAxMDIuNjc5IDI1Ljk4MSAxMDIuOTI2QzI2LjI0OTUgMTAzLjE3MyAyNi42MDk0IDEwMy4yOTYgMjcuMDYwNSAxMDMuMjk2QzI3LjU0MDQgMTAzLjI5NiAyNy45MDc0IDEwMy4xNzEgMjguMTYxNiAxMDIuOTJDMjguNDE1OSAxMDIuNjcgMjguNTQzIDEwMi4zMSAyOC41NDMgMTAxLjg0MUMyOC41NDMgMTAxLjM4NiAyOC40MDMzIDEwMS4wMzcgMjguMTI0IDEwMC43OTNDMjcuODQ0NyAxMDAuNTUgMjcuNDQxOSAxMDAuNDI1IDI2LjkxNTUgMTAwLjQxN0gyNi4xNjg5Vjk5LjYwNjRaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zMC43NjEyIDEwMS4wNDZDMzAuNzYxMiAxMDAuMTU0IDMwLjk3MjUgOTkuNDM4MiAzMS4zOTUgOTguODk3NUMzMS44MTc1IDk4LjM1MzIgMzIuMzcwOCA5OC4wODExIDMzLjA1NDcgOTguMDgxMUMzMy43MzUgOTguMDgxMSAzNC4yNzM5IDk4LjMxMzggMzQuNjcxNCA5OC43NzkzVjk1Ljc1SDM1LjY2NVYxMDRIMzQuNzUyTDM0LjcwMzYgMTAzLjM3N0MzNC4zMDYyIDEwMy44NjQgMzMuNzUyOSAxMDQuMTA3IDMzLjA0MzkgMTA0LjEwN0MzMi4zNzA4IDEwNC4xMDcgMzEuODIxMSAxMDMuODMyIDMxLjM5NSAxMDMuMjhDMzAuOTcyNSAxMDIuNzI5IDMwLjc2MTIgMTAyLjAwOSAzMC43NjEyIDEwMS4xMjFWMTAxLjA0NlpNMzEuNzU0OSAxMDEuMTU5QzMxLjc1NDkgMTAxLjgxOCAzMS44OTEgMTAyLjMzMyAzMi4xNjMxIDEwMi43MDZDMzIuNDM1MiAxMDMuMDc4IDMyLjgxMTIgMTAzLjI2NCAzMy4yOTEgMTAzLjI2NEMzMy45MjEyIDEwMy4yNjQgMzQuMzgxMyAxMDIuOTgxIDM0LjY3MTQgMTAyLjQxNlY5OS43NDYxQzM0LjM3NDIgOTkuMTk4MiAzMy45MTc2IDk4LjkyNDMgMzMuMzAxOCA5OC45MjQzQzMyLjgxNDggOTguOTI0MyAzMi40MzUyIDk5LjExMjMgMzIuMTYzMSA5OS40ODgzQzMxLjg5MSA5OS44NjQzIDMxLjc1NDkgMTAwLjQyMSAzMS43NTQ5IDEwMS4xNTlaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik00MC4wMjEgOTkuMDgwMUMzOS44NzA2IDk5LjA1NSAzOS43MDc3IDk5LjA0MjUgMzkuNTMyMiA5OS4wNDI1QzM4Ljg4MDUgOTkuMDQyNSAzOC40MzgzIDk5LjMyIDM4LjIwNTYgOTkuODc1VjEwNEgzNy4yMTE5Vjk4LjE4ODVIMzguMTc4N0wzOC4xOTQ4IDk4Ljg1OTlDMzguNTIwNyA5OC4zNDA3IDM4Ljk4MjYgOTguMDgxMSAzOS41ODA2IDk4LjA4MTFDMzkuNzczOSA5OC4wODExIDM5LjkyMDcgOTguMTA2MSA0MC4wMjEgOTguMTU2MlY5OS4wODAxWiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNDEuODc5NCA5OC4xODg1TDQxLjkxMTYgOTguOTE4OUM0Mi4zNTU2IDk4LjM2MDQgNDIuOTM1NyA5OC4wODExIDQzLjY1MTkgOTguMDgxMUM0NC44OCA5OC4wODExIDQ1LjQ5OTUgOTguNzczOSA0NS41MTAzIDEwMC4xNlYxMDRINDQuNTE2NlYxMDAuMTU0QzQ0LjUxMyA5OS43MzU0IDQ0LjQxNjMgOTkuNDI1NiA0NC4yMjY2IDk5LjIyNTFDNDQuMDQwNCA5OS4wMjQ2IDQzLjc0ODUgOTguOTI0MyA0My4zNTExIDk4LjkyNDNDNDMuMDI4OCA5OC45MjQzIDQyLjc0NTkgOTkuMDEwMyA0Mi41MDI0IDk5LjE4MjFDNDIuMjU5IDk5LjM1NCA0Mi4wNjkyIDk5LjU3OTYgNDEuOTMzMSA5OS44NTg5VjEwNEg0MC45Mzk1Vjk4LjE4ODVINDEuODc5NFoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTEyLjc3MzQgMTQzLjQ3OUMxMi43NzM0IDE0My4zMDcgMTIuODIzNiAxNDMuMTY0IDEyLjkyMzggMTQzLjA0OUMxMy4wMjc3IDE0Mi45MzUgMTMuMTgxNiAxNDIuODc3IDEzLjM4NTcgMTQyLjg3N0MxMy41ODk4IDE0Mi44NzcgMTMuNzQzOCAxNDIuOTM1IDEzLjg0NzcgMTQzLjA0OUMxMy45NTUxIDE0My4xNjQgMTQuMDA4OCAxNDMuMzA3IDE0LjAwODggMTQzLjQ3OUMxNC4wMDg4IDE0My42NDQgMTMuOTU1MSAxNDMuNzgyIDEzLjg0NzcgMTQzLjg5M0MxMy43NDM4IDE0NC4wMDQgMTMuNTg5OCAxNDQuMDU5IDEzLjM4NTcgMTQ0LjA1OUMxMy4xODE2IDE0NC4wNTkgMTMuMDI3NyAxNDQuMDA0IDEyLjkyMzggMTQzLjg5M0MxMi44MjM2IDE0My43ODIgMTIuNzczNCAxNDMuNjQ0IDEyLjc3MzQgMTQzLjQ3OVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTE1LjY3MzggMTQzLjQ3OUMxNS42NzM4IDE0My4zMDcgMTUuNzI0IDE0My4xNjQgMTUuODI0MiAxNDMuMDQ5QzE1LjkyODEgMTQyLjkzNSAxNi4wODIgMTQyLjg3NyAxNi4yODYxIDE0Mi44NzdDMTYuNDkwMiAxNDIuODc3IDE2LjY0NDIgMTQyLjkzNSAxNi43NDggMTQzLjA0OUMxNi44NTU1IDE0My4xNjQgMTYuOTA5MiAxNDMuMzA3IDE2LjkwOTIgMTQzLjQ3OUMxNi45MDkyIDE0My42NDQgMTYuODU1NSAxNDMuNzgyIDE2Ljc0OCAxNDMuODkzQzE2LjY0NDIgMTQ0LjAwNCAxNi40OTAyIDE0NC4wNTkgMTYuMjg2MSAxNDQuMDU5QzE2LjA4MiAxNDQuMDU5IDE1LjkyODEgMTQ0LjAwNCAxNS44MjQyIDE0My44OTNDMTUuNzI0IDE0My43ODIgMTUuNjczOCAxNDMuNjQ0IDE1LjY3MzggMTQzLjQ3OVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTIxLjM2MTggMTM5LjA4QzIxLjIxMTQgMTM5LjA1NSAyMS4wNDg1IDEzOS4wNDIgMjAuODczIDEzOS4wNDJDMjAuMjIxNCAxMzkuMDQyIDE5Ljc3OTEgMTM5LjMyIDE5LjU0NjQgMTM5Ljg3NVYxNDRIMTguNTUyN1YxMzguMTg4SDE5LjUxOTVMMTkuNTM1NiAxMzguODZDMTkuODYxNSAxMzguMzQxIDIwLjMyMzQgMTM4LjA4MSAyMC45MjE0IDEzOC4wODFDMjEuMTE0NyAxMzguMDgxIDIxLjI2MTYgMTM4LjEwNiAyMS4zNjE4IDEzOC4xNTZWMTM5LjA4WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNMjQuMjk0NCAxNDIuNjUyTDI1LjczMzkgMTM4LjE4OEgyNi43NDlMMjQuNjY1IDE0NEgyMy45MDc3TDIxLjgwMjIgMTM4LjE4OEgyMi44MTc0TDI0LjI5NDQgMTQyLjY1MloiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuODciLz4KPHBhdGggZD0iTTMxLjQxMTEgMTQwLjU2MkMzMS4yMDM1IDE0MC44MSAzMC45NTQ2IDE0MS4wMDggMzAuNjY0NiAxNDEuMTU5QzMwLjM3ODEgMTQxLjMwOSAzMC4wNjMgMTQxLjM4NCAyOS43MTkyIDE0MS4zODRDMjkuMjY4MSAxNDEuMzg0IDI4Ljg3NDIgMTQxLjI3MyAyOC41Mzc2IDE0MS4wNTFDMjguMjA0NiAxNDAuODI5IDI3Ljk0NjggMTQwLjUxOCAyNy43NjQyIDE0MC4xMTdDMjcuNTgxNSAxMzkuNzEyIDI3LjQ5MDIgMTM5LjI2NiAyNy40OTAyIDEzOC43NzlDMjcuNDkwMiAxMzguMjU3IDI3LjU4ODcgMTM3Ljc4NiAyNy43ODU2IDEzNy4zNjdDMjcuOTg2MiAxMzYuOTQ4IDI4LjI2OSAxMzYuNjI3IDI4LjYzNDMgMTM2LjQwNUMyOC45OTk1IDEzNi4xODMgMjkuNDI1NiAxMzYuMDcyIDI5LjkxMjYgMTM2LjA3MkMzMC42ODYgMTM2LjA3MiAzMS4yOTQ4IDEzNi4zNjIgMzEuNzM4OCAxMzYuOTQyQzMyLjE4NjQgMTM3LjUxOSAzMi40MTAyIDEzOC4zMDcgMzIuNDEwMiAxMzkuMzA2VjEzOS41OTZDMzIuNDEwMiAxNDEuMTE4IDMyLjEwOTQgMTQyLjIyOSAzMS41MDc4IDE0Mi45MzFDMzAuOTA2MiAxNDMuNjI5IDI5Ljk5ODUgMTQzLjk4NyAyOC43ODQ3IDE0NC4wMDVIMjguNTkxM1YxNDMuMTY3SDI4LjgwMDhDMjkuNjIwOCAxNDMuMTUzIDMwLjI1MSAxNDIuOTQgMzAuNjkxNCAxNDIuNTI4QzMxLjEzMTggMTQyLjExMyAzMS4zNzE3IDE0MS40NTggMzEuNDExMSAxNDAuNTYyWk0yOS44ODA0IDE0MC41NjJDMzAuMjEzNCAxNDAuNTYyIDMwLjUxOTUgMTQwLjQ2IDMwLjc5ODggMTQwLjI1NkMzMS4wODE3IDE0MC4wNTIgMzEuMjg3NiAxMzkuOCAzMS40MTY1IDEzOS40OTlWMTM5LjEwMkMzMS40MTY1IDEzOC40NSAzMS4yNzUxIDEzNy45MiAzMC45OTIyIDEzNy41MTJDMzAuNzA5MyAxMzcuMTA0IDMwLjM1MTIgMTM2Ljg5OSAyOS45MTggMTM2Ljg5OUMyOS40ODExIDEzNi44OTkgMjkuMTMwMiAxMzcuMDY4IDI4Ljg2NTIgMTM3LjQwNEMyOC42MDAzIDEzNy43MzcgMjguNDY3OCAxMzguMTc4IDI4LjQ2NzggMTM4LjcyNkMyOC40Njc4IDEzOS4yNTkgMjguNTk0OSAxMzkuNyAyOC44NDkxIDE0MC4wNDdDMjkuMTA2OSAxNDAuMzkxIDI5LjQ1MDcgMTQwLjU2MiAyOS44ODA0IDE0MC41NjJaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0zNS41MDM5IDE0MS4zMDlMMzQuODgwOSAxNDEuOTU5VjE0NEgzMy44ODcyVjEzNS43NUgzNC44ODA5VjE0MC43NEwzNS40MTI2IDE0MC4xMDFMMzcuMjIyNyAxMzguMTg4SDM4LjQzMTJMMzYuMTY5OSAxNDAuNjE2TDM4LjY5NDMgMTQ0SDM3LjUyODhMMzUuNTAzOSAxNDEuMzA5WiIgZmlsbD0iYmxhY2siIGZpbGwtb3BhY2l0eT0iMC44NyIvPgo8cGF0aCBkPSJNNDEuNzYxMiAxNDQuMTA3QzQwLjk3MzUgMTQ0LjEwNyA0MC4zMzI1IDE0My44NSAzOS44Mzg0IDE0My4zMzRDMzkuMzQ0MiAxNDIuODE1IDM5LjA5NzIgMTQyLjEyMiAzOS4wOTcyIDE0MS4yNTVWMTQxLjA3M0MzOS4wOTcyIDE0MC40OTYgMzkuMjA2NCAxMzkuOTgyIDM5LjQyNDggMTM5LjUzMUMzOS42NDY4IDEzOS4wNzYgMzkuOTU0OCAxMzguNzIyIDQwLjM0ODYgMTM4LjQ2OEM0MC43NDYxIDEzOC4yMSA0MS4xNzU4IDEzOC4wODEgNDEuNjM3NyAxMzguMDgxQzQyLjM5MzIgMTM4LjA4MSA0Mi45ODA1IDEzOC4zMyA0My4zOTk0IDEzOC44MjhDNDMuODE4NCAxMzkuMzI1IDQ0LjAyNzggMTQwLjAzOCA0NC4wMjc4IDE0MC45NjVWMTQxLjM3OUg0MC4wOTA4QzQwLjEwNTEgMTQxLjk1MiA0MC4yNzE2IDE0Mi40MTYgNDAuNTkwMyAxNDIuNzdDNDAuOTEyNiAxNDMuMTIxIDQxLjMyMDggMTQzLjI5NiA0MS44MTQ5IDE0My4yOTZDNDIuMTY1OSAxNDMuMjk2IDQyLjQ2MzEgMTQzLjIyNSA0Mi43MDY1IDE0My4wODJDNDIuOTUgMTQyLjkzOCA0My4xNjMxIDE0Mi43NDkgNDMuMzQ1NyAxNDIuNTEyTDQzLjk1MjYgMTQyLjk4NUM0My40NjU3IDE0My43MzMgNDIuNzM1MiAxNDQuMTA3IDQxLjc2MTIgMTQ0LjEwN1pNNDEuNjM3NyAxMzguODk3QzQxLjIzNjcgMTM4Ljg5NyA0MC45MDAxIDEzOS4wNDQgNDAuNjI3OSAxMzkuMzM4QzQwLjM1NTggMTM5LjYyOCA0MC4xODc1IDE0MC4wMzYgNDAuMTIzIDE0MC41NjJINDMuMDM0MlYxNDAuNDg3QzQzLjAwNTUgMTM5Ljk4MiA0Mi44Njk1IDEzOS41OTIgNDIuNjI2IDEzOS4zMTZDNDIuMzgyNSAxMzkuMDM3IDQyLjA1MzEgMTM4Ljg5NyA0MS42Mzc3IDEzOC44OTdaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjg3Ii8+CjxwYXRoIGQ9Ik0xNDYuNDQ1IDU3LjI3NTRIMTQ0LjAwN1Y2NEgxNDIuNjU5VjU3LjI3NTRIMTQwLjI0MlY1Ni4xNzk3SDE0Ni40NDVWNTcuMjc1NFoiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE0OC43MzkgNjRIMTQ3LjQzNFY1OC4xODg1SDE0OC43MzlWNjRaTTE0Ny4zNTMgNTYuNjc5MkMxNDcuMzUzIDU2LjQ3ODcgMTQ3LjQxNiA1Ni4zMTIyIDE0Ny41NDEgNTYuMTc5N0MxNDcuNjcgNTYuMDQ3MiAxNDcuODUzIDU1Ljk4MSAxNDguMDg5IDU1Ljk4MUMxNDguMzI1IDU1Ljk4MSAxNDguNTA4IDU2LjA0NzIgMTQ4LjYzNyA1Ni4xNzk3QzE0OC43NjYgNTYuMzEyMiAxNDguODMgNTYuNDc4NyAxNDguODMgNTYuNjc5MkMxNDguODMgNTYuODc2MSAxNDguNzY2IDU3LjA0MDkgMTQ4LjYzNyA1Ny4xNzMzQzE0OC41MDggNTcuMzAyMiAxNDguMzI1IDU3LjM2NjcgMTQ4LjA4OSA1Ny4zNjY3QzE0Ny44NTMgNTcuMzY2NyAxNDcuNjcgNTcuMzAyMiAxNDcuNTQxIDU3LjE3MzNDMTQ3LjQxNiA1Ny4wNDA5IDE0Ny4zNTMgNTYuODc2MSAxNDcuMzUzIDU2LjY3OTJaIiBmaWxsPSIjRkZBNTAwIi8+CjxwYXRoIGQ9Ik0xNTEuMzkyIDU4LjE4ODVMMTUxLjQzIDU4Ljc5NTRDMTUxLjgzOCA1OC4zMTkyIDE1Mi4zOTYgNTguMDgxMSAxNTMuMTA1IDU4LjA4MTFDMTUzLjg4MiA1OC4wODExIDE1NC40MTQgNTguMzc4MyAxNTQuNzAxIDU4Ljk3MjdDMTU1LjEyMyA1OC4zNzgzIDE1NS43MTggNTguMDgxMSAxNTYuNDg0IDU4LjA4MTFDMTU3LjEyNSA1OC4wODExIDE1Ny42MDEgNTguMjU4MyAxNTcuOTEzIDU4LjYxMjhDMTU4LjIyOCA1OC45NjczIDE1OC4zODkgNTkuNDkwMSAxNTguMzk2IDYwLjE4MTJWNjRIMTU3LjA5MVY2MC4yMTg4QzE1Ny4wOTEgNTkuODQ5OSAxNTcuMDEgNTkuNTc5NiAxNTYuODQ5IDU5LjQwNzdDMTU2LjY4OCA1OS4yMzU4IDE1Ni40MjEgNTkuMTQ5OSAxNTYuMDQ5IDU5LjE0OTlDMTU1Ljc1MiA1OS4xNDk5IDE1NS41MDggNTkuMjMwNSAxNTUuMzE4IDU5LjM5MTZDMTU1LjEzMiA1OS41NDkyIDE1NS4wMDEgNTkuNzU2OCAxNTQuOTI2IDYwLjAxNDZMMTU0LjkzMiA2NEgxNTMuNjI2VjYwLjE3NThDMTUzLjYwOSA1OS40OTE5IDE1My4yNTkgNTkuMTQ5OSAxNTIuNTc5IDU5LjE0OTlDMTUyLjA1NiA1OS4xNDk5IDE1MS42ODYgNTkuMzYzIDE1MS40NjcgNTkuNzg5MVY2NEgxNTAuMTYyVjU4LjE4ODVIMTUxLjM5MloiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE2Mi4yOTUgNjQuMTA3NEMxNjEuNDY4IDY0LjEwNzQgMTYwLjc5NyA2My44NDc4IDE2MC4yODEgNjMuMzI4NkMxNTkuNzY5IDYyLjgwNTggMTU5LjUxMyA2Mi4xMTEyIDE1OS41MTMgNjEuMjQ0NlY2MS4wODM1QzE1OS41MTMgNjAuNTAzNCAxNTkuNjI0IDU5Ljk4NiAxNTkuODQ2IDU5LjUzMTJDMTYwLjA3MiA1OS4wNzI5IDE2MC4zODcgNTguNzE2NiAxNjAuNzkyIDU4LjQ2MjRDMTYxLjE5NiA1OC4yMDgyIDE2MS42NDcgNTguMDgxMSAxNjIuMTQ1IDU4LjA4MTFDMTYyLjkzNiA1OC4wODExIDE2My41NDcgNTguMzMzNSAxNjMuOTc3IDU4LjgzODRDMTY0LjQxIDU5LjM0MzMgMTY0LjYyNiA2MC4wNTc2IDE2NC42MjYgNjAuOTgxNFY2MS41MDc4SDE2MC44MjlDMTYwLjg2OCA2MS45ODc2IDE2MS4wMjggNjIuMzY3MiAxNjEuMzA3IDYyLjY0NjVDMTYxLjU5IDYyLjkyNTggMTYxLjk0NCA2My4wNjU0IDE2Mi4zNzEgNjMuMDY1NEMxNjIuOTY5IDYzLjA2NTQgMTYzLjQ1NiA2Mi44MjM3IDE2My44MzIgNjIuMzQwM0wxNjQuNTM1IDYzLjAxMTdDMTY0LjMwMiA2My4zNTkgMTYzLjk5MSA2My42Mjk0IDE2My42MDEgNjMuODIyOEMxNjMuMjE0IDY0LjAxMjUgMTYyLjc3OSA2NC4xMDc0IDE2Mi4yOTUgNjQuMTA3NFpNMTYyLjE0IDU5LjEyODRDMTYxLjc4MiA1OS4xMjg0IDE2MS40OTIgNTkuMjUzNyAxNjEuMjcgNTkuNTA0NEMxNjEuMDUxIDU5Ljc1NSAxNjAuOTExIDYwLjEwNDIgMTYwLjg1MSA2MC41NTE4SDE2My4zMzdWNjAuNDU1MUMxNjMuMzA5IDYwLjAxODIgMTYzLjE5MiA1OS42ODg4IDE2Mi45ODggNTkuNDY2OEMxNjIuNzg0IDU5LjI0MTIgMTYyLjUwMSA1OS4xMjg0IDE2Mi4xNCA1OS4xMjg0WiIgZmlsbD0iI0ZGQTUwMCIvPgo8cGF0aCBkPSJNMTY1LjQgNjEuMDQwNUMxNjUuNCA2MC40NzEyIDE2NS41MTMgNTkuOTU5MSAxNjUuNzM4IDU5LjUwNDRDMTY1Ljk2NCA1OS4wNDYxIDE2Ni4yODEgNTguNjk1MSAxNjYuNjg5IDU4LjQ1MTdDMTY3LjA5NyA1OC4yMDQ2IDE2Ny41NjYgNTguMDgxMSAxNjguMDk2IDU4LjA4MTFDMTY4Ljg4IDU4LjA4MTEgMTY5LjUxNiA1OC4zMzM1IDE3MC4wMDMgNTguODM4NEMxNzAuNDkzIDU5LjM0MzMgMTcwLjc1OCA2MC4wMTI5IDE3MC43OTggNjAuODQ3MkwxNzAuODAzIDYxLjE1MzNDMTcwLjgwMyA2MS43MjYyIDE3MC42OTIgNjIuMjM4MyAxNzAuNDcgNjIuNjg5NUMxNzAuMjUyIDYzLjE0MDYgMTY5LjkzNyA2My40ODk3IDE2OS41MjUgNjMuNzM2OEMxNjkuMTE3IDYzLjk4MzkgMTY4LjY0NCA2NC4xMDc0IDE2OC4xMDcgNjQuMTA3NEMxNjcuMjg3IDY0LjEwNzQgMTY2LjYzIDYzLjgzNTMgMTY2LjEzNiA2My4yOTFDMTY1LjY0NSA2Mi43NDMyIDE2NS40IDYyLjAxNDUgMTY1LjQgNjEuMTA1VjYxLjA0MDVaTTE2Ni43MDUgNjEuMTUzM0MxNjYuNzA1IDYxLjc1MTMgMTY2LjgyOSA2Mi4yMjA0IDE2Ny4wNzYgNjIuNTYwNUMxNjcuMzIzIDYyLjg5NzEgMTY3LjY2NyA2My4wNjU0IDE2OC4xMDcgNjMuMDY1NEMxNjguNTQ3IDYzLjA2NTQgMTY4Ljg4OSA2Mi44OTM2IDE2OS4xMzMgNjIuNTQ5OEMxNjkuMzggNjIuMjA2MSAxNjkuNTAzIDYxLjcwMyAxNjkuNTAzIDYxLjA0MDVDMTY5LjUwMyA2MC40NTMzIDE2OS4zNzYgNTkuOTg3OCAxNjkuMTIyIDU5LjY0NEMxNjguODcxIDU5LjMwMDMgMTY4LjUyOSA1OS4xMjg0IDE2OC4wOTYgNTkuMTI4NEMxNjcuNjcgNTkuMTI4NCAxNjcuMzMyIDU5LjI5ODUgMTY3LjA4MSA1OS42Mzg3QzE2Ni44MyA1OS45NzUzIDE2Ni43MDUgNjAuNDgwMSAxNjYuNzA1IDYxLjE1MzNaIiBmaWxsPSIjRkZBNTAwIi8+CjxwYXRoIGQ9Ik0xNzUuNDI4IDYzLjQzMDdDMTc1LjA0NSA2My44ODE4IDE3NC41IDY0LjEwNzQgMTczLjc5NSA2NC4xMDc0QzE3My4xNjUgNjQuMTA3NCAxNzIuNjg3IDYzLjkyMyAxNzIuMzYxIDYzLjU1NDJDMTcyLjAzOSA2My4xODU0IDE3MS44NzcgNjIuNjUxOSAxNzEuODc3IDYxLjk1MzZWNTguMTg4NUgxNzMuMTgzVjYxLjkzNzVDMTczLjE4MyA2Mi42NzUxIDE3My40ODkgNjMuMDQzOSAxNzQuMTAxIDYzLjA0MzlDMTc0LjczNSA2My4wNDM5IDE3NS4xNjMgNjIuODE2NiAxNzUuMzg1IDYyLjM2MThWNTguMTg4NUgxNzYuNjlWNjRIMTc1LjQ2TDE3NS40MjggNjMuNDMwN1oiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE3OS42NTUgNTYuNzc1OVY1OC4xODg1SDE4MC42ODFWNTkuMTU1M0gxNzkuNjU1VjYyLjM5OTRDMTc5LjY1NSA2Mi42MjE0IDE3OS42OTggNjIuNzgyNiAxNzkuNzg0IDYyLjg4MjhDMTc5Ljg3MyA2Mi45Nzk1IDE4MC4wMzEgNjMuMDI3OCAxODAuMjU2IDYzLjAyNzhDMTgwLjQwNyA2My4wMjc4IDE4MC41NTkgNjMuMDA5OSAxODAuNzEzIDYyLjk3NDFWNjMuOTgzOUMxODAuNDE2IDY0LjA2NjIgMTgwLjEyOSA2NC4xMDc0IDE3OS44NTQgNjQuMTA3NEMxNzguODUxIDY0LjEwNzQgMTc4LjM1IDYzLjU1NDIgMTc4LjM1IDYyLjQ0NzhWNTkuMTU1M0gxNzcuMzk0VjU4LjE4ODVIMTc4LjM1VjU2Ljc3NTlIMTc5LjY1NVoiIGZpbGw9IiNGRkE1MDAiLz4KPHBhdGggZD0iTTE0NS4zMDEgMTAwLjY4NkgxNDIuMTU0VjEwNEgxNDAuNzk1Vjk2LjE3OTdIMTQ1Ljc2M1Y5Ny4yNzU0SDE0Mi4xNTRWOTkuNjAxMUgxNDUuMzAxVjEwMC42ODZaIiBmaWxsPSIjRkYzRTNFIi8+CjxwYXRoIGQ9Ik0xNTAuMDA2IDEwNEMxNDkuOTQ5IDEwMy44ODkgMTQ5Ljg5OSAxMDMuNzA4IDE0OS44NTYgMTAzLjQ1OEMxNDkuNDQxIDEwMy44OTEgMTQ4LjkzMiAxMDQuMTA3IDE0OC4zMzEgMTA0LjEwN0MxNDcuNzQ3IDEwNC4xMDcgMTQ3LjI3MSAxMDMuOTQxIDE0Ni45MDIgMTAzLjYwOEMxNDYuNTMzIDEwMy4yNzUgMTQ2LjM0OSAxMDIuODYzIDE0Ni4zNDkgMTAyLjM3M0MxNDYuMzQ5IDEwMS43NTMgMTQ2LjU3OCAxMDEuMjc5IDE0Ny4wMzYgMTAwLjk0OUMxNDcuNDk4IDEwMC42MTYgMTQ4LjE1NyAxMDAuNDUgMTQ5LjAxMyAxMDAuNDVIMTQ5LjgxM1YxMDAuMDY4QzE0OS44MTMgOTkuNzY3NiAxNDkuNzI5IDk5LjUyNzcgMTQ5LjU2MSA5OS4zNDg2QzE0OS4zOTIgOTkuMTY2IDE0OS4xMzYgOTkuMDc0NyAxNDguNzkyIDk5LjA3NDdDMTQ4LjQ5NSA5OS4wNzQ3IDE0OC4yNTIgOTkuMTQ5OSAxNDguMDYyIDk5LjMwMDNDMTQ3Ljg3MiA5OS40NDcxIDE0Ny43NzcgOTkuNjM1MSAxNDcuNzc3IDk5Ljg2NDNIMTQ2LjQ3MkMxNDYuNDcyIDk5LjU0NTYgMTQ2LjU3OCA5OS4yNDg0IDE0Ni43ODkgOTguOTcyN0MxNDcgOTguNjkzNCAxNDcuMjg3IDk4LjQ3NDkgMTQ3LjY0OCA5OC4zMTc0QzE0OC4wMTQgOTguMTU5OCAxNDguNDIgOTguMDgxMSAxNDguODY4IDk4LjA4MTFDMTQ5LjU0OCA5OC4wODExIDE1MC4wOSA5OC4yNTI5IDE1MC40OTUgOTguNTk2N0MxNTAuOSA5OC45MzY4IDE1MS4xMDcgOTkuNDE2NyAxNTEuMTE4IDEwMC4wMzZWMTAyLjY1N0MxNTEuMTE4IDEwMy4xOCAxNTEuMTkyIDEwMy41OTcgMTUxLjMzOCAxMDMuOTA5VjEwNEgxNTAuMDA2Wk0xNDguNTcyIDEwMy4wNkMxNDguODMgMTAzLjA2IDE0OS4wNzIgMTAyLjk5NyAxNDkuMjk3IDEwMi44NzJDMTQ5LjUyNyAxMDIuNzQ3IDE0OS42OTggMTAyLjU3OCAxNDkuODEzIDEwMi4zNjdWMTAxLjI3MUgxNDkuMTA5QzE0OC42MjYgMTAxLjI3MSAxNDguMjYzIDEwMS4zNTYgMTQ4LjAxOSAxMDEuNTI0QzE0Ny43NzYgMTAxLjY5MiAxNDcuNjU0IDEwMS45MyAxNDcuNjU0IDEwMi4yMzhDMTQ3LjY1NCAxMDIuNDg5IDE0Ny43MzYgMTAyLjY4OSAxNDcuOTAxIDEwMi44NEMxNDguMDY5IDEwMi45ODcgMTQ4LjI5MyAxMDMuMDYgMTQ4LjU3MiAxMDMuMDZaIiBmaWxsPSIjRkYzRTNFIi8+CjxwYXRoIGQ9Ik0xNTMuODc0IDEwNEgxNTIuNTY4Vjk4LjE4ODVIMTUzLjg3NFYxMDRaTTE1Mi40ODggOTYuNjc5MkMxNTIuNDg4IDk2LjQ3ODcgMTUyLjU1IDk2LjMxMjIgMTUyLjY3NiA5Ni4xNzk3QzE1Mi44MDUgOTYuMDQ3MiAxNTIuOTg3IDk1Ljk4MSAxNTMuMjI0IDk1Ljk4MUMxNTMuNDYgOTUuOTgxIDE1My42NDMgOTYuMDQ3MiAxNTMuNzcxIDk2LjE3OTdDMTUzLjkgOTYuMzEyMiAxNTMuOTY1IDk2LjQ3ODcgMTUzLjk2NSA5Ni42NzkyQzE1My45NjUgOTYuODc2MSAxNTMuOSA5Ny4wNDA5IDE1My43NzEgOTcuMTczM0MxNTMuNjQzIDk3LjMwMjIgMTUzLjQ2IDk3LjM2NjcgMTUzLjIyNCA5Ny4zNjY3QzE1Mi45ODcgOTcuMzY2NyAxNTIuODA1IDk3LjMwMjIgMTUyLjY3NiA5Ny4xNzMzQzE1Mi41NSA5Ny4wNDA5IDE1Mi40ODggOTYuODc2MSAxNTIuNDg4IDk2LjY3OTJaIiBmaWxsPSIjRkYzRTNFIi8+CjxwYXRoIGQ9Ik0xNTYuNjg4IDEwNEgxNTUuMzgzVjk1Ljc1SDE1Ni42ODhWMTA0WiIgZmlsbD0iI0ZGM0UzRSIvPgo8cGF0aCBkPSJNMTYwLjY3MyAxMDQuMTA3QzE1OS44NDYgMTA0LjEwNyAxNTkuMTc1IDEwMy44NDggMTU4LjY1OSAxMDMuMzI5QzE1OC4xNDcgMTAyLjgwNiAxNTcuODkxIDEwMi4xMTEgMTU3Ljg5MSAxMDEuMjQ1VjEwMS4wODNDMTU3Ljg5MSAxMDAuNTAzIDE1OC4wMDIgOTkuOTg2IDE1OC4yMjQgOTkuNTMxMkMxNTguNDUgOTkuMDcyOSAxNTguNzY1IDk4LjcxNjYgMTU5LjE2OSA5OC40NjI0QzE1OS41NzQgOTguMjA4MiAxNjAuMDI1IDk4LjA4MTEgMTYwLjUyMyA5OC4wODExQzE2MS4zMTQgOTguMDgxMSAxNjEuOTI1IDk4LjMzMzUgMTYyLjM1NCA5OC44Mzg0QzE2Mi43ODggOTkuMzQzMyAxNjMuMDA0IDEwMC4wNTggMTYzLjAwNCAxMDAuOTgxVjEwMS41MDhIMTU5LjIwN0MxNTkuMjQ2IDEwMS45ODggMTU5LjQwNiAxMDIuMzY3IDE1OS42ODUgMTAyLjY0NkMxNTkuOTY4IDEwMi45MjYgMTYwLjMyMiAxMDMuMDY1IDE2MC43NDkgMTAzLjA2NUMxNjEuMzQ3IDEwMy4wNjUgMTYxLjgzMyAxMDIuODI0IDE2Mi4yMDkgMTAyLjM0TDE2Mi45MTMgMTAzLjAxMkMxNjIuNjggMTAzLjM1OSAxNjIuMzY5IDEwMy42MjkgMTYxLjk3OSAxMDMuODIzQzE2MS41OTIgMTA0LjAxMyAxNjEuMTU3IDEwNC4xMDcgMTYwLjY3MyAxMDQuMTA3Wk0xNjAuNTE4IDk5LjEyODRDMTYwLjE2IDk5LjEyODQgMTU5Ljg2OSA5OS4yNTM3IDE1OS42NDcgOTkuNTA0NEMxNTkuNDI5IDk5Ljc1NSAxNTkuMjg5IDEwMC4xMDQgMTU5LjIyOSAxMDAuNTUySDE2MS43MTVWMTAwLjQ1NUMxNjEuNjg3IDEwMC4wMTggMTYxLjU3IDk5LjY4ODggMTYxLjM2NiA5OS40NjY4QzE2MS4xNjIgOTkuMjQxMiAxNjAuODc5IDk5LjEyODQgMTYwLjUxOCA5OS4xMjg0WiIgZmlsbD0iI0ZGM0UzRSIvPgo8cGF0aCBkPSJNMTYzLjc3OCAxMDEuMDUxQzE2My43NzggMTAwLjE1NiAxNjMuOTg2IDk5LjQzODIgMTY0LjQwMSA5OC44OTc1QzE2NC44MTYgOTguMzUzMiAxNjUuMzczIDk4LjA4MTEgMTY2LjA3MSA5OC4wODExQzE2Ni42ODcgOTguMDgxMSAxNjcuMTg1IDk4LjI5NTkgMTY3LjU2NCA5OC43MjU2Vjk1Ljc1SDE2OC44N1YxMDRIMTY3LjY4OEwxNjcuNjI0IDEwMy4zOThDMTY3LjIzMyAxMDMuODcxIDE2Ni43MTIgMTA0LjEwNyAxNjYuMDYxIDEwNC4xMDdDMTY1LjM4IDEwNC4xMDcgMTY0LjgyOSAxMDMuODMzIDE2NC40MDYgMTAzLjI4NkMxNjMuOTg3IDEwMi43MzggMTYzLjc3OCAxMDEuOTkzIDE2My43NzggMTAxLjA1MVpNMTY1LjA4MyAxMDEuMTY0QzE2NS4wODMgMTAxLjc1NSAxNjUuMTk2IDEwMi4yMTcgMTY1LjQyMSAxMDIuNTVDMTY1LjY1MSAxMDIuODc5IDE2NS45NzUgMTAzLjA0NCAxNjYuMzk0IDEwMy4wNDRDMTY2LjkyNyAxMDMuMDQ0IDE2Ny4zMTcgMTAyLjgwNiAxNjcuNTY0IDEwMi4zM1Y5OS44NDgxQzE2Ny4zMjUgOTkuMzgyNiAxNjYuOTM4IDk5LjE0OTkgMTY2LjQwNCA5OS4xNDk5QzE2NS45ODIgOTkuMTQ5OSAxNjUuNjU2IDk5LjMxODIgMTY1LjQyNyA5OS42NTQ4QzE2NS4xOTggOTkuOTg3OCAxNjUuMDgzIDEwMC40OTEgMTY1LjA4MyAxMDEuMTY0WiIgZmlsbD0iI0ZGM0UzRSIvPgo8cGF0aCBkPSJNMTQwLjc5NSAxNDRWMTM2LjE4SDE0My4xMDRDMTQzLjc5NiAxMzYuMTggMTQ0LjQwOCAxMzYuMzM0IDE0NC45NDEgMTM2LjY0MkMxNDUuNDc5IDEzNi45NSAxNDUuODk0IDEzNy4zODYgMTQ2LjE4OCAxMzcuOTUyQzE0Ni40ODEgMTM4LjUxOCAxNDYuNjI4IDEzOS4xNjYgMTQ2LjYyOCAxMzkuODk2VjE0MC4yODlDMTQ2LjYyOCAxNDEuMDMgMTQ2LjQ3OSAxNDEuNjgxIDE0Ni4xODIgMTQyLjI0NEMxNDUuODg5IDE0Mi44MDYgMTQ1LjQ2OCAxNDMuMjM5IDE0NC45MiAxNDMuNTQzQzE0NC4zNzYgMTQzLjg0OCAxNDMuNzUxIDE0NCAxNDMuMDQ1IDE0NEgxNDAuNzk1Wk0xNDIuMTU0IDEzNy4yNzVWMTQyLjkxNUgxNDMuMDRDMTQzLjc1MyAxNDIuOTE1IDE0NC4yOTkgMTQyLjY5MyAxNDQuNjc4IDE0Mi4yNDlDMTQ1LjA2MSAxNDEuODAxIDE0NS4yNTcgMTQxLjE2IDE0NS4yNjQgMTQwLjMyNlYxMzkuODkxQzE0NS4yNjQgMTM5LjA0MiAxNDUuMDc5IDEzOC4zOTQgMTQ0LjcxIDEzNy45NDdDMTQ0LjM0MiAxMzcuNDk5IDE0My44MDYgMTM3LjI3NSAxNDMuMTA0IDEzNy4yNzVIMTQyLjE1NFoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE1MC40MTUgMTQ0LjEwN0MxNDkuNTg3IDE0NC4xMDcgMTQ4LjkxNiAxNDMuODQ4IDE0OC40IDE0My4zMjlDMTQ3Ljg4OCAxNDIuODA2IDE0Ny42MzIgMTQyLjExMSAxNDcuNjMyIDE0MS4yNDVWMTQxLjA4M0MxNDcuNjMyIDE0MC41MDMgMTQ3Ljc0MyAxMzkuOTg2IDE0Ny45NjUgMTM5LjUzMUMxNDguMTkxIDEzOS4wNzMgMTQ4LjUwNiAxMzguNzE3IDE0OC45MTEgMTM4LjQ2MkMxNDkuMzE1IDEzOC4yMDggMTQ5Ljc2NiAxMzguMDgxIDE1MC4yNjQgMTM4LjA4MUMxNTEuMDU2IDEzOC4wODEgMTUxLjY2NiAxMzguMzMzIDE1Mi4wOTYgMTM4LjgzOEMxNTIuNTI5IDEzOS4zNDMgMTUyLjc0NiAxNDAuMDU4IDE1Mi43NDYgMTQwLjk4MVYxNDEuNTA4SDE0OC45NDhDMTQ4Ljk4OCAxNDEuOTg4IDE0OS4xNDcgMTQyLjM2NyAxNDkuNDI2IDE0Mi42NDZDMTQ5LjcwOSAxNDIuOTI2IDE1MC4wNjQgMTQzLjA2NSAxNTAuNDkgMTQzLjA2NUMxNTEuMDg4IDE0My4wNjUgMTUxLjU3NSAxNDIuODI0IDE1MS45NTEgMTQyLjM0TDE1Mi42NTQgMTQzLjAxMkMxNTIuNDIyIDE0My4zNTkgMTUyLjExIDE0My42MjkgMTUxLjcyIDE0My44MjNDMTUxLjMzMyAxNDQuMDEzIDE1MC44OTggMTQ0LjEwNyAxNTAuNDE1IDE0NC4xMDdaTTE1MC4yNTkgMTM5LjEyOEMxNDkuOTAxIDEzOS4xMjggMTQ5LjYxMSAxMzkuMjU0IDE0OS4zODkgMTM5LjUwNEMxNDkuMTcgMTM5Ljc1NSAxNDkuMDMxIDE0MC4xMDQgMTQ4Ljk3IDE0MC41NTJIMTUxLjQ1N1YxNDAuNDU1QzE1MS40MjggMTQwLjAxOCAxNTEuMzEyIDEzOS42ODkgMTUxLjEwNyAxMzkuNDY3QzE1MC45MDMgMTM5LjI0MSAxNTAuNjIgMTM5LjEyOCAxNTAuMjU5IDEzOS4xMjhaIiBmaWxsPSIjNENBRjUwIi8+CjxwYXRoIGQ9Ik0xNTUuMTUyIDE0NEgxNTMuODQ3VjEzNS43NUgxNTUuMTUyVjE0NFoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE1Ny45NjYgMTQ0SDE1Ni42NjFWMTM4LjE4OEgxNTcuOTY2VjE0NFpNMTU2LjU4MSAxMzYuNjc5QzE1Ni41ODEgMTM2LjQ3OSAxNTYuNjQzIDEzNi4zMTIgMTU2Ljc2OSAxMzYuMThDMTU2Ljg5NyAxMzYuMDQ3IDE1Ny4wOCAxMzUuOTgxIDE1Ny4zMTYgMTM1Ljk4MUMxNTcuNTUzIDEzNS45ODEgMTU3LjczNSAxMzYuMDQ3IDE1Ny44NjQgMTM2LjE4QzE1Ny45OTMgMTM2LjMxMiAxNTguMDU4IDEzNi40NzkgMTU4LjA1OCAxMzYuNjc5QzE1OC4wNTggMTM2Ljg3NiAxNTcuOTkzIDEzNy4wNDEgMTU3Ljg2NCAxMzcuMTczQzE1Ny43MzUgMTM3LjMwMiAxNTcuNTUzIDEzNy4zNjcgMTU3LjMxNiAxMzcuMzY3QzE1Ny4wOCAxMzcuMzY3IDE1Ni44OTcgMTM3LjMwMiAxNTYuNzY5IDEzNy4xNzNDMTU2LjY0MyAxMzcuMDQxIDE1Ni41ODEgMTM2Ljg3NiAxNTYuNTgxIDEzNi42NzlaIiBmaWxsPSIjNENBRjUwIi8+CjxwYXRoIGQ9Ik0xNjEuNDQxIDE0Mi4zNDZMMTYyLjY3MSAxMzguMTg4SDE2NC4wMkwxNjIuMDA1IDE0NEgxNjAuODcyTDE1OC44NDIgMTM4LjE4OEgxNjAuMTk1TDE2MS40NDEgMTQyLjM0NloiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE2Ny4zMjMgMTQ0LjEwN0MxNjYuNDk2IDE0NC4xMDcgMTY1LjgyNCAxNDMuODQ4IDE2NS4zMDkgMTQzLjMyOUMxNjQuNzk3IDE0Mi44MDYgMTY0LjU0MSAxNDIuMTExIDE2NC41NDEgMTQxLjI0NVYxNDEuMDgzQzE2NC41NDEgMTQwLjUwMyAxNjQuNjUyIDEzOS45ODYgMTY0Ljg3NCAxMzkuNTMxQzE2NS4wOTkgMTM5LjA3MyAxNjUuNDE0IDEzOC43MTcgMTY1LjgxOSAxMzguNDYyQzE2Ni4yMjMgMTM4LjIwOCAxNjYuNjc1IDEzOC4wODEgMTY3LjE3MiAxMzguMDgxQzE2Ny45NjQgMTM4LjA4MSAxNjguNTc0IDEzOC4zMzMgMTY5LjAwNCAxMzguODM4QzE2OS40MzcgMTM5LjM0MyAxNjkuNjU0IDE0MC4wNTggMTY5LjY1NCAxNDAuOTgxVjE0MS41MDhIMTY1Ljg1NkMxNjUuODk2IDE0MS45ODggMTY2LjA1NSAxNDIuMzY3IDE2Ni4zMzQgMTQyLjY0NkMxNjYuNjE3IDE0Mi45MjYgMTY2Ljk3MiAxNDMuMDY1IDE2Ny4zOTggMTQzLjA2NUMxNjcuOTk2IDE0My4wNjUgMTY4LjQ4MyAxNDIuODI0IDE2OC44NTkgMTQyLjM0TDE2OS41NjIgMTQzLjAxMkMxNjkuMzMgMTQzLjM1OSAxNjkuMDE4IDE0My42MjkgMTY4LjYyOCAxNDMuODIzQzE2OC4yNDEgMTQ0LjAxMyAxNjcuODA2IDE0NC4xMDcgMTY3LjMyMyAxNDQuMTA3Wk0xNjcuMTY3IDEzOS4xMjhDMTY2LjgwOSAxMzkuMTI4IDE2Ni41MTkgMTM5LjI1NCAxNjYuMjk3IDEzOS41MDRDMTY2LjA3OCAxMzkuNzU1IDE2NS45MzkgMTQwLjEwNCAxNjUuODc4IDE0MC41NTJIMTY4LjM2NVYxNDAuNDU1QzE2OC4zMzYgMTQwLjAxOCAxNjguMjIgMTM5LjY4OSAxNjguMDE2IDEzOS40NjdDMTY3LjgxMiAxMzkuMjQxIDE2Ny41MjkgMTM5LjEyOCAxNjcuMTY3IDEzOS4xMjhaIiBmaWxsPSIjNENBRjUwIi8+CjxwYXRoIGQ9Ik0xNzMuNzE0IDEzOS4zODFDMTczLjU0MiAxMzkuMzUyIDE3My4zNjUgMTM5LjMzOCAxNzMuMTgzIDEzOS4zMzhDMTcyLjU4NSAxMzkuMzM4IDE3Mi4xODIgMTM5LjU2NyAxNzEuOTc0IDE0MC4wMjVWMTQ0SDE3MC42NjlWMTM4LjE4OEgxNzEuOTE1TDE3MS45NDcgMTM4LjgzOEMxNzIuMjYyIDEzOC4zMzMgMTcyLjY5OSAxMzguMDgxIDE3My4yNTggMTM4LjA4MUMxNzMuNDQ0IDEzOC4wODEgMTczLjU5OCAxMzguMTA2IDE3My43MiAxMzguMTU2TDE3My43MTQgMTM5LjM4MVoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE3Ni45OTEgMTQ0LjEwN0MxNzYuMTY0IDE0NC4xMDcgMTc1LjQ5MiAxNDMuODQ4IDE3NC45NzcgMTQzLjMyOUMxNzQuNDY1IDE0Mi44MDYgMTc0LjIwOCAxNDIuMTExIDE3NC4yMDggMTQxLjI0NVYxNDEuMDgzQzE3NC4yMDggMTQwLjUwMyAxNzQuMzE5IDEzOS45ODYgMTc0LjU0MiAxMzkuNTMxQzE3NC43NjcgMTM5LjA3MyAxNzUuMDgyIDEzOC43MTcgMTc1LjQ4NyAxMzguNDYyQzE3NS44OTEgMTM4LjIwOCAxNzYuMzQzIDEzOC4wODEgMTc2Ljg0IDEzOC4wODFDMTc3LjYzMiAxMzguMDgxIDE3OC4yNDIgMTM4LjMzMyAxNzguNjcyIDEzOC44MzhDMTc5LjEwNSAxMzkuMzQzIDE3OS4zMjIgMTQwLjA1OCAxNzkuMzIyIDE0MC45ODFWMTQxLjUwOEgxNzUuNTI0QzE3NS41NjQgMTQxLjk4OCAxNzUuNzIzIDE0Mi4zNjcgMTc2LjAwMiAxNDIuNjQ2QzE3Ni4yODUgMTQyLjkyNiAxNzYuNjQgMTQzLjA2NSAxNzcuMDY2IDE0My4wNjVDMTc3LjY2NCAxNDMuMDY1IDE3OC4xNTEgMTQyLjgyNCAxNzguNTI3IDE0Mi4zNEwxNzkuMjMgMTQzLjAxMkMxNzguOTk4IDE0My4zNTkgMTc4LjY4NiAxNDMuNjI5IDE3OC4yOTYgMTQzLjgyM0MxNzcuOTA5IDE0NC4wMTMgMTc3LjQ3NCAxNDQuMTA3IDE3Ni45OTEgMTQ0LjEwN1pNMTc2LjgzNSAxMzkuMTI4QzE3Ni40NzcgMTM5LjEyOCAxNzYuMTg3IDEzOS4yNTQgMTc1Ljk2NSAxMzkuNTA0QzE3NS43NDYgMTM5Ljc1NSAxNzUuNjA3IDE0MC4xMDQgMTc1LjU0NiAxNDAuNTUySDE3OC4wMzNWMTQwLjQ1NUMxNzguMDA0IDE0MC4wMTggMTc3Ljg4OCAxMzkuNjg5IDE3Ny42ODQgMTM5LjQ2N0MxNzcuNDc5IDEzOS4yNDEgMTc3LjE5NyAxMzkuMTI4IDE3Ni44MzUgMTM5LjEyOFoiIGZpbGw9IiM0Q0FGNTAiLz4KPHBhdGggZD0iTTE4MC4wOTUgMTQxLjA1MUMxODAuMDk1IDE0MC4xNTYgMTgwLjMwMyAxMzkuNDM4IDE4MC43MTggMTM4Ljg5N0MxODEuMTM0IDEzOC4zNTMgMTgxLjY5IDEzOC4wODEgMTgyLjM4OSAxMzguMDgxQzE4My4wMDUgMTM4LjA4MSAxODMuNTAyIDEzOC4yOTYgMTgzLjg4MiAxMzguNzI2VjEzNS43NUgxODUuMTg3VjE0NEgxODQuMDA1TDE4My45NDEgMTQzLjM5OEMxODMuNTUxIDE0My44NzEgMTgzLjAzIDE0NC4xMDcgMTgyLjM3OCAxNDQuMTA3QzE4MS42OTggMTQ0LjEwNyAxODEuMTQ2IDE0My44MzMgMTgwLjcyNCAxNDMuMjg2QzE4MC4zMDUgMTQyLjczOCAxODAuMDk1IDE0MS45OTMgMTgwLjA5NSAxNDEuMDUxWk0xODEuNCAxNDEuMTY0QzE4MS40IDE0MS43NTUgMTgxLjUxMyAxNDIuMjE3IDE4MS43MzkgMTQyLjU1QzE4MS45NjggMTQyLjg3OSAxODIuMjkyIDE0My4wNDQgMTgyLjcxMSAxNDMuMDQ0QzE4My4yNDQgMTQzLjA0NCAxODMuNjM1IDE0Mi44MDYgMTgzLjg4MiAxNDIuMzNWMTM5Ljg0OEMxODMuNjQyIDEzOS4zODMgMTgzLjI1NSAxMzkuMTUgMTgyLjcyMiAxMzkuMTVDMTgyLjI5OSAxMzkuMTUgMTgxLjk3MyAxMzkuMzE4IDE4MS43NDQgMTM5LjY1NUMxODEuNTE1IDEzOS45ODggMTgxLjQgMTQwLjQ5MSAxODEuNCAxNDEuMTY0WiIgZmlsbD0iIzRDQUY1MCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE5NiAxSDRDMi4zNDMxNCAxIDEgMi4zNDMxNCAxIDRWMTU2QzEgMTU3LjY1NyAyLjM0MzE0IDE1OSA0IDE1OUgxOTZDMTk3LjY1NyAxNTkgMTk5IDE1Ny42NTcgMTk5IDE1NlY0QzE5OSAyLjM0MzE1IDE5Ny42NTcgMSAxOTYgMVpNNCAwSDE5NkMxOTguMjA5IDAgMjAwIDEuNzkwODYgMjAwIDRWMTU2QzIwMCAxNTguMjA5IDE5OC4yMDkgMTYwIDE5NiAxNjBINEMxLjc5MDg2IDE2MCAwIDE1OC4yMDkgMCAxNTZWNEMwIDEuNzkwODYgMS43OTA4NiAwIDQgMFoiIGZpbGw9IiNFMEUwRTAiLz4KPC9zdmc+Cg==", + "description": "Displays Persistent RPC requests that match selected alias and filter with the ability of pagination and sending persistent RPC requests.", + "descriptor": { + "type": "rpc", + "sizeX": 7.5, + "sizeY": 4, + "resources": [], + "templateHtml": "", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onResize = function() {\n}\n\nself.onDestroy = function() {\n}\n", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"PersistentTableSettings\",\n \"properties\": {\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableFilter\": {\n \"title\": \"Enable filter\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowSendRequest\": {\n \"title\": \"Allow send RPC request\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayDetails\": {\n \"title\": \"Display request details\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"allowDelete\": {\n \"title\": \"Allow delete request\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"-createdTime\"\n },\n \"displayColumns\": {\n \"title\": \"Columns for display\",\n \"type\": \"array\",\n \"minItems\": 1\n }\n },\n \"required\": [\"displayColumns\"]\n },\n \"uiSchema\": {\n \"type\": \"VerticalLayout\",\n \"elements\": [\n {\n \"type\": \"Control\",\n \"scope\": \"#/schema/properties/enableStickyHeader\"\n },\n {\n \"type\": \"Control\",\n \"scope\": \"#/schema/properties/enableFilter\"\n }\n ]\n },\n \"form\": [\n [\n \"enableStickyHeader\",\n \"enableFilter\",\n \"allowSendRequest\",\n \"enableStickyAction\",\n \"displayDetails\",\n \"allowDelete\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ],\n [\n {\n \"key\": \"displayColumns\",\n \"type\": \"rc-select\",\n \"multiple\": true,\n \"default\": [\"rpcId\", \"messageType\", \"status\", \"method\", \"createdTime\", \"expirationTime\"],\n \"items\": [\n {\n \"value\": \"rpcId\",\n \"label\": \"RPC ID\"\n },\n {\n \"value\": \"messageType\",\n \"label\": \"Message type\"\n },\n {\n \"value\": \"status\",\n \"label\": \"Status\"\n },\n {\n \"value\": \"method\",\n \"label\": \"Method\"\n },\n {\n \"value\": \"createdTime\",\n \"label\": \"Created time\"\n },\n {\n \"value\": \"expirationTime\",\n \"label\": \"Expiration time\"\n }\n ]\n }\n ]\n ],\n \"groupInfoes\": [{\n \"formIndex\": 0,\n \"GroupTitle\": \"General settings\"\n }, {\n \"formIndex\": 1,\n \"GroupTitle\": \"Columns settings\"\n }]\n}", + "dataKeySettingsSchema": "{}\n", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"enableStickyAction\":true,\"enableFilter\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"enableStickyHeader\":true,\"displayColumns\":[\"rpcId\",\"messageType\",\"status\",\"method\",\"createdTime\",\"expirationTime\"],\"displayDetails\":true,\"defaultSortOrder\":\"-createdTime\",\"allowSendRequest\":true,\"allowDelete\":true},\"title\":\"Persistent table\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px\"},\"targetDeviceAliasIds\":[]}" + } } ] } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index 389a767cdf..844795593c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -177,8 +177,8 @@ public class RpcV2Controller extends AbstractRpcController { @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @ApiParam(value = "Status of the RPC", required = true, allowableValues = RPC_STATUS_ALLOWABLE_VALUES) - @RequestParam RpcStatus rpcStatus, + @ApiParam(value = "Status of the RPC", allowableValues = RPC_STATUS_ALLOWABLE_VALUES) + @RequestParam(required = false) RpcStatus rpcStatus, @ApiParam(value = RPC_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RPC_SORT_PROPERTY_ALLOWABLE_VALUES) @@ -194,7 +194,12 @@ public class RpcV2Controller extends AbstractRpcController { accessValidator.validate(getCurrentUser(), Operation.RPC_CALL, deviceId, new HttpValidationCallback(response, new FutureCallback<>() { @Override public void onSuccess(@Nullable DeferredResult result) { - PageData rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); + PageData rpcCalls; + if (rpcStatus != null) { + rpcCalls = rpcService.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); + } else { + rpcCalls = rpcService.findAllByDeviceId(tenantId, deviceId, pageLink); + } response.setResult(new ResponseEntity<>(rpcCalls, HttpStatus.OK)); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java index 4bdb1a169d..46b82077a3 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/rpc/RpcService.java @@ -35,5 +35,7 @@ public interface RpcService { ListenableFuture findRpcByIdAsync(TenantId tenantId, RpcId id); + PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink); + PageData findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java index 02b4bbe433..a5538f56d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rpc/BaseRpcService.java @@ -82,7 +82,15 @@ public class BaseRpcService implements RpcService { log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], rpcStatus [{}], pageLink [{}]", tenantId, deviceId, rpcStatus, pageLink); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); validatePageLink(pageLink); - return rpcDao.findAllByDeviceId(tenantId, deviceId, rpcStatus, pageLink); + return rpcDao.findAllByDeviceIdAndStatus(tenantId, deviceId, rpcStatus, pageLink); + } + + @Override + public PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink) { + log.trace("Executing findAllByDeviceIdAndStatus, tenantId [{}], deviceId [{}], pageLink [{}]", tenantId, deviceId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validatePageLink(pageLink); + return rpcDao.findAllByDeviceId(tenantId, deviceId, pageLink); } private PaginatedRemover tenantRpcRemover = diff --git a/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java b/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java index 63af784dbb..3f34db3907 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rpc/RpcDao.java @@ -24,7 +24,9 @@ import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.dao.Dao; public interface RpcDao extends Dao { - PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink); + PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink); + + PageData findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink); PageData findAllRpcByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java index 221ef17361..6791de4122 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/JpaRpcDao.java @@ -50,7 +50,12 @@ public class JpaRpcDao extends JpaAbstractDao implements RpcDao } @Override - public PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) { + public PageData findAllByDeviceId(TenantId tenantId, DeviceId deviceId, PageLink pageLink) { + return DaoUtil.toPageData(rpcRepository.findAllByTenantIdAndDeviceId(tenantId.getId(), deviceId.getId(), DaoUtil.toPageable(pageLink))); + } + + @Override + public PageData findAllByDeviceIdAndStatus(TenantId tenantId, DeviceId deviceId, RpcStatus rpcStatus, PageLink pageLink) { return DaoUtil.toPageData(rpcRepository.findAllByTenantIdAndDeviceIdAndStatus(tenantId.getId(), deviceId.getId(), rpcStatus, DaoUtil.toPageable(pageLink))); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java index 76b67b3823..0ca33dbb2e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rpc/RpcRepository.java @@ -26,6 +26,8 @@ import org.thingsboard.server.dao.model.sql.RpcEntity; import java.util.UUID; public interface RpcRepository extends CrudRepository { + Page findAllByTenantIdAndDeviceId(UUID tenantId, UUID deviceId, Pageable pageable); + Page findAllByTenantIdAndDeviceIdAndStatus(UUID tenantId, UUID deviceId, RpcStatus status, Pageable pageable); Page findAllByTenantId(UUID tenantId, Pageable pageable); diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index d053efb601..c0bab56e4e 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -55,6 +55,8 @@ import { TranslateService } from '@ngx-translate/core'; import { AlarmDataService } from '@core/api/alarm-data.service'; import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; import { PopoverPlacement } from '@shared/components/popover.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -71,9 +73,9 @@ export interface WidgetSubscriptionApi { export interface RpcApi { sendOneWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string) => Observable; + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string) => Observable; sendTwoWayCommand: (method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string) => Observable; + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string) => Observable; completedCommand: () => void; } @@ -287,6 +289,8 @@ export interface IWidgetSubscription { comparisonEnabled?: boolean; comparisonTimeWindow?: WidgetTimewindow; + persistentRequests?: PageData; + alarms?: PageData; alarmSource?: Datasource; @@ -313,11 +317,13 @@ export interface IWidgetSubscription { updateTimewindowConfig(newTimewindow: Timewindow): void; sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string): Observable; + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable; sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string): Observable; + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable; clearRpcError(): void; + subscribeForPersistentRequests(pageLink: PageLink, keyFileter: RpcStatus): Observable; + subscribe(): void; subscribeAllForPaginatedData(pageLink: EntityDataPageLink, diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 21f95a9158..c548b0fbab 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -70,6 +70,7 @@ import { import { distinct, filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { AlarmDataListener } from '@core/api/alarm-data.service'; import { RpcStatus } from '@shared/models/rpc.models'; +import { PageLink } from '@shared/models/page/page-link'; const moment = moment_; @@ -656,13 +657,13 @@ export class WidgetSubscription implements IWidgetSubscription { } sendOneWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string): Observable { - return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, requestUUID); + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable { + return this.sendCommand(true, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID); } sendTwoWayCommand(method: string, params?: any, timeout?: number, persistent?: boolean, - persistentPollingInterval?: number, requestUUID?: string): Observable { - return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, requestUUID); + persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable { + return this.sendCommand(false, method, params, timeout, persistent, persistentPollingInterval, retries, additionalInfo, requestUUID); } clearRpcError(): void { @@ -679,7 +680,8 @@ export class WidgetSubscription implements IWidgetSubscription { } sendCommand(oneWayElseTwoWay: boolean, method: string, params?: any, timeout?: number, - persistent?: boolean, persistentPollingInterval?: number, requestUUID?: string): Observable { + persistent?: boolean, persistentPollingInterval?: number, retries?: number, + additionalInfo?: any, requestUUID?: string): Observable { if (!this.rpcEnabled) { return throwError(new Error('Rpc disabled!')); } else { @@ -692,6 +694,8 @@ export class WidgetSubscription implements IWidgetSubscription { method, params, persistent, + retries, + additionalInfo, requestUUID }; if (timeout && timeout > 0) { @@ -777,6 +781,15 @@ export class WidgetSubscription implements IWidgetSubscription { } } + subscribeForPersistentRequests(pageLink: PageLink, keyFilter: RpcStatus): Observable { + if (!this.rpcEnabled) { + return throwError(new Error('Rpc disabled!')); + } else if (!this.targetDeviceId) { + return throwError(new Error('Target device is not set!')); + } + return this.ctx.deviceService.getPersistedRpcRequests(this.targetDeviceId, pageLink, keyFilter); + } + private extractRejectionErrorText(rejection: HttpErrorResponse) { let error = null; if (rejection.error) { diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index c8ea7e65ff..9b5119d317 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -31,7 +31,7 @@ import { import { EntitySubtype } from '@app/shared/models/entity-type.models'; import { AuthService } from '@core/auth/auth.service'; import { BulkImportRequest, BulkImportResult } from '@home/components/import-export/import-export.models'; -import { PersistentRpc } from '@shared/models/rpc.models'; +import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; @Injectable({ providedIn: 'root' @@ -143,6 +143,17 @@ export class DeviceService { return this.http.get(`/api/rpc/persistent/${rpcId}`, defaultHttpOptionsFromConfig(config)); } + public deletePersistedRpc(rpcId: string, config?: RequestConfig) { + return this.http.delete(`/api/rpc/persistent/${rpcId}`, defaultHttpOptionsFromConfig(config)); + } + + public getPersistedRpcRequests(deviceId: string, pageLink: PageLink, + keyFilter: RpcStatus, config?: RequestConfig): Observable> { + const rpcStatus = keyFilter ? '&rpcStatus=' + keyFilter : ''; + return this.http.get>(`/api/rpc/persistent/device/${deviceId}${pageLink.toQuery()}${rpcStatus}`, + defaultHttpOptionsFromConfig(config)); + } + public findByQuery(query: DeviceSearchQuery, config?: RequestConfig): Observable> { return this.http.post>('/api/devices', query, defaultHttpOptionsFromConfig(config)); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html new file mode 100644 index 0000000000..7e0ae3056f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html @@ -0,0 +1,92 @@ + +
+ +

{{ 'widgets.persistent-table.add-title' | translate }}

+ + +
+ + +
+
+
+
+ + {{ 'widgets.persistent-table.message-types.' + persistentFormGroup.get('oneWayElseTwoWay').value | translate }} + +
+
+ + widgets.persistent-table.method + + + {{'widgets.persistent-table.method-error' | translate}} + + + {{'widgets.persistent-table.white-space-error' | translate}} + + + + widgets.persistent-table.retries + + +
+
+ + +
+ + + + widgets.persistent-table.additional-info + + + + + + + +
+
+
+
+ + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss new file mode 100644 index 0000000000..e450fa4a09 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2021 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 { + .add-dialog ::ng-deep { + + .params-json-editor, + .additional-json-editor { + .tb-json-object-panel { + margin: 0 0 16px; + } + + .mat-expansion-panel-body { + padding-bottom: 0 !important; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts new file mode 100644 index 0000000000..c4e87c5e6b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2021 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 { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { RequestData } from '@shared/models/rpc.models'; + +@Component({ + selector: 'tb-persistent-add-dialog', + templateUrl: './persistent-add-dialog.component.html', + styleUrls: ['./persistent-add-dialog.component.scss'] +}) + +export class PersistentAddDialogComponent extends DialogComponent implements OnInit { + + public persistentFormGroup: FormGroup; + + private requestData: RequestData = { + persistentUpdated: false + }; + + constructor(protected store: Store, + protected router: Router, + public dialogRef: MatDialogRef, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.persistentFormGroup = this.fb.group( + { + method: ['', [Validators.required, Validators.pattern(/^\S+$/)]], + oneWayElseTwoWay: [false], + retries: [null, [Validators.pattern(/^-?[0-9]+$/), Validators.min(0)]], + params: [{}], + additionalInfo: [{}] + } + ); + } + + save() { + if (this.persistentFormGroup.valid) { + this.requestData = { + persistentUpdated: true, + method: this.persistentFormGroup.get('method').value, + oneWayElseTwoWay: this.persistentFormGroup.get('oneWayElseTwoWay').value, + params: this.persistentFormGroup.get('params').value, + additionalInfo: this.persistentFormGroup.get('additionalInfo').value, + retries: this.persistentFormGroup.get('retries').value + }; + this.close(); + } + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(this.requestData); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html new file mode 100644 index 0000000000..3a9451db67 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html @@ -0,0 +1,118 @@ + +
+ +

{{ persistentFormGroup.get('rpcId').value }}

+ + +
+ + +
+
+
+
+ + widgets.persistent-table.created-time + + + + widgets.persistent-table.expiration-time + + +
+
+ + widgets.persistent-table.message-type + + + + widgets.persistent-table.status + + + + widgets.persistent-table.method + + + + widgets.persistent-table.retries + + +
+ + + + + + {{ 'widgets.persistent-table.response' | translate }} + + + + + + + + {{ 'widgets.persistent-table.params' | translate }} + + + + + + + + + + {{ 'widgets.persistent-table.additional-info' | translate }} + + + + + + + + +
+
+
+ + +
+ +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss new file mode 100644 index 0000000000..ec456d5d9e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2021 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 { + .rpc-dialog ::ng-deep { + .mat-expansion-panel-body { + padding-bottom: 0 !important; + } + + .tb-json-object-panel { + margin: 0 0 16px 0; + } + } + .tb-audit-log-response-data { + width: 100%; + min-width: 400px; + height: 100%; + min-height: 100px; + border: 1px solid #c0c0c0; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts new file mode 100644 index 0000000000..552175b905 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts @@ -0,0 +1,151 @@ +/// +/// Copyright © 2016-2021 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, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { DatePipe } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { DeviceService } from '@core/http/device.service'; +import { + PersistentRpc, + rpcStatusColors, + RpcStatus, + rpcStatusTranslation +} from '@shared/models/rpc.models'; +import { isDefinedAndNotNull } from '@core/utils'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { DialogService } from '@core/services/dialog.service'; + +export interface PersistentDetailsDialogData { + persistentRequest: PersistentRpc; + allowDelete: boolean; +} + +@Component({ + selector: 'tb-persistent-details-dialog', + templateUrl: './persistent-details-dialog.component.html', + styleUrls: ['./persistent-details-dialog.component.scss'] +}) + +export class PersistentDetailsDialogComponent extends DialogComponent implements OnInit { + + @ViewChild('responseDataEditor', {static: true}) + responseDataEditorElmRef: ElementRef; + + public persistentFormGroup: FormGroup; + public rpcStatusColorsMap = rpcStatusColors; + public rpcStatus = RpcStatus; + public allowDelete: boolean; + + private persistentUpdated = false; + private responseData: string; + + constructor(protected store: Store, + protected router: Router, + private datePipe: DatePipe, + private translate: TranslateService, + @Inject(MAT_DIALOG_DATA) public data: PersistentDetailsDialogData, + public dialogRef: MatDialogRef, + private dialogService: DialogService, + private deviceService: DeviceService, + private fb: FormBuilder) { + super(store, router, dialogRef); + + this.allowDelete = data.allowDelete; + + this.persistentFormGroup = this.fb.group( + { + rpcId: [''], + createdTime: [''], + expirationTime: [''], + messageType: [''], + status: [''], + method: [''], + params: [''], + retries: [''], + response: [''], + additionalInfo: [null] + } + ); + this.loadPersistentFields(data.persistentRequest); + this.responseData = JSON.stringify(data.persistentRequest.response, null, 2); + } + + loadPersistentFields(request: PersistentRpc) { + this.persistentFormGroup.get('rpcId') + .patchValue(this.translate.instant('widgets.persistent-table.details-title') + request.id.id); + this.persistentFormGroup.get('createdTime') + .patchValue(this.datePipe.transform(request.createdTime, 'yyyy-MM-dd HH:mm:ss')); + this.persistentFormGroup.get('expirationTime') + .patchValue(this.datePipe.transform(request.expirationTime, 'yyyy-MM-dd HH:mm:ss')); + this.persistentFormGroup.get('messageType') + .patchValue(this.translate.instant('widgets.persistent-table.message-types.' + request.request.oneway) + ); + this.persistentFormGroup.get('status') + .patchValue(this.translate.instant(rpcStatusTranslation.get(request.status))); + this.persistentFormGroup.get('method') + .patchValue(request.request.body.method); + if (isDefinedAndNotNull(request.request.retries)) { + this.persistentFormGroup.get('retries') + .patchValue(request.request.retries); + } + if (isDefinedAndNotNull(request.response)) { + this.persistentFormGroup.get('response') + .patchValue(request.response); + } + if (isDefinedAndNotNull(request.request.body.params)) { + this.persistentFormGroup.get('params') + .patchValue(JSON.parse(request.request.body.params)); + } + if (isDefinedAndNotNull(request.additionalInfo)) { + this.persistentFormGroup.get('additionalInfo') + .patchValue(request.additionalInfo); + } + } + + ngOnInit(): void { + } + + close(): void { + this.dialogRef.close(this.persistentUpdated); + } + + deleteRpcRequest() { + const persistentRpc = this.data.persistentRequest; + if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { + this.dialogService.confirm( + this.translate.instant('widgets.persistent-table.delete-request-title'), + this.translate.instant('widgets.persistent-table.delete-request-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + if (res) { + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.persistentUpdated = true; + this.close(); + }); + } + } + }); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html new file mode 100644 index 0000000000..ffbcab5749 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html @@ -0,0 +1,44 @@ + +
+ + widgets.persistent-table.rpc-status-list + + + {{ 'widgets.persistent-table.rpc-search-status-all' | translate }} + + + {{ rpcSearchStatusTranslationMap.get(searchStatus) | translate }} + + + +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss new file mode 100644 index 0000000000..63f832d78c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2021 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%; + min-width: 300px; + overflow: hidden; + background: #fff; + border-radius: 4px; + box-shadow: + 0 7px 8px -4px rgba(0, 0, 0, .2), + 0 13px 19px 2px rgba(0, 0, 0, .14), + 0 5px 24px 4px rgba(0, 0, 0, .12); + + .mat-content { + overflow: hidden; + background-color: #fff; + } + + .mat-padding { + padding: 16px; + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts new file mode 100644 index 0000000000..d60c80ac43 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts @@ -0,0 +1,71 @@ +/// +/// Copyright © 2016-2021 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, InjectionToken } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { RpcStatus, rpcStatusTranslation } from '@shared/models/rpc.models'; + +export const PERSISTENT_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); + +export interface PersistentFilterPanelData { + rpcStatus: RpcStatus; +} + +@Component({ + selector: 'tb-persistent-filter-panel', + templateUrl: './persistent-filter-panel.component.html', + styleUrls: ['./persistent-filter-panel.component.scss'] +}) +export class PersistentFilterPanelComponent { + + public persistentFilterFormGroup: FormGroup; + public result: PersistentFilterPanelData; + public rpcSearchStatusTranslationMap = rpcStatusTranslation; + + public persistentSearchStatuses = [ + RpcStatus.QUEUED, + RpcStatus.SENT, + RpcStatus.DELIVERED, + RpcStatus.SUCCESSFUL, + RpcStatus.TIMEOUT, + RpcStatus.EXPIRED, + RpcStatus.FAILED + ]; + + constructor(@Inject(PERSISTENT_FILTER_PANEL_DATA) + public data: PersistentFilterPanelData, + public overlayRef: OverlayRef, + private fb: FormBuilder) { + this.persistentFilterFormGroup = this.fb.group( + { + rpcStatus: this.data.rpcStatus + } + ); + } + + update() { + this.result = { + rpcStatus: this.persistentFilterFormGroup.get('rpcStatus').value + }; + this.overlayRef.dispose(); + } + + cancel() { + this.overlayRef.dispose(); + } +} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html new file mode 100644 index 0000000000..c4fc2428be --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html @@ -0,0 +1,125 @@ + +
+
+
+ + + + {{ 'widgets.persistent-table.rpc-id' | translate }} + + + {{ column.id.id }} + + + + + {{ 'widgets.persistent-table.created-time' | translate }} + + + {{ column.createdTime | date:'yyyy-MM-dd HH:mm:ss' }} + + + + + {{ 'widgets.persistent-table.expiration-time' | translate }} + + + {{ column.expirationTime | date:'yyyy-MM-dd HH:mm:ss' }} + + + + + {{ 'widgets.persistent-table.status' | translate }} + + + {{ rpcStatusTranslation.get(column.status) | translate }} + + + + + {{ 'widgets.persistent-table.message-type' | translate }} + + + {{ 'widgets.persistent-table.message-types.' + column.request.oneway | translate }} + + + + + {{ 'widgets.persistent-table.method' | translate }} + + + {{ column.request.body.method }} + + + + + + +
+ + + +
+
+ + + + + + +
+
+
+ + +
+ {{ noDataDisplayMessageText }} + {{ 'common.loading' | translate }} +
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss new file mode 100644 index 0000000000..ec079f54cb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.scss @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2021 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%; + .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; + } + + .column-id { + min-width: 250px; + max-width: 250px; + width: 250px; + } + + .column-time { + min-width: 120px; + max-width: 120px; + width: 120px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts new file mode 100644 index 0000000000..8f5ccab14b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -0,0 +1,476 @@ +/// +/// Copyright © 2016-2021 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, + ElementRef, + Injector, + Input, + OnInit, + StaticProvider, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { WidgetConfig } from '@shared/models/widget.models'; +import { IWidgetSubscription } from '@core/api/widget-api.models'; +import { BehaviorSubject, merge, Observable, of, ReplaySubject } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; +import { + constructTableCssString, noDataMessage, + TableCellButtonActionDescriptor, + TableWidgetSettings +} from '@home/components/widget/lib/table-widget.models'; +import cssjs from '@core/css/css'; +import { UtilsService } from '@core/services/utils.service'; +import { TranslateService } from '@ngx-translate/core'; +import { hashCode, isDefined, isNumber } from '@core/utils'; +import { CollectionViewer, DataSource } from '@angular/cdk/collections'; +import { emptyPageData, PageData } from '@shared/models/page/page-data'; +import { + PersistentRpc, + PersistentRpcData, RequestData, + RpcStatus, + rpcStatusColors, rpcStatusTranslation +} from '@shared/models/rpc.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { DialogService } from '@core/services/dialog.service'; +import { DeviceService } from '@core/http/device.service'; +import { MatDialog } from '@angular/material/dialog'; +import { + PersistentDetailsDialogComponent, + PersistentDetailsDialogData +} from '@home/components/widget/lib/rpc/persistent-details-dialog.component'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { + PERSISTENT_FILTER_PANEL_DATA, PersistentFilterPanelComponent, PersistentFilterPanelData +} from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; +import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; + +interface PersistentTableWidgetSettings extends TableWidgetSettings { + defaultSortOrder: string; + defaultPageSize: number; + displayPagination: boolean; + enableStickyAction: boolean; + enableStickyHeader: boolean; + enableFilter: boolean; + displayColumns: string[]; + displayDetails: boolean; + allowDelete: boolean; + allowSendRequest: boolean; +} + +interface PersistentTableWidgetActionDescriptor extends TableCellButtonActionDescriptor { + details?: boolean; + delete?: boolean; +} + +@Component({ + selector: 'tb-persistent-table-widget', + templateUrl: './persistent-table.component.html', + styleUrls: ['./persistent-table.component.scss' , '../table-widget.scss'] +}) + +export class PersistentTableComponent extends PageComponent implements OnInit { + + @Input() + ctx: WidgetContext; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + private settings: PersistentTableWidgetSettings; + private widgetConfig: WidgetConfig; + private subscription: IWidgetSubscription; + private enableFilterAction = true; + private allowSendRequest = true; + private defaultPageSize = 10; + private defaultSortOrder = '-createdTime'; + private rpcStatusFilter: RpcStatus | null = null; + private displayDetails = true; + private allowDelete = true; + private displayTableColumns: string[]; + + public persistentDatasource: PersistentDatasource; + public noDataDisplayMessageText: string; + public rpcStatusColor = rpcStatusColors; + public rpcStatusTranslation = rpcStatusTranslation; + public displayPagination = true; + public enableStickyHeader = true; + public enableStickyAction = true; + public pageLink: PageLink; + public pageSizeOptions; + public actionCellButtonAction: PersistentTableWidgetActionDescriptor[] = []; + public displayedColumns: string[]; + + constructor(protected store: Store, + private elementRef: ElementRef, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef, + private utils: UtilsService, + private translate: TranslateService, + private dialogService: DialogService, + private deviceService: DeviceService, + private dialog: MatDialog) { + super(store); + } + + ngOnInit() { + this.ctx.$scope.persistentTableWidget = this; + this.settings = this.ctx.settings; + this.widgetConfig = this.ctx.widgetConfig; + this.subscription = this.ctx.defaultSubscription; + this.initializeConfig(); + this.ctx.updateWidgetParams(); + } + + ngAfterViewInit(): void { + if (this.displayPagination) { + this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + } + ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) : this.sort.sortChange) as Observable) + .pipe( + tap(() => this.updateData()) + ) + .subscribe(); + this.updateData(); + } + + private initializeConfig() { + + this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; + this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; + this.displayTableColumns = isDefined(this.settings.displayColumns) ? this.settings.displayColumns : []; + this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; + this.enableFilterAction = isDefined(this.settings.enableFilter) ? this.settings.enableFilter : true; + this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true; + this.allowDelete = isDefined(this.settings.allowDelete) ? this.settings.allowDelete : true; + this.allowSendRequest = isDefined(this.settings.allowSendRequest) ? this.settings.allowSendRequest : true; + + this.noDataDisplayMessageText = + noDataMessage(this.widgetConfig.noDataDisplayMessage, 'widgets.persistent-table.no-request-prompt', this.utils, this.translate); + + this.displayedColumns = [...this.displayTableColumns]; + + const pageSize = this.settings.defaultPageSize; + if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) { + this.defaultPageSize = pageSize; + } + this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { + this.defaultSortOrder = this.settings.defaultSortOrder; + } + const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder); + this.pageLink = new PageLink(this.defaultPageSize, 0, null, sortOrder); + this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : 1024; + + + this.ctx.widgetActions = [ + { + name: 'widgets.persistent-table.add', + show: this.allowSendRequest, + icon: 'add', + onAction: $event => this.addPersistentRpcRequest($event) + }, + { + name: 'widgets.persistent-table.refresh', + show: true, + icon: 'refresh', + onAction: () => this.reloadPersistentRequests() + }, + { + name: 'widgets.persistent-table.filter', + show: this.enableFilterAction, + icon: 'filter_list', + onAction: $event => this.editFilter($event) + } + ]; + + if (this.settings.displayDetails) { + this.actionCellButtonAction.push( + { + displayName: this.translate.instant('widgets.persistent-table.details'), + icon: 'more_horiz', + details: true + } as PersistentTableWidgetActionDescriptor + ); + } + if (this.settings.allowDelete) { + this.actionCellButtonAction.push( + { + displayName: this.translate.instant('widgets.persistent-table.delete'), + icon: 'delete', + delete: true + } as PersistentTableWidgetActionDescriptor + ); + } + if (this.actionCellButtonAction.length) { + this.displayedColumns.push('actions'); + } + + this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription); + + const cssString = constructTableCssString(this.widgetConfig); + const cssParser = new cssjs(); + cssParser.testMode = false; + const namespace = 'persistent-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $(this.elementRef.nativeElement).addClass(namespace); + } + + private updateData() { + if (this.displayPagination) { + this.pageLink.page = this.paginator.pageIndex; + this.pageLink.pageSize = this.paginator.pageSize; + } else { + this.pageLink.page = 0; + } + if (this.settings.defaultSortOrder && this.settings.defaultSortOrder.length) { + this.defaultSortOrder = this.utils.customTranslation(this.settings.defaultSortOrder, this.settings.defaultSortOrder); + } + this.pageLink.sortOrder.property = this.sort.active; + this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; + this.persistentDatasource.loadPersistent(this.pageLink, this.rpcStatusFilter); + this.ctx.detectChanges(); + } + + public onDataUpdated() { + this.ctx.detectChanges(); + } + + reloadPersistentRequests() { + if (this.displayPagination) { + this.paginator.pageIndex = 0; + } + this.updateData(); + } + + deleteRpcRequest($event: Event, persistentRpc: PersistentRpc) { + if ($event) { + $event.stopPropagation(); + } + if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { + this.dialogService.confirm( + this.translate.instant('widgets.persistent-table.delete-request-title'), + this.translate.instant('widgets.persistent-table.delete-request-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes') + ).subscribe((res) => { + if (res) { + if (res) { + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.reloadPersistentRequests(); + }); + } + } + }); + } + } + + openRequestDetails($event: Event, persistentRpc: PersistentRpc) { + if ($event) { + $event.stopPropagation(); + } + if (persistentRpc && persistentRpc.id && persistentRpc.id.id !== NULL_UUID) { + this.dialog.open + (PersistentDetailsDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + persistentRequest: persistentRpc, + allowDelete: this.allowDelete + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.reloadPersistentRequests(); + } + } + ); + } + } + + addPersistentRpcRequest($event: Event){ + if ($event) { + $event.stopPropagation(); + } + this.dialog.open + (PersistentAddDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] + }).afterClosed().subscribe( + (requestData) => { + if (requestData.persistentUpdated) { + this.sendRequests(requestData); + } + } + ); + } + + private sendRequests(requestData: RequestData) { + let commandPromise; + if (requestData.oneWayElseTwoWay) { + commandPromise = this.ctx.controlApi.sendOneWayCommand( + requestData.method, + requestData.params, null, + true, null, + requestData.retries, + requestData.additionalInfo + ); + } else { + commandPromise = this.ctx.controlApi.sendTwoWayCommand( + requestData.method, + requestData.params, + null, + true, null, + requestData.retries, + requestData.additionalInfo + ); + } + commandPromise.subscribe( + () => { + this.reloadPersistentRequests(); + } + ); + } + + public onActionButtonClick($event: Event, persistentRpc: PersistentRpc, actionDescriptor: PersistentTableWidgetActionDescriptor) { + if (actionDescriptor.details) { + this.openRequestDetails($event, persistentRpc); + } + if (actionDescriptor.delete) { + this.deleteRpcRequest($event, persistentRpc); + } + } + + private editFilter($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: PERSISTENT_FILTER_PANEL_DATA, + useValue: { + rpcStatus: this.rpcStatusFilter + } as PersistentFilterPanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + const componentRef = overlayRef.attach(new ComponentPortal(PersistentFilterPanelComponent, + this.viewContainerRef, injector)); + componentRef.onDestroy(() => { + if (componentRef.instance.result) { + const result = componentRef.instance.result; + this.rpcStatusFilter = result.rpcStatus; + this.reloadPersistentRequests(); + } + }); + this.ctx.detectChanges(); + } +} + +class PersistentDatasource implements DataSource { + + private persistentSubject = new BehaviorSubject([]); + private pageDataSubject = new BehaviorSubject>(emptyPageData()); + + public dataLoading = true; + public pageData$ = this.pageDataSubject.asObservable(); + + constructor(private translate: TranslateService, + private subscription: IWidgetSubscription) { + } + + connect(collectionViewer: CollectionViewer): Observable> { + return this.persistentSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.persistentSubject.complete(); + this.pageDataSubject.complete(); + } + + reset() { + const pageData = emptyPageData(); + this.persistentSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + } + + loadPersistent(pageLink: PageLink, keyFilter: RpcStatus) { + this.dataLoading = true; + + const result = new ReplaySubject>(); + this.fetchEntities(pageLink, keyFilter).pipe( + catchError(() => of(emptyPageData())), + ).subscribe( + (pageData) => { + this.persistentSubject.next(pageData.data); + this.pageDataSubject.next(pageData); + result.next(pageData); + this.dataLoading = false; + } + ); + return result; + } + + fetchEntities(pageLink: PageLink, keyFilter: RpcStatus): Observable> { + return this.subscription.subscribeForPersistentRequests(pageLink, keyFilter); + } + + isEmpty(): Observable { + return this.persistentSubject.pipe( + map((requests) => !requests.length) + ); + } + + total(): Observable { + return this.pageDataSubject.pipe( + map((pageData) => pageData.totalElements) + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts index 330429ce10..a4da8f4b82 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/rpc-widgets.module.ts @@ -21,6 +21,10 @@ import { LedIndicatorComponent } from '@home/components/widget/lib/rpc/led-indic import { RoundSwitchComponent } from '@home/components/widget/lib/rpc/round-switch.component'; import { SwitchComponent } from '@home/components/widget/lib/rpc/switch.component'; import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component'; +import { PersistentTableComponent } from '@home/components/widget/lib/rpc/persistent-table.component'; +import { PersistentDetailsDialogComponent } from '@home/components/widget/lib/rpc/persistent-details-dialog.component'; +import { PersistentFilterPanelComponent } from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; +import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; @NgModule({ declarations: @@ -28,7 +32,11 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component'; LedIndicatorComponent, RoundSwitchComponent, SwitchComponent, - KnobComponent + KnobComponent, + PersistentTableComponent, + PersistentDetailsDialogComponent, + PersistentAddDialogComponent, + PersistentFilterPanelComponent ], imports: [ CommonModule, @@ -38,7 +46,10 @@ import { KnobComponent } from '@home/components/widget/lib/rpc/knob.component'; LedIndicatorComponent, RoundSwitchComponent, SwitchComponent, - KnobComponent + KnobComponent, + PersistentTableComponent, + PersistentDetailsDialogComponent, + PersistentAddDialogComponent ] }) export class RpcWidgetsModule { } diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 2a530c8b4e..f63f53d032 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -196,16 +196,18 @@ export class WidgetContext { }; controlApi: RpcApi = { - sendOneWayCommand: (method, params, timeout, persistent, requestUUID) => { + sendOneWayCommand: (method, params, timeout, persistent, + retries, additionalInfo, requestUUID) => { if (this.defaultSubscription) { - return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, requestUUID); + return this.defaultSubscription.sendOneWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID); } else { return of(null); } }, - sendTwoWayCommand: (method, params, timeout, persistent, requestUUID) => { + sendTwoWayCommand: (method, params, timeout, persistent, + retries, additionalInfo, requestUUID) => { if (this.defaultSubscription) { - return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, requestUUID); + return this.defaultSubscription.sendTwoWayCommand(method, params, timeout, persistent, retries, additionalInfo, requestUUID); } else { return of(null); } diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.html b/ui-ngx/src/app/shared/components/json-object-view.component.html new file mode 100644 index 0000000000..379c557b44 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-object-view.component.html @@ -0,0 +1,22 @@ + +
+ + +
+
diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.scss b/ui-ngx/src/app/shared/components/json-object-view.component.scss new file mode 100644 index 0000000000..2b754b0ef2 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-object-view.component.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2021 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 { + #tb-json-view { + width: 100%; + height: 100%; + margin-bottom: 16px; + border: 1px solid #c0c0c0; + + &:not(.fill-height) { + min-height: 100px; + } + } +} diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.ts b/ui-ngx/src/app/shared/components/json-object-view.component.ts new file mode 100644 index 0000000000..5ce00b0189 --- /dev/null +++ b/ui-ngx/src/app/shared/components/json-object-view.component.ts @@ -0,0 +1,166 @@ +/// +/// Copyright © 2016-2021 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, ElementRef, forwardRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Ace } from 'ace-builds'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { RafService } from '@core/services/raf.service'; +import { isDefinedAndNotNull, isUndefined } from '@core/utils'; +import { getAce } from '@shared/models/ace/ace.models'; + +@Component({ + selector: 'tb-json-object-view', + templateUrl: './json-object-view.component.html', + styleUrls: ['./json-object-view.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => JsonObjectViewComponent), + multi: true + } + ] +}) +export class JsonObjectViewComponent implements OnInit { + + @ViewChild('jsonViewer', {static: true}) + jsonViewerElmRef: ElementRef; + + private jsonViewer: Ace.Editor; + private viewerElement: Ace.Editor; + private propagateChange = null; + private modelValue: any; + private contentValue: string; + + @Input() label: string; + + @Input() fillHeight: boolean; + + @Input() editorStyle: { [klass: string]: any }; + + @Input() sort: (key: string, value: any) => any; + + private widthValue: boolean; + + get autoWidth(): boolean { + return this.widthValue; + } + + @Input() + set autoWidth(value: boolean) { + this.widthValue = coerceBooleanProperty(value); + } + + private heigthValue: boolean; + + get autoHeight(): boolean { + return this.heigthValue; + } + + @Input() + set autoHeight(value: boolean) { + this.heigthValue = coerceBooleanProperty(value); + } + + constructor(public elementRef: ElementRef, + protected store: Store, + private raf: RafService, + private renderer: Renderer2) { + } + + ngOnInit(): void { + this.viewerElement = this.jsonViewerElmRef.nativeElement; + let editorOptions: Partial = { + mode: 'ace/mode/java', + theme: 'ace/theme/github', + showGutter: false, + showPrintMargin: false, + readOnly: true + }; + + const advancedOptions = { + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false + }; + + editorOptions = {...editorOptions, ...advancedOptions}; + getAce().subscribe( + (ace) => { + this.jsonViewer = ace.edit(this.viewerElement, editorOptions); + this.jsonViewer.session.setUseWrapMode(false); + this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1); + if (this.contentValue && (this.widthValue || this.heigthValue)) { + this.updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer); + } + } + ); + } + + updateEditorSize(editorElement: any, content: string, editor: Ace.Editor) { + let newHeight = 200; + let newWidth = 600; + if (content && content.length > 0) { + const lines = content.split('\n'); + newHeight = 17 * lines.length + 17; + let maxLineLength = 0; + lines.forEach((row) => { + const line = row.replace(/\t/g, ' ').replace(/\n/g, ''); + const lineLength = line.length; + maxLineLength = Math.max(maxLineLength, lineLength); + }); + newWidth = 8 * maxLineLength + 16; + } + if (this.heigthValue) { + // this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); + this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); + } + if (this.widthValue) { + this.renderer.setStyle(editorElement, 'width', newWidth.toString() + 'px'); + } + editor.resize(); + } + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + writeValue(value: any): void { + this.modelValue = value; + this.contentValue = ''; + try { + if (isDefinedAndNotNull(this.modelValue)) { + this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined : + (key, objectValue) => { + return this.sort(key, objectValue); + }, 2); + } + } catch (e) { + // + } + if (this.jsonViewer) { + this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1); + if (this.contentValue && (this.widthValue || this.heigthValue)) { + this.updateEditorSize(this.viewerElement, this.contentValue, this.jsonViewer); + } + } + } + +} diff --git a/ui-ngx/src/app/shared/models/rpc.models.ts b/ui-ngx/src/app/shared/models/rpc.models.ts index 75ac132394..61a94a401e 100644 --- a/ui-ngx/src/app/shared/models/rpc.models.ts +++ b/ui-ngx/src/app/shared/models/rpc.models.ts @@ -17,15 +17,42 @@ import { TenantId } from '@shared/models/id/tenant-id'; import { RpcId } from '@shared/models/id/rpc-id'; import { DeviceId } from '@shared/models/id/device-id'; +import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models'; export enum RpcStatus { QUEUED = 'QUEUED', DELIVERED = 'DELIVERED', SUCCESSFUL = 'SUCCESSFUL', TIMEOUT = 'TIMEOUT', - FAILED = 'FAILED' + FAILED = 'FAILED', + SENT = 'SENT', + EXPIRED = 'EXPIRED' } +export const rpcStatusColors = new Map( + [ + [RpcStatus.QUEUED, 'black'], + [RpcStatus.DELIVERED, 'green'], + [RpcStatus.SUCCESSFUL, 'green'], + [RpcStatus.TIMEOUT, 'orange'], + [RpcStatus.FAILED, 'red'], + [RpcStatus.SENT, 'green'], + [RpcStatus.EXPIRED, 'red'] + ] +); + +export const rpcStatusTranslation = new Map( + [ + [RpcStatus.QUEUED, 'widgets.persistent-table.rpc-status.QUEUED'], + [RpcStatus.DELIVERED, 'widgets.persistent-table.rpc-status.DELIVERED'], + [RpcStatus.SUCCESSFUL, 'widgets.persistent-table.rpc-status.SUCCESSFUL'], + [RpcStatus.TIMEOUT, 'widgets.persistent-table.rpc-status.TIMEOUT'], + [RpcStatus.FAILED, 'widgets.persistent-table.rpc-status.FAILED'], + [RpcStatus.SENT, 'widgets.persistent-table.rpc-status.SENT'], + [RpcStatus.EXPIRED, 'widgets.persistent-table.rpc-status.EXPIRED'] + ] +); + export interface PersistentRpc { id: RpcId; createdTime: number; @@ -34,7 +61,29 @@ export interface PersistentRpc { response: any; request: { id: string; + oneway: boolean; + body: { + method: string; + params: string; + }; + retries: null | number; }; deviceId: DeviceId; tenantId: TenantId; + additionalInfo?: string; +} + +export interface PersistentRpcData extends PersistentRpc { + actionCellButtons?: TableCellButtonActionDescriptor[]; + hasActions?: boolean; +} + +export interface RequestData { + persistentUpdated: boolean; + method?: string; + oneWayElseTwoWay?: boolean; + persistentPollingInterval?: number; + retries?: number; + params?: object; + additionalInfo?: object; } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 90b80393dd..3941dd902a 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -94,6 +94,7 @@ import { SocialSharePanelComponent } from '@shared/components/socialshare-panel. import { RelationTypeAutocompleteComponent } from '@shared/components/relation/relation-type-autocomplete.component'; import { EntityListSelectComponent } from '@shared/components/entity/entity-list-select.component'; import { JsonObjectEditComponent } from '@shared/components/json-object-edit.component'; +import { JsonObjectViewComponent, } from '@shared/components/json-object-view.component'; import { FooterFabButtonsComponent } from '@shared/components/footer-fab-buttons.component'; import { CircularProgressDirective } from '@shared/components/circular-progress.directive'; import { @@ -231,6 +232,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) RelationTypeAutocompleteComponent, SocialSharePanelComponent, JsonObjectEditComponent, + JsonObjectViewComponent, JsonContentComponent, JsFuncComponent, FabTriggerDirective, @@ -376,6 +378,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) RelationTypeAutocompleteComponent, SocialSharePanelComponent, JsonObjectEditComponent, + JsonObjectViewComponent, JsonContentComponent, JsFuncComponent, FabTriggerDirective, 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 bd14f73e27..79f8e18030 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3238,7 +3238,49 @@ "update-timeseries": "Update timeseries", "value": "Value" }, - "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type" + "invalid-qr-code-text": "Invalid input text for QR code. Input should have a string type", + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Message type", + "method": "Method", + "params": "Params", + "created-time": "Created time", + "expiration-time": "Expiration time", + "retries": "Retries", + "status": "Status", + "filter": "Filter", + "refresh": "Refresh", + "add": "Add RPC request", + "details": "Details", + "delete": "Delete", + "delete-request-title": "Delete Persistent RPC request", + "delete-request-text": "Are you sure you want to delete request?", + "details-title": "Details RPC ID: ", + "additional-info": "Additional info", + "response": "Response", + "any-status": "Any status", + "rpc-status-list": "RPC status list", + "no-request-prompt": "No request to display", + "send-request": "Send request", + "add-title": "Create Persistent RPC request", + "method-error": "Method is required.", + "timeout-error": "Min timeout value is 5000 (5 seconds).", + "white-space-error": "White space is not allowed.", + "rpc-status": { + "QUEUED": "QUEUED", + "SENT": "SENT", + "DELIVERED": "DELIVERED", + "SUCCESSFUL": "SUCCESSFUL", + "TIMEOUT": "TIMEOUT", + "EXPIRED": "EXPIRED", + "FAILED": "FAILED" + }, + "rpc-search-status-all": "ALL", + "message-types": { + "false": "Two-way", + "true": "One-way" + } + } }, "icon": { "icon": "Icon", diff --git a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json index 66d99281bb..dfc5226d6d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ru_RU.json +++ b/ui-ngx/src/assets/locale/locale.constant-ru_RU.json @@ -1787,6 +1787,47 @@ "update-attribute": "Обновить атрибут", "update-timeseries": "Обновить телеметрию", "value": "Значение" + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Тип сообщения", + "method": "Метод", + "params": "Параметры", + "created-time": "Время создания", + "expiration-time": "Время жизни", + "retries": "Повторные попытки", + "status": "Статус", + "filter": "Фильтр", + "refresh": "Обновить", + "add": "Добавить RPC запрос", + "details": "Детали", + "delete": "Удалить", + "delete-request-title": "Удалить RPC запрос", + "delete-request-text": "Вы точно хотите удалить RPC запрос?", + "details-title": "Детали RPC ID: ", + "additional-info": "Дополнительная информация", + "response": "Ответ", + "any-status": "Любой статус", + "rpc-status-list": "Список RPC статусов", + "no-request-prompt": "Запросы не найдены", + "send-request": "Отправить запрос", + "add-title": "Добавить новый RPC запрос", + "method-error": "Метод обязателен.", + "white-space-error": "Пробелы не допускаются.", + "rpc-status": { + "QUEUED": "В ОЧЕРЕДИ", + "SENT": "ОТПРАВЛЕННО", + "DELIVERED": "ДОСТАВЛЕННО", + "SUCCESSFUL": "УСПЕШНО", + "TIMEOUT": "ВРЕМЯ ИСТЕКЛО", + "EXPIRED": "ПРОСРОЧЕНО", + "FAILED": "НЕУДАЧНО" + }, + "rpc-search-status-all": "ВСЕ", + "message-types": { + "false": "Двусторонний", + "true": "Односторонний" + } } }, "icon": { diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index 43691918dd..be31aa7375 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -2359,6 +2359,47 @@ "update-attribute": "Оновити атрибут", "update-timeseries": "Оновити телеметрію", "value": "Значення" + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Тип повідомлення", + "method": "Метод", + "params": "Параметри", + "created-time": "Час створення", + "expiration-time": "Час життя", + "retries": "Повторні спроби", + "status": "Статус", + "filter": "Фільтр", + "refresh": "Оновити", + "add": "Додати RPC запит", + "details": "Деталі", + "delete": "Видалити", + "delete-request-title": "Видалити RPC запит", + "delete-request-text": "Ви впевнені, що хочете видалити RPC запит?", + "details-title": "Деталі RPC ID: ", + "additional-info": "Додаткова інформація", + "response": "Відповідь", + "any-status": "Будь-який статус", + "rpc-status-list": "Список RPC статусів", + "no-request-prompt": "Запитів не знайдено", + "send-request": "Відправити запит", + "add-title": "Додати новий RPC запит", + "method-error": "Необхідно вказати метод.", + "white-space-error": "Пробіли не допускаються.", + "rpc-status": { + "QUEUED": "В ЧЕРЗІ", + "SENT": "ВІДПРАВЛЕНО", + "DELIVERED": "ДОСТАВЛЕННО", + "SUCCESSFUL": "УСПІШНО", + "TIMEOUT": "ЧАС МИНУВ", + "EXPIRED": "ПРОСРОЧЕНО", + "FAILED": "НЕ ВДАЛО" + }, + "rpc-search-status-all": "ВСІ", + "message-types": { + "false": "Двусторонній", + "true": "Односторонній" + } } }, "white-labeling": { From 721906b9214128966c318fa2fc14536c18deef9c Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Mon, 29 Nov 2021 17:46:38 +0200 Subject: [PATCH 05/72] UI: Add error message for widget --- .../src/app/core/api/widget-subscription.ts | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index c548b0fbab..1634bb7b87 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -787,7 +787,38 @@ export class WidgetSubscription implements IWidgetSubscription { } else if (!this.targetDeviceId) { return throwError(new Error('Target device is not set!')); } - return this.ctx.deviceService.getPersistedRpcRequests(this.targetDeviceId, pageLink, keyFilter); + const rpcSubject: Subject = new Subject(); + + this.ctx.deviceService.getPersistedRpcRequests(this.targetDeviceId, pageLink, keyFilter).subscribe( + (responseBody) => { + rpcSubject.next(responseBody); + rpcSubject.complete(); + }, + (rejection: HttpErrorResponse) => { + const index = this.executingSubjects.indexOf(rpcSubject); + if (index >= 0) { + this.executingSubjects.splice( index, 1 ); + } + this.executingRpcRequest = this.executingSubjects.length > 0; + this.callbacks.rpcStateChanged(this); + if (!this.executingRpcRequest || rejection.status === 504) { + this.rpcRejection = rejection; + if (rejection.status === 504) { + this.rpcErrorText = 'Request Timeout.'; + } else { + this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; + const error = this.extractRejectionErrorText(rejection); + if (error) { + this.rpcErrorText += '
'; + this.rpcErrorText += error; + } + } + this.callbacks.onRpcFailed(this); + } + rpcSubject.error(rejection); + } + ); + return rpcSubject.asObservable(); } private extractRejectionErrorText(rejection: HttpErrorResponse) { From 05408d357ed2cbd396a40a1dc9b16930923e687c Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 1 Dec 2021 11:32:32 +0200 Subject: [PATCH 06/72] UI: On tables hide page size option on mobile view --- .../attribute/attribute-table.component.html | 1 + .../attribute/attribute-table.component.ts | 23 ++++++++++-- .../entity/entities-table.component.html | 1 + .../entity/entities-table.component.ts | 21 ++++++++++- .../relation/relation-table.component.html | 3 +- .../relation/relation-table.component.ts | 35 +++++++++++++++++-- .../lib/alarms-table-widget.component.html | 3 +- .../lib/alarms-table-widget.component.ts | 15 ++++++++ .../lib/entities-table-widget.component.html | 3 +- .../lib/entities-table-widget.component.ts | 20 +++++++++++ .../timeseries-table-widget.component.html | 3 +- .../lib/timeseries-table-widget.component.ts | 15 ++++++++ 12 files changed, 133 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index ead92fda03..c0612646f3 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -196,6 +196,7 @@ [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" [pageSizeOptions]="[10, 20, 30]" + [hidePageSize]="hidePageSize" showFirstLastButtons> , private attributeService: AttributeService, private telemetryWsService: TelemetryWebsocketService, @@ -184,7 +189,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI private dashboardUtils: DashboardUtilsService, private widgetService: WidgetService, private zone: NgZone, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private breakpointObserver: BreakpointObserver) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; @@ -193,6 +199,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI } ngOnInit() { + this.breakpointObserverSubscription$ = this.breakpointObserver + .observe(MediaBreakpoints['gt-xs']).subscribe( + () => { + this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); + this.cd.detectChanges(); + } + ); + } + + ngOnDestroy() { + if (this.breakpointObserverSubscription$) { + this.breakpointObserverSubscription$.unsubscribe(); + } } attributeScopeChanged(attributeScope: TelemetryType) { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index c4f470bbd2..a7b15670bb 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -266,6 +266,7 @@ [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" [pageSizeOptions]="pageSizeOptions" + [hidePageSize]="hidePageSize" showFirstLastButtons> diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 49386db131..cca386570d 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -64,6 +64,8 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; import { HasUUID } from '@shared/models/id/has-uuid'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { BreakpointObserver } from '@angular/cdk/layout'; @Component({ selector: 'tb-entities-table', @@ -120,6 +122,9 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private updateDataSubscription: Subscription; private viewInited = false; + private breakpointObserverSubscription$: Subscription; + public hidePageSize = true; + constructor(protected store: Store, public route: ActivatedRoute, public translate: TranslateService, @@ -127,7 +132,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private dialogService: DialogService, private domSanitizer: DomSanitizer, private cd: ChangeDetectorRef, - private componentFactoryResolver: ComponentFactoryResolver) { + private componentFactoryResolver: ComponentFactoryResolver, + private breakpointObserver: BreakpointObserver) { super(store); } @@ -137,6 +143,19 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn } else { this.init(this.route.snapshot.data.entitiesTableConfig); } + this.breakpointObserverSubscription$ = this.breakpointObserver + .observe(MediaBreakpoints['gt-xs']).subscribe( + () => { + this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); + this.cd.detectChanges(); + } + ); + } + + ngOnDestroy() { + if (this.breakpointObserverSubscription$) { + this.breakpointObserverSubscription$.unsubscribe(); + } } ngOnChanges(changes: SimpleChanges): void { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index 01c2e0e4ab..b5ae8c2d52 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -165,6 +165,7 @@ + [pageSizeOptions]="[10, 20, 30]" + [hidePageSize]="hidePageSize"> diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index b10ed82249..2ea0ccfa12 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -14,7 +14,16 @@ /// limitations under the License. /// -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnInit, + ViewChild +} from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { PageLink } from '@shared/models/page/page-link'; import { MatPaginator } from '@angular/material/paginator'; @@ -26,7 +35,7 @@ import { MatDialog } from '@angular/material/dialog'; import { DialogService } from '@core/services/dialog.service'; import { EntityRelationService } from '@core/http/entity-relation.service'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; +import { forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { EntityRelation, @@ -38,6 +47,8 @@ import { import { EntityId } from '@shared/models/id/entity-id'; import { RelationsDatasource } from '../../models/datasource/relation-datasource'; import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; +import { MediaBreakpoints } from '@shared/models/constants'; +import { BreakpointObserver } from '@angular/cdk/layout'; @Component({ selector: 'tb-relation-table', @@ -65,6 +76,9 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn viewsInited = false; + private breakpointObserverSubscription$: Subscription; + public hidePageSize = true; + @Input() set active(active: boolean) { if (this.activeValue !== active) { @@ -100,7 +114,9 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn private entityRelationService: EntityRelationService, public translate: TranslateService, public dialog: MatDialog, - private dialogService: DialogService) { + private dialogService: DialogService, + private cd: ChangeDetectorRef, + private breakpointObserver: BreakpointObserver) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; @@ -111,6 +127,19 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn } ngOnInit() { + this.breakpointObserverSubscription$ = this.breakpointObserver + .observe(MediaBreakpoints['gt-xs']).subscribe( + () => { + this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); + this.cd.detectChanges(); + } + ); + } + + ngOnDestroy() { + if (this.breakpointObserverSubscription$) { + this.breakpointObserverSubscription$.unsubscribe(); + } } updateColumns() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index a86da23f59..872e040bad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@@ -147,6 +147,7 @@ [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" [pageSizeOptions]="pageSizeOptions" + [hidePageSize]="hidePageSize" showFirstLastButtons>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 7b32d84e71..73c7ea20cd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -123,6 +123,7 @@ import { } from '@home/components/widget/lib/alarm-filter-panel.component'; import { entityFields } from '@shared/models/entity.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ResizeObserver } from '@juggle/resize-observer'; interface AlarmsTableWidgetSettings extends TableWidgetSettings { alarmsTitle: string; @@ -152,6 +153,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, @Input() ctx: WidgetContext; + @ViewChild('alarmWidgetContainer', {static: true}) alarmWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -168,6 +170,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public displayedColumns: string[] = []; public alarmsDatasource: AlarmsDatasource; public noDataDisplayMessageText: string; + public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -177,6 +180,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private settings: AlarmsTableWidgetSettings; private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; + private widgetResize$: ResizeObserver; private alarmsTitlePattern: string; @@ -257,6 +261,14 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.widgetTimewindowChanged$ = this.ctx.defaultSubscription.widgetTimewindowChanged$.subscribe( () => this.pageLink.page = 0 ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.ctx.detectChanges(); + } + }); + this.widgetResize$.observe(this.alarmWidgetContainerRef.nativeElement); } } @@ -265,6 +277,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.widgetTimewindowChanged$.unsubscribe(); this.widgetTimewindowChanged$ = null; } + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index 74ccd53ec2..7f8ae9b9b1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@@ -106,6 +106,7 @@ [pageIndex]="pageLink.page" [pageSize]="pageLink.pageSize" [pageSizeOptions]="pageSizeOptions" + [hidePageSize]="hidePageSize" showFirstLastButtons>
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index bd52f2ab9b..a89c1ebe82 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -107,6 +107,7 @@ import { sortItems } from '@shared/models/page/page-link'; import { entityFields } from '@shared/models/entity.models'; import { DatePipe } from '@angular/common'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ResizeObserver } from '@juggle/resize-observer'; interface EntitiesTableWidgetSettings extends TableWidgetSettings { entitiesTitle: string; @@ -129,6 +130,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @Input() ctx: WidgetContext; + @ViewChild('entitiesWidgetContainer', {static: true}) entitiesWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -144,6 +146,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public displayedColumns: string[] = []; public entityDatasource: EntityDatasource; public noDataDisplayMessageText: string; + public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -153,6 +156,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni private settings: EntitiesTableWidgetSettings; private widgetConfig: WidgetConfig; private subscription: IWidgetSubscription; + private widgetResize$: ResizeObserver; private entitiesTitlePattern: string; @@ -211,6 +215,22 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.initializeConfig(); this.updateDatasources(); this.ctx.updateWidgetParams(); + if (this.displayPagination) { + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.ctx.detectChanges(); + } + }); + this.widgetResize$.observe(this.entitiesWidgetContainerRef.nativeElement); + } + } + + ngOnDestroy(): void { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 8c3b9084b0..236d81036c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@@ -112,6 +112,7 @@ [pageIndex]="source.pageLink.page" [pageSize]="source.pageLink.pageSize" [pageSizeOptions]="pageSizeOptions" + [hidePageSize]="hidePageSize" showFirstLastButtons> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index de8cec8dc7..183832ae90 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -71,6 +71,7 @@ import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { DatePipe } from '@angular/common'; import { parseData } from '@home/components/widget/lib/maps/common-maps-utils'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { ResizeObserver } from '@juggle/resize-observer'; export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { showTimestamp: boolean; @@ -115,6 +116,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @Input() ctx: WidgetContext; + @ViewChild('timeseriesWidgetContainer', {static: true}) timeseriesWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChildren(MatPaginator) paginators: QueryList; @ViewChildren(MatSort) sorts: QueryList; @@ -128,6 +130,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI public sources: TimeseriesTableSource[]; public sourceIndex: number; public noDataDisplayMessageText: string; + public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -150,6 +153,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private subscriptions: Subscription[] = []; private widgetTimewindowChanged$: Subscription; + private widgetResize$: ResizeObserver; private searchAction: WidgetAction = { name: 'action.search', @@ -190,6 +194,14 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI }); } ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.ctx.detectChanges(); + } + }); + this.widgetResize$.observe(this.timeseriesWidgetContainerRef.nativeElement); } } @@ -198,6 +210,9 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI this.widgetTimewindowChanged$.unsubscribe(); this.widgetTimewindowChanged$ = null; } + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit(): void { From 5f11fa9b6cd04370d85a7075fe00ae85880e8035 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 1 Dec 2021 12:23:06 +0200 Subject: [PATCH 07/72] UI: On tables hide page size option on mobile view --- .../home/components/widget/lib/alarms-table-widget.component.ts | 2 +- .../components/widget/lib/entities-table-widget.component.ts | 2 +- .../components/widget/lib/timeseries-table-widget.component.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 73c7ea20cd..8f003141e8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -262,7 +262,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, () => this.pageLink.page = 0 ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index a89c1ebe82..2c887a1668 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -217,7 +217,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.ctx.updateWidgetParams(); if (this.displayPagination) { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 183832ae90..cc35c481a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -195,7 +195,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 450; + const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); From 12dcbf0e5e7e73abd3920b18dfaa74273a7f4006 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 1 Dec 2021 12:37:30 +0200 Subject: [PATCH 08/72] UI: Hide pageSizeOption on persistent table widget --- .../lib/rpc/persistent-table.component.html | 3 ++- .../lib/rpc/persistent-table.component.ts | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html index c4fc2428be..fd86fd92cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts index 8f5ccab14b..f2c6f9e13f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -67,6 +67,7 @@ import { PERSISTENT_FILTER_PANEL_DATA, PersistentFilterPanelComponent, PersistentFilterPanelData } from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; +import { ResizeObserver } from '@juggle/resize-observer'; interface PersistentTableWidgetSettings extends TableWidgetSettings { defaultSortOrder: string; @@ -97,6 +98,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { @Input() ctx: WidgetContext; + @ViewChild('persistentWidgetContainer', {static: true}) persistentWidgetContainerRef: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -111,6 +113,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { private displayDetails = true; private allowDelete = true; private displayTableColumns: string[]; + private widgetResize$: ResizeObserver; public persistentDatasource: PersistentDatasource; public noDataDisplayMessageText: string; @@ -123,6 +126,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { public pageSizeOptions; public actionCellButtonAction: PersistentTableWidgetActionDescriptor[] = []; public displayedColumns: string[]; + public hidePageSize = false; constructor(protected store: Store, private elementRef: ElementRef, @@ -143,6 +147,22 @@ export class PersistentTableComponent extends PageComponent implements OnInit { this.subscription = this.ctx.defaultSubscription; this.initializeConfig(); this.ctx.updateWidgetParams(); + if (this.displayPagination) { + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.ctx.detectChanges(); + } + }); + this.widgetResize$.observe(this.persistentWidgetContainerRef.nativeElement); + } + } + + ngOnDestroy() { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit(): void { From 33508f558e531b10801408b6b42a415ec368a96b Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 2 Dec 2021 13:18:42 +0200 Subject: [PATCH 09/72] UI: Hide pageSizeOption on persistent table widget --- .../components/widget/lib/rpc/persistent-table.component.ts | 3 ++- ui-ngx/src/app/shared/models/constants.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts index f2c6f9e13f..c7d4da0295 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.ts @@ -68,6 +68,7 @@ import { } from '@home/components/widget/lib/rpc/persistent-filter-panel.component'; import { PersistentAddDialogComponent } from '@home/components/widget/lib/rpc/persistent-add-dialog.component'; import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; interface PersistentTableWidgetSettings extends TableWidgetSettings { defaultSortOrder: string; @@ -149,7 +150,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { this.ctx.updateWidgetParams(); if (this.displayPagination) { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + const showHidePageSize = this.persistentWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 03b38345ca..4699a6338c 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -241,6 +241,7 @@ export const contentTypesMap = new Map( ] ); +export const hidePageSizePixelValue = 550; export const customTranslationsPrefix = 'custom.'; export const i18nPrefix = 'i18n'; From 76dccab3f7326698d03f6f3c1473e35e0237d7d5 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 2 Dec 2021 13:53:00 +0200 Subject: [PATCH 10/72] UI: On tables hide page size option on mobile view --- .../attribute/attribute-table.component.html | 2 +- .../attribute/attribute-table.component.ts | 33 ++++++++++--------- .../entity/entities-table.component.html | 2 +- .../entity/entities-table.component.ts | 32 +++++++++--------- .../relation/relation-table.component.html | 2 +- .../relation/relation-table.component.ts | 33 ++++++++++--------- .../manage-widget-actions.component.html | 2 +- .../action/manage-widget-actions.component.ts | 31 +++++++++++++++-- .../lib/alarms-table-widget.component.ts | 3 +- .../lib/entities-table-widget.component.ts | 3 +- .../lib/timeseries-table-widget.component.ts | 3 +- ui-ngx/src/app/shared/models/constants.ts | 1 + 12 files changed, 91 insertions(+), 56 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index c0612646f3..493bfe3e2d 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index e4a41251d8..1bd4dea53f 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -38,7 +38,7 @@ import { TranslateService } from '@ngx-translate/core'; import { MatDialog } from '@angular/material/dialog'; import { DialogService } from '@core/services/dialog.service'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { fromEvent, merge, Subscription } from 'rxjs'; +import { fromEvent, merge } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { EntityId } from '@shared/models/id/entity-id'; import { @@ -84,8 +84,8 @@ import { } from '@home/components/attribute/add-widget-to-dashboard-dialog.component'; import { deepClone } from '@core/utils'; import { Filters } from '@shared/models/query/query.models'; -import { MediaBreakpoints } from '@shared/models/constants'; -import { BreakpointObserver } from '@angular/cdk/layout'; +import { hidePageSizePixelValue } from '@shared/models/constants'; +import { ResizeObserver } from '@juggle/resize-observer'; @Component({ @@ -168,13 +168,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI @Input() entityName: string; + @ViewChild('attributeTableContainer', {static: true}) attributeTableContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; - private breakpointObserverSubscription$: Subscription; - public hidePageSize = true; + public hidePageSize = false; + private widgetResize$: ResizeObserver; constructor(protected store: Store, private attributeService: AttributeService, @@ -189,8 +190,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI private dashboardUtils: DashboardUtilsService, private widgetService: WidgetService, private zone: NgZone, - private cd: ChangeDetectorRef, - private breakpointObserver: BreakpointObserver) { + private cd: ChangeDetectorRef) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; @@ -199,18 +199,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI } ngOnInit() { - this.breakpointObserverSubscription$ = this.breakpointObserver - .observe(MediaBreakpoints['gt-xs']).subscribe( - () => { - this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); - this.cd.detectChanges(); - } - ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.attributeTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.detectChanges(); + } + }); + this.widgetResize$.observe(this.attributeTableContainerRef.nativeElement); } ngOnDestroy() { - if (this.breakpointObserverSubscription$) { - this.breakpointObserverSubscription$.unsubscribe(); + if (this.widgetResize$) { + this.widgetResize$.disconnect(); } } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index a7b15670bb..aa86660ebe 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -32,7 +32,7 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index cca386570d..1851d5e085 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -64,8 +64,8 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; import { HasUUID } from '@shared/models/id/has-uuid'; -import { MediaBreakpoints } from '@shared/models/constants'; -import { BreakpointObserver } from '@angular/cdk/layout'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; @Component({ selector: 'tb-entities-table', @@ -111,6 +111,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn isDetailsOpen = false; detailsPanelOpened = new EventEmitter(); + @ViewChild('entitiesTableContainer', {static: true}) entitiesTableContainerRef: ElementRef; + @ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent; @ViewChild('searchInput') searchInputField: ElementRef; @@ -122,8 +124,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private updateDataSubscription: Subscription; private viewInited = false; - private breakpointObserverSubscription$: Subscription; - public hidePageSize = true; + private widgetResize$: ResizeObserver; + public hidePageSize = false; constructor(protected store: Store, public route: ActivatedRoute, @@ -132,8 +134,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private dialogService: DialogService, private domSanitizer: DomSanitizer, private cd: ChangeDetectorRef, - private componentFactoryResolver: ComponentFactoryResolver, - private breakpointObserver: BreakpointObserver) { + private componentFactoryResolver: ComponentFactoryResolver) { super(store); } @@ -143,18 +144,19 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn } else { this.init(this.route.snapshot.data.entitiesTableConfig); } - this.breakpointObserverSubscription$ = this.breakpointObserver - .observe(MediaBreakpoints['gt-xs']).subscribe( - () => { - this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); - this.cd.detectChanges(); - } - ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.entitiesTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.detectChanges(); + } + }); + this.widgetResize$.observe(this.entitiesTableContainerRef.nativeElement); } ngOnDestroy() { - if (this.breakpointObserverSubscription$) { - this.breakpointObserverSubscription$.unsubscribe(); + if (this.widgetResize$) { + this.widgetResize$.disconnect(); } } diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index b5ae8c2d52..edf5d1d04d 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index 2ea0ccfa12..8f0b503717 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -35,7 +35,7 @@ import { MatDialog } from '@angular/material/dialog'; import { DialogService } from '@core/services/dialog.service'; import { EntityRelationService } from '@core/http/entity-relation.service'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; -import { forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs'; +import { forkJoin, fromEvent, merge, Observable } from 'rxjs'; import { debounceTime, distinctUntilChanged, tap } from 'rxjs/operators'; import { EntityRelation, @@ -47,8 +47,8 @@ import { import { EntityId } from '@shared/models/id/entity-id'; import { RelationsDatasource } from '../../models/datasource/relation-datasource'; import { RelationDialogComponent, RelationDialogData } from '@home/components/relation/relation-dialog.component'; -import { MediaBreakpoints } from '@shared/models/constants'; -import { BreakpointObserver } from '@angular/cdk/layout'; +import { hidePageSizePixelValue } from '@shared/models/constants'; +import { ResizeObserver } from '@juggle/resize-observer'; @Component({ selector: 'tb-relation-table', @@ -76,8 +76,8 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn viewsInited = false; - private breakpointObserverSubscription$: Subscription; - public hidePageSize = true; + private widgetResize$: ResizeObserver; + public hidePageSize = false; @Input() set active(active: boolean) { @@ -105,6 +105,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn } } + @ViewChild('relationTableContainer', {static: true}) relationTableContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -115,8 +116,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn public translate: TranslateService, public dialog: MatDialog, private dialogService: DialogService, - private cd: ChangeDetectorRef, - private breakpointObserver: BreakpointObserver) { + private cd: ChangeDetectorRef) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; @@ -127,18 +127,19 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn } ngOnInit() { - this.breakpointObserverSubscription$ = this.breakpointObserver - .observe(MediaBreakpoints['gt-xs']).subscribe( - () => { - this.hidePageSize = !this.breakpointObserver.isMatched(MediaBreakpoints['gt-xs']); - this.cd.detectChanges(); - } - ); + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.relationTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.detectChanges(); + } + }); + this.widgetResize$.observe(this.relationTableContainerRef.nativeElement); } ngOnDestroy() { - if (this.breakpointObserverSubscription$) { - this.breakpointObserverSubscription$.unsubscribe(); + if (this.widgetResize$) { + this.widgetResize$.disconnect(); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html index 53523bc0cf..d0c1db566c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index d3fdd47bba..5173e00954 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -14,7 +14,17 @@ /// limitations under the License. /// -import { AfterViewInit, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ElementRef, + forwardRef, + Input, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { TranslateService } from '@ngx-translate/core'; import { PageComponent } from '@shared/components/page.component'; @@ -43,6 +53,8 @@ import { } from '@home/components/widget/action/widget-action-dialog.component'; import { deepClone } from '@core/utils'; import { widgetType } from '@shared/models/widget.models'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; @Component({ selector: 'tb-manage-widget-actions', @@ -73,7 +85,10 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni viewsInited = false; dirtyValue = false; + public hidePageSize = false; + private widgetResize$: ResizeObserver; + @ViewChild('manageActionWidgetContainer', {static: true}) manageActionWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -85,7 +100,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni private translate: TranslateService, private utils: UtilsService, private dialog: MatDialog, - private dialogs: DialogService) { + private dialogs: DialogService, + private cd: ChangeDetectorRef) { super(store); const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; this.pageLink = new PageLink(10, 0, null, sortOrder); @@ -94,9 +110,20 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } ngOnInit(): void { + this.widgetResize$ = new ResizeObserver(() => { + const showHidePageSize = this.manageActionWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + if (showHidePageSize !== this.hidePageSize) { + this.hidePageSize = showHidePageSize; + this.cd.detectChanges(); + } + }); + this.widgetResize$.observe(this.manageActionWidgetContainerRef.nativeElement); } ngOnDestroy(): void { + if (this.widgetResize$) { + this.widgetResize$.disconnect(); + } } ngAfterViewInit() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 8f003141e8..e12fcfcebd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -124,6 +124,7 @@ import { import { entityFields } from '@shared/models/entity.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; interface AlarmsTableWidgetSettings extends TableWidgetSettings { alarmsTitle: string; @@ -262,7 +263,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, () => this.pageLink.page = 0 ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + const showHidePageSize = this.alarmWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index 2c887a1668..32a07cecb8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -108,6 +108,7 @@ import { entityFields } from '@shared/models/entity.models'; import { DatePipe } from '@angular/common'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; interface EntitiesTableWidgetSettings extends TableWidgetSettings { entitiesTitle: string; @@ -217,7 +218,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.ctx.updateWidgetParams(); if (this.displayPagination) { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + const showHidePageSize = this.entitiesWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index cc35c481a8..a5a2cbc12e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -72,6 +72,7 @@ import { DatePipe } from '@angular/common'; import { parseData } from '@home/components/widget/lib/maps/common-maps-utils'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; +import { hidePageSizePixelValue } from '@shared/models/constants'; export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { showTimestamp: boolean; @@ -195,7 +196,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.ctx.$container[0].offsetWidth < 500; + const showHidePageSize = this.timeseriesWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; this.ctx.detectChanges(); diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 03b38345ca..4699a6338c 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -241,6 +241,7 @@ export const contentTypesMap = new Map( ] ); +export const hidePageSizePixelValue = 550; export const customTranslationsPrefix = 'custom.'; export const i18nPrefix = 'i18n'; From 09c8f2bdc7fabe3850bda97fa32ecdd5b91857bd Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 14 Dec 2021 12:40:18 +0200 Subject: [PATCH 11/72] Hide page size on tables refactoring --- .../attribute/attribute-table.component.html | 2 +- .../attribute/attribute-table.component.scss | 1 + .../attribute/attribute-table.component.ts | 18 ++++++++------- .../entity/entities-table.component.html | 2 +- .../entity/entities-table.component.scss | 1 + .../entity/entities-table.component.ts | 22 ++++++++----------- .../relation/relation-table.component.html | 2 +- .../relation/relation-table.component.scss | 1 + .../relation/relation-table.component.ts | 12 +++++----- .../manage-widget-actions.component.html | 5 +++-- .../manage-widget-actions.component.scss | 1 + .../action/manage-widget-actions.component.ts | 16 +++++++------- .../lib/alarms-table-widget.component.html | 2 +- .../lib/alarms-table-widget.component.scss | 1 + .../lib/alarms-table-widget.component.ts | 13 ++++++----- .../lib/entities-table-widget.component.html | 2 +- .../lib/entities-table-widget.component.scss | 1 + .../lib/entities-table-widget.component.ts | 13 ++++++----- .../timeseries-table-widget.component.html | 2 +- .../timeseries-table-widget.component.scss | 1 + .../lib/timeseries-table-widget.component.ts | 13 ++++++----- 21 files changed, 70 insertions(+), 61 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html index 493bfe3e2d..c0612646f3 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss index df6b8e7184..715eb71fbc 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.scss @@ -18,6 +18,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-entity-table { .tb-entity-table-content { width: 100%; diff --git a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts index 1bd4dea53f..f197f995a6 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts @@ -111,6 +111,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI pageLink: PageLink; textSearchMode = false; dataSource: AttributeDatasource; + hidePageSize = false; activeValue = false; dirtyValue = false; @@ -129,10 +130,14 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI aliasController: IAliasController; private widgetDatasource: Datasource; + private widgetResize$: ResizeObserver; + private disableAttributeScopeSelectionValue: boolean; + get disableAttributeScopeSelection(): boolean { return this.disableAttributeScopeSelectionValue; } + @Input() set disableAttributeScopeSelection(value: boolean) { this.disableAttributeScopeSelectionValue = coerceBooleanProperty(value); @@ -168,15 +173,11 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI @Input() entityName: string; - @ViewChild('attributeTableContainer', {static: true}) attributeTableContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; - public hidePageSize = false; - private widgetResize$: ResizeObserver; - constructor(protected store: Store, private attributeService: AttributeService, private telemetryWsService: TelemetryWebsocketService, @@ -190,7 +191,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI private dashboardUtils: DashboardUtilsService, private widgetService: WidgetService, private zone: NgZone, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private elementRef: ElementRef) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'key', direction: Direction.ASC }; @@ -200,13 +202,13 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI ngOnInit() { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.attributeTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.cd.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.attributeTableContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy() { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html index aa86660ebe..a7b15670bb 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.html @@ -32,7 +32,7 @@ -
+
diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss index ac7648d07a..dde91efc26 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss @@ -18,6 +18,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-entity-table { .tb-entity-table-content { width: 100%; diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 1851d5e085..79166bb116 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -43,7 +43,8 @@ import { TranslateService } from '@ngx-translate/core'; import { BaseData, HasId } from '@shared/models/base-data'; import { ActivatedRoute } from '@angular/router'; import { - CellActionDescriptor, CellActionDescriptorType, + CellActionDescriptor, + CellActionDescriptorType, EntityActionTableColumn, EntityColumn, EntityTableColumn, @@ -55,11 +56,7 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models'; import { DialogService } from '@core/services/dialog.service'; import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; -import { - calculateIntervalStartEndTime, - HistoryWindowType, - Timewindow -} from '@shared/models/time/time.models'; +import { calculateIntervalStartEndTime, HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { isDefined, isUndefined } from '@core/utils'; @@ -100,6 +97,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn defaultPageSize = 10; displayPagination = true; + hidePageSize = false; pageSizeOptions; pageLink: PageLink; textSearchMode = false; @@ -111,8 +109,6 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn isDetailsOpen = false; detailsPanelOpened = new EventEmitter(); - @ViewChild('entitiesTableContainer', {static: true}) entitiesTableContainerRef: ElementRef; - @ViewChild('entityTableHeader', {static: true}) entityTableHeaderAnchor: TbAnchorComponent; @ViewChild('searchInput') searchInputField: ElementRef; @@ -125,7 +121,6 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private viewInited = false; private widgetResize$: ResizeObserver; - public hidePageSize = false; constructor(protected store: Store, public route: ActivatedRoute, @@ -134,7 +129,8 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn private dialogService: DialogService, private domSanitizer: DomSanitizer, private cd: ChangeDetectorRef, - private componentFactoryResolver: ComponentFactoryResolver) { + private componentFactoryResolver: ComponentFactoryResolver, + private elementRef: ElementRef) { super(store); } @@ -145,13 +141,13 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn this.init(this.route.snapshot.data.entitiesTableConfig); } this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.entitiesTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.cd.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.entitiesTableContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy() { diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index edf5d1d04d..b5ae8c2d52 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss index dfb94a7383..b5c43647e3 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.scss @@ -18,6 +18,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-entity-table { .tb-entity-table-content { width: 100%; diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts index 8f0b503717..db1fad53b7 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.ts @@ -67,6 +67,7 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn displayedColumns: string[]; direction: EntitySearchDirection; pageLink: PageLink; + hidePageSize = false; textSearchMode = false; dataSource: RelationsDatasource; @@ -77,7 +78,6 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn viewsInited = false; private widgetResize$: ResizeObserver; - public hidePageSize = false; @Input() set active(active: boolean) { @@ -105,7 +105,6 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn } } - @ViewChild('relationTableContainer', {static: true}) relationTableContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -116,7 +115,8 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn public translate: TranslateService, public dialog: MatDialog, private dialogService: DialogService, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private elementRef: ElementRef) { super(store); this.dirtyValue = !this.activeValue; const sortOrder: SortOrder = { property: 'type', direction: Direction.ASC }; @@ -128,13 +128,13 @@ export class RelationTableComponent extends PageComponent implements AfterViewIn ngOnInit() { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.relationTableContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.cd.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.relationTableContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy() { diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html index d0c1db566c..9512c58b4a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
@@ -119,6 +119,7 @@ + [pageSizeOptions]="[10, 20, 30]" + [hidePageSize]="hidePageSize">
diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss index d71ec2e0d1..3406215b33 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.scss @@ -16,6 +16,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-entity-table { .tb-entity-table-content { width: 100%; diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index 5173e00954..f131b84783 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -46,13 +46,12 @@ import { WidgetActionsDatasource } from '@home/components/widget/action/manage-widget-actions.component.models'; import { UtilsService } from '@core/services/utils.service'; -import { WidgetActionDescriptor, WidgetActionSource } from '@shared/models/widget.models'; +import { WidgetActionDescriptor, WidgetActionSource, widgetType } from '@shared/models/widget.models'; import { WidgetActionDialogComponent, WidgetActionDialogData } from '@home/components/widget/action/widget-action-dialog.component'; import { deepClone } from '@core/utils'; -import { widgetType } from '@shared/models/widget.models'; import { ResizeObserver } from '@juggle/resize-observer'; import { hidePageSizePixelValue } from '@shared/models/constants'; @@ -81,14 +80,14 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni displayedColumns: string[]; pageLink: PageLink; textSearchMode = false; + hidePageSize = false; dataSource: WidgetActionsDatasource; viewsInited = false; dirtyValue = false; - public hidePageSize = false; + private widgetResize$: ResizeObserver; - @ViewChild('manageActionWidgetContainer', {static: true}) manageActionWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -101,7 +100,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni private utils: UtilsService, private dialog: MatDialog, private dialogs: DialogService, - private cd: ChangeDetectorRef) { + private cd: ChangeDetectorRef, + private elementRef: ElementRef) { super(store); const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; this.pageLink = new PageLink(10, 0, null, sortOrder); @@ -111,13 +111,13 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni ngOnInit(): void { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.manageActionWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.cd.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.manageActionWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } ngOnDestroy(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html index 872e040bad..a2b0868370 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss index edc52a9c48..15664c4750 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss @@ -16,6 +16,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-table-widget { .table-container { position: relative; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index e12fcfcebd..5cd0f9643d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -16,6 +16,7 @@ import { AfterViewInit, + ChangeDetectorRef, Component, ElementRef, EventEmitter, @@ -154,7 +155,6 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, @Input() ctx: WidgetContext; - @ViewChild('alarmWidgetContainer', {static: true}) alarmWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -167,11 +167,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, public pageLink: AlarmDataPageLink; public sortOrderProperty: string; public textSearchMode = false; + public hidePageSize = false; public columns: Array = []; public displayedColumns: string[] = []; public alarmsDatasource: AlarmsDatasource; public noDataDisplayMessageText: string; - public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -240,7 +240,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private datePipe: DatePipe, private dialog: MatDialog, private dialogService: DialogService, - private alarmService: AlarmService) { + private alarmService: AlarmService, + private cd: ChangeDetectorRef) { super(store); this.pageLink = { page: 0, @@ -263,13 +264,13 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, () => this.pageLink.page = 0 ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.alarmWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.ctx.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.alarmWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index 7f8ae9b9b1..c2d1997945 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss index edc52a9c48..15664c4750 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.scss @@ -16,6 +16,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-table-widget { .table-container { position: relative; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index 32a07cecb8..f4429fb4cc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -16,6 +16,7 @@ import { AfterViewInit, + ChangeDetectorRef, Component, ElementRef, Injector, @@ -131,7 +132,6 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni @Input() ctx: WidgetContext; - @ViewChild('entitiesWidgetContainer', {static: true}) entitiesWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; @@ -143,11 +143,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni public pageLink: EntityDataPageLink; public sortOrderProperty: string; public textSearchMode = false; + public hidePageSize = false; public columns: Array = []; public displayedColumns: string[] = []; public entityDatasource: EntityDatasource; public noDataDisplayMessageText: string; - public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -198,7 +198,8 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni private utils: UtilsService, private datePipe: DatePipe, private translate: TranslateService, - private domSanitizer: DomSanitizer) { + private domSanitizer: DomSanitizer, + private cd: ChangeDetectorRef) { super(store); this.pageLink = { page: 0, @@ -218,13 +219,13 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni this.ctx.updateWidgetParams(); if (this.displayPagination) { this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.entitiesWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.ctx.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.entitiesWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 236d81036c..6e37d38762 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss index 57052d4bf8..eb309c2cf7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.scss @@ -16,6 +16,7 @@ :host { width: 100%; height: 100%; + display: block; .tb-table-widget { mat-footer-row, mat-row { height: 38px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index a5a2cbc12e..07d35a1405 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -16,6 +16,7 @@ import { AfterViewInit, + ChangeDetectorRef, Component, ElementRef, Input, @@ -117,7 +118,6 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI @Input() ctx: WidgetContext; - @ViewChild('timeseriesWidgetContainer', {static: true}) timeseriesWidgetContainerRef: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef; @ViewChildren(MatPaginator) paginators: QueryList; @ViewChildren(MatSort) sorts: QueryList; @@ -127,11 +127,11 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI public enableStickyAction = true; public pageSizeOptions; public textSearchMode = false; + public hidePageSize = false; public textSearch: string = null; public sources: TimeseriesTableSource[]; public sourceIndex: number; public noDataDisplayMessageText: string; - public hidePageSize = false; private setCellButtonAction: boolean; private cellContentCache: Array = []; @@ -172,7 +172,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI private utils: UtilsService, private translate: TranslateService, private domSanitizer: DomSanitizer, - private datePipe: DatePipe) { + private datePipe: DatePipe, + private cd: ChangeDetectorRef) { super(store); } @@ -196,13 +197,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI } ); this.widgetResize$ = new ResizeObserver(() => { - const showHidePageSize = this.timeseriesWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.ctx.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.timeseriesWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } } From 9c44530ab92d8490debf5cc856745b8539f2d656 Mon Sep 17 00:00:00 2001 From: dlandiak Date: Tue, 21 Dec 2021 15:19:30 +0200 Subject: [PATCH 12/72] added boolean param to the mqtt node to allow adding a suffix for client id param --- .../main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java | 3 ++- .../thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 4f4dc7ab60..ac5fa9e7ea 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -104,7 +104,8 @@ public class TbMqttNode implements TbNode { protected MqttClient initClient(TbContext ctx) throws Exception { MqttClientConfig config = new MqttClientConfig(getSslContext()); if (!StringUtils.isEmpty(this.mqttNodeConfiguration.getClientId())) { - config.setClientId(this.mqttNodeConfiguration.getClientId()); + config.setClientId(this.mqttNodeConfiguration.isAppendClientIdSuffix() ? + this.mqttNodeConfiguration.getClientId() + "_" + ctx.getServiceId() : this.mqttNodeConfiguration.getClientId()); } config.setCleanSession(this.mqttNodeConfiguration.isCleanSession()); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java index 9ac797a98a..46c81f3ddd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeConfiguration.java @@ -28,6 +28,7 @@ public class TbMqttNodeConfiguration implements NodeConfiguration Date: Wed, 22 Dec 2021 18:12:17 +0200 Subject: [PATCH 13/72] UI: Update and refactoring --- .../server/controller/RpcV2Controller.java | 2 +- ui-ngx/src/app/core/api/widget-api.models.ts | 5 +- .../src/app/core/api/widget-subscription.ts | 41 ------ ui-ngx/src/app/core/http/device.service.ts | 10 +- ui-ngx/src/app/modules/common/modules-map.ts | 2 + .../rpc/persistent-add-dialog.component.html | 2 +- .../rpc/persistent-add-dialog.component.scss | 5 +- .../rpc/persistent-add-dialog.component.ts | 32 ++--- .../persistent-details-dialog.component.html | 2 +- .../persistent-details-dialog.component.scss | 5 +- .../persistent-details-dialog.component.ts | 61 +++------ .../persistent-filter-panel.component.html | 2 +- .../persistent-filter-panel.component.scss | 1 + .../rpc/persistent-filter-panel.component.ts | 16 +-- .../lib/rpc/persistent-table.component.html | 2 +- .../lib/rpc/persistent-table.component.scss | 1 + .../lib/rpc/persistent-table.component.ts | 129 ++++++++++++++---- .../json-object-view.component.html | 2 +- .../components/json-object-view.component.ts | 2 +- ui-ngx/src/app/shared/models/rpc.models.ts | 1 - ui-ngx/src/app/shared/shared.module.ts | 2 +- .../assets/locale/locale.constant-uk_UA.json | 2 +- 22 files changed, 171 insertions(+), 156 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java index 3f85841b3c..81e9a9e394 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java +++ b/application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java @@ -188,7 +188,7 @@ public class RpcV2Controller extends AbstractRpcController { @RequestParam(required = false) String sortOrder) throws ThingsboardException { checkParameter("DeviceId", strDeviceId); try { - if (rpcStatus.equals(RpcStatus.DELETED)) { + if (rpcStatus != null && rpcStatus.equals(RpcStatus.DELETED)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "RpcStatus: DELETED"); } diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index c0bab56e4e..7559692561 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -55,8 +55,7 @@ import { TranslateService } from '@ngx-translate/core'; import { AlarmDataService } from '@core/api/alarm-data.service'; import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; import { PopoverPlacement } from '@shared/components/popover.models'; -import { PageLink } from '@shared/models/page/page-link'; -import { PersistentRpc, RpcStatus } from '@shared/models/rpc.models'; +import { PersistentRpc } from '@shared/models/rpc.models'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -322,8 +321,6 @@ export interface IWidgetSubscription { persistentPollingInterval?: number, retries?: number, additionalInfo?: any, requestUUID?: string): Observable; clearRpcError(): void; - subscribeForPersistentRequests(pageLink: PageLink, keyFileter: RpcStatus): Observable; - subscribe(): void; subscribeAllForPaginatedData(pageLink: EntityDataPageLink, diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 1634bb7b87..0f864335aa 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -70,7 +70,6 @@ import { import { distinct, filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { AlarmDataListener } from '@core/api/alarm-data.service'; import { RpcStatus } from '@shared/models/rpc.models'; -import { PageLink } from '@shared/models/page/page-link'; const moment = moment_; @@ -781,46 +780,6 @@ export class WidgetSubscription implements IWidgetSubscription { } } - subscribeForPersistentRequests(pageLink: PageLink, keyFilter: RpcStatus): Observable { - if (!this.rpcEnabled) { - return throwError(new Error('Rpc disabled!')); - } else if (!this.targetDeviceId) { - return throwError(new Error('Target device is not set!')); - } - const rpcSubject: Subject = new Subject(); - - this.ctx.deviceService.getPersistedRpcRequests(this.targetDeviceId, pageLink, keyFilter).subscribe( - (responseBody) => { - rpcSubject.next(responseBody); - rpcSubject.complete(); - }, - (rejection: HttpErrorResponse) => { - const index = this.executingSubjects.indexOf(rpcSubject); - if (index >= 0) { - this.executingSubjects.splice( index, 1 ); - } - this.executingRpcRequest = this.executingSubjects.length > 0; - this.callbacks.rpcStateChanged(this); - if (!this.executingRpcRequest || rejection.status === 504) { - this.rpcRejection = rejection; - if (rejection.status === 504) { - this.rpcErrorText = 'Request Timeout.'; - } else { - this.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; - const error = this.extractRejectionErrorText(rejection); - if (error) { - this.rpcErrorText += '
'; - this.rpcErrorText += error; - } - } - this.callbacks.onRpcFailed(this); - } - rpcSubject.error(rejection); - } - ); - return rpcSubject.asObservable(); - } - private extractRejectionErrorText(rejection: HttpErrorResponse) { let error = null; if (rejection.error) { diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index 9b5119d317..88f0cacb46 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -148,10 +148,12 @@ export class DeviceService { } public getPersistedRpcRequests(deviceId: string, pageLink: PageLink, - keyFilter: RpcStatus, config?: RequestConfig): Observable> { - const rpcStatus = keyFilter ? '&rpcStatus=' + keyFilter : ''; - return this.http.get>(`/api/rpc/persistent/device/${deviceId}${pageLink.toQuery()}${rpcStatus}`, - defaultHttpOptionsFromConfig(config)); + rpcStatus?: RpcStatus, config?: RequestConfig): Observable> { + let url = `/api/rpc/persistent/device/${deviceId}${pageLink.toQuery()}`; + if (rpcStatus && rpcStatus.length) { + url += `&rpcStatus=${rpcStatus}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } public findByQuery(query: DeviceSearchQuery, diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 3cd4139ec4..147492ec29 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -139,6 +139,7 @@ import * as QueueTypeListComponent from '@shared/components/queue/queue-type-lis import * as RelationTypeAutocompleteComponent from '@shared/components/relation/relation-type-autocomplete.component'; import * as SocialSharePanelComponent from '@shared/components/socialshare-panel.component'; import * as JsonObjectEditComponent from '@shared/components/json-object-edit.component'; +import * as JsonObjectViewComponent from '@shared/components/json-object-view.component'; import * as JsonContentComponent from '@shared/components/json-content.component'; import * as JsFuncComponent from '@shared/components/js-func.component'; import * as FabToolbarComponent from '@shared/components/fab-toolbar.component'; @@ -420,6 +421,7 @@ class ModulesMap implements IModulesMap { '@shared/components/relation/relation-type-autocomplete.component': RelationTypeAutocompleteComponent, '@shared/components/socialshare-panel.component': SocialSharePanelComponent, '@shared/components/json-object-edit.component': JsonObjectEditComponent, + '@shared/components/json-object-view.component': JsonObjectViewComponent, '@shared/components/json-content.component': JsonContentComponent, '@shared/components/js-func.component': JsFuncComponent, '@shared/components/fab-toolbar.component': FabToolbarComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html index 7e0ae3056f..1e74657946 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.html @@ -32,7 +32,7 @@
- {{ 'widgets.persistent-table.message-types.' + persistentFormGroup.get('oneWayElseTwoWay').value | translate }} + {{ rpcMessageTypeText }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss index e450fa4a09..3c9d0b43a2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.scss @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { - .add-dialog ::ng-deep { + +:host ::ng-deep { + .add-dialog { .params-json-editor, .additional-json-editor { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts index c4e87c5e6b..8c4f47a904 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-add-dialog.component.ts @@ -22,6 +22,7 @@ import { Router } from '@angular/router'; import { MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { RequestData } from '@shared/models/rpc.models'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-persistent-add-dialog', @@ -32,15 +33,15 @@ import { RequestData } from '@shared/models/rpc.models'; export class PersistentAddDialogComponent extends DialogComponent implements OnInit { public persistentFormGroup: FormGroup; + public rpcMessageTypeText: string; - private requestData: RequestData = { - persistentUpdated: false - }; + private requestData: RequestData = null; constructor(protected store: Store, protected router: Router, public dialogRef: MatDialogRef, - private fb: FormBuilder) { + private fb: FormBuilder, + private translate: TranslateService) { super(store, router, dialogRef); this.persistentFormGroup = this.fb.group( @@ -48,27 +49,24 @@ export class PersistentAddDialogComponent extends DialogComponent { + this.rpcMessageTypeText = this.translate.instant(`widgets.persistent-table.message-types.${this.persistentFormGroup.get('oneWayElseTwoWay').value}`); + } + ); } close(): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html index 3a9451db67..440b142db2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.html @@ -48,7 +48,7 @@ widgets.persistent-table.status + [ngStyle]="{ fontWeight: 'bold', color: rpcStatusColorsMap.get(data.persistentRequest.status) }"> widgets.persistent-table.method diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss index ec456d5d9e..2a6a06c2a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.scss @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { - .rpc-dialog ::ng-deep { +:host ::ng-deep { + .rpc-dialog { .mat-expansion-panel-body { padding-bottom: 0 !important; } @@ -23,6 +23,7 @@ margin: 0 0 16px 0; } } + .tb-audit-log-response-data { width: 100%; min-width: 400px; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts index 552175b905..8e7e755e86 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-details-dialog.component.ts @@ -24,13 +24,7 @@ import { TranslateService } from '@ngx-translate/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { FormBuilder, FormGroup } from '@angular/forms'; import { DeviceService } from '@core/http/device.service'; -import { - PersistentRpc, - rpcStatusColors, - RpcStatus, - rpcStatusTranslation -} from '@shared/models/rpc.models'; -import { isDefinedAndNotNull } from '@core/utils'; +import { PersistentRpc, RpcStatus, rpcStatusColors, rpcStatusTranslation } from '@shared/models/rpc.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { DialogService } from '@core/services/dialog.service'; @@ -82,7 +76,7 @@ export class PersistentDetailsDialogComponent extends DialogComponent { if (res) { - if (res) { - this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { - this.persistentUpdated = true; - this.close(); - }); - } + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.persistentUpdated = true; + this.close(); + }); } }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html index ffbcab5749..d5eca15a5f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.html @@ -19,7 +19,7 @@ widgets.persistent-table.rpc-status-list + placeholder="{{ rpcSearchPlaceholder }}"> {{ 'widgets.persistent-table.rpc-search-status-all' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss index 63f832d78c..2ad57a3f7f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.scss @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + :host { width: 100%; height: 100%; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts index d60c80ac43..c8a72ffc3b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-filter-panel.component.ts @@ -18,6 +18,7 @@ import { Component, Inject, InjectionToken } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { OverlayRef } from '@angular/cdk/overlay'; import { RpcStatus, rpcStatusTranslation } from '@shared/models/rpc.models'; +import { TranslateService } from '@ngx-translate/core'; export const PERSISTENT_FILTER_PANEL_DATA = new InjectionToken('AlarmFilterPanelData'); @@ -35,26 +36,21 @@ export class PersistentFilterPanelComponent { public persistentFilterFormGroup: FormGroup; public result: PersistentFilterPanelData; public rpcSearchStatusTranslationMap = rpcStatusTranslation; + public rpcSearchPlaceholder: string; - public persistentSearchStatuses = [ - RpcStatus.QUEUED, - RpcStatus.SENT, - RpcStatus.DELIVERED, - RpcStatus.SUCCESSFUL, - RpcStatus.TIMEOUT, - RpcStatus.EXPIRED, - RpcStatus.FAILED - ]; + public persistentSearchStatuses = Object.keys(RpcStatus); constructor(@Inject(PERSISTENT_FILTER_PANEL_DATA) public data: PersistentFilterPanelData, public overlayRef: OverlayRef, - private fb: FormBuilder) { + private fb: FormBuilder, + private translate: TranslateService) { this.persistentFilterFormGroup = this.fb.group( { rpcStatus: this.data.rpcStatus } ); + this.rpcSearchPlaceholder = this.translate.instant('widgets.persistent-table.any-status'); } update() { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html index fd86fd92cc..e786efec7b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
{ - const showHidePageSize = this.persistentWidgetContainerRef.nativeElement.offsetWidth < hidePageSizePixelValue; + const showHidePageSize = this.elementRef.nativeElement.offsetWidth < hidePageSizePixelValue; if (showHidePageSize !== this.hidePageSize) { this.hidePageSize = showHidePageSize; - this.ctx.detectChanges(); + this.cd.markForCheck(); } }); - this.widgetResize$.observe(this.persistentWidgetContainerRef.nativeElement); + this.widgetResize$.observe(this.elementRef.nativeElement); } } @@ -250,7 +257,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { this.displayedColumns.push('actions'); } - this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription); + this.persistentDatasource = new PersistentDatasource(this.translate, this.subscription, this.ctx); const cssString = constructTableCssString(this.widgetConfig); const cssParser = new cssjs(); @@ -300,11 +307,9 @@ export class PersistentTableComponent extends PageComponent implements OnInit { this.translate.instant('action.yes') ).subscribe((res) => { if (res) { - if (res) { - this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { - this.reloadPersistentRequests(); - }); - } + this.deviceService.deletePersistedRpc(persistentRpc.id.id).subscribe(() => { + this.reloadPersistentRequests(); + }); } }); } @@ -345,7 +350,7 @@ export class PersistentTableComponent extends PageComponent implements OnInit { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] }).afterClosed().subscribe( (requestData) => { - if (requestData.persistentUpdated) { + if (requestData) { this.sendRequests(requestData); } } @@ -440,11 +445,16 @@ class PersistentDatasource implements DataSource { private persistentSubject = new BehaviorSubject([]); private pageDataSubject = new BehaviorSubject>(emptyPageData()); + private rpcErrorText: string; + private executingSubjects: Array>; + private executingRpcRequest = false; + public dataLoading = true; public pageData$ = this.pageDataSubject.asObservable(); constructor(private translate: TranslateService, - private subscription: IWidgetSubscription) { + private subscription: IWidgetSubscription, + private ctx: WidgetContext) { } connect(collectionViewer: CollectionViewer): Observable> { @@ -462,11 +472,11 @@ class PersistentDatasource implements DataSource { this.pageDataSubject.next(pageData); } - loadPersistent(pageLink: PageLink, keyFilter: RpcStatus) { + loadPersistent(pageLink: PageLink, rpcStatusFilter: RpcStatus) { this.dataLoading = true; const result = new ReplaySubject>(); - this.fetchEntities(pageLink, keyFilter).pipe( + this.fetchEntities(pageLink, rpcStatusFilter).pipe( catchError(() => of(emptyPageData())), ).subscribe( (pageData) => { @@ -479,8 +489,81 @@ class PersistentDatasource implements DataSource { return result; } - fetchEntities(pageLink: PageLink, keyFilter: RpcStatus): Observable> { - return this.subscription.subscribeForPersistentRequests(pageLink, keyFilter); + fetchEntities(pageLink: PageLink, rpcStatusFilter: RpcStatus): Observable> { + if (!this.subscription.rpcEnabled) { + return throwError(new Error('Rpc disabled!')); + } else if (!this.subscription.targetDeviceId) { + return throwError(new Error('Target device is not set!')); + } + const rpcSubject: Subject = new Subject(); + + this.ctx.deviceService.getPersistedRpcRequests(this.subscription.targetDeviceId, pageLink, rpcStatusFilter).subscribe( + (responseBody) => { + rpcSubject.next(responseBody); + rpcSubject.complete(); + }, + (rejection: HttpErrorResponse) => { + this.rpcErrorText = null; + this.executingSubjects = []; + + const index = this.executingSubjects.indexOf(rpcSubject); + if (index >= 0) { + this.executingSubjects.splice(index, 1); + } + this.executingRpcRequest = this.executingSubjects.length > 0; + this.subscription.options.callbacks.rpcStateChanged(this.subscription); + if (!this.executingRpcRequest || rejection.status === 504) { + this.subscription.rpcRejection = rejection; + if (rejection.status === 504) { + this.subscription.rpcErrorText = 'Request Timeout.'; + } else { + this.subscription.rpcErrorText = 'Error : ' + rejection.status + ' - ' + rejection.statusText; + const error = this.extractRejectionErrorText(rejection); + if (error) { + this.subscription.rpcErrorText += '
'; + this.subscription.rpcErrorText += error.message || ''; + } + } + this.subscription.callbacks.onRpcFailed(this.subscription); + } + rpcSubject.error(rejection); + } + ); + return rpcSubject.asObservable(); + } + + extractRejectionErrorText(rejection: HttpErrorResponse) { + let error = null; + if (rejection.error) { + error = rejection.error; + try { + error = rejection.error ? JSON.parse(rejection.error) : null; + } catch (e) {} + } + if (error && !error.message) { + error = this.prepareMessageFromData(error); + } else if (error && error.message) { + error = error.message; + } + return error; + } + + prepareMessageFromData(data) { + if (typeof data === 'object' && data.constructor === ArrayBuffer) { + const msg = String.fromCharCode.apply(null, new Uint8Array(data)); + try { + const msgObj = JSON.parse(msg); + if (msgObj.message) { + return msgObj.message; + } else { + return msg; + } + } catch (e) { + return msg; + } + } else { + return data; + } } isEmpty(): Observable { diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.html b/ui-ngx/src/app/shared/components/json-object-view.component.html index 379c557b44..b3fbcef2de 100644 --- a/ui-ngx/src/app/shared/components/json-object-view.component.html +++ b/ui-ngx/src/app/shared/components/json-object-view.component.html @@ -18,5 +18,5 @@
-
+
diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.ts b/ui-ngx/src/app/shared/components/json-object-view.component.ts index 5ce00b0189..b85988c3d3 100644 --- a/ui-ngx/src/app/shared/components/json-object-view.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-view.component.ts @@ -153,7 +153,7 @@ export class JsonObjectViewComponent implements OnInit { }, 2); } } catch (e) { - // + console.error(e); } if (this.jsonViewer) { this.jsonViewer.setValue(this.contentValue ? this.contentValue : '', -1); diff --git a/ui-ngx/src/app/shared/models/rpc.models.ts b/ui-ngx/src/app/shared/models/rpc.models.ts index 61a94a401e..4512b590a0 100644 --- a/ui-ngx/src/app/shared/models/rpc.models.ts +++ b/ui-ngx/src/app/shared/models/rpc.models.ts @@ -79,7 +79,6 @@ export interface PersistentRpcData extends PersistentRpc { } export interface RequestData { - persistentUpdated: boolean; method?: string; oneWayElseTwoWay?: boolean; persistentPollingInterval?: number; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 71a4d344bf..bb16964806 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -150,7 +150,7 @@ import { TogglePasswordComponent } from '@shared/components/button/toggle-passwo import { HelpPopupComponent } from '@shared/components/help-popup.component'; import { TbPopoverComponent, TbPopoverDirective } from '@shared/components/popover.component'; import { TbStringTemplateOutletDirective } from '@shared/components/directives/sring-template-outlet.directive'; -import { TbComponentOutletDirective} from '@shared/components/directives/component-outlet.directive'; +import { TbComponentOutletDirective } from '@shared/components/directives/component-outlet.directive'; import { HelpMarkdownComponent } from '@shared/components/help-markdown.component'; import { MarkedOptionsService } from '@shared/components/marked-options.service'; import { TbPopoverService } from '@shared/components/popover.service'; diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index 3942a5193c..943598baef 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -2395,7 +2395,7 @@ }, "rpc-search-status-all": "ВСІ", "message-types": { - "false": "Двусторонній", + "false": "Двосторонній", "true": "Односторонній" } } From c20f6e4021a29bc0f3f6a38ac19392cbbdf21258 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 23 Dec 2021 11:20:22 +0200 Subject: [PATCH 14/72] UI: Add in map widgets new group setting - Editor setting --- ui-ngx/package.json | 2 +- .../components/widget/lib/maps/leaflet-map.ts | 52 +++++---- .../components/widget/lib/maps/map-models.ts | 19 +++- .../components/widget/lib/maps/map-widget2.ts | 3 + .../components/widget/lib/maps/markers.ts | 29 ++--- .../components/widget/lib/maps/polygon.ts | 7 +- .../components/widget/lib/maps/schemes.ts | 58 ++++++++++ ui-ngx/yarn.lock | 107 ++++++++---------- 8 files changed, 178 insertions(+), 99 deletions(-) diff --git a/ui-ngx/package.json b/ui-ngx/package.json index adec5e815a..1153524a26 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -28,7 +28,7 @@ "@date-io/date-fns": "^2.11.0", "@flowjs/flow.js": "^2.14.1", "@flowjs/ngx-flow": "~0.4.6", - "@geoman-io/leaflet-geoman-free": "^2.11.3", + "@geoman-io/leaflet-geoman-free": "^2.11.4", "@juggle/resize-observer": "^3.3.1", "@mat-datetimepicker/core": "~7.0.1", "@material-ui/core": "^4.12.3", diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 78f459990e..9d661ae8d2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -24,7 +24,9 @@ import { defaultSettings, FormattedData, MapSettings, - MarkerSettings, MarkerIconInfo, MarkerImageInfo, + MarkerIconInfo, + MarkerImageInfo, + MarkerSettings, PolygonSettings, PolylineSettings, ReplaceInfo, @@ -182,7 +184,7 @@ export default abstract class LeafletMap { addEditControl() { // Customize edit marker - if (this.options.draggableMarker) { + if (this.options.draggableMarker && !this.options.hideDrawControlButton) { const actions = [{ text: L.PM.Utils.getTranslation('actions.cancel'), onClick: () => this.toggleDrawMode('tbMarker') @@ -197,7 +199,7 @@ export default abstract class LeafletMap { } // Customize edit polygon - if (this.editPolygons) { + if (this.editPolygons && !this.options.hideDrawControlButton) { const rectangleActions = [ { text: L.PM.Utils.getTranslation('actions.cancel'), @@ -231,18 +233,27 @@ export default abstract class LeafletMap { const translateService = this.ctx.$injector.get(TranslateService); this.map.pm.setLang('en', translateService.instant('widgets.maps'), 'en'); - this.map.pm.addControls({ - position: 'topleft', - drawMarker: false, - drawCircle: false, - drawCircleMarker: false, - drawRectangle: false, - drawPolyline: false, - drawPolygon: false, - editMode: this.editPolygons, - cutPolygon: this.editPolygons, - rotateMode: this.editPolygons - }); + if (!this.options.hideAllControlButton) { + this.map.pm.addControls({ + position: 'topleft', + drawControls: !this.options.hideDrawControlButton, + drawMarker: false, + drawCircle: false, + drawCircleMarker: false, + drawRectangle: false, + drawPolyline: false, + drawPolygon: false, + dragMode: !this.options.hideEditControlButton, + editMode: this.editPolygons && !this.options.hideEditControlButton, + cutPolygon: this.editPolygons && !this.options.hideEditControlButton, + removalMode: !this.options.hideRemoveControlButton, + rotateMode: this.editPolygons && !this.options.hideEditControlButton + }); + } + + if (this.options.initDragMode) { + this.map.pm.enableGlobalDragMode(); + } this.map.on('pm:create', (e) => { if (e.shape === 'tbMarker') { @@ -344,6 +355,7 @@ export default abstract class LeafletMap { } if (this.options.draggableMarker || this.editPolygons) { map.pm.setGlobalOptions({ snappable: false } as L.PM.GlobalOptions); + map.pm.applyGlobalOptions(); this.addEditControl(); } else { this.map.pm.disableDraw(); @@ -555,7 +567,7 @@ export default abstract class LeafletMap { updatedMarkers.push(m); } } else { - m = this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings, updateBounds, callback); + m = this.createMarker(data.entityName, data, markersData, this.options, updateBounds, callback); if (m) { createdMarkers.push(m); } @@ -569,7 +581,7 @@ export default abstract class LeafletMap { } }); this.markersData = markersData; - if ((this.options as MarkerSettings).useClusterMarkers) { + if (this.options.useClusterMarkers) { if (createdMarkers.length) { this.markersCluster.addLayers(createdMarkers.map(marker => marker.leafletMarker)); } @@ -589,7 +601,7 @@ export default abstract class LeafletMap { this.saveLocation(data, this.convertToCustomFormat(e.target._latlng)).subscribe(); } - private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, + private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings, updateBounds = true, callback?): Marker { const newMarker = new Marker(this, this.convertPosition(data), settings, data, dataSources, this.dragMarker); if (callback) { @@ -597,7 +609,7 @@ export default abstract class LeafletMap { callback(data, true); }); } - if (this.bounds && updateBounds && !(this.options as MarkerSettings).useClusterMarkers) { + if (this.bounds && updateBounds && !this.options.useClusterMarkers) { this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); } this.markers.set(key, newMarker); @@ -786,7 +798,7 @@ export default abstract class LeafletMap { this.saveLocation(data, this.convertPolygonToCustomFormat(coordinates)).subscribe(() => {}); } - createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: PolygonSettings, updateBounds = true) { + createPolygon(polyData: FormattedData, dataSources: FormattedData[], settings: UnitedMapSettings, updateBounds = true) { const polygon = new Polygon(this.map, polyData, dataSources, settings, this.dragPolygonVertex); if (updateBounds) { const bounds = polygon.leafletPoly.getBounds(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index e65479f588..de94bdc156 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -192,6 +192,15 @@ export type PolylineSettings = { strokeWeightFunction: GenericFunction; }; +export interface EditorSettings { + snappable: boolean; + initDragMode: boolean; + hideAllControlButton: boolean; + hideDrawControlButton: boolean; + hideEditControlButton: boolean; + hideRemoveControlButton: boolean; +} + export interface HistorySelectSettings { buttonColor: string; } @@ -231,7 +240,7 @@ export interface TripAnimationSettings extends PolygonSettings { export type actionsHandler = ($event: Event, datasource: Datasource) => void; -export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings; +export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSettings & EditorSettings; export const defaultSettings: any = { xPosKeyName: 'xPos', @@ -271,7 +280,13 @@ export const defaultSettings: any = { draggableMarker: false, editablePolygon: false, fitMapBounds: true, - mapPageSize: DEFAULT_MAP_PAGE_SIZE + mapPageSize: DEFAULT_MAP_PAGE_SIZE, + snappable: false, + initDragMode: false, + hideAllControlButton: false, + hideDrawControlButton: false, + hideEditControlButton: false, + hideRemoveControlButton: false }; export const hereProviders = [ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index 565887b160..bc042e4fc0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -18,6 +18,7 @@ import { defaultSettings, FormattedData, hereProviders, MapProviders, UnitedMapS import LeafletMap from './leaflet-map'; import { commonMapSettingsSchema, + editorSettingSchema, mapPolygonSchema, markerClusteringSettingsSchema, markerClusteringSettingsSchemaLeaflet, @@ -115,6 +116,8 @@ export class MapWidgetController implements MapWidgetInterface { `model.useClusterMarkers === true && model.provider !== "image-map"`)]); addToSchema(schema, clusteringSchema); addGroupInfo(schema, 'Markers Clustering Settings'); + addToSchema(schema, addCondition(editorSettingSchema, '(model.editablePolygon === true || model.draggableMarker === true)')); + addGroupInfo(schema, 'Editor settings'); } return schema; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 85020b7f33..741cf22719 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -14,19 +14,17 @@ /// limitations under the License. /// -import L, { Icon, LeafletMouseEvent } from 'leaflet'; -import { FormattedData, MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, MarkerSettings } from './map-models'; +import L, { LeafletMouseEvent } from 'leaflet'; import { - bindPopupActions, - createTooltip, -} from './maps-utils'; -import { - aspectCache, - fillPattern, - parseWithTranslation, - processPattern, - safeExecute -} from './common-maps-utils'; + FormattedData, + MarkerIconInfo, + MarkerIconReadyFunction, + MarkerImageInfo, + MarkerSettings, + UnitedMapSettings +} from './map-models'; +import { bindPopupActions, createTooltip, } from './maps-utils'; +import { aspectCache, fillPattern, parseWithTranslation, processPattern, safeExecute } from './common-maps-utils'; import tinycolor from 'tinycolor2'; import { isDefined, isDefinedAndNotNull } from '@core/utils'; import LeafletMap from './leaflet-map'; @@ -40,10 +38,13 @@ export class Marker { data: FormattedData; dataSources: FormattedData[]; - constructor(private map: LeafletMap, private location: L.LatLng, public settings: MarkerSettings, + constructor(private map: LeafletMap, private location: L.LatLng, public settings: UnitedMapSettings, data?: FormattedData, dataSources?, onDragendListener?) { this.setDataSources(data, dataSources); - this.leafletMarker = L.marker(location, {pmIgnore: !settings.draggableMarker}); + this.leafletMarker = L.marker(location, { + pmIgnore: !settings.draggableMarker, + snapIgnore: !settings.snappable + }); this.markerOffset = [ isDefined(settings.markerOffsetX) ? settings.markerOffsetX : 0.5, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts index 908952e7d2..de55d9b1b0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -23,7 +23,7 @@ import { processPattern, safeExecute } from './common-maps-utils'; -import { FormattedData, MarkerSettings, PolygonSettings } from './map-models'; +import { FormattedData, PolygonSettings, UnitedMapSettings } from './map-models'; export class Polygon { @@ -32,7 +32,7 @@ export class Polygon { data: FormattedData; dataSources: FormattedData[]; - constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: PolygonSettings, + constructor(public map, data: FormattedData, dataSources: FormattedData[], private settings: UnitedMapSettings, private onDragendListener?) { this.dataSources = dataSources; this.data = data; @@ -47,7 +47,8 @@ export class Polygon { weight: settings.polygonStrokeWeight, fillOpacity: settings.polygonOpacity, opacity: settings.polygonStrokeOpacity, - pmIgnore: !settings.editablePolygon + pmIgnore: !settings.editablePolygon, + snapIgnore: !settings.snappable }).addTo(this.map); this.updateLabel(settings); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts index 2924ff688e..1c383b75e0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts @@ -1311,3 +1311,61 @@ export const providerSets: { [key: string]: IProvider } = { name: 'image-map' } }; + +export const editorSettingSchema = + { + schema: { + title: 'Editor settings', + type: 'object', + properties: { + snappable: { + title: 'Enable snapping to other vertices for precision drawing', + type: 'boolean', + default: false + }, + initDragMode: { + title: 'Initialize map in draggable mode', + type: 'boolean', + default: false + }, + hideAllControlButton: { + title: 'Hide all button', + type: 'boolean', + default: false + }, + hideDrawControlButton: { + title: 'Hide draw buttons', + type: 'boolean', + default: false + }, + hideEditControlButton: { + title: 'Hide edit buttons', + type: 'boolean', + default: false + }, + hideRemoveControlButton: { + title: 'Hide remove button', + type: 'boolean', + default: false + }, + }, + required: [] + }, + form: [ + 'snappable', + 'initDragMode', + 'hideAllControlButton', + { + key: 'hideDrawControlButton', + condition: 'model.hideAllControlButton == false' + }, + { + key: 'hideEditControlButton', + condition: 'model.hideAllControlButton == false' + }, + { + key: 'hideRemoveControlButton', + condition: 'model.hideAllControlButton == false' + } + ] + }; diff --git a/ui-ngx/yarn.lock b/ui-ngx/yarn.lock index f1e5a9abc9..25ca2b4b64 100644 --- a/ui-ngx/yarn.lock +++ b/ui-ngx/yarn.lock @@ -1308,15 +1308,15 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== -"@geoman-io/leaflet-geoman-free@^2.11.3": - version "2.11.3" - resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.11.3.tgz#480164ab76c2b2a885003e0c111284f3c3160a36" - integrity sha512-LsiurEgKEHBcTnAVl8h7EfS5V/doCuxePzPE9SnfrhtJBN7IzP6UwkEo35Agwko+BnIuw/o2bE4F7irvKwQzjw== - dependencies: - "@turf/boolean-contains" "6.3.0" - "@turf/kinks" "6.3.0" - "@turf/line-intersect" "6.3.0" - "@turf/line-split" "6.3.0" +"@geoman-io/leaflet-geoman-free@^2.11.4": + version "2.11.4" + resolved "https://registry.yarnpkg.com/@geoman-io/leaflet-geoman-free/-/leaflet-geoman-free-2.11.4.tgz#4a43fa8d3d5d2bca751135b775c19c6cc0063699" + integrity sha512-uWfgaGDhrtoCMHdHi2oNVKb8WXFMQvyNnan1sS/+Yn5jMPuhijWFyAjy0G5kTCamXhGXg4vUvlEpiRSrBwewKg== + dependencies: + "@turf/boolean-contains" "^6.5.0" + "@turf/kinks" "^6.5.0" + "@turf/line-intersect" "^6.5.0" + "@turf/line-split" "^6.5.0" lodash "4.17.21" polygon-clipping "0.15.3" @@ -1601,7 +1601,7 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== -"@turf/bbox@*", "@turf/bbox@^6.3.0": +"@turf/bbox@*", "@turf/bbox@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.5.0.tgz#bec30a744019eae420dac9ea46fb75caa44d8dc5" integrity sha512-RBbLaao5hXTYyyg577iuMtDB8ehxMlUqHEJiMs8jT1GHkFhr6sYre3lmLsPeYEi/ZKj5TP5tt7fkzNdJ4GIVyw== @@ -1617,18 +1617,18 @@ "@turf/helpers" "^6.5.0" "@turf/invariant" "^6.5.0" -"@turf/boolean-contains@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@turf/boolean-contains/-/boolean-contains-6.3.0.tgz#fe4fc359e408c8c3c89e7fb159c9d31fde48779a" - integrity sha512-1MW7B5G5tIu1lnAv3pXyFzl75wfBYnbA2GhwHDb4okIXMhloy/r5uIqAZHo0fOXykKVJS/gIfA/MioKIftoTug== +"@turf/boolean-contains@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/boolean-contains/-/boolean-contains-6.5.0.tgz#f802e7432fb53109242d5bf57393ef2f53849bbf" + integrity sha512-4m8cJpbw+YQcKVGi8y0cHhBUnYT+QRfx6wzM4GI1IdtYH3p4oh/DOBJKrepQyiDzFDaNIjxuWXBh0ai1zVwOQQ== dependencies: - "@turf/bbox" "^6.3.0" - "@turf/boolean-point-in-polygon" "^6.3.0" - "@turf/boolean-point-on-line" "^6.3.0" - "@turf/helpers" "^6.3.0" - "@turf/invariant" "^6.3.0" + "@turf/bbox" "^6.5.0" + "@turf/boolean-point-in-polygon" "^6.5.0" + "@turf/boolean-point-on-line" "^6.5.0" + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" -"@turf/boolean-point-in-polygon@^6.3.0": +"@turf/boolean-point-in-polygon@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz#6d2e9c89de4cd2e4365004c1e51490b7795a63cf" integrity sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A== @@ -1636,7 +1636,7 @@ "@turf/helpers" "^6.5.0" "@turf/invariant" "^6.5.0" -"@turf/boolean-point-on-line@^6.3.0": +"@turf/boolean-point-on-line@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/boolean-point-on-line/-/boolean-point-on-line-6.5.0.tgz#a8efa7bad88760676f395afb9980746bc5b376e9" integrity sha512-A1BbuQ0LceLHvq7F/P7w3QvfpmZqbmViIUPHdNLvZimFNLo4e6IQunmzbe+8aSStH9QRZm3VOflyvNeXvvpZEQ== @@ -1660,37 +1660,26 @@ "@turf/helpers" "^6.5.0" "@turf/invariant" "^6.5.0" -"@turf/helpers@6.x", "@turf/helpers@^6.3.0", "@turf/helpers@^6.5.0": +"@turf/helpers@6.x", "@turf/helpers@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.5.0.tgz#f79af094bd6b8ce7ed2bd3e089a8493ee6cae82e" integrity sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw== -"@turf/invariant@^6.3.0", "@turf/invariant@^6.5.0": +"@turf/invariant@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.5.0.tgz#970afc988023e39c7ccab2341bd06979ddc7463f" integrity sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg== dependencies: "@turf/helpers" "^6.5.0" -"@turf/kinks@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@turf/kinks/-/kinks-6.3.0.tgz#a16b4ccc5a5aae139d43e36271e0a0494fdb4bf7" - integrity sha512-BLWvbl2/fa4SeJzVMbleT6Vo1cmzwmzRfxL2xxMei2jmf6JSvqDoMJFwIHGXrLZXvhOCb1b2C+MhBfhtc7kYkQ== - dependencies: - "@turf/helpers" "^6.3.0" - -"@turf/line-intersect@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-6.3.0.tgz#726a50edc66bb7b5e798b052b103fb0da4d1c4f4" - integrity sha512-3naxR7XpkPd2vst3Mw6DFry4C9m3o0/f2n/xu5UAyxb88Ie4m2k+1eqkhzMMx/0L+E6iThWpLx7DASM6q6o9ow== +"@turf/kinks@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/kinks/-/kinks-6.5.0.tgz#80e7456367535365012f658cf1a988b39a2c920b" + integrity sha512-ViCngdPt1eEL7hYUHR2eHR662GvCgTc35ZJFaNR6kRtr6D8plLaDju0FILeFFWSc+o8e3fwxZEJKmFj9IzPiIQ== dependencies: - "@turf/helpers" "^6.3.0" - "@turf/invariant" "^6.3.0" - "@turf/line-segment" "^6.3.0" - "@turf/meta" "^6.3.0" - geojson-rbush "3.x" + "@turf/helpers" "^6.5.0" -"@turf/line-intersect@^6.3.0", "@turf/line-intersect@^6.5.0": +"@turf/line-intersect@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/line-intersect/-/line-intersect-6.5.0.tgz#dea48348b30c093715d2195d2dd7524aee4cf020" integrity sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA== @@ -1701,7 +1690,7 @@ "@turf/meta" "^6.5.0" geojson-rbush "3.x" -"@turf/line-segment@^6.3.0", "@turf/line-segment@^6.5.0": +"@turf/line-segment@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/line-segment/-/line-segment-6.5.0.tgz#ee73f3ffcb7c956203b64ed966d96af380a4dd65" integrity sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw== @@ -1710,30 +1699,30 @@ "@turf/invariant" "^6.5.0" "@turf/meta" "^6.5.0" -"@turf/line-split@6.3.0": - version "6.3.0" - resolved "https://registry.yarnpkg.com/@turf/line-split/-/line-split-6.3.0.tgz#ee218f66cd65ce84eafc4956c24083663f6082ea" - integrity sha512-Q0nUJ0vczy11piyEz0FaKScFwSQtb1HJ2RPEMCw1coUJhTCB02KBWQLImhYqwsD3uLg+H/fxaJ1Gva6EPWoDNQ== - dependencies: - "@turf/bbox" "^6.3.0" - "@turf/helpers" "^6.3.0" - "@turf/invariant" "^6.3.0" - "@turf/line-intersect" "^6.3.0" - "@turf/line-segment" "^6.3.0" - "@turf/meta" "^6.3.0" - "@turf/nearest-point-on-line" "^6.3.0" - "@turf/square" "^6.3.0" - "@turf/truncate" "^6.3.0" +"@turf/line-split@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/line-split/-/line-split-6.5.0.tgz#116d7fbf714457878225187f5820ef98db7b02c2" + integrity sha512-/rwUMVr9OI2ccJjw7/6eTN53URtGThNSD5I0GgxyFXMtxWiloRJ9MTff8jBbtPWrRka/Sh2GkwucVRAEakx9Sw== + dependencies: + "@turf/bbox" "^6.5.0" + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/line-intersect" "^6.5.0" + "@turf/line-segment" "^6.5.0" + "@turf/meta" "^6.5.0" + "@turf/nearest-point-on-line" "^6.5.0" + "@turf/square" "^6.5.0" + "@turf/truncate" "^6.5.0" geojson-rbush "3.x" -"@turf/meta@6.x", "@turf/meta@^6.3.0", "@turf/meta@^6.5.0": +"@turf/meta@6.x", "@turf/meta@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-6.5.0.tgz#b725c3653c9f432133eaa04d3421f7e51e0418ca" integrity sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA== dependencies: "@turf/helpers" "^6.5.0" -"@turf/nearest-point-on-line@^6.3.0": +"@turf/nearest-point-on-line@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/nearest-point-on-line/-/nearest-point-on-line-6.5.0.tgz#8e1cd2cdc0b5acaf4c8d8b3b33bb008d3cb99e7b" integrity sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg== @@ -1746,7 +1735,7 @@ "@turf/line-intersect" "^6.5.0" "@turf/meta" "^6.5.0" -"@turf/square@^6.3.0": +"@turf/square@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/square/-/square-6.5.0.tgz#ab43eef99d39c36157ab5b80416bbeba1f6b2122" integrity sha512-BM2UyWDmiuHCadVhHXKIx5CQQbNCpOxB6S/aCNOCLbhCeypKX5Q0Aosc5YcmCJgkwO5BERCC6Ee7NMbNB2vHmQ== @@ -1754,7 +1743,7 @@ "@turf/distance" "^6.5.0" "@turf/helpers" "^6.5.0" -"@turf/truncate@^6.3.0": +"@turf/truncate@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/truncate/-/truncate-6.5.0.tgz#c3a16cad959f1be1c5156157d5555c64b19185d8" integrity sha512-pFxg71pLk+eJj134Z9yUoRhIi8vqnnKvCYwdT4x/DQl/19RVdq1tV3yqOT3gcTQNfniteylL5qV1uTBDV5sgrg== From d7f4bd5436c72b1701fd806d9661bbd1ce203371 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 23 Dec 2021 14:16:22 +0200 Subject: [PATCH 15/72] UI: Show legend fieldset only in timeseries and latest widget --- .../home/components/widget/legend.component.ts | 12 +++++++----- .../components/widget/widget-config.component.html | 2 +- .../components/widget/widget-config.component.ts | 4 ++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts b/ui-ngx/src/app/modules/home/components/widget/legend.component.ts index 004fd5087b..3f66fc8aba 100644 --- a/ui-ngx/src/app/modules/home/components/widget/legend.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/legend.component.ts @@ -60,11 +60,13 @@ export class LegendComponent implements OnInit { } legendKeys(): LegendKey[] { - let keys = this.legendData.keys; - if (this.legendConfig.sortDataKeys) { - keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); - } - return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend); + try { + let keys = this.legendData.keys; + if (this.legendConfig.sortDataKeys) { + keys = this.legendData.keys.sort((key1, key2) => key1.dataKey.label.localeCompare(key2.dataKey.label)); + } + return keys.filter(legendKey => this.legendData.keys[legendKey.dataIndex].dataKey.inLegend); + } catch (e) {} } } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 97542fd3cb..6878a19d60 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -421,7 +421,7 @@ -
+
widget-config.legend diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 7d779a3a6c..e64cc4de42 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -159,6 +159,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont modelValue: WidgetConfigComponentData; + showLegendFieldset = true; + private propagateChange = null; public dataSettings: FormGroup; @@ -322,6 +324,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont this.datasourceTypes.push(DatasourceType.entityCount); } } + this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest); + this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({}); From 94d502e6eedde28c9360789c43cfda454efd2b4b Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 24 Dec 2021 11:13:28 +0200 Subject: [PATCH 16/72] UI: Refactoring --- .../modules/home/components/widget/widget-config.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index e64cc4de42..6b485dd66b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -324,7 +324,6 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont this.datasourceTypes.push(DatasourceType.entityCount); } } - this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest); this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); @@ -389,6 +388,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont if (this.modelValue) { if (this.widgetType !== this.modelValue.widgetType) { this.widgetType = this.modelValue.widgetType; + this.showLegendFieldset = (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.latest); this.buildForms(); } const config = this.modelValue.config; From cfead9941ed1a66e29b89adaf28cad40ca28b982 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 24 Dec 2021 11:28:20 +0200 Subject: [PATCH 17/72] UI: Fixed code style --- ui-ngx/src/app/shared/components/json-object-view.component.ts | 1 - ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/components/json-object-view.component.ts b/ui-ngx/src/app/shared/components/json-object-view.component.ts index b85988c3d3..483d1a014a 100644 --- a/ui-ngx/src/app/shared/components/json-object-view.component.ts +++ b/ui-ngx/src/app/shared/components/json-object-view.component.ts @@ -127,7 +127,6 @@ export class JsonObjectViewComponent implements OnInit { newWidth = 8 * maxLineLength + 16; } if (this.heigthValue) { - // this.renderer.setStyle(editorElement, 'minHeight', newHeight.toString() + 'px'); this.renderer.setStyle(editorElement, 'height', newHeight.toString() + 'px'); } if (this.widthValue) { 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 bd33c10376..e4fa66b4e5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3279,7 +3279,7 @@ "EXPIRED": "EXPIRED", "FAILED": "FAILED" }, - "rpc-search-status-all": "ALL", + "rpc-search-status-all": "ALL", "message-types": { "false": "Two-way", "true": "One-way" From 8a421e3aa0cf399573940437f98116f0b9ebd271 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 24 Dec 2021 16:05:24 +0200 Subject: [PATCH 18/72] UI: Fixed constant boolean filters creating form --- .../filter/boolean-filter-predicate.component.ts | 7 +++++-- .../home/components/filter/key-filter-dialog.component.ts | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts index d5299a52ea..e0a4868898 100644 --- a/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts @@ -109,8 +109,11 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, Va } private updateModel() { - const predicate: BooleanFilterPredicate = this.booleanFilterPredicateFormGroup.getRawValue(); - predicate.type = FilterPredicateType.BOOLEAN; + let predicate: BooleanFilterPredicate = null; + if (this.booleanFilterPredicateFormGroup.valid) { + predicate = this.booleanFilterPredicateFormGroup.getRawValue(); + predicate.type = FilterPredicateType.BOOLEAN; + } this.propagateChange(predicate); } diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index 6c9c85330a..41520eede7 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -134,6 +134,10 @@ export class KeyFilterDialogComponent extends } ); } + if (valueType === EntityKeyValueType.BOOLEAN) { + this.keyFilterFormGroup.get('value').clearValidators(); + this.keyFilterFormGroup.get('value').updateValueAndValidity(); + } }); this.keyFilterFormGroup.get('key.type').valueChanges.pipe( @@ -148,7 +152,7 @@ export class KeyFilterDialogComponent extends this.showAutocomplete = false; } if (this.data.telemetryKeysOnly) { - if (type === EntityKeyType.CONSTANT) { + if (type === EntityKeyType.CONSTANT && (this.keyFilterFormGroup.get('valueType').value !== EntityKeyValueType.BOOLEAN)) { this.keyFilterFormGroup.get('value').setValidators(Validators.required); this.keyFilterFormGroup.get('value').updateValueAndValidity(); } else { From fef7b2605066cb39322de5e9b941c8b39140067b Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 24 Dec 2021 17:03:51 +0200 Subject: [PATCH 19/72] UI: Added a condition for ckeck boolean key type only for constant type --- .../home/components/filter/key-filter-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts index 41520eede7..c96bc66419 100644 --- a/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.ts @@ -134,7 +134,7 @@ export class KeyFilterDialogComponent extends } ); } - if (valueType === EntityKeyValueType.BOOLEAN) { + if (valueType === EntityKeyValueType.BOOLEAN && this.isConstantKeyType) { this.keyFilterFormGroup.get('value').clearValidators(); this.keyFilterFormGroup.get('value').updateValueAndValidity(); } From edcde714c7fbb1255c9e628317e30cf598b57744 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 28 Dec 2021 12:01:26 +0200 Subject: [PATCH 20/72] UI: Added entity details page --- ui-ngx/src/app/core/utils.ts | 14 ++ .../entity/entity-details-page.component.html | 57 ++++++ .../entity/entity-details-page.component.scss | 119 +++++++++++++ .../entity/entity-details-page.component.ts | 154 ++++++++++++++++ .../entity/entity-details-panel.component.ts | 16 +- .../components/entity/entity.component.ts | 2 + .../home/components/home-components.module.ts | 3 + ...device-profile-autocomplete.component.html | 7 + ...device-profile-autocomplete.component.scss | 21 +++ .../device-profile-autocomplete.component.ts | 9 +- ...ofile-provision-configuration.component.ts | 2 +- .../profile/device-profile.component.html | 8 + ...tenant-profile-autocomplete.component.html | 7 + ...tenant-profile-autocomplete.component.scss | 21 +++ .../tenant-profile-autocomplete.component.ts | 16 +- .../profile/tenant-profile.component.html | 6 + .../relation/relation-table.component.html | 8 +- .../relation/relation-table.component.scss | 3 + .../models/datasource/relation-datasource.ts | 5 +- .../home/pages/admin/admin-routing.module.ts | 38 +++- .../resources-library-table-config.resolve.ts | 13 +- .../resource/resources-library.component.html | 6 + .../resource/resources-library.component.ts | 5 +- .../home/pages/asset/asset-routing.module.ts | 42 ++++- .../home/pages/asset/asset.component.html | 6 + .../asset/assets-table-config.resolver.ts | 10 ++ .../pages/customer/customer-routing.module.ts | 167 +++++++++++++++--- .../pages/customer/customer.component.html | 6 + .../customers-table-config.resolver.ts | 10 ++ .../device-profile-routing.module.ts | 20 +++ .../device-profiles-table-config.resolver.ts | 13 +- .../pages/device/device-routing.module.ts | 42 ++++- .../home/pages/device/device.component.html | 7 + .../home/pages/device/device.component.ts | 1 + .../device/devices-table-config.resolver.ts | 10 ++ .../home/pages/edge/edge-routing.module.ts | 131 ++++++++++++-- .../home/pages/edge/edge.component.html | 6 + .../pages/edge/edges-table-config.resolver.ts | 10 ++ .../entity-view/entity-view-routing.module.ts | 42 ++++- .../entity-view/entity-view.component.html | 6 + .../entity-views-table-config.resolver.ts | 10 ++ .../ota-update/ota-update-routing.module.ts | 39 +++- .../ota-update-table-config.resolve.ts | 13 +- .../ota-update/ota-update.component.html | 9 +- .../tenant-profile-routing.module.ts | 20 +++ .../tenant-profiles-table-config.resolver.ts | 15 +- .../pages/tenant/tenant-routing.module.ts | 55 +++++- .../home/pages/tenant/tenant.component.html | 7 + .../tenant/tenants-table-config.resolver.ts | 10 ++ .../home/pages/user/user-routing.module.ts | 43 ++++- .../home/pages/user/user.component.html | 6 + .../pages/user/users-table-config.resolver.ts | 27 ++- .../shared/components/breadcrumb.component.ts | 8 +- .../ota-package-autocomplete.component.html | 7 + .../ota-package-autocomplete.component.scss | 21 +++ .../ota-package-autocomplete.component.ts | 10 +- ui-ngx/src/app/shared/models/constants.ts | 15 ++ .../src/app/shared/models/relation.models.ts | 1 + ui-ngx/src/app/shared/shared.module.ts | 1 + .../assets/locale/locale.constant-en_US.json | 3 +- 60 files changed, 1271 insertions(+), 118 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html create mode 100644 ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/profile/tenant-profile-autocomplete.component.scss create mode 100644 ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.scss diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index b986583171..c09488a593 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -20,6 +20,10 @@ import { finalize, share } from 'rxjs/operators'; import { Datasource } from '@app/shared/models/widget.models'; import { EntityId } from '@shared/models/id/entity-id'; import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; +import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; +import { baseDetailsPageByEntityType } from '@shared/models/constants'; +import { EntityType } from '@shared/models/entity-type.models'; const varsRegex = /\${([^}]*)}/g; @@ -460,3 +464,13 @@ export function randomAlphanumeric(length: number): string { } return result; } + +export const entityDetailsPageBreadcrumbLabelFunction: BreadCrumbLabelFunction + = ((route, translate, component) => { + return component.entity?.name || component.headerSubtitle; +}); + + +export function getEntityDetailsPageURL(id: string, entityType: EntityType): string { + return `${baseDetailsPageByEntityType.get(entityType)}/${id}`; +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html new file mode 100644 index 0000000000..e025534f2b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html @@ -0,0 +1,57 @@ + + + +
+
+
{{ headerTitle }}
+
{{ headerSubtitle }}
+
+
+ +
+ + +
+
+
+ + + + + + + + +
+ diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss new file mode 100644 index 0000000000..a74e9e46c1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.scss @@ -0,0 +1,119 @@ +/** + * Copyright © 2016-2021 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 "../../../../../scss/constants"; + +:host { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + + .settings-card { + margin: 8px; + padding: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + + .details-toolbar { + height: 84px; + min-height: 84px; + border-radius: 4px 4px 0 0; + background: #fff; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + + .mat-toolbar-tools { + padding: 0 8px; + } + + .tb-details-title-header { + min-width: 0; + width: auto; + } + + .tb-details-title { + font-size: 1rem; + font-weight: 500; + + @media #{$mat-gt-sm} { + font-size: 1.2rem; + } + } + + .tb-details-subtitle { + font-size: 0.9rem; + opacity: .8; + } + + .tb-ellipsis { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + @media #{$mat-md} { + width: 80%; + } + + @media #{$mat-gt-md} { + width: 60%; + } + + .tb-header-button { + .tb-btn-header { + position: relative !important; + display: inline-block !important; + animation: tbMoveFromTopFade .3s ease both; + + &.tb-hide { + animation: tbMoveToTopFade .3s ease both; + } + } + } + } +} + +:host ::ng-deep { + .tb-help { + .mat-icon-button.mat-primary { + color: rgba(0, 0, 0, 0.52); + } + } + + .mat-card-content { + position: relative; + overflow: hidden; + + > .mat-tab-group { + > .mat-tab-body-wrapper { + position: absolute; + top: 49px; + left: 0; + right: 0; + bottom: 0; + } + > .mat-tab-header { + .mat-tab-label { + min-width: 40px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts new file mode 100644 index 0000000000..27ed899c79 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2021 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 { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ComponentFactoryResolver, + HostBinding, + Injector, + OnDestroy, + OnInit +} from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormGroup } from '@angular/forms'; +import { AssetId } from '@shared/models/id/asset-id'; +import { TranslateService } from '@ngx-translate/core'; +import { deepClone, mergeDeep } from '@core/utils'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; + +@Component({ + selector: 'tb-entity-details-page', + templateUrl: './entity-details-page.component.html', + styleUrls: ['./entity-details-page.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class EntityDetailsPageComponent extends EntityDetailsPanelComponent implements OnInit, OnDestroy { + + headerTitle: string; + headerSubtitle: string; + + isReadOnly = false; + + set entitiesTableConfig(entitiesTableConfig: EntityTableConfig>) { + if (this.entitiesTableConfigValue !== entitiesTableConfig) { + this.entitiesTableConfigValue = entitiesTableConfig; + if (this.entitiesTableConfigValue) { + this.isEdit = false; + this.entity = null; + } + } + } + + get entitiesTableConfig(): EntityTableConfig> { + return this.entitiesTableConfigValue; + } + + @HostBinding('class') 'tb-absolute-fill'; + + constructor(private route: ActivatedRoute, + private router: Router, + protected injector: Injector, + protected cd: ChangeDetectorRef, + protected componentFactoryResolver: ComponentFactoryResolver, + private broadcast: BroadcastService, + private translate: TranslateService, + protected store: Store) { + super(store, injector, cd, componentFactoryResolver); + this.entitiesTableConfig = this.route.snapshot.data.entitiesTableConfig; + } + + ngOnInit() { + this.headerSubtitle = ''; + this.route.paramMap.subscribe( paramMap => { + this.entityId = new AssetId(paramMap.get('entityId')); + }); + this.headerSubtitle = this.translate.instant(this.entitiesTableConfig.entityTranslations.details); + super.init(); + this.entityComponent.isDetailsPage = true; + } + + ngOnDestroy() { + super.ngOnDestroy(); + } + + reload(): void { + this.isEdit = false; + this.entitiesTableConfig.loadEntity(this.currentEntityId).subscribe( + (entity) => { + this.entity = entity; + this.broadcast.broadcast('updateBreadcrumb'); + this.isReadOnly = this.entitiesTableConfig.detailsReadonly(entity); + this.headerTitle = this.entitiesTableConfig.entityTitle(entity); + this.entityComponent.entity = entity; + this.entityComponent.isEdit = false; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = entity; + } + } + ); + } + + onToggleDetailsEditMode() { + if (this.isEdit) { + this.entityComponent.entity = this.entity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = this.entity; + } + this.isEdit = !this.isEdit; + } else { + this.isEdit = !this.isEdit; + this.editingEntity = deepClone(this.entity); + this.entityComponent.entity = this.editingEntity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = this.editingEntity; + } + if (this.entitiesTableConfig.hideDetailsTabsOnEdit) { + this.selectedTab = 0; + } + } + } + + onApplyDetails() { + if (this.detailsForm && this.detailsForm.valid) { + const editingEntity = {...this.editingEntity, ...this.detailsForm.getRawValue()}; + if (this.detailsForm.hasOwnProperty('additionalInfo')) { + editingEntity.additionalInfo = + mergeDeep((this.editingEntity as any).additionalInfo, this.detailsForm.getRawValue()?.additionalInfo); + } + this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity).subscribe( + (entity) => { + this.entity = entity; + this.entityComponent.entity = entity; + if (this.entityTabsComponent) { + this.entityTabsComponent.entity = entity; + } + this.isEdit = false; + } + ); + } + } + + confirmForm(): FormGroup { + return this.detailsForm; + } +} diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts index d6378620f9..c5e7968935 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts @@ -88,15 +88,15 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV entity: BaseData; editingEntity: BaseData; - private currentEntityId: HasId; - private subscriptions: Subscription[] = []; - private viewInited = false; - private pendingTabs: MatTab[]; + protected currentEntityId: HasId; + protected subscriptions: Subscription[] = []; + protected viewInited = false; + protected pendingTabs: MatTab[]; constructor(protected store: Store, - private injector: Injector, - private cd: ChangeDetectorRef, - private componentFactoryResolver: ComponentFactoryResolver) { + protected injector: Injector, + protected cd: ChangeDetectorRef, + protected componentFactoryResolver: ComponentFactoryResolver) { super(store); } @@ -139,7 +139,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV return this.isEditValue; } - private init() { + protected init() { this.translations = this.entitiesTableConfig.entityTranslations; this.resources = this.entitiesTableConfig.entityResources; this.buildEntityComponent(); diff --git a/ui-ngx/src/app/modules/home/components/entity/entity.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts index 8d868eff98..5da90b49ff 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity.component.ts @@ -38,6 +38,8 @@ export abstract class EntityComponent, isEditValue: boolean; + isDetailsPage = false; + @Input() set entitiesTableConfig(entitiesTableConfig: C) { this.setEntitiesTableConfig(entitiesTableConfig); 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 73361eddce..0960d98620 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 @@ -147,6 +147,7 @@ import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens'; import { DashboardStateComponent } from '@home/components/dashboard-page/dashboard-state.component'; +import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; @NgModule({ declarations: @@ -155,6 +156,7 @@ import { DashboardStateComponent } from '@home/components/dashboard-page/dashboa AddEntityDialogComponent, DetailsPanelComponent, EntityDetailsPanelComponent, + EntityDetailsPageComponent, AuditLogTableComponent, AuditLogDetailsDialogComponent, EventContentDialogComponent, @@ -282,6 +284,7 @@ import { DashboardStateComponent } from '@home/components/dashboard-page/dashboa AddEntityDialogComponent, DetailsPanelComponent, EntityDetailsPanelComponent, + EntityDetailsPageComponent, AuditLogTableComponent, EventTableComponent, EdgeDownlinkTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html index 20f271d660..2b73fc32c4 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html @@ -38,6 +38,13 @@ (click)="editDeviceProfile($event)"> edit + DeviceProfileAutocompleteComponent), @@ -79,6 +80,9 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, @Input() addNewProfile = true; + @Input() + showDetailsPageLink = false; + @Input() transportType: DeviceTransportType = null; @@ -110,6 +114,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, filteredDeviceProfiles: Observable>; searchText = ''; + deviceProfileURL: string; private dirty = false; @@ -240,6 +245,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe( (profile) => { this.modelValue = new DeviceProfileId(profile.id.id); + this.deviceProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType); this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); this.deviceProfileChanged.emit(profile); } @@ -278,6 +284,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, const idValue = deviceProfile && deviceProfile.id ? new DeviceProfileId(deviceProfile.id.id) : null; if (!entityIdEquals(this.modelValue, idValue)) { this.modelValue = idValue; + this.deviceProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType); this.propagateChange(this.modelValue); this.deviceProfileChanged.emit(deviceProfile); } diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts index 28559dc4f7..3693c82f8f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-provision-configuration.component.ts @@ -130,7 +130,7 @@ export class DeviceProfileProvisionConfigurationComponent implements ControlValu setDisabledState(isDisabled: boolean){ this.disabled = isDisabled; if (this.disabled){ - this.provisionConfigurationFormGroup.disable(); + this.provisionConfigurationFormGroup.disable({emitEvent: false}); } else { if (this.provisionConfigurationFormGroup.get('type').value !== DeviceProvisionType.DISABLED) { this.provisionConfigurationFormGroup.enable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html index 917ad25dd2..df00bdf8fa 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile.component.html @@ -16,6 +16,12 @@ -->
+ + TenantProfileAutocompleteComponent), @@ -66,6 +67,9 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, @Input() disabled: boolean; + @Input() + showDetailsPageLink = false; + @Output() tenantProfileUpdated = new EventEmitter(); @@ -74,6 +78,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, filteredTenantProfiles: Observable>; searchText = ''; + tenantProfileURL: string; private dirty = false; @@ -123,6 +128,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, (profile) => { if (profile) { this.modelValue = new TenantProfileId(profile.id.id); + this.tenantProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType); this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); this.propagateChange(this.modelValue); } @@ -133,6 +139,11 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; + if (this.disabled) { + this.selectTenantProfileFormGroup.disable(); + } else { + this.selectTenantProfileFormGroup.enable(); + } } writeValue(value: TenantProfileId | null): void { @@ -141,6 +152,7 @@ export class TenantProfileAutocompleteComponent implements ControlValueAccessor, this.tenantProfileService.getTenantProfileInfo(value.id).subscribe( (profile) => { this.modelValue = new TenantProfileId(profile.id.id); + this.tenantProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType); this.selectTenantProfileFormGroup.get('tenantProfile').patchValue(profile, {emitEvent: false}); } ); diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index e5a3a54293..f98d0b6a80 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -16,6 +16,12 @@ -->
+ + diff --git a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.scss b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.scss new file mode 100644 index 0000000000..38af76b016 --- /dev/null +++ b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2021 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{ + .mat-icon-button a { + border-bottom: none; + color: inherit; + } +} diff --git a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts index 6609d4ceb7..4d22d1f298 100644 --- a/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/ota-package/ota-package-autocomplete.component.ts @@ -31,12 +31,13 @@ import { OtaPackageInfo, OtaUpdateTranslation, OtaUpdateType } from '@shared/mod import { OtaPackageService } from '@core/http/ota-package.service'; import { PageLink } from '@shared/models/page/page-link'; import { Direction } from '@shared/models/page/sort-order'; -import { emptyPageData } from "@shared/models/page/page-data"; +import { emptyPageData } from '@shared/models/page/page-data'; +import { getEntityDetailsPageURL } from '@core/utils'; @Component({ selector: 'tb-ota-package-autocomplete', templateUrl: './ota-package-autocomplete.component.html', - styleUrls: [], + styleUrls: ['./ota-package-autocomplete.component.scss'], providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => OtaPackageAutocompleteComponent), @@ -64,6 +65,9 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On @Input() useFullEntityId = false; + @Input() + showDetailsPageLink = false; + private requiredValue: boolean; get required(): boolean { @@ -83,6 +87,7 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On filteredPackages: Observable>; searchText = ''; + packageURL: string; private dirty = false; @@ -166,6 +171,7 @@ export class OtaPackageAutocompleteComponent implements ControlValueAccessor, On if (packageId !== '') { this.entityService.getEntity(EntityType.OTA_PACKAGE, packageId, {ignoreLoading: true, ignoreErrors: true}).subscribe( (entity) => { + this.packageURL = getEntityDetailsPageURL(entity.id.id, EntityType.OTA_PACKAGE); this.modelValue = this.useFullEntityId ? entity.id : entity.id.id; this.otaPackageFormGroup.get('packageId').patchValue(entity, {emitEvent: false}); }, diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 9bf120f8b7..fa73e0d40e 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -16,6 +16,7 @@ import { InjectionToken } from '@angular/core'; import { IModulesMap } from '@modules/common/modules-map.models'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; export const Constants = { serverErrorCode: { @@ -137,6 +138,20 @@ export const HelpLinks = { } }; +export const baseDetailsPageByEntityType = new Map([ + [EntityType.TENANT, '/tenants'], + [EntityType.TENANT_PROFILE, '/tenantProfiles'], + [EntityType.CUSTOMER, '/customers'], + [EntityType.USER, '/users'], + [EntityType.ASSET, '/assets'], + [EntityType.DEVICE, '/devices'], + [EntityType.DEVICE_PROFILE, '/deviceProfiles'], + [EntityType.EDGE, '/edgeInstances'], + [EntityType.ENTITY_VIEW, '/entityViews'], + [EntityType.TB_RESOURCE, '/settings/resources-library'], + [EntityType.OTA_PACKAGE, '/otaUpdates'] +]); + export interface ValueTypeData { name: string; icon: string; diff --git a/ui-ngx/src/app/shared/models/relation.models.ts b/ui-ngx/src/app/shared/models/relation.models.ts index c9037a5dab..b1601753d8 100644 --- a/ui-ngx/src/app/shared/models/relation.models.ts +++ b/ui-ngx/src/app/shared/models/relation.models.ts @@ -89,4 +89,5 @@ export interface EntityRelationInfo extends EntityRelation { toEntityTypeName?: string; toName: string; fromEntityTypeName?: string; + entityURL?: string; } diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 086768c854..50975b9f11 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -455,6 +455,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) KeyboardShortcutPipe, FileSizePipe, SelectableColumnsPipe, + RouterModule, TranslateModule, JsonObjectEditDialogComponent, HistorySelectorComponent, 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 807f3781a9..4d3a51d087 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -622,7 +622,8 @@ "enter-search": "Enter search", "created-time": "Created time", "loading": "Loading...", - "proceed": "Proceed" + "proceed": "Proceed", + "open-details-page": "Open details page" }, "content-type": { "json": "Json", From b58b3a303b4861f71635faa626eaa9d81021bec2 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 28 Dec 2021 18:37:37 +0200 Subject: [PATCH 21/72] UI: Added support delete entity in details page --- .../entity/entity-details-page.component.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts index 27ed899c79..c2878680e5 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.ts @@ -35,6 +35,7 @@ import { TranslateService } from '@ngx-translate/core'; import { deepClone, mergeDeep } from '@core/utils'; import { BroadcastService } from '@core/services/broadcast.service'; import { EntityDetailsPanelComponent } from '@home/components/entity/entity-details-panel.component'; +import { DialogService } from '@core/services/dialog.service'; @Component({ selector: 'tb-entity-details-page', @@ -72,6 +73,7 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl protected componentFactoryResolver: ComponentFactoryResolver, private broadcast: BroadcastService, private translate: TranslateService, + private dialogService: DialogService, protected store: Store) { super(store, injector, cd, componentFactoryResolver); this.entitiesTableConfig = this.route.snapshot.data.entitiesTableConfig; @@ -85,6 +87,11 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl this.headerSubtitle = this.translate.instant(this.entitiesTableConfig.entityTranslations.details); super.init(); this.entityComponent.isDetailsPage = true; + this.subscriptions.push(this.entityAction.subscribe((action) => { + if (action.action === 'delete') { + this.deleteEntity(action.event, action.entity); + } + })); } ngOnDestroy() { @@ -151,4 +158,25 @@ export class EntityDetailsPageComponent extends EntityDetailsPanelComponent impl confirmForm(): FormGroup { return this.detailsForm; } + + private deleteEntity($event: Event, entity: BaseData) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.entitiesTableConfig.deleteEntityTitle(entity), + this.entitiesTableConfig.deleteEntityContent(entity), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.entitiesTableConfig.deleteEntity(entity.id).subscribe( + () => { + this.router.navigate(['../'], {relativeTo: this.route}); + } + ); + } + }); + } } From d7f9b1d430fbb9009346da5458c522a862cd1b5e Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 29 Dec 2021 12:59:39 +0200 Subject: [PATCH 22/72] UI: Disable column sort if enable post processing func --- .../widget/lib/entities-table-widget.component.html | 2 +- .../components/widget/lib/entities-table-widget.component.ts | 4 ++++ .../home/components/widget/lib/table-widget.models.ts | 1 + .../widget/lib/timeseries-table-widget.component.html | 2 +- .../widget/lib/timeseries-table-widget.component.ts | 5 ++++- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html index 74ccd53ec2..bfb775306b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html @@ -41,7 +41,7 @@
- {{ column.title }} + {{ column.title }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index bd52f2ab9b..a2e4f7b922 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -324,6 +324,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni label: 'entityName', def: 'entityName', title: entityNameColumnTitle, + sortable: true, entityKey: { key: 'name', type: EntityKeyType.ENTITY_FIELD @@ -347,6 +348,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni label: 'entityLabel', def: 'entityLabel', title: entityLabelColumnTitle, + sortable: true, entityKey: { key: 'label', type: EntityKeyType.ENTITY_FIELD @@ -370,6 +372,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni label: 'entityType', def: 'entityType', title: this.translate.instant('entity.entity-type'), + sortable: true, entityKey: { key: 'entityType', type: EntityKeyType.ENTITY_FIELD @@ -403,6 +406,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); dataKey.title = dataKey.label; dataKey.def = 'def' + this.columns.length; + dataKey.sortable = !dataKey.usePostProcessing; const keySettings: TableWidgetDataKeySettings = dataKey.settings; if (dataKey.type === DataKeyType.entityField && !isDefined(keySettings.columnWidth) || keySettings.columnWidth === '0px') { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index f90c287111..1d798a88ca 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -73,6 +73,7 @@ export interface EntityData { export interface EntityColumn extends DataKey { def: string; title: string; + sortable: boolean; entityKey?: EntityKey; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 8c3b9084b0..d209a649b6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -52,7 +52,7 @@ - {{ h.dataKey.label }} + {{ h.dataKey.label }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index de8cec8dc7..198e36c919 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -88,6 +88,7 @@ interface TimeseriesRow { interface TimeseriesHeader { index: number; dataKey: DataKey; + sortable: boolean; } interface TimeseriesTableSource { @@ -308,10 +309,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI for (let a = 0; a < datasource.dataKeys.length; a++ ) { const dataKey = datasource.dataKeys[a]; const keySettings: TableWidgetDataKeySettings = dataKey.settings; + const sortable = !dataKey.usePostProcessing; const index = a + 1; source.header.push({ index, - dataKey + dataKey, + sortable }); source.displayedColumns.push(index + ''); source.rowDataTemplate[dataKey.label] = null; From e69871d8869af3513a746119d14e884939d57bb5 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 29 Dec 2021 17:50:56 +0200 Subject: [PATCH 23/72] UI: Added constants in baseDetailsPageByEntityType --- ui-ngx/src/app/shared/models/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index fa73e0d40e..251f310f9b 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -143,9 +143,11 @@ export const baseDetailsPageByEntityType = new Map([ [EntityType.TENANT_PROFILE, '/tenantProfiles'], [EntityType.CUSTOMER, '/customers'], [EntityType.USER, '/users'], + [EntityType.DASHBOARD, '/dashboards'], [EntityType.ASSET, '/assets'], [EntityType.DEVICE, '/devices'], [EntityType.DEVICE_PROFILE, '/deviceProfiles'], + [EntityType.RULE_CHAIN, '/ruleChains'], [EntityType.EDGE, '/edgeInstances'], [EntityType.ENTITY_VIEW, '/entityViews'], [EntityType.TB_RESOURCE, '/settings/resources-library'], From c655b58977990fc57bfe570509c1a7faf2e88e8d Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Thu, 30 Dec 2021 12:31:21 +0200 Subject: [PATCH 24/72] lwm2m delete security files jks and in yml security enable = false --- .../src/main/resources/thingsboard.yml | 10 +- .../credentials/shell/lwM2M_credentials.sh | 359 ----------------- .../credentials/shell/lwM2M_keygen.properties | 57 --- .../lwm2m/src/main/resources/lwm2mserver.jks | Bin 3849 -> 0 bytes pom.xml | 34 -- .../credentials/shell/lwM2M_credentials.sh | 360 ------------------ .../credentials/shell/lwM2M_keygen.properties | 57 --- transport/lwm2m/src/main/data/lwm2mserver.jks | Bin 4017 -> 0 bytes .../src/main/resources/tb-lwm2m-transport.yml | 10 +- 9 files changed, 10 insertions(+), 877 deletions(-) delete mode 100644 common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh delete mode 100644 common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties delete mode 100644 common/transport/lwm2m/src/main/resources/lwm2mserver.jks delete mode 100755 transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh delete mode 100644 transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties delete mode 100644 transport/lwm2m/src/main/data/lwm2mserver.jks diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a502afb297..6865aebf9a 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -733,7 +733,7 @@ transport: # Server X509 Certificates support credentials: # Whether to enable LWM2M server X509 Certificate/RPK support - enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:false}" # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) type: "${LWM2M_SERVER_CREDENTIALS_TYPE:PEM}" # PEM server credentials @@ -769,7 +769,7 @@ transport: # Bootstrap server X509 Certificates support credentials: # Whether to enable LWM2M bootstrap server X509 Certificate/RPK support - enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:false}" # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) type: "${LWM2M_BS_CREDENTIALS_TYPE:PEM}" # PEM server credentials @@ -796,19 +796,19 @@ transport: # X509 trust certificates trust-credentials: # Whether to load X509 trust certificates - enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:false}" # Trust certificates store type (PEM - pem certificates file; KEYSTORE - java keystore) type: "${LWM2M_TRUST_CREDENTIALS_TYPE:PEM}" # PEM certificates pem: # Path to the certificates file (holds trust certificates) - cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mserver.pem}" + cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mtruststorechain.pem}" # Keystore with trust certificates keystore: # Type of the key store type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}" # Path to the key store that holds the X509 certificates - store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mserver.jks}" + store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mtruststorechain.jks}" # Password used to access the key store store_password: "${LWM2M_TRUST_KEY_STORE_PASSWORD:server_ks_password}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" diff --git a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh b/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh deleted file mode 100644 index f68ca30005..0000000000 --- a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh +++ /dev/null @@ -1,359 +0,0 @@ -#!/bin/sh -# -# Copyright © 2016-2021 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. -# - -#/home/nick/Igor_project/Thingsboard_Perfrmance_test/performance-tests/src/main/resources/credentials/shell/lwM2M_credentials.sh -p LwX509 -s 0 -f 2000 -a client_alias_ -e client_self_signed_ -b bootstrap -d server -j serverKeyStore.jks -k clientKeyStore.jks -c client_ks_password -w server_ks_password - -#p) CLIENT_CN=$CLIENT_PREFIX00000000 -#s) client_start=0 -#f) client_finish=1 -#a) CLIENT_ALIAS=CLIENT_ALIAS_PREFIX_00000000 -#e) CLIENT_SELF_ALIAS=CLIENT_SELF_ALIAS_PREFIX_00000000 -#b) BOOTSTRAP_ALIAS=bootstrap -#d) SERVER_ALIAS=server -#j) SERVER_STORE=serverKeyStore.jks -#k) CLIENT_STORE=clientKeyStore.jks -#c) CLIENT_STORE_PWD=client_ks_password -#w) SERVER_STORE_PWD=server_ks_password -#l) ROOT_KEY_ALIAS=root_key_alias - -while getopts p:s:f:a:e:b:d:j:k:c:w:l: flag; do - case "${flag}" in - p) client_pref=${OPTARG} ;; - s) client_start=${OPTARG} ;; - f) client_finish=${OPTARG} ;; - a) client_alias_pref=${OPTARG} ;; - e) client_self_alias_pref=${OPTARG} ;; - b) bootstrap_alias=${OPTARG} ;; - d) server_alias=${OPTARG} ;; - j) key_store_server_file=${OPTARG} ;; - k) key_store_client_file=${OPTARG} ;; - c) client_key_store_pwd=${OPTARG} ;; - w) server_key_store_pwd=${OPTARG} ;; - w) root_key_alias=${OPTARG} ;; - esac -done - -# cd to dir of script -script_dir=$(dirname $0) -echo "script_dir: $script_dir" -cd $script_dir -# source the properties: -. ./lwM2M_keygen.properties - -if [ -n "$client_pref" ]; then - CLIENT_PREFIX=$client_pref -fi - -if [ -z "$client_start" ]; then - client_start=0 -fi - -if [ -z "$client_finish" ]; then - client_finish=1 -fi - -if [ -n "$client_alias_pref" ]; then - CLIENT_ALIAS_PREFIX=$client_alias_pref -fi - -if [ -n "$client_self_alias_pref" ]; then - CLIENT_SELF_ALIAS_PREFIX=$client_self_alias_pref -fi - -if [ -n "$bootstrap_alias" ]; then - BOOTSTRAP_ALIAS=$bootstrap_alias -fi - -if [ -n "$server_alias" ]; then - SERVER_ALIAS=$server_alias -fi - -if [ -n "$key_store_server_file" ]; then - SERVER_STORE=$key_store_server_file -fi - -if [ -n "$key_store_client_file" ]; then - CLIENT_STORE=$key_store_client_file -fi - -if [ -n "$client_key_store_pwd" ]; then - CLIENT_STORE_PWD=$client_key_store_pwd -fi - -if [ -n "$server_key_store_pwd" ]; then - SERVER_STORE_PWD=$server_key_store_pwd -fi - -if [ -n "$root_key_alias" ]; then - ROOT_KEY_ALIAS=$root_key_alias -fi - -CLIENT_NUMBER=$client_start - -echo "==Start==" -echo "CLIENT_PREFIX: $CLIENT_PREFIX" -echo "client_start: $client_start" -echo "client_finish: $client_finish" -echo "CLIENT_ALIAS_PREFIX: $CLIENT_ALIAS_PREFIX" -echo "CLIENT_SELF_ALIAS_PREFIX: $CLIENT_SELF_ALIAS_PREFIX" -echo "BOOTSTRAP_ALIAS: $BOOTSTRAP_ALIAS" -echo "SERVER_ALIAS: $SERVER_ALIAS" -echo "SERVER_STORE: $SERVER_STORE" -echo "CLIENT_STORE: $CLIENT_STORE" -echo "CLIENT_STORE_PWD: $CLIENT_STORE_PWD" -echo "SERVER_STORE_PWD: $SERVER_STORE_PWD" -echo "CLIENT_NUMBER: $CLIENT_NUMBER" -echo "ROOT_KEY_ALIAS: $ROOT_KEY_ALIAS" - -end_point() { - echo "$CLIENT_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -client_alias_point() { - echo "$CLIENT_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -client_self_alias_point() { - echo "$CLIENT_SELF_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -# Generation of the keystore. -echo "${H0}====START========${RESET}" -echo "${H1}Server Keystore : ${RESET}" -echo "${H1}==================${RESET}" -echo "${H2}Creating the trusted root CA key and certificate...${RESET}" -# -keysize -# 1024 (when using -genkeypair) -keytool \ - -genkeypair \ - -alias $ROOT_KEY_ALIAS \ - -keyalg EC \ - -dname "CN=$ROOT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -echo -echo "${H2}Creating server key and self-signed certificate ...${RESET}" -keytool \ - -genkeypair \ - -alias $SERVER_ALIAS \ - -keyalg EC \ - -dname "CN=$SERVER_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD -keytool \ - -exportcert \ - -alias $SERVER_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $SERVER_SELF_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -noprompt - -echo -echo "${H2}Creating server certificate signed by root CA...${RESET}" -keytool \ - -certreq \ - -alias $SERVER_ALIAS \ - -dname "CN=$SERVER_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $SERVER_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -echo -echo "${H2}Creating bootstrap key and self-signed certificate ...${RESET}" -keytool \ - -genkeypair \ - -alias $BOOTSTRAP_ALIAS \ - -keyalg EC \ - -dname "CN=$BOOTSTRAP_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD -keytool \ - -exportcert \ - -alias $BOOTSTRAP_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $BOOTSTRAP_SELF_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -noprompt - -echo -echo "${H2}Creating bootstrap certificate signed by root CA...${RESET}" -keytool \ - -certreq \ - -alias $BOOTSTRAP_ALIAS \ - -dname "CN=$BOOTSTRAP_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $BOOTSTRAP_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "${H2}Import root certificate just to be able to import need by root CA with expected CN to $CLIENT_STORE${RESET}" - keytool \ - -exportcert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -fi - -cert_end_point() { - echo - echo "${H1}Client Keystore : ${RESET}" - echo "${H1}==================${RESET}" - echo "${H2}Creating client key and self-signed certificate with expected CN CLIENT_ALIAS: $CLIENT_ALIAS${RESET}" - keytool \ - -genkeypair \ - -alias $CLIENT_ALIAS \ - -keyalg EC \ - -dname "CN=$CLIENT_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $CLIENT_STORE_PWD \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD - keytool \ - -exportcert \ - -alias $CLIENT_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD | - keytool \ - -importcert \ - -alias $CLIENT_SELF_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -# -# echo -# echo "${H2}Import root certificate just to be able to import ned by root CA with expected CN...${RESET}" -# keytool \ -# -exportcert \ -# -alias $ROOT_KEY_ALIAS \ -# -keystore $SERVER_STORE \ -# -storepass $SERVER_STORE_PWD | -# keytool \ -# -importcert \ -# -alias $ROOT_KEY_ALIAS \ -# -keystore $CLIENT_STORE \ -# -storepass $CLIENT_STORE_PWD \ -# -noprompt -# - - echo - echo "${H2}Creating client certificate signed by root CA with expected CN CLIENT_ALIAS: $CLIENT_ALIAS CLIENT_CN: $CLIENT_CN${RESET}" - keytool \ - -certreq \ - -alias $CLIENT_ALIAS \ - -dname "CN=$CLIENT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $CLIENT_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -} - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "==Start Client==" - while [ "$CLIENT_NUMBER" -lt "$client_finish" ]; do - echo "number $CLIENT_NUMBER" - echo "finish $client_finish" - CLIENT_CN=$(end_point) - CLIENT_ALIAS=$(client_alias_point) - CLIENT_SELF_ALIAS=$(client_self_alias_point) - echo "CLIENT_CN $CLIENT_CN" - echo "CLIENT_ALIAS $CLIENT_ALIAS" - echo "CLIENT_SELF_ALIAS $CLIENT_SELF_ALIAS" - cert_end_point - CLIENT_NUMBER=$(($CLIENT_NUMBER + 1)) - echo - done -fi - -echo -echo "${H0}!!! Warning ${H2}Migrate ${H1}${SERVER_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" -keytool \ - -importkeystore \ - -srckeystore $SERVER_STORE \ - -destkeystore $SERVER_STORE \ - -deststoretype pkcs12 \ - -srcstorepass $SERVER_STORE_PWD - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "${H0}!!! Warning ${H2}Migrate ${H1}${CLIENT_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" - keytool \ - -importkeystore \ - -srckeystore $CLIENT_STORE \ - -destkeystore $CLIENT_STORE \ - -deststoretype pkcs12 \ - -srcstorepass $CLIENT_STORE_PWD -fi diff --git a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties b/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties deleted file mode 100644 index 7b3cd9c09a..0000000000 --- a/common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_keygen.properties +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright © 2016-2017 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. -# - -# Keystore common parameters -ROOT_KEY_ALIAS=rootCA -DOMAIN_SUFFIX="$(hostname)" -ROOT_CN="$DOMAIN_SUFFIX $ROOT_KEY_ALIAS" -ORGANIZATIONAL_UNIT=Thingsboard -ORGANIZATION=Thingsboard -CITY=SF -STATE_OR_PROVINCE=CA -TWO_LETTER_COUNTRY_CODE=US -VALIDITY=36500 #days -STORETYPE="JKS" - -#Server -SERVER_STORE=serverKeyStore1.jks -SERVER_STORE_PWD=server_ks_password1 -SERVER_ALIAS=server1 -SERVER_CN="$DOMAIN_SUFFIX server LwM2M signed by root CA" -SERVER_SELF_ALIAS=server_self_signed -SERVER_SELF_CN="$DOMAIN_SUFFIX server LwM2M self-signed" -BOOTSTRAP_ALIAS=bootstrap1 -BOOTSTRAP_CN="$DOMAIN_SUFFIX bootstrap server LwM2M signed by root CA" -BOOTSTRAP_SELF_ALIAS=bootstrap_self_signed -BOOTSTRAP_SELF_CN="$DOMAIN_SUFFIX bootstrap server LwM2M self-signed" - -# Client -CLIENT_STORE=clientKeyStore1.jks -CLIENT_STORE_PWD=client_ks_password1 -CLIENT_ALIAS_PREFIX=client_alias_1 -CLIENT_PREFIX=LwX509___ -CLIENT_SELF_ALIAS_PREFIX=client_self_signed_1 -CLIENT_SELF_CN="$DOMAIN_SUFFIX client LwM2M self-signed" - -# Color output stuff -red=`tput setaf 1` -green=`tput setaf 2` -blue=`tput setaf 4` -bold=`tput bold` -H0=${red}${bold} -H1=${green}${bold} -H2=${blue} -RESET=`tput sgr0` diff --git a/common/transport/lwm2m/src/main/resources/lwm2mserver.jks b/common/transport/lwm2m/src/main/resources/lwm2mserver.jks deleted file mode 100644 index 5fab824aa1b19928fe711701cd27857472a9e739..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3849 zcma))cQ738*2k5#+A3Kq(YqjRcL_F1L>K%-FB_t+8ZBy+V6l1$qW9jSCDF?Y!3rTn zi{2%y-eQ&O-nn(={o|c8bI#27d(QLB`OJZ#>BvZkNnvQ(b|B>)j0)z2hM1f<7fo9Y zK+{%S%Ox;0$@BlQNYnvn5|wLN0fr`K1ycWW3PeRrk{g7B1@*v$fK>kyub*ZGK<*M} z={8PCcFVlAro7urVR?jwWRj2)b!h^EQgg>cnZ9_dBi7mrZl=C^`YJ^4Mwty}WZR0C zPQ-gm(fzX0-@T*x|zp44h)t@B? zY_f0D4EtZp_rA=3XLiAL`ZT!(KOmt+Y}|5c=e|dC*|5v*ja6N;P!Fd-Q7Q`#tIifw z(8tq0NuqRgD873qAUK+6Kh@$e%9I-Z1jhRBy(uAZS`HAA8xe}=6_MxPGmQH`CL zotg#=i4W;>Ob9Ot&9k&?^5MQpmW7E%3N-Utl0R8PkN5qGa#;OUfqhDFrT-=XfkQaJ zL>5HO*WrGR$^9CWIg!hMxBOp_9z7I+p+O4&CP@y=MT11IB{&H&(RI1=ABXsVt_t|~ zt6F7ER@7MOM!a^#FN=&4yAoJB+5Uko5KX-c!%Mvx8ue-o@|Y8>V=p&sOo11EWwN|r zj`X=_(lIw_*u>$y#?99EnlxHCuJ1sFLi1tvjUxeaUwu%Hl}w(?Dc^mU%SF|OSmTo$&6>m_Hq&%@zRJN|j9z8-8ugmY(5pMw@S`gcpZV=*L=9&ZUr;wmRYs&KNw`qCPGcvsC`=$Kz5GHI9WLoGw?Rut|#MUOiLQvr2Z(+2bYV11c z?0JoX3+wRi$WadS8;I!G-}+8!AOZGAbWTc7{bpo(e5Lu_>f?`7kwSeU8sn+o#A3sI z5-twp+FSDY{;;v0?ZD%O^1KPYTCyu89Yv+yHH>+IcG}B2(XMjCRET{#$TkpGm<8|PpkIa zD*_wKhsF8tT6RD&?TTM^|3oRL-r;Ept2LYEz2|9%Tz1f4HR%KyGbjuHvZYaamfdiV z3D7b;@!%7x>zUhWsL36>6fcQgztN^p{rFiZHSf5vuwEFv@N# zQsFQ1@GoT*tO=Y3d$HMnZ1dTWiu9>9sNgUCCT&|E$FbG<44A4nWxQ?#lDM+Yie>d#EuLBauH zgj`MgEeG3#g+(rB58z~09U}TZ^@KjTMJ{Z5Wf_|>cN!_>G8hKEME;U9#mWW+5p=7g zT!-Zd6RC@Vq;ky+gUOxNM`U>7txb`IXl0DKl*O3QI|dVX`cI=nH&ov~UMVN9OobR58$-80m8T zWl`BdzdWZD$+^mJt|o>kD>LW8?e5PxA#}$1{=UT-RZ!1Ct+5!m{)91j7okSnTprCT zK=bKzJXyyvx=#(b>FIHyj()0>;ANv&;! z_)RJh?TOP-HIA&P@SxIw=16K0(3L11dtqYXr?gq)J2$hP)`4E`FOK~bhZpr(x#2>y zgk-q=JjTO$WVI+I_LIF-V!g&ACh?~3F!J2>6kYRBf64(AH2+VivIo&k^Wi0cL8~8~ zmaDIDy-~ECGu@X(6e|)G(X(mx3v_F-^rO^YOuK4>54u$@azF6?Fos9Lx!z%Wf#N~> zoD=JI-ziyZoi_Uo?Sy_Md0l<|u%;DtG3;62{-I8vGksE5Y94yFNwLrjPN_h8e*581bHb7nwR+aY6P{HsoGZ5L=5H5JXoh`OiQjUo#Sc+9f95VHpu2m zm6|V?j@f`ZNng6BSyMq7k_j)je*_PO$gO9%>vW}ltSSPOY1ufO21C!&gp@rF%?e+h z*h)E9E@x$p#Dt=i)?Um^5?U!3AQGUeoYl7M(g-|0I^EQ(RllS){vVv)h$7II3}< zj>%Xa|CyE;(o*qC9(NgOj4ep@vHmyar-`uk>i1as`YKSHtw+qjP@`<>qf@X zfTW)6A~531(u>h(bzlX}hZCf}Znqcdl8lgAN;yxlq$!m9&ku@FVUYUfk!~Bjqz9Q7 zto$}nueJm{J%+3&+i5>TyyND`jM#sjR4|qyeL^~Vl`mA_Zy&K;MLXE}OF;ZeFg|-= zX4x}dOuK+Txf9nIVc)YN`=;yh{H@_SM{y!rPMt1xig;>~Dv-n?U%{(yPB(K>H!#ez z(7FIN1#uU{+U!-hxM1n$`~AZ86Cu;%-pzgThH?{Phyr^dHQyz;K}wFKPN9-4pD>4? z-rk?Qb5xiQ7g)w71ej~P#U;BeFikx&RlWYLTauoB%CTaJes~Q_tLU5n+p9v*e4A1E zz8OOHyym7|?nI;Hm*BIGiXqXmO_|~vx&Hi30X*fhvaD?RnFJM=Qta0q-x7vy!}qSlED(*#K$qn3=IqHN>esF#h$G|Z4A|GJ5aLqx9cXDwzfOk%8Tcquhv#>mBpUu z;r6Q07B#4S9v;mZ!nT9)3R{t0>2@Q%@V54d@-ur5$lNlyG-EH|%aSeOe9c(D6T>OU zXNylOO|Qd@%pJcUL6FOH(>$xw#NK8*#4(|;Wq~M zBa!vg_V^_ehS&qEo=%pXEyZvtT5n;dtmU`@pHXx1xHUnMwho64x17)eu7Ia<8o1FS zG4;M6<%J7g^a@NZQCw?rhWx3fa?Dl2qy7fmp2b;WI)h%%ot>y1MPYKP%u^vvrNteFCnDCe=`Sj#KteNow|(Q8d;a)JZhpYqoePcLA7QTAM`SCA~<_BDhA#= zMtKeSANuU+M){zwq9(y=5#Q9!z;SDYuQd#SOYQk9lPQAvn0IEpVUtsPgD&~X!($WB zM+Tl9rQCa)Xa3!Dg+{7duIhE(?=R-A9t^I0&QeFVdwLwQ_$2Jri4s59;$3~TP;|am zn!9z;+C22g(CJHg2+CK5n03Cns4;wzsrPV;+2#qUgnRPjOV@kO%|9o(RqHFx3u6U0 zos!a*#sD7?R9dIa8X%cMHn(@6sU{WE48Pb3*5id4tQ2FO7X9Iq{Iz#fI~&b#_6n(! zg{#6bH|=tob*20lMhkP86* b(C|b)G3l|ClS0(tPLrtJO%hUKOzz(RVYoVD diff --git a/pom.xml b/pom.xml index c06960c6ca..9dd8c043b2 100755 --- a/pom.xml +++ b/pom.xml @@ -403,39 +403,9 @@ false - - ../common/transport/lwm2m/src/main/resources - - **/*.xml - **/*.jks - - false - - - - - - copy-lwm2m-resources - ${pkg.process-resources.phase} - - copy-resources - - - ../transport/lwm2m/src/main/data - - - ../common/transport/lwm2m/src/main/resources - - **/*.xml - **/*.jks - - false - - - copy-docker-config ${pkg.process-resources.phase} @@ -829,10 +799,6 @@ **/*.proto.js docker/haproxy/** docker/tb-node/** - src/main/resources/models/*.xml - src/main/resources/credentials/*.jks - src/main/resources/credentials/shell/*.jks - src/main/resources/credentials/shell/*.jks.old ui/** src/.browserslistrc **/yarn.lock diff --git a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh b/transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh deleted file mode 100755 index d623bfad42..0000000000 --- a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_credentials.sh +++ /dev/null @@ -1,360 +0,0 @@ -#!/bin/sh -# -# Copyright © 2016-2021 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. -# - -#/home/nick/Igor_project/Thingsboard_Perfrmance_test/performance-tests/src/main/resources/credentials/shell/lwM2M_credentials.sh -p LwX509 -s 0 -f 2000 -a client_alias_ -e client_self_signed_ -b bootstrap -d server -j serverKeyStore.jks -k clientKeyStore.jks -c client_ks_password -w server_ks_password - -#p) CLIENT_CN=$CLIENT_PREFIX00000000 -#s) client_start=0 -#f) client_finish=1 -#a) CLIENT_ALIAS=CLIENT_ALIAS_PREFIX_00000000 -#e) CLIENT_SELF_ALIAS=CLIENT_SELF_ALIAS_PREFIX_00000000 -#b) BOOTSTRAP_ALIAS=bootstrap -#d) SERVER_ALIAS=server -#j) SERVER_STORE=serverKeyStore.jks -#k) CLIENT_STORE=clientKeyStore.jks -#c) CLIENT_STORE_PWD=client_ks_password -#w) SERVER_STORE_PWD=server_ks_password -#l) ROOT_KEY_ALIAS=root_key_alias - -while getopts p:s:f:a:e:b:d:j:k:c:w:l: flag; do - case "${flag}" in - p) client_pref=${OPTARG} ;; - s) client_start=${OPTARG} ;; - f) client_finish=${OPTARG} ;; - a) client_alias_pref=${OPTARG} ;; - e) client_self_alias_pref=${OPTARG} ;; - b) bootstrap_alias=${OPTARG} ;; - d) server_alias=${OPTARG} ;; - j) key_store_server_file=${OPTARG} ;; - k) key_store_client_file=${OPTARG} ;; - c) client_key_store_pwd=${OPTARG} ;; - w) server_key_store_pwd=${OPTARG} ;; - w) root_key_alias=${OPTARG} ;; - esac -done - -# cd to dir of script -script_dir=$(dirname $0) -echo "script_dir: $script_dir" -cd $script_dir -# source the properties: -. ./lwM2M_keygen.properties - -if [ -n "$client_pref" ]; then - CLIENT_PREFIX=$client_pref -fi - -if [ -z "$client_start" ]; then - client_start=0 -fi - -if [ -z "$client_finish" ]; then - client_finish=1 -fi - -if [ -n "$client_alias_pref" ]; then - CLIENT_ALIAS_PREFIX=$client_alias_pref -fi - -if [ -n "$client_self_alias_pref" ]; then - CLIENT_SELF_ALIAS_PREFIX=$client_self_alias_pref -fi - -if [ -n "$bootstrap_alias" ]; then - BOOTSTRAP_ALIAS=$bootstrap_alias -fi - -if [ -n "$server_alias" ]; then - SERVER_ALIAS=$server_alias -fi - -if [ -n "$key_store_server_file" ]; then - SERVER_STORE=$key_store_server_file -fi - -if [ -n "$key_store_client_file" ]; then - CLIENT_STORE=$key_store_client_file -fi - -if [ -n "$client_key_store_pwd" ]; then - CLIENT_STORE_PWD=$client_key_store_pwd -fi - -if [ -n "$server_key_store_pwd" ]; then - SERVER_STORE_PWD=$server_key_store_pwd -fi - -if [ -n "$root_key_alias" ]; then - ROOT_KEY_ALIAS=$root_key_alias -fi - -CLIENT_NUMBER=$client_start - -echo "==Start==" -echo "CLIENT_PREFIX: $CLIENT_PREFIX" -echo "client_start: $client_start" -echo "client_finish: $client_finish" -echo "CLIENT_ALIAS_PREFIX: $CLIENT_ALIAS_PREFIX" -echo "CLIENT_SELF_ALIAS_PREFIX: $CLIENT_SELF_ALIAS_PREFIX" -echo "BOOTSTRAP_ALIAS: $BOOTSTRAP_ALIAS" -echo "SERVER_ALIAS: $SERVER_ALIAS" -echo "SERVER_STORE: $SERVER_STORE" -echo "CLIENT_STORE: $CLIENT_STORE" -echo "CLIENT_STORE_PWD: $CLIENT_STORE_PWD" -echo "SERVER_STORE_PWD: $SERVER_STORE_PWD" -echo "CLIENT_NUMBER: $CLIENT_NUMBER" -echo "ROOT_KEY_ALIAS: $ROOT_KEY_ALIAS" - -end_point() { - echo "$CLIENT_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -client_alias_point() { - echo "$CLIENT_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -client_self_alias_point() { - echo "$CLIENT_SELF_ALIAS_PREFIX$(printf "%08d" $CLIENT_NUMBER)" -} - -# Generation of the keystore. -echo "${H0}====START========${RESET}" -echo "${H1}Server Keystore : ${RESET}" -echo "${H1}==================${RESET}" -echo "${H2}Creating the trusted root CA key and certificate...${RESET}" -# -keysize -# 1024 (when using -genkeypair) -keytool \ - -genkeypair \ - -alias $ROOT_KEY_ALIAS \ - -keyalg EC \ - -dname "CN=$ROOT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -echo -echo "${H2}Creating server key and self-signed certificate ...${RESET}" -keytool \ - -genkeypair \ - -alias $SERVER_ALIAS \ - -keyalg EC \ - -dname "CN=$SERVER_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD -keytool \ - -exportcert \ - -alias $SERVER_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $SERVER_SELF_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -noprompt - -echo -echo "${H2}Creating server certificate signed by root CA...${RESET}" -keytool \ - -certreq \ - -alias $SERVER_ALIAS \ - -dname "CN=$SERVER_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $SERVER_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -echo -echo "${H2}Creating bootstrap key and self-signed certificate ...${RESET}" -keytool \ - -genkeypair \ - -alias $BOOTSTRAP_ALIAS \ - -keyalg EC \ - -dname "CN=$BOOTSTRAP_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $SERVER_STORE_PWD \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD -keytool \ - -exportcert \ - -alias $BOOTSTRAP_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $BOOTSTRAP_SELF_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -noprompt - -echo -echo "${H2}Creating bootstrap certificate signed by root CA...${RESET}" -keytool \ - -certreq \ - -alias $BOOTSTRAP_ALIAS \ - -dname "CN=$BOOTSTRAP_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $BOOTSTRAP_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "${H2}Import root certificate just to be able to import need by root CA with expected CN to $CLIENT_STORE${RESET}" - keytool \ - -exportcert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD | - keytool \ - -importcert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -fi - -cert_end_point() { - echo - echo "${H1}Client Keystore : ${RESET}" - echo "${H1}==================${RESET}" - echo "${H2}Creating client key and self-signed certificate with expected CN CLIENT_ALIAS: $CLIENT_ALIAS${RESET}" - keytool \ - -genkeypair \ - -alias $CLIENT_ALIAS \ - -keyalg EC \ - -dname "CN=$CLIENT_SELF_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -validity $VALIDITY \ - -storetype $STORETYPE \ - -keypass $CLIENT_STORE_PWD \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD - keytool \ - -exportcert \ - -alias $CLIENT_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD | - keytool \ - -importcert \ - -alias $CLIENT_SELF_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -# -# echo -# echo "${H2}Import root certificate just to be able to import ned by root CA with expected CN...${RESET}" -# keytool \ -# -exportcert \ -# -alias $ROOT_KEY_ALIAS \ -# -keystore $SERVER_STORE \ -# -storepass $SERVER_STORE_PWD | -# keytool \ -# -importcert \ -# -alias $ROOT_KEY_ALIAS \ -# -keystore $CLIENT_STORE \ -# -storepass $CLIENT_STORE_PWD \ -# -noprompt -# - - echo - echo "${H2}Creating client certificate signed by root CA with expected CN CLIENT_ALIAS: $CLIENT_ALIAS CLIENT_CN: $CLIENT_CN${RESET}" - keytool \ - -certreq \ - -alias $CLIENT_ALIAS \ - -dname "CN=$CLIENT_CN, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD | - keytool \ - -gencert \ - -alias $ROOT_KEY_ALIAS \ - -keystore $SERVER_STORE \ - -storepass $SERVER_STORE_PWD \ - -storetype $STORETYPE \ - -validity $VALIDITY | - keytool \ - -importcert \ - -alias $CLIENT_ALIAS \ - -keystore $CLIENT_STORE \ - -storepass $CLIENT_STORE_PWD \ - -noprompt -} - -if [ "$client_start" -lt "$client_finish" ]; then - - echo - echo "==Start Client==" - while [ "$CLIENT_NUMBER" -lt "$client_finish" ]; do - echo "number $CLIENT_NUMBER" - echo "finish $client_finish" - CLIENT_CN=$(end_point) - CLIENT_ALIAS=$(client_alias_point) - CLIENT_SELF_ALIAS=$(client_self_alias_point) - echo "CLIENT_CN $CLIENT_CN" - echo "CLIENT_ALIAS $CLIENT_ALIAS" - echo "CLIENT_SELF_ALIAS $CLIENT_SELF_ALIAS" - cert_end_point - CLIENT_NUMBER=$(($CLIENT_NUMBER + 1)) - echo - done -fi - -echo -echo "${H0}!!! Warning ${H2}Migrate ${H1}${SERVER_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" -keytool \ - -importkeystore \ - -srckeystore $SERVER_STORE \ - -destkeystore $SERVER_STORE \ - -deststoretype pkcs12 \ - -srcstorepass $SERVER_STORE_PWD - -if [ "$client_start" -lt "$client_finish" ]; then - echo - echo "${H0}!!! Warning ${H2}Migrate ${H1}${CLIENT_STORE} ${H2}to ${H1}PKCS12 ${H2}which is an industry standard format..${RESET}" - keytool \ - -importkeystore \ - -srckeystore $CLIENT_STORE \ - -destkeystore $CLIENT_STORE \ - -deststoretype pkcs12 \ - -srcstorepass $CLIENT_STORE_PWD -fi diff --git a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties b/transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties deleted file mode 100644 index 7b3cd9c09a..0000000000 --- a/transport/lwm2m/src/main/data/credentials/shell/lwM2M_keygen.properties +++ /dev/null @@ -1,57 +0,0 @@ -# -# Copyright © 2016-2017 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. -# - -# Keystore common parameters -ROOT_KEY_ALIAS=rootCA -DOMAIN_SUFFIX="$(hostname)" -ROOT_CN="$DOMAIN_SUFFIX $ROOT_KEY_ALIAS" -ORGANIZATIONAL_UNIT=Thingsboard -ORGANIZATION=Thingsboard -CITY=SF -STATE_OR_PROVINCE=CA -TWO_LETTER_COUNTRY_CODE=US -VALIDITY=36500 #days -STORETYPE="JKS" - -#Server -SERVER_STORE=serverKeyStore1.jks -SERVER_STORE_PWD=server_ks_password1 -SERVER_ALIAS=server1 -SERVER_CN="$DOMAIN_SUFFIX server LwM2M signed by root CA" -SERVER_SELF_ALIAS=server_self_signed -SERVER_SELF_CN="$DOMAIN_SUFFIX server LwM2M self-signed" -BOOTSTRAP_ALIAS=bootstrap1 -BOOTSTRAP_CN="$DOMAIN_SUFFIX bootstrap server LwM2M signed by root CA" -BOOTSTRAP_SELF_ALIAS=bootstrap_self_signed -BOOTSTRAP_SELF_CN="$DOMAIN_SUFFIX bootstrap server LwM2M self-signed" - -# Client -CLIENT_STORE=clientKeyStore1.jks -CLIENT_STORE_PWD=client_ks_password1 -CLIENT_ALIAS_PREFIX=client_alias_1 -CLIENT_PREFIX=LwX509___ -CLIENT_SELF_ALIAS_PREFIX=client_self_signed_1 -CLIENT_SELF_CN="$DOMAIN_SUFFIX client LwM2M self-signed" - -# Color output stuff -red=`tput setaf 1` -green=`tput setaf 2` -blue=`tput setaf 4` -bold=`tput bold` -H0=${red}${bold} -H1=${green}${bold} -H2=${blue} -RESET=`tput sgr0` diff --git a/transport/lwm2m/src/main/data/lwm2mserver.jks b/transport/lwm2m/src/main/data/lwm2mserver.jks deleted file mode 100644 index 301f4e2c3ab90931eb166859e71b2c443fa35660..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4017 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 diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 631968ad35..d7513bdf1f 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -114,7 +114,7 @@ transport: # Server X509 Certificates support credentials: # Whether to enable LWM2M server X509 Certificate/RPK support - enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:false}" # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) type: "${LWM2M_SERVER_CREDENTIALS_TYPE:PEM}" # PEM server credentials @@ -150,7 +150,7 @@ transport: # Bootstrap server X509 Certificates support credentials: # Whether to enable LWM2M bootstrap server X509 Certificate/RPK support - enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:false}" # Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore) type: "${LWM2M_BS_CREDENTIALS_TYPE:PEM}" # PEM server credentials @@ -177,19 +177,19 @@ transport: # X509 trust certificates trust-credentials: # Whether to load X509 trust certificates - enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:true}" + enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:false}" # Trust certificates store type (PEM - pem certificates file; KEYSTORE - java keystore) type: "${LWM2M_TRUST_CREDENTIALS_TYPE:PEM}" # PEM certificates pem: # Path to the certificates file (holds trust certificates) - cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mserver.pem}" + cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mtruststorechain.pem}" # Keystore with trust certificates keystore: # Type of the key store type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}" # Path to the key store that holds the X509 certificates - store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mserver.jks}" + store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mtruststorechain.jks}" # Password used to access the key store store_password: "${LWM2M_TRUST_KEY_STORE_PASSWORD:server_ks_password}" recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}" From ff56e0e6220e349515f929159503f49e3695ef9f Mon Sep 17 00:00:00 2001 From: van-vanich Date: Thu, 30 Dec 2021 17:48:51 +0200 Subject: [PATCH 25/72] realize solve problems for issue for with saving last ts --- .../rule/engine/telemetry/TbMsgTimeseriesNode.java | 4 ++-- .../engine/telemetry/TbMsgTimeseriesNodeConfiguration.java | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 1d52b7cdfa..711b7c1558 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -49,7 +49,7 @@ import java.util.concurrent.TimeUnit; "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' timestamp will be applied. " + "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.", uiResources = {"static/rulenode/rulenode-core-config.js"}, - configDirective = "tbActionNodeTimeseriesConfig", + configDirective = "tbActionNodeTimeseriesConfig!", icon = "file_upload" ) public class TbMsgTimeseriesNode implements TbNode { @@ -77,7 +77,7 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } - long ts = getTs(msg); + long ts = config.isSaveWithMsgTs() ? msg.getTs() : getTs(msg); String src = msg.getData(); Map> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); if (tsKvMap.isEmpty()) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java index bb661df905..89abac99fe 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java @@ -23,12 +23,14 @@ public class TbMsgTimeseriesNodeConfiguration implements NodeConfiguration Date: Fri, 31 Dec 2021 13:20:40 +0200 Subject: [PATCH 26/72] UI: Fixed help link for OTA updates --- ui-ngx/src/app/shared/models/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 9bf120f8b7..9b1b110a84 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -124,7 +124,7 @@ export const HelpLinks = { rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', resources: helpBaseUrl + '/docs/user-guide/ui/resources', dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', - otaUpdates: helpBaseUrl + '/docs/user-guide/ui/ota-updates', + otaUpdates: helpBaseUrl + '/docs/user-guide/ota-updates', widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles', widgetsConfig: helpBaseUrl + '/docs/user-guide/ui/dashboards#widget-configuration', widgetsConfigTimeseries: helpBaseUrl + '/docs/user-guide/ui/dashboards#timeseries', From bbab11d5aa2d01a4d11a5d92bdefc3047ea5466f Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 4 Jan 2022 13:03:26 +0200 Subject: [PATCH 27/72] UI: Fixed cancel event on color picker --- .../shared/components/dialog/color-picker-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts index 499f857df8..2374749cbc 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -67,7 +67,7 @@ export class ColorPickerDialogComponent extends DialogComponent Date: Wed, 5 Jan 2022 13:58:58 +0200 Subject: [PATCH 28/72] add test for sequential timeseries persistence and improve code for TbMsgTimeseriesNode rule node and config --- .../server/service/ServiceSqlTestSuite.java | 1 + .../SequentialTimeseriesPersistenceTest.java | 194 ++++++++++++++++++ .../engine/telemetry/TbMsgTimeseriesNode.java | 6 +- .../TbMsgTimeseriesNodeConfiguration.java | 4 +- 4 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java index 508272085c..d3ad80f614 100644 --- a/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/service/ServiceSqlTestSuite.java @@ -23,6 +23,7 @@ import org.thingsboard.server.queue.memory.InMemoryStorage; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ "org.thingsboard.server.service.resource.sql.*Test", + "org.thingsboard.server.service.sql.*Test" }) public class ServiceSqlTestSuite { diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java new file mode 100644 index 0000000000..9e6b6f0bc1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -0,0 +1,194 @@ +package org.thingsboard.server.service.sql; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; +import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNodeConfiguration; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.common.msg.TbMsgDataType; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.timeseries.TimeseriesService; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DaoSqlTest +@Slf4j +public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest { + + static final int TIMEOUT = 30; + + final String TOTALIZER = "Totalizer"; + final int TTL = 99999; + final String GENERIC_CUMULATIVE_OBJ = "genericCumulativeObj"; + final List ts = List.of(10L, 20L, 30L, 40L, 60L, 70L, 50L, 80L); + final List msgValue = List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L); + + @Autowired + TimeseriesService timeseriesService; + + TbMsgTimeseriesNodeConfiguration configuration; + Tenant savedTenant; + User tenantAdmin; + + @Before + public void beforeTest() throws Exception { + configuration = new TbMsgTimeseriesNodeConfiguration(); + configuration.setIgnoreMetadataTs(true); + + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk()); + } + + @Test + public void testSequentialTimeseriesPersistence() throws Exception { + Asset asset = saveAsset("Asset"); + + Device deviceA = saveDevice("Device A"); + Device deviceB = saveDevice("Device B"); + Device deviceC = saveDevice("Device C"); + Device deviceD = saveDevice("Device D"); + List devices = List.of(deviceA, deviceB, deviceC, deviceD); + + for (int i = 0; i < 2; i++) { + int idx = i * (devices.size()); + saveLatestTsForAssetAndDevice(devices, asset, idx); + checkDiffBetweenLatestTsForDevicesAndAsset(devices, asset); + } + } + + Device saveDevice(String name) throws Exception { + Device device = new Device(); + device.setName(name); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + Assert.assertNotNull(savedDevice); + return savedDevice; + } + + Asset saveAsset(String name) throws Exception { + Asset asset = new Asset(); + asset.setName(name); + asset.setType("default"); + Asset savedAsset = doPost("/api/asset", asset, Asset.class); + Assert.assertNotNull(savedAsset); + return savedAsset; + } + + private void saveLatestTsForAssetAndDevice(List devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException { + for (Device device : devices) { + TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), + device.getId(), + getTbMsgMetadata(device.getName(), ts.get(idx)), + TbMsgDataType.JSON, + getTbMsgData(msgValue.get(idx))); + saveDeviceTsEntry(device.getId(), tbMsg, msgValue.get(idx)); + saveAssetTsEntry(asset, device.getName(), msgValue.get(idx), TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isIgnoreMetadataTs())); + idx++; + } + } + + void checkDiffBetweenLatestTsForDevicesAndAsset(List devices, Asset asset) throws ExecutionException, InterruptedException, TimeoutException { + TsKvEntry assetTsKvEntry = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ); + Assert.assertTrue(assetTsKvEntry.getJsonValue().isPresent()); + JsonObject assetJsonObject = new JsonParser().parse(assetTsKvEntry.getJsonValue().get()).getAsJsonObject(); + for (Device device : devices) { + Long assetValue = assetJsonObject.get(device.getName()).getAsLong(); + TsKvEntry deviceLatest = getTsKvLatest(device.getId(), TOTALIZER); + Assert.assertTrue(deviceLatest.getLongValue().isPresent()); + Long deviceValue = deviceLatest.getLongValue().get(); + Assert.assertEquals(assetValue, deviceValue); + } + } + + String getTbMsgData(long value) { + return "{\"Totalizer\": " + value + "}"; + } + + TbMsgMetaData getTbMsgMetadata(String name, long ts) { + Map metadata = new HashMap<>(); + metadata.put("deviceName", name); + metadata.put("ts", String.valueOf(ts)); + return new TbMsgMetaData(metadata); + } + + void saveDeviceTsEntry(EntityId entityId, TbMsg tbMsg, long value) throws ExecutionException, InterruptedException, TimeoutException { + TsKvEntry tsKvEntry = new BasicTsKvEntry(TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isIgnoreMetadataTs()), new LongDataEntry(TOTALIZER, value)); + saveTimeseries(entityId, tsKvEntry); + } + + void saveAssetTsEntry(Asset asset, String key, long value, long ts) throws ExecutionException, InterruptedException, TimeoutException { + Optional tsKvEntryOpt = getTsKvLatest(asset.getId(), GENERIC_CUMULATIVE_OBJ).getJsonValue(); + TsKvEntry saveTsKvEntry = new BasicTsKvEntry(ts, new JsonDataEntry(GENERIC_CUMULATIVE_OBJ, getJsonObject(key, value, tsKvEntryOpt).toString())); + saveTimeseries(asset.getId(), saveTsKvEntry); + } + + @NotNull + private JsonObject getJsonObject(String key, long value, Optional tsKvEntryOpt) { + JsonObject jsonObject = new JsonObject(); + if (tsKvEntryOpt.isPresent()) { + jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject(); + } + jsonObject.addProperty(key, value); + return jsonObject; + } + + private void saveTimeseries(EntityId entityId, TsKvEntry saveTsKvEntry) throws InterruptedException, ExecutionException, TimeoutException { + timeseriesService.save(savedTenant.getId(), entityId, List.of(saveTsKvEntry), TTL).get(TIMEOUT, TimeUnit.SECONDS); + } + + TsKvEntry getTsKvLatest(EntityId entityId, String key) throws InterruptedException, ExecutionException, TimeoutException { + List tsKvEntries = timeseriesService.findLatest( + savedTenant.getTenantId(), + entityId, + List.of(key)).get(TIMEOUT, TimeUnit.SECONDS); + Assert.assertEquals(1, tsKvEntries.size()); + return tsKvEntries.get(0); + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 711b7c1558..47cf0f1bf2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -77,7 +77,7 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } - long ts = config.isSaveWithMsgTs() ? msg.getTs() : getTs(msg); + long ts = computeTs(msg, config.isIgnoreMetadataTs()); String src = msg.getData(); Map> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); if (tsKvMap.isEmpty()) { @@ -102,6 +102,10 @@ public class TbMsgTimeseriesNode implements TbNode { } } + public static long computeTs(TbMsg msg, boolean saveWithMsgTs) { + return saveWithMsgTs ? System.currentTimeMillis() : getTs(msg); + } + public static long getTs(TbMsg msg) { long ts = -1; String tsStr = msg.getMetaData().getValue("ts"); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java index 89abac99fe..6c9f7ac884 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java @@ -23,14 +23,14 @@ public class TbMsgTimeseriesNodeConfiguration implements NodeConfiguration Date: Wed, 5 Jan 2022 14:48:45 +0200 Subject: [PATCH 29/72] improve node details and add license for test class --- .../sql/SequentialTimeseriesPersistenceTest.java | 15 +++++++++++++++ .../engine/telemetry/TbMsgTimeseriesNode.java | 15 ++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index 9e6b6f0bc1..c360adf509 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.thingsboard.server.service.sql; import com.google.gson.JsonObject; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 47cf0f1bf2..2334851a08 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -46,10 +46,15 @@ import java.util.concurrent.TimeUnit; configClazz = TbMsgTimeseriesNodeConfiguration.class, nodeDescription = "Saves timeseries data", nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type. " + - "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' timestamp will be applied. " + - "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.", + "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' message timestamp will be applied. " + + "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.\n " + + "Enable 'ignoreMetadataTs' param to ignore the timestamp that arrives from message metadata. " + + "Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).\n" + + "For example, if you count number of messages from multiple devices into asset time-series value. " + + "Typically, you fetch the previous value of the counter, increment it and then save the value. " + + "If you use timestamp of the original message, the value may be ignored, since it has outdated timestamp comparing to the previous message.", uiResources = {"static/rulenode/rulenode-core-config.js"}, - configDirective = "tbActionNodeTimeseriesConfig!", + configDirective = "tbActionNodeTimeseriesConfig", icon = "file_upload" ) public class TbMsgTimeseriesNode implements TbNode { @@ -102,8 +107,8 @@ public class TbMsgTimeseriesNode implements TbNode { } } - public static long computeTs(TbMsg msg, boolean saveWithMsgTs) { - return saveWithMsgTs ? System.currentTimeMillis() : getTs(msg); + public static long computeTs(TbMsg msg, boolean ignoreMetadataTs) { + return ignoreMetadataTs ? System.currentTimeMillis() : getTs(msg); } public static long getTs(TbMsg msg) { From 93bf95c17a63cf5e40e898172fb2a91d3f8ce28b Mon Sep 17 00:00:00 2001 From: van-vanich Date: Wed, 5 Jan 2022 15:00:13 +0200 Subject: [PATCH 30/72] improve code style --- .../sql/SequentialTimeseriesPersistenceTest.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index c360adf509..e4fb0318b4 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.sql; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Assert; @@ -55,7 +54,6 @@ import java.util.concurrent.TimeoutException; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest -@Slf4j public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest { static final int TIMEOUT = 30; @@ -112,7 +110,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest List devices = List.of(deviceA, deviceB, deviceC, deviceD); for (int i = 0; i < 2; i++) { - int idx = i * (devices.size()); + int idx = i * devices.size(); saveLatestTsForAssetAndDevice(devices, asset, idx); checkDiffBetweenLatestTsForDevicesAndAsset(devices, asset); } @@ -136,7 +134,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest return savedAsset; } - private void saveLatestTsForAssetAndDevice(List devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException { + void saveLatestTsForAssetAndDevice(List devices, Asset asset, int idx) throws ExecutionException, InterruptedException, TimeoutException { for (Device device : devices) { TbMsg tbMsg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), device.getId(), @@ -185,7 +183,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest } @NotNull - private JsonObject getJsonObject(String key, long value, Optional tsKvEntryOpt) { + JsonObject getJsonObject(String key, long value, Optional tsKvEntryOpt) { JsonObject jsonObject = new JsonObject(); if (tsKvEntryOpt.isPresent()) { jsonObject = new JsonParser().parse(tsKvEntryOpt.get()).getAsJsonObject(); @@ -194,7 +192,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest return jsonObject; } - private void saveTimeseries(EntityId entityId, TsKvEntry saveTsKvEntry) throws InterruptedException, ExecutionException, TimeoutException { + void saveTimeseries(EntityId entityId, TsKvEntry saveTsKvEntry) throws InterruptedException, ExecutionException, TimeoutException { timeseriesService.save(savedTenant.getId(), entityId, List.of(saveTsKvEntry), TTL).get(TIMEOUT, TimeUnit.SECONDS); } From b64eccf3332a2812a931ef697af44a5463d7f0b7 Mon Sep 17 00:00:00 2001 From: desoliture Date: Wed, 5 Jan 2022 16:24:12 +0200 Subject: [PATCH 31/72] add noXss validation for ruleNodes name --- .../java/org/thingsboard/server/common/data/rule/RuleNode.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java index 8201807808..8904d2821d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @Data @@ -41,6 +42,7 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo impl @Length(fieldName = "type") @ApiModelProperty(position = 4, value = "Full Java Class Name of the rule node implementation. ", example = "com.mycompany.iot.rule.engine.ProcessingNode") private String type; + @NoXss @Length(fieldName = "name") @ApiModelProperty(position = 5, value = "User defined name of the rule node. Used on UI and for logging. ", example = "Process sensor reading") private String name; From ec602248c0cc22da1461e50176fdbca2ebf75519 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 5 Jan 2022 16:28:37 +0200 Subject: [PATCH 32/72] lwm2m validate trust certs --- .../TbLwM2MDtlsCertificateVerifier.java | 50 ++++++++++--------- .../config/ssl/AbstractSslCredentials.java | 26 +++++++--- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java index bae7522c78..babf385bc8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java @@ -50,13 +50,22 @@ import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore; import javax.annotation.PostConstruct; import javax.security.auth.x500.X500Principal; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateFactory; import java.security.cert.CertificateNotYetValidException; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.Collections; import java.util.List; import static org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer.CLIENT; @@ -119,8 +128,8 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer TbLwM2MSecurityInfo securityInfo = null; // verify if trust - if (config.getTrustSslCredentials().getTrustedCertificates().length > 0) { - if (verifyIssuer(cert, config.getTrustSslCredentials().getTrustedCertificates()) != null) { + if (config.getTrustSslCredentials() != null && config.getTrustSslCredentials().getTrustedCertificates().length > 0) { + if (verifyTrust(cert, config.getTrustSslCredentials().getTrustedCertificates()) != null) { String endpoint = config.getTrustSslCredentials().getValueFromSubjectNameByKey(cert.getSubjectX500Principal().getName(), "CN"); securityInfo = StringUtils.isNotEmpty(endpoint) ? securityInfoValidator.getEndpointSecurityInfoByCredentialsId(endpoint, CLIENT) : null; } @@ -193,31 +202,26 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer } - private X509Certificate verifyIssuer(X509Certificate certificate, X509Certificate[] certificates) { - String issuerCN = config.getTrustSslCredentials().getValueFromSubjectNameByKey(certificate.getIssuerX500Principal().getName(), "CN"); - if (!StringUtils.isBlank(issuerCN)) { + private X509Certificate verifyTrust(X509Certificate certificate, X509Certificate[] certificates) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + CertPath cp = cf.generateCertPath(Arrays.asList(new X509Certificate[]{certificate})); for (int index = 0; index < certificates.length; ++index) { - X509Certificate trust = certificates[index]; - String trustCN = config.getTrustSslCredentials().getValueFromSubjectNameByKey(trust.getSubjectX500Principal().getName(), "CN"); - if (!StringUtils.isBlank(trustCN) && issuerCN.length() >= trustCN.length() && issuerCN.substring(issuerCN.length()-trustCN.length()).equals(trustCN)) { - if (verifyCertificate(certificate)) { - return certificate; - } + X509Certificate caCert = certificates[index]; + try { + TrustAnchor trustAnchor = new TrustAnchor(caCert, null); + CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); + PKIXParameters pkixParams = new PKIXParameters( + Collections.singleton(trustAnchor)); + pkixParams.setRevocationEnabled(false); + if (cpv.validate(cp, pkixParams) != null) return certificate; + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertPathValidatorException e) { + log.trace("[{}]. [{}]", certificate.getSubjectDN(), e.getMessage()); } } + } catch (CertificateException e) { + log.trace("[{}] certPath not valid. [{}]", certificate.getSubjectDN(), e.getMessage()); } return null; } - - private static boolean verifyCertificate(X509Certificate certificate) { - try { - // date - certificate.checkValidity(); - // Validate X509. - SecurityUtil.certificate.decode(certificate.getEncoded()); - return true; - } catch (Exception e) { - return false; - } - } } diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java index 01b7242805..3170aea9a2 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java @@ -61,7 +61,7 @@ public abstract class AbstractSslCredentials implements SslCredentials { this.keyPasswordArray = keyPassword.toCharArray(); } this.keyStore = this.loadKeyStore(trustsOnly, this.keyPasswordArray); - Set trustedCerts = getTrustedCerts(this.keyStore); + Set trustedCerts = getTrustedCerts(this.keyStore, trustsOnly); this.trusts = trustedCerts.toArray(new X509Certificate[0]); if (!trustsOnly) { PrivateKeyEntry privateKeyEntry = null; @@ -179,7 +179,7 @@ public abstract class AbstractSslCredentials implements SslCredentials { return entry; } - private static Set getTrustedCerts(KeyStore ks) { + private static Set getTrustedCerts(KeyStore ks, boolean trustsOnly) { Set set = new HashSet<>(); try { for (Enumeration e = ks.aliases(); e.hasMoreElements(); ) { @@ -187,19 +187,33 @@ public abstract class AbstractSslCredentials implements SslCredentials { if (ks.isCertificateEntry(alias)) { Certificate cert = ks.getCertificate(alias); if (cert instanceof X509Certificate) { - set.add((X509Certificate)cert); + if (trustsOnly) { + // is CA certificate + if (((X509Certificate) cert).getBasicConstraints()>=0) { + set.add((X509Certificate) cert); + } + } else { + set.add((X509Certificate) cert); + } } } else if (ks.isKeyEntry(alias)) { Certificate[] certs = ks.getCertificateChain(alias); if ((certs != null) && (certs.length > 0) && (certs[0] instanceof X509Certificate)) { - set.add((X509Certificate)certs[0]); + if (trustsOnly) { + for (Certificate cert : certs) { + // is CA certificate + if (((X509Certificate) cert).getBasicConstraints()>=0) { + set.add((X509Certificate) cert); + } + } + } else { + set.add((X509Certificate)certs[0]); + } } } } } catch (KeyStoreException ignored) {} return Collections.unmodifiableSet(set); } - - } From e5e79a22f6e5ac0ae48634a77b67a841c61346f8 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 5 Jan 2022 18:11:34 +0200 Subject: [PATCH 33/72] lwm2m tests with credentials RPK and X509 - ignore --- .../security/sql/RpkLwM2MIntegrationTest.java | 2 ++ .../sql/X509_NoTrustLwM2MIntegrationTest.java | 2 ++ .../sql/X509_TrustLwM2MIntegrationTest.java | 2 ++ .../resources/application-test.properties | 34 +++++++++---------- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index 9e74beaa6b..87ca415bd0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -29,6 +30,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { + @Ignore @Test public void testConnectWithRPKAndObserveTelemetry() throws Exception { RPKClientCredential rpkClientCredentials = new RPKClientCredential(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index f55c21dcc2..d1bc813b50 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredential; import org.thingsboard.server.common.transport.util.SslUtil; @@ -28,6 +29,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { + @Ignore @Test public void testConnectWithCertAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java index 32d176d598..7c5e48a4b7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -27,6 +28,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { + @Ignore @Test public void testConnectAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 651b00ed67..856745797a 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -1,20 +1,20 @@ -transport.lwm2m.server.security.credentials.enabled=true -transport.lwm2m.server.security.credentials.type=KEYSTORE -transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks -transport.lwm2m.server.security.credentials.keystore.store_password=server -transport.lwm2m.server.security.credentials.keystore.key_alias=server -transport.lwm2m.server.security.credentials.keystore.key_password=server -transport.lwm2m.bootstrap.enabled=false -transport.lwm2m.bootstrap.security.credentials.enabled=true -transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE -transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks -transport.lwm2m.bootstrap.security.credentials.keystore.store_password=server -transport.lwm2m.bootstrap.security.credentials.keystore.key_alias=server -transport.lwm2m.bootstrap.security.credentials.keystore.key_password=server -transport.lwm2m.security.trust-credentials.enabled=true -transport.lwm2m.security.trust-credentials.type=KEYSTORE -transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks -transport.lwm2m.security.trust-credentials.keystore.store_password=server +#transport.lwm2m.server.security.credentials.enabled=true +#transport.lwm2m.server.security.credentials.type=KEYSTORE +#transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +#transport.lwm2m.server.security.credentials.keystore.store_password=server +#transport.lwm2m.server.security.credentials.keystore.key_alias=server +#transport.lwm2m.server.security.credentials.keystore.key_password=server +#transport.lwm2m.bootstrap.enabled=false +#transport.lwm2m.bootstrap.security.credentials.enabled=true +#transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE +#transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +#transport.lwm2m.bootstrap.security.credentials.keystore.store_password=server +#transport.lwm2m.bootstrap.security.credentials.keystore.key_alias=server +#transport.lwm2m.bootstrap.security.credentials.keystore.key_password=server +#transport.lwm2m.security.trust-credentials.enabled=true +#transport.lwm2m.security.trust-credentials.type=KEYSTORE +#transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +#transport.lwm2m.security.trust-credentials.keystore.store_password=server edges.enabled=true edges.storage.no_read_records_sleep=500 From 659d8abf35fa6f41024f70f299b88e96ce96e302 Mon Sep 17 00:00:00 2001 From: van-vanich Date: Wed, 5 Jan 2022 18:15:16 +0200 Subject: [PATCH 34/72] improve node details and improve field naming --- .../sql/SequentialTimeseriesPersistenceTest.java | 6 +++--- .../rule/engine/telemetry/TbMsgTimeseriesNode.java | 13 ++++++++----- .../telemetry/TbMsgTimeseriesNodeConfiguration.java | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java index e4fb0318b4..e0a0260f7f 100644 --- a/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sql/SequentialTimeseriesPersistenceTest.java @@ -74,7 +74,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest @Before public void beforeTest() throws Exception { configuration = new TbMsgTimeseriesNodeConfiguration(); - configuration.setIgnoreMetadataTs(true); + configuration.setUseServerTs(true); loginSysAdmin(); @@ -142,7 +142,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest TbMsgDataType.JSON, getTbMsgData(msgValue.get(idx))); saveDeviceTsEntry(device.getId(), tbMsg, msgValue.get(idx)); - saveAssetTsEntry(asset, device.getName(), msgValue.get(idx), TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isIgnoreMetadataTs())); + saveAssetTsEntry(asset, device.getName(), msgValue.get(idx), TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isUseServerTs())); idx++; } } @@ -172,7 +172,7 @@ public class SequentialTimeseriesPersistenceTest extends AbstractControllerTest } void saveDeviceTsEntry(EntityId entityId, TbMsg tbMsg, long value) throws ExecutionException, InterruptedException, TimeoutException { - TsKvEntry tsKvEntry = new BasicTsKvEntry(TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isIgnoreMetadataTs()), new LongDataEntry(TOTALIZER, value)); + TsKvEntry tsKvEntry = new BasicTsKvEntry(TbMsgTimeseriesNode.computeTs(tbMsg, configuration.isUseServerTs()), new LongDataEntry(TOTALIZER, value)); saveTimeseries(entityId, tsKvEntry); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 2334851a08..2c426dfdec 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -48,11 +48,14 @@ import java.util.concurrent.TimeUnit; nodeDetails = "Saves timeseries telemetry data based on configurable TTL parameter. Expects messages with 'POST_TELEMETRY_REQUEST' message type. " + "Timestamp in milliseconds will be taken from metadata.ts, otherwise 'now' message timestamp will be applied. " + "Allows stopping updating values for incoming keys in the latest ts_kv table if 'skipLatestPersistence' is set to true.\n " + - "Enable 'ignoreMetadataTs' param to ignore the timestamp that arrives from message metadata. " + + "
" + + "Enable 'useServerTs' param to use the timestamp of the message processing instead of the timestamp from the message. " + "Useful for all sorts of sequential processing if you merge messages from multiple sources (devices, assets, etc).\n" + - "For example, if you count number of messages from multiple devices into asset time-series value. " + - "Typically, you fetch the previous value of the counter, increment it and then save the value. " + - "If you use timestamp of the original message, the value may be ignored, since it has outdated timestamp comparing to the previous message.", + "
" + + "In the case of sequential processing, the platform guarantees that the messages are processed in the order of their submission to the queue. " + + "However, the timestamp of the messages originated by multiple devices/servers may be unsynchronized long before they are pushed to the queue. " + + "The DB layer has certain optimizations to ignore the updates of the \"attributes\" and \"latest values\" tables if the new record has a timestamp that is older than the previous record. " + + "So, to make sure that all the messages will be processed correctly, one should enable this parameter for sequential message processing scenarios.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbActionNodeTimeseriesConfig", icon = "file_upload" @@ -82,7 +85,7 @@ public class TbMsgTimeseriesNode implements TbNode { ctx.tellFailure(msg, new IllegalArgumentException("Unsupported msg type: " + msg.getType())); return; } - long ts = computeTs(msg, config.isIgnoreMetadataTs()); + long ts = computeTs(msg, config.isUseServerTs()); String src = msg.getData(); Map> tsKvMap = JsonConverter.convertToTelemetry(new JsonParser().parse(src), ts); if (tsKvMap.isEmpty()) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java index 6c9f7ac884..0d59c325fa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeConfiguration.java @@ -23,14 +23,14 @@ public class TbMsgTimeseriesNodeConfiguration implements NodeConfiguration Date: Wed, 5 Jan 2022 17:47:01 +0200 Subject: [PATCH 35/72] lwm2m add to test shell credentials --- ...cfssl_chain_trusts_and_clients_for_test.sh | 299 ++++++++++++++++++ .../shell/lwm2m_cfssl_chain_for_test_All.sh | 65 ++++ .../lwm2m_cfssl_chain_server_for_test.sh | 298 +++++++++++++++++ 3 files changed, 662 insertions(+) create mode 100755 application/src/test/resources/lwm2m/credentials/shell/lwM2M_cfssl_chain_trusts_and_clients_for_test.sh create mode 100755 application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh create mode 100755 application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_server_for_test.sh diff --git a/application/src/test/resources/lwm2m/credentials/shell/lwM2M_cfssl_chain_trusts_and_clients_for_test.sh b/application/src/test/resources/lwm2m/credentials/shell/lwM2M_cfssl_chain_trusts_and_clients_for_test.sh new file mode 100755 index 0000000000..26f47266a7 --- /dev/null +++ b/application/src/test/resources/lwm2m/credentials/shell/lwM2M_cfssl_chain_trusts_and_clients_for_test.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash + +# Change working directory +cd -- "$( + dirname "${0}" +)" || exit 1 + +readonly TRUST_PATH="Trust" +readonly CA_ROOT_CERT_KEY="ca-root" +readonly CA_ROOT_ALIAS="root" +readonly CA_INTERMEDIATE_CERT_KEY_PREF="intermediate_ca" +CA_INTERMEDIATE_START=0 +CA_INTERMEDIATE_FINISH=2 +CA_INTERMEDIATE_NUMBER=${CA_INTERMEDIATE_START} +CA_INTERMEDIATE_CERT_SIGN=${CA_ROOT_CERT_KEY} +CA_LIST_CERT_FOR_CAT="" +readonly CA_TRUST_STORE_ALL_CHAIN="lwm2mtruststorechain" +readonly CA_TRUST_STORE_PWD="server_ks_password" +readonly CA_TRUST_CERT_ALIAS="root" +readonly CA_TRUST_CERT_CHAIN_JKS="lwm2mtruststorechain" +readonly CA_TRUST_STORE_CHAIN_ALIAS="trust_cert_chain_alias" + +readonly CLIENT_PATH="Client" +readonly CLIENT_JKS_FOR_TEST="lwm2mclient" +readonly CLIENT_CERT_KEY_PREF="LwX509" +readonly CLIENT_CERT_ALIAS_PREF="client_alias_" +readonly CLIENT_STORE_PWD="client_ks_password" +readonly CLIENT_HOST_NAME="thingsboard_test.io" +CLIENT_START=0 +CLIENT_FINISH=1 +CLIENT_NUMBER=${CLIENT_START} + +SERVER_HOST_NAME="localhost.localdomain" +SERVER_LOCAL_HOST_NAME="localhost" +SERVER_PUBLIC_HOST_NAMES="-" + +readonly CF_COMMANDS=" + cfssl + cfssljson +" + +if [ ! -z "$1" ]; then + CA_INTERMEDIATE_START=$1 + CA_INTERMEDIATE_NUMBER=${CA_INTERMEDIATE_START} +fi + +if [ ! -z "$2" ]; then + CA_INTERMEDIATE_FINISH=$2 +fi + +if [ ! -z "$3" ]; then + CLIENT_START=$1 + CLIENT_NUMBER=${CLIENT_START} +fi + +if [ ! -z "$4" ]; then + CLIENT_FINISH=$4 +fi + +# Change working directory +rm -rf ${TRUST_PATH} +mkdir -p ${TRUST_PATH} +rm -rf ${CLIENT_PATH} +mkdir -p ${CLIENT_PATH} +cd -- "$( + dirname "${0}" +)" || exit 1 + + +rm *.csr +rm *.p12 +rm *.json +rm *.pem +rm *.jks + +intermediate_common_name() { + echo "${CA_INTERMEDIATE_CERT_KEY_PREF}${CA_INTERMEDIATE_NUMBER}" +} + +set_list_sert_for_cat() { + local first="$1" + echo "$first ${CA_LIST_CERT_FOR_CAT}" +} + +client_common_name() { + echo "${CLIENT_CERT_KEY_PREF}$(printf "%08d" ${CLIENT_NUMBER})" +} + +client_alias_name() { + echo "${CLIENT_CERT_ALIAS_PREF}$(printf "%08d" ${CLIENT_NUMBER})" +} + +for COMMAND in ${CF_COMMANDS}; do + if ! command -v ${COMMAND} &> /dev/null; then + echo "ERROR: Missing command ${COMMAND}" >&2 + echo "Install the package from: https://pkg.cfssl.org/" >&2 + exit 1 + fi +done + +tee ./${TRUST_PATH}/ca-config.json 1> /dev/null <<-CONFIG +{ + "signing": { + "default": { + "expiry": "8760h", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] + }, + "profiles": { + "server": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + }, + "client": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + }, + "client-server": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ] + } + } + } +} +CONFIG + +tee ./${TRUST_PATH}/ca-root-to-intermediate-config.json 1> /dev/null <<-CONFIG +{ + "signing": { + "default": { + "expiry": "43800h", + "ca_constraint": { + "is_ca": true, + "max_path_len": 0, + "max_path_len_zero": true + }, + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "digital signature", + "cert sign", + "crl sign", + "signing" + ] + } + } +} +CONFIG + +echo "====================================================" +echo -e "Generate the root of certificates: \n-${CA_ROOT_KEY}-key.pem (certificate key)\n-${CA_ROOT_KEY}.pem (certificate)\n-${CA_ROOT_KEY}.csr (sign request)" +echo "====================================================" +cfssl genkey \ + -initca \ + - \ + <<-CONFIG | cfssljson -bare ./${TRUST_PATH}/${CA_ROOT_CERT_KEY} +{ + "CN": "ROOT CA", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ], + "ca": { + "expiry": "131400h" + } +} +CONFIG +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${TRUST_PATH}/${CA_ROOT_CERT_KEY}.pem) + +echo "====================================================" +echo -e "Generate and Signed the intermediates of our certificates: \n-${CA_INTERMEDIATE_CERT_KEY_PREF}?-key.pem (certificate key)\n-${CA_INTERMEDIATE_CERT_KEY_PREF}?.pem (certificate)\n-${CA_INTERMEDIATE_CERT_KEY_PREF}?.csr (sign request)" +echo "====================================================" + +while [[ ${CA_INTERMEDIATE_NUMBER} -lt ${CA_INTERMEDIATE_FINISH} ]]; +do + CA_INTERMEDIATE_CERT_KEY=$(intermediate_common_name) + CA_INTERMEDIATE_NUMBER=$((${CA_INTERMEDIATE_NUMBER} + 1)) + + cfssl gencert \ + -ca ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_SIGN}.pem \ + -ca-key ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_SIGN}-key.pem \ + -config ./${TRUST_PATH}/ca-root-to-intermediate-config.json \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY} + { + "CN": "${CA_INTERMEDIATE_CERT_KEY}", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] + } +CONFIG + #openssl x509 -in ${CA_INTERMEDIATE_CERT_KEY}.pem -text -noout + CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem) + CA_INTERMEDIATE_CERT_SIGN=${CA_INTERMEDIATE_CERT_KEY} +done + +echo "====================================================" +echo -e "Add the CA_certificate to keystore: ${CA_TRUST_CERT_CHAIN_JKS}.jks" +echo "====================================================" +cat ${CA_LIST_CERT_FOR_CAT} > ./${TRUST_PATH}/${CA_TRUST_STORE_ALL_CHAIN}.pem +openssl pkcs12 -export -in ./${TRUST_PATH}/${CA_TRUST_STORE_ALL_CHAIN}.pem -inkey ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem -out ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.p12 -name ${CA_TRUST_STORE_CHAIN_ALIAS} -CAfile ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_ALIAS} -passin pass:${CA_TRUST_STORE_PWD} -passout pass:${CA_TRUST_STORE_PWD} +keytool -importkeystore -deststorepass ${CA_TRUST_STORE_PWD} -destkeypass ${CA_TRUST_STORE_PWD} -destkeystore ./${TRUST_PATH}/${CA_TRUST_CERT_CHAIN_JKS}.jks -srckeystore ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${CA_TRUST_STORE_PWD} -alias ${CA_TRUST_STORE_CHAIN_ALIAS} + +keytool -list -v -keystore ./${TRUST_PATH}/lwm2mtruststorechain.jks -storepass server_ks_password -storetype PKCS12 + +echo "====================================================" +echo -e "Generate and Signed the clients of our certificates: \n-${CLIENT_CERT_KEY_PREF}?-key.pem (certificate key)\n-${CLIENT_CERT_KEY_PREF}?.pem (certificate)\n-${CCLIENT_CERT_KEY_PREF}?.csr (sign request)" +echo "====================================================" + + +while [[ ${CLIENT_NUMBER} -lt ${CLIENT_FINISH} ]]; +do + CLIENT_CERT_KEY=$(client_common_name) + CLIENT_CERT_ALIAS=$(client_alias_name) + CLIENT_NUMBER=$((${CLIENT_NUMBER} + 1)) + + cfssl gencert \ + -ca ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem \ + -ca-key ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem \ + -config ./${TRUST_PATH}/ca-config.json \ + -profile client \ + -hostname "${CLIENT_HOST_NAME}" \ + - \ + <<-CONFIG | cfssljson -bare ./${CLIENT_PATH}/${CLIENT_CERT_KEY} +{ + "CN": "${CLIENT_CERT_KEY}" +} +CONFIG + +echo "====================================================" +echo -e "Add the client certificate (${CLIENT_CERT_KEY}.pem) to keystore: ${CLIENT_JKS_FOR_TEST}.jks" +echo "====================================================" +cat ./${CLIENT_PATH}/${CLIENT_CERT_KEY}.pem ${CA_LIST_CERT_FOR_CAT} > ./${CLIENT_PATH}/${CLIENT_CERT_KEY}_chain.pem +openssl pkcs12 -export -in ./${CLIENT_PATH}/${CLIENT_CERT_KEY}_chain.pem -inkey ./${CLIENT_PATH}/${CLIENT_CERT_KEY}-key.pem -out ./${CLIENT_PATH}/${CLIENT_CERT_KEY}.p12 -name ${CLIENT_CERT_ALIAS} -CAfile ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_ALIAS} -passin pass:${CLIENT_STORE_PWD} -passout pass:${CLIENT_STORE_PWD} +keytool -importkeystore -deststorepass ${CLIENT_STORE_PWD} -destkeypass ${CLIENT_STORE_PWD} -destkeystore ./${CLIENT_PATH}/${CLIENT_JKS_FOR_TEST}.jks -srckeystore ./${CLIENT_PATH}/${CLIENT_CERT_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${CLIENT_STORE_PWD} -alias ${CLIENT_CERT_ALIAS} + +done + +keytool -list -v -keystore ./${CLIENT_PATH}/lwm2mclient.jks -storepass client_ks_password -storetype PKCS12 + +rm ./${TRUST_PATH}/*.p12 +rm ./${TRUST_PATH}/*.csr +rm ./${TRUST_PATH}/*.json +rm ./${TRUST_PATH}/${CA_ROOT_CERT_KEY}* +rm ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY_PREF}* + + +rm ./${CLIENT_PATH}/*.p12 2> /dev/null +rm ./${CLIENT_PATH}/*.csr 2> /dev/null diff --git a/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh new file mode 100755 index 0000000000..b3b114cb28 --- /dev/null +++ b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash + +readonly INTERMEDIATE_START=0 +readonly INTERMEDIATE_FINISH=2 +readonly CLIENT_START=0 +readonly CLIENT_FINISH=5 + +IS_IHFO=false +IS_SERVER_CREATED_KEY=true +IS_TRUST_CLIENT_CREATED_KEY=true + +cd -- "$( + dirname "${0}" +)" || exit 1 + +Help() +{ + # Display Help + echo "Description of the script functions." + echo + echo "Syntax: scriptTemplate [-g|h|v|V]" + echo "options:" + echo "h Print this Help." + echo "v Verbose mode." + echo "V Print software version and exit." + echo +} + +if [ "$1" == "-h" ] ; then + echo -e "Usage 2: ./`basename $0` \"Information is not displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 1: ./`basename $0` true \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 3: ./`basename $0` true false \"Information is displayed\" : \"Keys for the server are not generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 4: ./`basename $0` true false false \"Information is displayed\" : \"Keys for the server are not generated\" : \"Keys for the clients and trusts are not generated\"" + echo -e "Usage 4: ./`basename $0` true true false \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are not generated\"" + echo "This Help File: ./`basename $0` -h" + exit 0 +fi + +if [ -n "$1" ]; then + IS_IHFO=$1 +fi + +if [ -n "$2" ]; then + IS_SERVER_CREATED_KEY=$2 +fi + +if [ -n "$3" ]; then + IS_TRUST_CLIENT_CREATED_KEY=$3 +fi + +if [ "$IS_IHFO" = false ] ; then + if [ "$IS_SERVER_CREATED_KEY" = true ] ; then + ./lwm2m_cfssl_chain_server_for_test.sh > /dev/null 2>&1 & + fi + if [ "$IS_TRUST_CLIENT_CREATED_KEY" = true ] ; then + ./lwM2M_cfssl_chain_trusts_and_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} > /dev/null 2>&1 & + fi +else + if [ "$IS_SERVER_CREATED_KEY" = true ] ; then + ./lwm2m_cfssl_chain_server_for_test.sh + fi + if [ "$IS_TRUST_CLIENT_CREATED_KEY" = true ] ; then + ./lwM2M_cfssl_chain_trusts_and_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} + fi +fi \ No newline at end of file diff --git a/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_server_for_test.sh b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_server_for_test.sh new file mode 100755 index 0000000000..efe6ed46dd --- /dev/null +++ b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_server_for_test.sh @@ -0,0 +1,298 @@ +#!/usr/bin/env bash + +# REF: https://github.com/cloudflare/cfssl + +# Change working directory +cd -- "$( + dirname "${0}" +)" || exit 1 + +readonly CA_ROOT_CERT_KEY="ca-root" +readonly CA_ROOT_ALIAS="root" +readonly CA_INTERMEDIATE_CERT_KEY_PREF="intermediate_ca" +CA_INTERMEDIATE_NUMBER=0 +CA_LIST_CERT_FOR_CAT="" + +readonly CF_COMMANDS=" + cfssl + cfssljson +" + +readonly SERVER_JKS_FOR_TEST="lwm2mserver" +readonly STORE_PASS_PWD="server_ks_password" +readonly SERVER_PATH="Server" +readonly SERVER_CERT_KEY="lwm2mserver" +readonly SERVER_CERT_CHAIN="lwm2mserver_chain" +readonly SERVER_CERT_ALIAS="server" +readonly BS_SERVER_CERT_KEY="lwm2mserverbs" +readonly BS_SERVER_CERT_CHAIN="lwm2mserverbs_chain" +readonly BS_SERVER_CERT_ALIAS="bootstrap" + +SERVER_HOST_NAME="localhost.localdomain" +SERVER_LOCAL_HOST_NAME="localhost" +SERVER_PUBLIC_HOST_NAMES="-" + +intermediate_common_name() { + echo "${CA_INTERMEDIATE_CERT_KEY_PREF}${CA_INTERMEDIATE_NUMBER}" +} + +set_list_sert_for_cat() { + local first="$1" + echo "$first ${CA_LIST_CERT_FOR_CAT}" +} + + +# Change working directory +rm -rf ${SERVER_PATH} +mkdir -p ${SERVER_PATH} + +cd -- "$( + dirname ./${SERVER_PATH} +)" || exit 1 + + +rm *.csr +rm *.p12 +rm *.json +rm *.pem +rm *.jks + +CA_INTERMEDIATE_CERT_SIGN=${CA_ROOT_CERT_KEY} +CA_INTERMEDIATE_CERT_KEY=$(intermediate_common_name) +CA_INTERMEDIATE_NUMBER=$((${CA_INTERMEDIATE_NUMBER} + 1)) +CA_LIST_CERT_FOR_CAT="" + +for COMMAND in ${CF_COMMANDS}; do + if ! command -v ${COMMAND} &> /dev/null; then + echo "ERROR: Missing command ${COMMAND}" >&2 + echo "Install the package from: https://pkg.cfssl.org/" >&2 + exit 1 + fi +done + +tee ./${SERVER_PATH}/ca-config.json 1> /dev/null <<-CONFIG +{ + "signing": { + "default": { + "expiry": "8760h", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] + }, + "profiles": { + "server": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "server auth" + ] + }, + "client": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + }, + "client-server": { + "expiry": "43800h", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "signing", + "key encipherment", + "server auth", + "client auth" + ] + } + } + } +} +CONFIG + +tee ./${SERVER_PATH}/ca-root-to-intermediate-config.json 1> /dev/null <<-CONFIG +{ + "signing": { + "default": { + "expiry": "43800h", + "ca_constraint": { + "is_ca": true, + "max_path_len": 0, + "max_path_len_zero": true + }, + "key": { + "algo": "ecdsa", + "size": 256 + }, + "usages": [ + "digital signature", + "cert sign", + "crl sign", + "signing" + ] + } + } +} +CONFIG + +echo "====================================================" +echo -e "Generate the root of certificates: \n-${CA_ROOT_KEY}-key.pem (certificate key)\n-${CA_ROOT_KEY}.pem (certificate)\n-${CA_ROOT_KEY}.csr (sign request)" +echo "====================================================" +cfssl genkey \ + -initca \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${CA_ROOT_CERT_KEY} +{ + "CN": "ROOT CA for servers", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ], + "ca": { + "expiry": "131400h" + } +} +CONFIG +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${SERVER_PATH}/${CA_ROOT_CERT_KEY}.pem) + +echo "====================================================" +echo -e "Generate and Signed the first intermediates of our certificates: \n-${CA_INTERMEDIATE_CERT_KEY}-key.pem (certificate key)\n-${CA_INTERMEDIATE_CERT_KEY}.pem (certificate)\n-${CA_INTERMEDIATE_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl gencert \ + -ca ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_SIGN}.pem \ + -ca-key ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_SIGN}-key.pem \ + -config ./${SERVER_PATH}/ca-root-to-intermediate-config.json \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY} +{ + "CN": "${CA_INTERMEDIATE_CERT_KEY}", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] +} +CONFIG +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem) + + +## Lwm2m Server certificate +echo "====================================================" +echo -e "Generate and Signed the server certificate: \n-${SERVER_CERT_KEY}-key.pem (certificate key)\n-${SERVER_CERT_KEY}.pem (certificate)\n-${SERVER_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl gencert \ + -ca ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem \ + -ca-key ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem \ + -config ./${SERVER_PATH}/ca-config.json \ + -profile server \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${SERVER_CERT_KEY} +{ + "CN": "${SERVER_LOCAL_HOST_NAME}" +} +CONFIG + +echo "====================================================" +echo -e "Add the server certificate (${SERVER_CERT_KEY}.pem) to keystore: ${SERVER_JKS_FOR_TEST}.jks" +echo "====================================================" +cat ./${SERVER_PATH}/${SERVER_CERT_KEY}.pem ${CA_LIST_CERT_FOR_CAT} > ./${SERVER_PATH}/${SERVER_CERT_CHAIN}.pem +openssl pkcs12 -export -in ./${SERVER_PATH}/${SERVER_CERT_CHAIN}.pem -inkey ./${SERVER_PATH}/${SERVER_CERT_KEY}-key.pem -out ./${SERVER_PATH}/${SERVER_CERT_KEY}.p12 -name ${SERVER_CERT_ALIAS} -CAfile ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_ALIAS} -passin pass:${STORE_PASS_PWD} -passout pass:${STORE_PASS_PWD} +keytool -importkeystore -deststorepass ${STORE_PASS_PWD} -destkeypass ${STORE_PASS_PWD} -destkeystore ./${SERVER_PATH}/${SERVER_JKS_FOR_TEST}.jks -srckeystore ./${SERVER_PATH}/${SERVER_CERT_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${STORE_PASS_PWD} -alias ${SERVER_CERT_ALIAS} + + +CA_INTERMEDIATE_CERT_SIGN=${CA_INTERMEDIATE_CERT_KEY} +CA_INTERMEDIATE_CERT_KEY=$(intermediate_common_name) +CA_INTERMEDIATE_NUMBER=$((${CA_INTERMEDIATE_NUMBER} + 1)) +echo "====================================================" +echo -e "Generate and Signed the second intermediates of our certificates: \n-${CA_INTERMEDIATE_CERT_KEY}-key.pem (certificate key)\n-${CA_INTERMEDIATE_CERT_KEY}.pem (certificate)\n-${CA_INTERMEDIATE_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl gencert \ + -ca ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_SIGN}.pem \ + -ca-key ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_SIGN}-key.pem \ + -config ./${SERVER_PATH}/ca-root-to-intermediate-config.json \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY} +{ + "CN": "${CA_INTERMEDIATE_CERT_KEY}", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] +} +CONFIG +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem) + +## Bootstrap server certificate +echo "====================================================" +echo -e "Generate and Signed the server certificate: \n-${BS_SERVER_CERT_KEY}-key.pem (certificate key)\n-${BS_SERVER_CERT_KEY}.pem (certificate)\n-${BS_SERVER_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl gencert \ + -ca ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem \ + -ca-key ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem \ + -config ./${SERVER_PATH}/ca-config.json \ + -profile server \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${SERVER_PATH}/${BS_SERVER_CERT_KEY} +{ + "CN": "${SERVER_LOCAL_HOST_NAME}" +} +CONFIG + +echo "====================================================" +echo -e "Add the Bootstrap server certificate (${BS_SERVER_CERT_KEY}.pem) to keystore: ${SERVER_JKS_FOR_TEST}.jks" +echo "====================================================" +cat ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}.pem ${CA_LIST_CERT_FOR_CAT} > ./${SERVER_PATH}/${BS_SERVER_CERT_CHAIN}.pem +openssl pkcs12 -export -in ./${SERVER_PATH}/${BS_SERVER_CERT_CHAIN}.pem -inkey ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}-key.pem -out ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}.p12 -name ${BS_SERVER_CERT_ALIAS} -CAfile ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_ALIAS} -passin pass:${STORE_PASS_PWD} -passout pass:${STORE_PASS_PWD} +keytool -importkeystore -deststorepass ${STORE_PASS_PWD} -destkeypass ${STORE_PASS_PWD} -destkeystore ./${SERVER_PATH}/${SERVER_JKS_FOR_TEST}.jks -srckeystore ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${STORE_PASS_PWD} -alias ${BS_SERVER_CERT_ALIAS} + + +keytool -list -v -keystore ./${SERVER_PATH}/lwm2mserver.jks -storepass server_ks_password -storetype PKCS12 + +rm ./${SERVER_PATH}/*.p12 2> /dev/null +rm ./${SERVER_PATH}/*.csr 2> /dev/null +rm ./${SERVER_PATH}/*.json 2> /dev/null +rm ./${SERVER_PATH}/${CA_INTERMEDIATE_CERT_KEY_PREF}* 2> /dev/null +rm ./${SERVER_PATH}/${CA_ROOT_CERT_KEY}* 2> /dev/null +mv ./${SERVER_PATH}/${SERVER_CERT_KEY}-key.pem ./${SERVER_PATH}/${SERVER_CERT_KEY}_key.pem +mv ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}-key.pem ./${SERVER_PATH}/${BS_SERVER_CERT_KEY}_key.pem + From 60758375ed02f06e06cce51c31ccf5171bd7c8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emre=20Karda=C5=9Flar?= Date: Sat, 8 Jan 2022 01:03:54 +0300 Subject: [PATCH 36/72] fix problematic letters for different lang --- .../org/thingsboard/server/dao/audit/AuditLogLevelFilter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java index b73cafc121..effc79f711 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java @@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionType; import java.util.HashMap; +import java.util.Locale; import java.util.Map; public class AuditLogLevelFilter { @@ -28,7 +29,7 @@ public class AuditLogLevelFilter { public AuditLogLevelFilter(Map mask) { entityTypeMask.clear(); mask.forEach((entityTypeStr, logLevelMaskStr) -> { - EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase()); + EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase(Locale.ENGLISH)); AuditLogLevelMask logLevelMask = AuditLogLevelMask.valueOf(logLevelMaskStr.toUpperCase()); entityTypeMask.put(entityType, logLevelMask); }); From eccbd3290c5c443ae60fae111ce9539d28a6e85d Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 13:06:46 +0200 Subject: [PATCH 37/72] lwm2m tests with NoSec, PSK, X509-trust. RPK, X509_NoTrust - ignore --- .../AbstractSecurityLwM2MIntegrationTest.java | 198 ++++++++++-------- 1 file changed, 105 insertions(+), 93 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index ea30b43436..0c983f9dcf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.transport.lwm2m.security; -import org.eclipse.leshan.core.util.Hex; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; import org.thingsboard.server.dao.service.DaoSqlTest; @@ -24,144 +23,157 @@ import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import java.io.IOException; import java.io.InputStream; -import java.math.BigInteger; -import java.security.AlgorithmParameters; import java.security.GeneralSecurityException; -import java.security.KeyFactory; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.security.spec.ECGenParameterSpec; -import java.security.spec.ECParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPrivateKeySpec; -import java.security.spec.ECPublicKeySpec; -import java.security.spec.KeySpec; @DaoSqlTest public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { + protected final String CREDENTIALS_PATH = "lwm2m/credentials/"; // client public key or id used for PSK protected final String pskIdentity; // client public key or id used for PSK protected final String pskKey; // client private/secret key used for PSK - protected final PublicKey clientPublicKey; // client public key used for RPK - protected final PrivateKey clientPrivateKey; // client private key used for RPK - protected final PublicKey serverPublicKey; // server public key used for RPK - protected final PrivateKey serverPrivateKey; // server private key used for RPK - - // client private key used for X509 - protected final PrivateKey clientPrivateKeyFromCert; - // server private key used for X509 - protected final PrivateKey serverPrivateKeyFromCert; - // client certificate signed by rootCA with a good CN (CN start by leshan_integration_test) - protected final X509Certificate clientX509Cert; - // client certificate signed by rootCA but with bad CN (CN does not start by leshan_integration_test) - protected final X509Certificate clientX509CertWithBadCN; - // client certificate self-signed with a good CN (CN start by leshan_integration_test) - protected final X509Certificate clientX509CertSelfSigned; - // client certificate signed by another CA (not rootCA) with a good CN (CN start by leshan_integration_test) - protected final X509Certificate clientX509CertNotTrusted; - // server certificate signed by rootCA - protected final X509Certificate serverX509Cert; +// protected final PublicKey clientPublicKey; // client public key used for RPK +// protected final PrivateKey clientPrivateKey; // client private key used for RPK + + + +// // client certificate signed by rootCA but with bad CN (CN does not start by leshan_integration_test) +// protected final X509Certificate clientX509CertWithBadCN; +// // client certificate self-signed with a good CN (CN start by leshan_integration_test) +// protected final X509Certificate clientX509CertSelfSigned; +// // client certificate signed by another CA (not rootCA) with a good CN (CN start by leshan_integration_test) +// protected final X509Certificate clientX509CertNotTrusted; + // self-signed server certificate - protected final X509Certificate serverX509CertSelfSigned; - // rootCA used by the server - protected final X509Certificate rootCAX509Cert; +// protected final X509Certificate serverX509CertSelfSigned; +// // rootCA used by the server +// protected final X509Certificate rootCAX509Cert; // certificates trustedby the server (should contain rootCA) - protected final Certificate[] trustedCertificates = new Certificate[1]; - protected static final String ENDPOINT = "deviceAEndpoint"; + // Server + protected static final String SERVER_JKS_FOR_TEST = "lwm2mserver"; + protected static final String SERVER_STORE_PWD = "server_ks_password"; + protected static final String SERVER_CERT_ALIAS = "server"; + protected final X509Certificate serverX509Cert; // server certificate signed by rootCA +// protected final PrivateKey serverPrivateKeyFromCert; // server private key used for RPK and X509 + protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK +// // Server Trust +// protected final Certificate[] trustedCertificates = new Certificate[1]; + + // Client protected LwM2MTestClient client; + protected static final String CLIENT_ENDPOINT_NO_SEC = "deviceNoSec"; + protected static final String CLIENT_ENDPOINT_RPK = "deviceRPK"; + protected static final String CLIENT_ENDPOINT_NO_TRUST = "deviceAEndpoint"; + protected static final String CLIENT_ENDPOINT_TRUST = "LwX50900000000"; + protected static final String CLIENT_JKS_FOR_TEST = "lwm2mclient"; + protected static final String CLIENT_STORE_PWD = "client_ks_password"; + + protected static final String CLIENT_CERT_ALIAS = "client_alias_00000000"; + + protected final X509Certificate clientX509Cert; // client certificate signed by intermediate, rootCA with a good CN ("host name") + protected final PrivateKey clientPrivateKeyFromCert; // client private key used for X509 and RPK + protected final PublicKey clientPublicKeyFromCert; // client public key used for RPK + private final String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; + private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; - private final String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; + public AbstractSecurityLwM2MIntegrationTest() { // create client credentials setResources(this.resources); - setEndpoint(ENDPOINT); + setEndpoint(CLIENT_ENDPOINT_NO_TRUST); try { - // Get keys PSK +// Get keys PSK this.pskIdentity = "SOME_PSK_ID"; this.pskKey = "73656372657450534b73656372657450"; - // Get point values - byte[] publicX = Hex - .decodeHex("89c048261979208666f2bfb188be1968fc9021c416ce12828c06f4e314c167b5".toCharArray()); - byte[] publicY = Hex - .decodeHex("cbf1eb7587f08e01688d9ada4be859137ca49f79394bad9179326b3090967b68".toCharArray()); - byte[] privateS = Hex - .decodeHex("e67b68d2aaeb6550f19d98cade3ad62b39532e02e6b422e1f7ea189dabaea5d2".toCharArray()); - - // Get Elliptic Curve Parameter spec for secp256r1 - AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); - algoParameters.init(new ECGenParameterSpec("secp256r1")); - ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); - - // Create key specs - KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), - parameterSpec); - KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); - - // Get keys RPK - clientPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); - clientPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); +// // Get point values +// byte[] publicX = Hex +// .decodeHex("89c048261979208666f2bfb188be1968fc9021c416ce12828c06f4e314c167b5".toCharArray()); +// byte[] publicY = Hex +// .decodeHex("cbf1eb7587f08e01688d9ada4be859137ca49f79394bad9179326b3090967b68".toCharArray()); +// byte[] privateS = Hex +// .decodeHex("e67b68d2aaeb6550f19d98cade3ad62b39532e02e6b422e1f7ea189dabaea5d2".toCharArray()); +// +// // Get Elliptic Curve Parameter spec for secp256r1 +// AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); +// algoParameters.init(new ECGenParameterSpec("secp256r1")); +// ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); +// +// // Create key specs +// KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), +// parameterSpec); +// KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); +// +// // Get keys RPK +// clientPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); +// clientPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); // Get certificates from key store - char[] clientKeyStorePwd = "client".toCharArray(); + char[] clientKeyStorePwd = CLIENT_STORE_PWD.toCharArray(); KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - try (InputStream clientKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream("lwm2m/credentials/clientKeyStore.jks")) { + try (InputStream clientKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream(CREDENTIALS_PATH + CLIENT_JKS_FOR_TEST + ".jks")) { clientKeyStore.load(clientKeyStoreFile, clientKeyStorePwd); } - clientPrivateKeyFromCert = (PrivateKey) clientKeyStore.getKey("client", clientKeyStorePwd); - clientX509Cert = (X509Certificate) clientKeyStore.getCertificate("client"); - clientX509CertWithBadCN = (X509Certificate) clientKeyStore.getCertificate("client_bad_cn"); - clientX509CertSelfSigned = (X509Certificate) clientKeyStore.getCertificate("client_self_signed"); - clientX509CertNotTrusted = (X509Certificate) clientKeyStore.getCertificate("client_not_trusted"); + clientPrivateKeyFromCert = (PrivateKey) clientKeyStore.getKey(CLIENT_CERT_ALIAS, clientKeyStorePwd); + clientX509Cert = (X509Certificate) clientKeyStore.getCertificate(CLIENT_CERT_ALIAS); + clientPublicKeyFromCert = clientX509Cert.getPublicKey(); + +// clientX509CertWithBadCN = (X509Certificate) clientKeyStore.getCertificate("client_bad_cn"); +// clientX509CertSelfSigned = (X509Certificate) clientKeyStore.getCertificate("client_self_signed"); +// clientX509CertNotTrusted = (X509Certificate) clientKeyStore.getCertificate("client_not_trusted"); } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e); } // create server credentials try { - // Get point values - byte[] publicX = Hex - .decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73".toCharArray()); - byte[] publicY = Hex - .decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a".toCharArray()); - byte[] privateS = Hex - .decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400".toCharArray()); - - // Get Elliptic Curve Parameter spec for secp256r1 - AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); - algoParameters.init(new ECGenParameterSpec("secp256r1")); - ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); - - // Create key specs - KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), - parameterSpec); - KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); - - // Get keys - serverPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); - serverPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); +// // Get point values +// byte[] publicX = Hex +// .decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73".toCharArray()); +// byte[] publicY = Hex +// .decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a".toCharArray()); +// byte[] privateS = Hex +// .decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400".toCharArray()); +// +// // Get Elliptic Curve Parameter spec for secp256r1 +// AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); +// algoParameters.init(new ECGenParameterSpec("secp256r1")); +// ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); +// +// // Create key specs +// KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), +// parameterSpec); +// KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); +// +// // Get keys +// serverPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); +// serverPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); + // Get certificates from key store - char[] serverKeyStorePwd = "server".toCharArray(); + char[] serverKeyStorePwd = SERVER_STORE_PWD.toCharArray(); KeyStore serverKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - try (InputStream serverKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream("lwm2m/credentials/serverKeyStore.jks")) { + try (InputStream serverKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream(CREDENTIALS_PATH + SERVER_JKS_FOR_TEST + ".jks")) { serverKeyStore.load(serverKeyStoreFile, serverKeyStorePwd); } - serverPrivateKeyFromCert = (PrivateKey) serverKeyStore.getKey("server", serverKeyStorePwd); - rootCAX509Cert = (X509Certificate) serverKeyStore.getCertificate("rootCA"); - serverX509Cert = (X509Certificate) serverKeyStore.getCertificate("server"); - serverX509CertSelfSigned = (X509Certificate) serverKeyStore.getCertificate("server_self_signed"); - trustedCertificates[0] = serverX509Cert; +// serverPrivateKeyFromCert = (PrivateKey) serverKeyStore.getKey("server", serverKeyStorePwd); + serverX509Cert = (X509Certificate) serverKeyStore.getCertificate(SERVER_CERT_ALIAS); + serverPublicKeyFromCert = serverX509Cert.getPublicKey(); +// rootCAX509Cert = (X509Certificate) serverKeyStore.getCertificate("rootCA"); + +// serverX509CertSelfSigned = (X509Certificate) serverKeyStore.getCertificate("server_self_signed"); +// trustedCertificates[0] = serverX509Cert; } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e); } From ba970c5d88eb1a723b12e2d8da7822e79c352554 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 13:07:55 +0200 Subject: [PATCH 38/72] lwm2m tests with NoSec, PSK, X509-trust. RPK, X509_NoTrust - ignore --- .../sql/NoSecLwM2MIntegrationTest.java | 5 +++-- .../security/sql/PskLwm2mIntegrationTest.java | 5 +++-- .../security/sql/RpkLwM2MIntegrationTest.java | 12 ++++++------ .../sql/X509_NoTrustLwM2MIntegrationTest.java | 18 +++++++++--------- .../sql/X509_TrustLwM2MIntegrationTest.java | 6 ++---- .../resources/application-test.properties | 18 +++++++++--------- .../lwm2m/credentials/clientKeyStore.jks | Bin 4810 -> 0 bytes .../lwm2m/credentials/lwm2mclient.jks | Bin 0 -> 17660 bytes .../lwm2m/credentials/lwm2mserver.jks | Bin 0 -> 6432 bytes .../credentials/lwm2mtruststorechain.jks | Bin 0 -> 2982 bytes .../lwm2m/credentials/serverKeyStore.jks | Bin 3806 -> 0 bytes 11 files changed, 32 insertions(+), 32 deletions(-) delete mode 100644 application/src/test/resources/lwm2m/credentials/clientKeyStore.jks create mode 100644 application/src/test/resources/lwm2m/credentials/lwm2mclient.jks create mode 100644 application/src/test/resources/lwm2m/credentials/lwm2mserver.jks create mode 100644 application/src/test/resources/lwm2m/credentials/lwm2mtruststorechain.jks delete mode 100644 application/src/test/resources/lwm2m/credentials/serverKeyStore.jks diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 0e86c6a438..8331b99fff 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import lombok.extern.slf4j.Slf4j; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -28,8 +29,8 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT @Test public void testConnectAndObserveTelemetry() throws Exception { - NoSecClientCredential clientCredentials = createNoSecClientCredentials(ENDPOINT); - super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, ENDPOINT); + NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_NO_SEC); + super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_NO_SEC); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index af9a668376..5850e52015 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.core.util.Hex; +import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -33,13 +34,13 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); - clientCredentials.setEndpoint(ENDPOINT); + clientCredentials.setEndpoint(CLIENT_ENDPOINT_NO_TRUST); clientCredentials.setKey(pskKey); clientCredentials.setIdentity(pskIdentity); Security security = psk(SECURE_URI, SHORT_SERVER_ID, pskIdentity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(pskKey.toCharArray())); - super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, ENDPOINT); + super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_NO_TRUST); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index 87ca415bd0..e6c26a05d0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -34,13 +34,13 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithRPKAndObserveTelemetry() throws Exception { RPKClientCredential rpkClientCredentials = new RPKClientCredential(); - rpkClientCredentials.setEndpoint(ENDPOINT); - rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKey.getEncoded()))); + rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_RPK); + rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPrivateKeyFromCert.getEncoded()))); Security security = rpk(SECURE_URI, SHORT_SERVER_ID, - clientPublicKey.getEncoded(), - clientPrivateKey.getEncoded(), - serverX509Cert.getPublicKey().getEncoded()); - super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, ENDPOINT); + clientPublicKeyFromCert.getEncoded(), + clientPrivateKeyFromCert.getEncoded(), + serverPublicKeyFromCert.getEncoded()); + super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_RPK); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index d1bc813b50..6ca430327d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -32,15 +32,15 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg @Ignore @Test public void testConnectWithCertAndObserveTelemetry() throws Exception { - X509ClientCredential credentials = new X509ClientCredential(); - credentials.setEndpoint(ENDPOINT); - credentials.setCert(SslUtil.getCertificateString(clientX509CertNotTrusted)); - Security security = x509(SECURE_URI, - SHORT_SERVER_ID, - clientX509CertNotTrusted.getEncoded(), - clientPrivateKeyFromCert.getEncoded(), - serverX509Cert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, ENDPOINT); +// X509ClientCredential credentials = new X509ClientCredential(); +// credentials.setEndpoint(CLIENT_ENDPOINT_NO_TRUST); +// credentials.setCert(SslUtil.getCertificateString(clientX509CertNotTrusted)); +// Security security = x509(SECURE_URI, +// SHORT_SERVER_ID, +// clientX509CertNotTrusted.getEncoded(), +// clientPrivateKeyNotTrustedFromCert.getEncoded(), +// serverX509Cert.getEncoded()); +// super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_NO_TRUST); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java index 7c5e48a4b7..28a10fd278 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -28,17 +27,16 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { - @Ignore @Test public void testConnectAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); - credentials.setEndpoint(ENDPOINT); + credentials.setEndpoint(CLIENT_ENDPOINT_TRUST); Security security = x509(SECURE_URI, SHORT_SERVER_ID, clientX509Cert.getEncoded(), clientPrivateKeyFromCert.getEncoded(), serverX509Cert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, ENDPOINT); + super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); } } diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 856745797a..dd1cd2807c 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -1,19 +1,19 @@ -#transport.lwm2m.server.security.credentials.enabled=true -#transport.lwm2m.server.security.credentials.type=KEYSTORE -#transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.server.security.credentials.enabled=true +transport.lwm2m.server.security.credentials.type=KEYSTORE +transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/lwm2mserver.jks #transport.lwm2m.server.security.credentials.keystore.store_password=server #transport.lwm2m.server.security.credentials.keystore.key_alias=server #transport.lwm2m.server.security.credentials.keystore.key_password=server #transport.lwm2m.bootstrap.enabled=false -#transport.lwm2m.bootstrap.security.credentials.enabled=true -#transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE -#transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.bootstrap.security.credentials.enabled=true +transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE +transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/lwm2mserver.jks #transport.lwm2m.bootstrap.security.credentials.keystore.store_password=server #transport.lwm2m.bootstrap.security.credentials.keystore.key_alias=server #transport.lwm2m.bootstrap.security.credentials.keystore.key_password=server -#transport.lwm2m.security.trust-credentials.enabled=true -#transport.lwm2m.security.trust-credentials.type=KEYSTORE -#transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks +transport.lwm2m.security.trust-credentials.enabled=true +transport.lwm2m.security.trust-credentials.type=KEYSTORE +transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/lwm2mtruststorechain.jks #transport.lwm2m.security.trust-credentials.keystore.store_password=server edges.enabled=true diff --git a/application/src/test/resources/lwm2m/credentials/clientKeyStore.jks b/application/src/test/resources/lwm2m/credentials/clientKeyStore.jks deleted file mode 100644 index a6c9ae7faed05c48ec1218e23a42c3025ae852d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4810 zcmeHKdpwls9)IVZ8?$1V+`8z^r7faoTyo7Kw~~aYeB{y?OqdySK}BLOgxw-rlBDFa z(TZxxaVZgsq;!#Oed=sfBHG$cn>de|>D0_gr#|2u2XxmVxY?(+F|Bkr}Xrw_K2ni-bWfz@s=6CB*GFPk0g!dufX^N|u{P18lHZ>cz+#X=+GmPnADKMv-z)w>@4JBhb z95ADsN(Mgk=M8{HB8%2Ragqh`I5OS=A$$_)db__`Kf&{*+vqJu#2QlLoO2`e+65gI z-$gJeM>`{JIjxqb>8Ge_!lDMXA86YTCXKbnuGg^n=F+^#I@_T_2Ve*0!Y$8LB&ZQ) z8fIy2sm?Jp&$_ZJ{#<&$?-nG%2PjVctc1f&65w0nNI+@oo;Gz)1bBFnJ_Nig0-VtK z$i+`9(rT0kqX*_j_w@*Fd+BSSSe=pDsrHJU_wP4v8bC4krd)?AT1kHN7-kN~$tF(BaxIDJ$Zha)0{i0J#> zovXR|mu?hxEl}b7x^;3Dt#7!s^lHm~yIG-8)Li9+;k)X(YBi1vEBDnnA>Ez!mT$3`Q4`n9W+^X!+{4QYO-S3QT6X}EDl&Ek_+k0!5+ zYpLBXV#z;PQ2-f{2g6>%3gV9>L+Jzg5U|p$u#jbig&&wj*8pl_N-2dfV^|EPy%Rfx z%VDtvV0v2U9!Lqw-7z#C_5+1q>%r{Zrb<@rA-bN~cDcLZR##D3?^iAxJ|7PzzqBvV z*yMKZMJcE6p^Zs|bIl@!0%}5XejyKV-=T%)i-5C@=hRPSl+Ag>Zzi$gZEP1x(*mCg z~3 zcUGX)I%_?*E_{3-ZJa+cD?i<-R#=W=gwmu2hr~i!y5^m%St(XSs=P&_3t+#~|>9H!PwQ{m$UWGO2k zOFy7X*9MqKKnj|XB2NyK>?oEc2$yW@k1ZvUBV)Y!3qJ=m9_lR#FWA#Q!FtBRKlEK@R$tJlo_n3b=vB)WQS;b+(7BZC~gcdc{5>TRX>Hkx@c6+Pa||hi){oR?wITG zsg(^leJ5-T^ZT;2SlX2DLO$JrktE@ie zV(3OUHZe6@YW@KxHl`D?&h_4cZl}NScqNqV38S?a8?N=awxM`xRG?1AJYVaiudOGe zpVML!quB?GvXVcrfBp2AXyKVh6d6+wN(pE9O zsn;F7ks3QNc{q&6s497^$v>WtV0af}+7+|4Ma=vf{-UO`3IbZ2(e6)Sd+W8OGKhnve&F{pX!=}TYj4pS)bm=q zHg>r3!Tw%uPva9JIo&dQB!vMD}d)xMoZa~hgv(AwfIo@}ty=^|8i z?n)v@R27DTkau$Rd!dmn;Z)#ji%U2;K!PP4ijz*-ZqJH)6dp%CKfEigBQr|i7~(ZE zSNc%JWgeC&#P;~!=vb~|O|B&94a$Fi-o|$Jf-!&9>IK^k`kO|fskCT;0hJLwRNGo% zCZ`=e)~M|P>N&lva`&9jLkU_gBIznXQxTUp{5Q4tLBl^m{$qix3_<2fg!XQil~?o@ z=_63e`7j(La*k_O?v!R8_|N5RCM#z%zkh4i=|vq~di=F*tao-9r*Ip2(xUaGp`_g-CJ{w$T`QZdmIwKlWK4#oVF!}tB|TGZ8X?OTd* z)Y^xIg29viIhbnLsgyi}V&_7UU{xeV6q_Rm6!3(60W(Bg+a1{O7R+)8WGRd{wH@(s zK${GhI`sLXb^*4`8j_Y-$d*hvP4oP|D)0ZkP5Rn*Om0!NWp4tgDGpG|L5z=m>HIMn zbp-;Pp~*g28Bx;KSa>euVO`z+-kczeW;a<=?+S=f1MQ;`&`d}d^5AfFY+egkWc!CP39AfyQ}=1VlHt()E_ z1M7$I_@8ZhJ%PtNiS+k2y&uujiVJc5Gsor4Iq{=&2E7sHiH7dql=}VmE~1%wAjS=W z-US%MD#xzn+KIdbuX4}O1#U6Evu~aWyJvEnGFWtBU;QUP+JGyKj%mKTb$|Y4AvgC` M^f%}Gep;{ccMhiIP5=M^ diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mclient.jks b/application/src/test/resources/lwm2m/credentials/lwm2mclient.jks new file mode 100644 index 0000000000000000000000000000000000000000..490812c01ab483d3462a09cdd18bc405fa23abe5 GIT binary patch literal 17660 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 KeSQK10fwM$Q%GO{ literal 0 HcmV?d00001 diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mserver.jks b/application/src/test/resources/lwm2m/credentials/lwm2mserver.jks new file mode 100644 index 0000000000000000000000000000000000000000..a1923e942f923b8d84a989055ec50c2441f79259 GIT binary patch literal 6432 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 literal 0 HcmV?d00001 diff --git a/application/src/test/resources/lwm2m/credentials/serverKeyStore.jks b/application/src/test/resources/lwm2m/credentials/serverKeyStore.jks deleted file mode 100644 index fc541a3b18111312545da774be682ddb893d72c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3806 zcmezO_TO6u1_mY|W&_iFnRz9tMY*XdnTaK-$%zaMjFr!(OnJ+|8lh)uUJkHs&Ff*|-7)Tn58;G(ohq5pW^ZBF}XC&q+6y@ia zC^$PBD2VeK85)=wm>U`cNz*6^ej`JmfT^JgRKP&dP~JcmqFD^88QDo7Lm~}?+1SDM zF)>0dW@cn(c4A;T@VS|9lH1dLZ_jKt-`U6Fea+QH`BYo9IL#mhY`ZKUix`Ut^Mt&kdKuN{mQQtB zUi5*fk(H(TpaCDyakBi3jQ?4HzGY-H5CZXqL3|DaHXy~s$Y3B35>sZ8Fc51H(VZk$ zxqXJN`J@Z|H3z%zhgCAvhN1=-vnPXr8zz+tX`Lo>Xmwt;`4ciQ-RUJ$iP{g zT2z)=1Wd({+%)YhP`KWp7MPN_wAmP07`2!L85vm_Sh9C4soL}0b=E$Mg=v$soQ%&E ze-UkGh(7!8*z23GK6Ux3JW7}-CdGRxUb*$s#*H}>yS#3hWc=9SwbZeCr>?;9ccr&_ zYgxQJ(!V{(HT$V`QR>5%H$^P@+qt&id0fzvyI|3@t`COBEccC7VLd&rY7L^CxjgF5%vL_s_{?G7bKy zS)IAVKm?R*1X*|uxSKe0@{<#DGV+T{3^>7wiG|4kHQ!?`JcL1NRatlpxY#&=anH)i z&di7!(9G@(1};nrn!6{v?BTePd(kMjNR;uU^|$ugrFS$Q9umsi${YQ5&H^TdsFeM= z90yik-kWEXGgr=O`{J&$y|KM7LUoxI7hXH8CBN4cMod%AvKSoW?FOfOlv-vGHtp+D%Gs zJc(4Dp!3?mQ2w@XY2U+&4qJBf-UtR0M-HvV>(w&A3ge>joNbbJ!JgUYuAZ6QqPAQv zQ)->$ngpAsU+XTp#rL^@s{=MjR72u+>TO(gfe)|D(-6Qb2h|ya67LSJ{;U~ z=j!t7JN^Y!?~YK;neEaj@$>WA(44J%&My4R*S)D{)LMO8UGTuGsl5-*N3oSb>J z)Lvu@%h8K!V%cK1e3=wtzAp%{I$kwDp0DD0XHV~}Lt>(a&rX)h7S2h_+IP<}98nI8 zw&b9#Z#uT*m>7g0Sr@a2o|KM=nlfPCDFR|qSR>n^QvogiV9x@mC9xTLNo*EnfL$LA zvreFaKLuHbh@1iK`FxVtS7y-vtNrrlO-x^(POD+NzGweDCVTEHScmMzZ From 4450df292f73ce0b58ac52b95bd7a1366f47cbea Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 18:24:10 +0200 Subject: [PATCH 39/72] lwm2m tests aad logs with NoSec, PSK, X509-trust. RPK, X509_NoTrust - ignore --- .../server/transport/lwm2m/AbstractLwM2MIntegrationTest.java | 3 +++ .../lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java | 1 + .../transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java | 4 ++-- application/src/test/resources/application-test.properties | 2 +- application/src/test/resources/logback.xml | 1 + 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 5d4c32641d..13e0858199 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.transport.lwm2m; import com.fasterxml.jackson.core.type.TypeReference; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.eclipse.californium.core.network.config.NetworkConfig; import org.eclipse.leshan.client.object.Security; @@ -66,6 +67,7 @@ import java.util.concurrent.ScheduledExecutorService; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@Slf4j @DaoSqlTest public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { @@ -199,6 +201,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest createNewClient(security, coapConfig, false); String msg = wsClient.waitForUpdate(); + log.info("msg5555: [{}]", msg); EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); Assert.assertEquals(1, update.getCmdId()); List eData = update.getUpdate(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index 0c983f9dcf..d537062def 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -68,6 +68,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M // Client protected LwM2MTestClient client; protected static final String CLIENT_ENDPOINT_NO_SEC = "deviceNoSec"; + protected static final String CLIENT_ENDPOINT_PSK = "devicePSK"; protected static final String CLIENT_ENDPOINT_RPK = "deviceRPK"; protected static final String CLIENT_ENDPOINT_NO_TRUST = "deviceAEndpoint"; protected static final String CLIENT_ENDPOINT_TRUST = "LwX50900000000"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index 5850e52015..d7296fd47c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -34,13 +34,13 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); - clientCredentials.setEndpoint(CLIENT_ENDPOINT_NO_TRUST); + clientCredentials.setEndpoint(CLIENT_ENDPOINT_PSK); clientCredentials.setKey(pskKey); clientCredentials.setIdentity(pskIdentity); Security security = psk(SECURE_URI, SHORT_SERVER_ID, pskIdentity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(pskKey.toCharArray())); - super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_NO_TRUST); + super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_PSK); } } diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index dd1cd2807c..7f7cb6e8b9 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -4,7 +4,7 @@ transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credential #transport.lwm2m.server.security.credentials.keystore.store_password=server #transport.lwm2m.server.security.credentials.keystore.key_alias=server #transport.lwm2m.server.security.credentials.keystore.key_password=server -#transport.lwm2m.bootstrap.enabled=false +transport.lwm2m.bootstrap.enabled=false transport.lwm2m.bootstrap.security.credentials.enabled=true transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/lwm2mserver.jks diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index d3301bf660..91ca9c2b6c 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,6 +10,7 @@ + From fe9b61586f3f907289d59a9ecca839965f26c07c Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 20:11:39 +0200 Subject: [PATCH 40/72] lwm2m tests ignore test del 3/0/9 --- .../sql/RpcLwm2mIntegrationDeleteTest.java | 24 +++++++++---------- application/src/test/resources/logback.xml | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java index 6f24d7176c..90a9dd7a62 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java @@ -74,18 +74,18 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe } - /** - * delete resource - * Delete {"id":"/3/0/9"} - * {"result":"METHOD_NOT_ALLOWED"} - */ - @Test - public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { - String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_9; - String actualResult = sendRPCDeleteById(expectedPath); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); - } +// /** +// * delete resource +// * Delete {"id":"/3/0/9"} +// * {"result":"METHOD_NOT_ALLOWED"} +// */ +// @Test +// public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { +// String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_9; +// String actualResult = sendRPCDeleteById(expectedPath); +// ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); +// assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); +// } private String sendRPCDeleteById(String path) throws Exception { diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index 91ca9c2b6c..4cca303e8f 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,7 +10,7 @@ - + From 98331c79a0ccb70a4c8459e8bdc56eb92e50c07a Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sat, 8 Jan 2022 23:48:24 +0200 Subject: [PATCH 41/72] lwm2m tests add RPK --- .../transport/lwm2m/Lwm2mTestHelper.java | 1 + .../sql/RpcLwm2mIntegrationDeleteTest.java | 25 ++++++++++--------- .../AbstractSecurityLwM2MIntegrationTest.java | 3 --- .../sql/NoSecLwM2MIntegrationTest.java | 4 +-- .../security/sql/PskLwm2mIntegrationTest.java | 4 +-- .../security/sql/RpkLwM2MIntegrationTest.java | 7 +++--- application/src/test/resources/logback.xml | 1 - 7 files changed, 21 insertions(+), 24 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 232332e554..0ea700d46c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -55,6 +55,7 @@ public class Lwm2mTestHelper { public static final int resourceId_2 = 2; public static final int resourceId_3 = 3; public static final int resourceId_4 = 4; + public static final int resourceId_7 = 7; public static final int resourceId_8 = 8; public static final int resourceId_9 = 9; public static final int resourceId_11 = 11; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java index 90a9dd7a62..ebc0f6d783 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_12; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_7; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; @@ -74,18 +75,18 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe } -// /** -// * delete resource -// * Delete {"id":"/3/0/9"} -// * {"result":"METHOD_NOT_ALLOWED"} -// */ -// @Test -// public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { -// String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_9; -// String actualResult = sendRPCDeleteById(expectedPath); -// ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); -// assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); -// } + /** + * delete resource + * Delete {"id":"/3/0/7"} + * {"result":"METHOD_NOT_ALLOWED"} + */ + @Test + public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { + String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_7; + String actualResult = sendRPCDeleteById(expectedPath); + ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); + assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); + } private String sendRPCDeleteById(String path) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index d537062def..b0fadf1d23 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -67,9 +67,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M // Client protected LwM2MTestClient client; - protected static final String CLIENT_ENDPOINT_NO_SEC = "deviceNoSec"; - protected static final String CLIENT_ENDPOINT_PSK = "devicePSK"; - protected static final String CLIENT_ENDPOINT_RPK = "deviceRPK"; protected static final String CLIENT_ENDPOINT_NO_TRUST = "deviceAEndpoint"; protected static final String CLIENT_ENDPOINT_TRUST = "LwX50900000000"; protected static final String CLIENT_JKS_FOR_TEST = "lwm2mclient"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 8331b99fff..4daac68f9a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -29,8 +29,8 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT @Test public void testConnectAndObserveTelemetry() throws Exception { - NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_NO_SEC); - super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_NO_SEC); + NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_TRUST); + super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_TRUST); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index d7296fd47c..11b6f3f6e7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -34,13 +34,13 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); - clientCredentials.setEndpoint(CLIENT_ENDPOINT_PSK); + clientCredentials.setEndpoint(CLIENT_ENDPOINT_TRUST); clientCredentials.setKey(pskKey); clientCredentials.setIdentity(pskIdentity); Security security = psk(SECURE_URI, SHORT_SERVER_ID, pskIdentity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(pskKey.toCharArray())); - super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_PSK); + super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index e6c26a05d0..1f73a2a739 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -30,17 +30,16 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { - @Ignore @Test public void testConnectWithRPKAndObserveTelemetry() throws Exception { RPKClientCredential rpkClientCredentials = new RPKClientCredential(); - rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_RPK); - rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPrivateKeyFromCert.getEncoded()))); + rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_TRUST); + rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCert.getEncoded()))); Security security = rpk(SECURE_URI, SHORT_SERVER_ID, clientPublicKeyFromCert.getEncoded(), clientPrivateKeyFromCert.getEncoded(), serverPublicKeyFromCert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_RPK); + super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); } } diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index 4cca303e8f..d3301bf660 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,7 +10,6 @@ - From c981ff4c55016e4a8c153ed4121236c381dfa26c Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sun, 9 Jan 2022 15:22:43 +0200 Subject: [PATCH 42/72] lwm2m tests add no trust --- .../controller/TbTestWebSocketClient.java | 2 +- .../lwm2m/AbstractLwM2MIntegrationTest.java | 16 +-- .../ota/AbstractOtaLwM2MIntegrationTest.java | 3 + .../ota/sql/OtaLwM2MIntegrationTest.java | 24 ++-- .../rpc/AbstractRpcLwM2MIntegrationTest.java | 6 +- .../AbstractSecurityLwM2MIntegrationTest.java | 38 +++--- .../sql/NoSecLwM2MIntegrationTest.java | 4 +- .../security/sql/PskLwm2mIntegrationTest.java | 4 +- .../security/sql/RpkLwM2MIntegrationTest.java | 10 +- .../sql/X509_NoTrustLwM2MIntegrationTest.java | 21 +-- .../sql/X509_TrustLwM2MIntegrationTest.java | 8 +- application/src/test/resources/logback.xml | 1 + .../lwm2m/credentials/lwm2mclient.jks | Bin 17660 -> 20462 bytes .../lwm2m/credentials/lwm2mserver.jks | Bin 6432 -> 6448 bytes .../credentials/lwm2mtruststorechain.jks | Bin 2982 -> 2982 bytes ... => lwM2M_cfssl_chain_clients_for_test.sh} | 120 +++++++++++++++++- ...l.sh => lwm2m_cfssl_chain_all_for_test.sh} | 10 +- 17 files changed, 194 insertions(+), 73 deletions(-) rename application/src/test/resources/lwm2m/credentials/shell/{lwM2M_cfssl_chain_trusts_and_clients_for_test.sh => lwM2M_cfssl_chain_clients_for_test.sh} (63%) rename application/src/test/resources/lwm2m/credentials/shell/{lwm2m_cfssl_chain_for_test_All.sh => lwm2m_cfssl_chain_all_for_test.sh} (78%) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index ff6b004405..2bb68737ac 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -74,7 +74,7 @@ public class TbTestWebSocketClient extends WebSocketClient { } public String waitForUpdate() { - return waitForUpdate(TimeUnit.SECONDS.toMillis(3)); + return waitForUpdate(TimeUnit.SECONDS.toMillis(8)); } public String waitForUpdate(long ms) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 13e0858199..0a95dfdbc9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -135,7 +135,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest protected LwM2MTestClient client; private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; private String[] resources; - protected String endpoint; +// protected String endpoint; public AbstractLwM2MIntegrationTest() { this.defaultBootstrapCredentials = new LwM2MBootstrapClientCredentials(); @@ -197,8 +197,8 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest wsClient.waitForReply(); wsClient.registerWaitForUpdate(); - this.endpoint = endpoint; - createNewClient(security, coapConfig, false); +// this.endpoint = endpoint; + createNewClient(security, coapConfig, false, endpoint); String msg = wsClient.waitForUpdate(); log.info("msg5555: [{}]", msg); @@ -264,13 +264,13 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest this.resources = resources; } - public void setEndpoint(String endpoint) { - this.endpoint = endpoint; - } +// public void setEndpoint(String endpoint) { +// this.endpoint = endpoint; +// } - public void createNewClient(Security security, NetworkConfig coapConfig, boolean isRpc) throws Exception { + public void createNewClient(Security security, NetworkConfig coapConfig, boolean isRpc, String endpoint) throws Exception { clientDestroy(); - client = new LwM2MTestClient(this.executor, this.endpoint); + client = new LwM2MTestClient(this.executor, endpoint); int clientPort = SocketUtils.findAvailableTcpPort(); client.init(security, coapConfig, clientPort, isRpc); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index e78d74bdcd..6cf35aeb94 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -30,6 +30,9 @@ import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { private final String[] resources = new String[]{"3.xml", "5.xml", "9.xml"}; + protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice"; + protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device"; + protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device"; public AbstractOtaLwM2MIntegrationTest() { setResources(this.resources); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 903750663a..95a0a774a2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -138,12 +138,12 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile() throws Exception { - String endpoint = "WithoutFirmwareInfoDevice"; - setEndpoint(endpoint); +// String endpoint = "WithoutFirmwareInfoDevice"; +// setEndpoint(endpoint); createDeviceProfile(transportConfiguration); - NoSecClientCredential credentials = createNoSecClientCredentials(endpoint); + NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); final Device device = createDevice(credentials); - createNewClient(SECURITY, COAP_CONFIG, false); + createNewClient(SECURITY, COAP_CONFIG, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); Thread.sleep(1000); @@ -165,12 +165,12 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateByObject5() throws Exception { - String endpoint = "Ota5_Device"; - setEndpoint(endpoint); +// String endpoint = "Ota5_Device"; +// setEndpoint(endpoint); createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); - NoSecClientCredential credentials = createNoSecClientCredentials(endpoint); + NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5); final Device device = createDevice(credentials); - createNewClient(SECURITY, COAP_CONFIG, false); + createNewClient(SECURITY, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA5); Thread.sleep(1000); @@ -204,12 +204,12 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { * */ @Test public void testSoftwareUpdateByObject9() throws Exception { - String endpoint = "Ota9_Device"; - setEndpoint(endpoint); +// String endpoint = "Ota9_Device"; +// setEndpoint(endpoint); createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); - NoSecClientCredential credentials = createNoSecClientCredentials(endpoint); + NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9); final Device device = createDevice(credentials); - createNewClient(SECURITY, COAP_CONFIG, false); + createNewClient(SECURITY, COAP_CONFIG, false, this.CLIENT_ENDPOINT_OTA9); Thread.sleep(1000); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 95878b74d1..2310fe1659 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -73,6 +73,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String objectIdVer_50 = "/50"; protected String objectIdVer_3303; protected static AtomicInteger endpointSequence = new AtomicInteger(); + protected static String endpointRpcPref = "deviceEndpointRpc"; public AbstractRpcLwM2MIntegrationTest(){ setResources(resources); @@ -80,9 +81,10 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg @Before public void beforeTest() throws Exception { - setEndpoint("deviceEndpointRpc" + endpointSequence.incrementAndGet()); + String endpoint = endpointRpcPref + endpointSequence.incrementAndGet(); +// setEndpoint(endpoint); init(); - createNewClient (SECURITY, COAP_CONFIG, true); + createNewClient (SECURITY, COAP_CONFIG, true, endpoint); expectedObjects = ConcurrentHashMap.newKeySet(); expectedObjectIdVers = ConcurrentHashMap.newKeySet(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index b0fadf1d23..c10eb46620 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -27,7 +27,6 @@ import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.cert.Certificate; import java.security.cert.X509Certificate; @DaoSqlTest @@ -67,16 +66,22 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M // Client protected LwM2MTestClient client; - protected static final String CLIENT_ENDPOINT_NO_TRUST = "deviceAEndpoint"; - protected static final String CLIENT_ENDPOINT_TRUST = "LwX50900000000"; + protected static final String CLIENT_ENDPOINT_NO_SEC = "LwNoSec00000000"; + protected static final String CLIENT_ENDPOINT_PSK = "LwPsk00000000"; + protected static final String CLIENT_ENDPOINT_RPK = "LwRpk00000000"; + protected static final String CLIENT_ENDPOINT_X509_TRUST = "LwX50900000000"; + protected static final String CLIENT_ENDPOINT_X509_TRUST_NO = "LwX509TrustNo"; protected static final String CLIENT_JKS_FOR_TEST = "lwm2mclient"; protected static final String CLIENT_STORE_PWD = "client_ks_password"; - - protected static final String CLIENT_CERT_ALIAS = "client_alias_00000000"; - - protected final X509Certificate clientX509Cert; // client certificate signed by intermediate, rootCA with a good CN ("host name") - protected final PrivateKey clientPrivateKeyFromCert; // client private key used for X509 and RPK - protected final PublicKey clientPublicKeyFromCert; // client public key used for RPK + protected static final String CLIENT_ALIAS_CERT_TRUST = "client_alias_00000000"; + protected static final String CLIENT_ALIAS_CERT_TRUST_NO = "client_alias_trust_no"; + + protected final X509Certificate clientX509CertTrust; // client certificate signed by intermediate, rootCA with a good CN ("host name") + protected final PrivateKey clientPrivateKeyFromCertTrust; // client private key used for X509 and RPK + protected final PublicKey clientPublicKeyFromCertTrust; // client public key used for RPK + protected final X509Certificate clientX509CertTrustNo; // client certificate signed by intermediate, rootCA with a good CN ("host name") + protected final PrivateKey clientPrivateKeyFromCertTrustNo; // client private key used for X509 and RPK + protected final PublicKey clientPublicKeyFromCertTrustNo; // client public key used for RPK private final String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; @@ -87,7 +92,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M public AbstractSecurityLwM2MIntegrationTest() { // create client credentials setResources(this.resources); - setEndpoint(CLIENT_ENDPOINT_NO_TRUST); +// setEndpoint(CLIENT_ENDPOINT_NO_TRUST); try { // Get keys PSK this.pskIdentity = "SOME_PSK_ID"; @@ -122,13 +127,14 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M clientKeyStore.load(clientKeyStoreFile, clientKeyStorePwd); } - clientPrivateKeyFromCert = (PrivateKey) clientKeyStore.getKey(CLIENT_CERT_ALIAS, clientKeyStorePwd); - clientX509Cert = (X509Certificate) clientKeyStore.getCertificate(CLIENT_CERT_ALIAS); - clientPublicKeyFromCert = clientX509Cert.getPublicKey(); + clientPrivateKeyFromCertTrust = (PrivateKey) clientKeyStore.getKey(CLIENT_ALIAS_CERT_TRUST, clientKeyStorePwd); + clientX509CertTrust = (X509Certificate) clientKeyStore.getCertificate(CLIENT_ALIAS_CERT_TRUST); + clientPublicKeyFromCertTrust = clientX509CertTrust != null ? clientX509CertTrust.getPublicKey() : null; + + clientPrivateKeyFromCertTrustNo = (PrivateKey) clientKeyStore.getKey(CLIENT_ALIAS_CERT_TRUST_NO, clientKeyStorePwd); + clientX509CertTrustNo = (X509Certificate) clientKeyStore.getCertificate(CLIENT_ALIAS_CERT_TRUST_NO); + clientPublicKeyFromCertTrustNo = clientX509CertTrustNo != null ? clientX509CertTrustNo.getPublicKey() : null; -// clientX509CertWithBadCN = (X509Certificate) clientKeyStore.getCertificate("client_bad_cn"); -// clientX509CertSelfSigned = (X509Certificate) clientKeyStore.getCertificate("client_self_signed"); -// clientX509CertNotTrusted = (X509Certificate) clientKeyStore.getCertificate("client_not_trusted"); } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 4daac68f9a..8331b99fff 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -29,8 +29,8 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT @Test public void testConnectAndObserveTelemetry() throws Exception { - NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_TRUST); - super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_TRUST); + NoSecClientCredential clientCredentials = createNoSecClientCredentials(CLIENT_ENDPOINT_NO_SEC); + super.basicTestConnectionObserveTelemetry(SECURITY, clientCredentials, COAP_CONFIG, CLIENT_ENDPOINT_NO_SEC); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index 11b6f3f6e7..d7296fd47c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -34,13 +34,13 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); - clientCredentials.setEndpoint(CLIENT_ENDPOINT_TRUST); + clientCredentials.setEndpoint(CLIENT_ENDPOINT_PSK); clientCredentials.setKey(pskKey); clientCredentials.setIdentity(pskIdentity); Security security = psk(SECURE_URI, SHORT_SERVER_ID, pskIdentity.getBytes(StandardCharsets.UTF_8), Hex.decodeHex(pskKey.toCharArray())); - super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); + super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_PSK); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index 1f73a2a739..0066014a9b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -33,13 +33,13 @@ public class RpkLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTes @Test public void testConnectWithRPKAndObserveTelemetry() throws Exception { RPKClientCredential rpkClientCredentials = new RPKClientCredential(); - rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_TRUST); - rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCert.getEncoded()))); + rpkClientCredentials.setEndpoint(CLIENT_ENDPOINT_RPK); + rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCertTrust.getEncoded()))); Security security = rpk(SECURE_URI, SHORT_SERVER_ID, - clientPublicKeyFromCert.getEncoded(), - clientPrivateKeyFromCert.getEncoded(), + clientPublicKeyFromCertTrust.getEncoded(), + clientPrivateKeyFromCertTrust.getEncoded(), serverPublicKeyFromCert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); + super.basicTestConnectionObserveTelemetry(security, rpkClientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_RPK); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index 6ca430327d..b2ce6c470d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.security.sql; +import org.apache.commons.codec.binary.Base64; import org.eclipse.leshan.client.object.Security; import org.junit.Ignore; import org.junit.Test; @@ -29,18 +30,18 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SHORT_SERVE public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationTest { - @Ignore @Test public void testConnectWithCertAndObserveTelemetry() throws Exception { -// X509ClientCredential credentials = new X509ClientCredential(); -// credentials.setEndpoint(CLIENT_ENDPOINT_NO_TRUST); -// credentials.setCert(SslUtil.getCertificateString(clientX509CertNotTrusted)); -// Security security = x509(SECURE_URI, -// SHORT_SERVER_ID, -// clientX509CertNotTrusted.getEncoded(), -// clientPrivateKeyNotTrustedFromCert.getEncoded(), -// serverX509Cert.getEncoded()); -// super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_NO_TRUST); + X509ClientCredential credentials = new X509ClientCredential(); + credentials.setEndpoint(CLIENT_ENDPOINT_X509_TRUST_NO); +// rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCertTrust.getEncoded()))); + credentials.setCert(SslUtil.getCertificateString(clientX509CertTrustNo)); + Security security = x509(SECURE_URI, + SHORT_SERVER_ID, + clientX509CertTrustNo.getEncoded(), + clientPrivateKeyFromCertTrustNo.getEncoded(), + serverX509Cert.getEncoded()); + super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_X509_TRUST_NO); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java index 28a10fd278..a51ea98be6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_TrustLwM2MIntegrationTest.java @@ -30,13 +30,13 @@ public class X509_TrustLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegra @Test public void testConnectAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); - credentials.setEndpoint(CLIENT_ENDPOINT_TRUST); + credentials.setEndpoint(CLIENT_ENDPOINT_X509_TRUST); Security security = x509(SECURE_URI, SHORT_SERVER_ID, - clientX509Cert.getEncoded(), - clientPrivateKeyFromCert.getEncoded(), + clientX509CertTrust.getEncoded(), + clientPrivateKeyFromCertTrust.getEncoded(), serverX509Cert.getEncoded()); - super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_TRUST); + super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_X509_TRUST); } } diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index d3301bf660..175eda993c 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,6 +10,7 @@ + diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mclient.jks b/application/src/test/resources/lwm2m/credentials/lwm2mclient.jks index 490812c01ab483d3462a09cdd18bc405fa23abe5..ca8c8ed1d77f31bab0711a31eeb63ae952c933ef 100644 GIT binary patch literal 20462 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 z z8Ycse&LxPB6tj(~84FM=Ymk`uyhaOc1nPhjqtTVUuEPve(TnAH*8{gmfzk(e9|^nC zO&8dv0X@^?xRbtoX#HlQM3P%yl%fT-|Xshij+iQ&3{+cYb}op*0%%%FFVG`WXAnuzfs3vuZVyiwg+qhQ&#YfQmnOev3YR&Fs|`_w zvd6=6<22})n8JA-Pc`K5Xu|ITWkSh0{loqpH9TxHe5Q8#ZkA@M7}rRDd97vcr{(aK(_NO*AuoQXip%VO3*IND{R*i`{< z)R%+f)}YwwP^@Jsa4xv|@u4q5q%|6rsl5{(;mplDe5J}dYhn8@VG>ix_j5M!h~27f zp~PsH79uu#tr|%pj?%+;SmeA&?Y8eB7>_Z$TMI+WpRz@Sk*W}x4H_${CqBv#a zf>W*e-W}7yPptkfa~ov-1>zP|_UoGnmkUKZ6ujCTAd-1Z3pzcPP$`|&2h8Ag@9<+&Z*!iX)(*Q`Mjx{K\hylOan*;kqm$M}?1Cu_<6QnGg~ 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 BIWPbK literal 17660 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 KeSQK10fwM$Q%GO{ diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mserver.jks b/application/src/test/resources/lwm2m/credentials/lwm2mserver.jks index a1923e942f923b8d84a989055ec50c2441f79259..d16967343fe4d56689c12353dc34808625155eec 100644 GIT binary patch delta 6129 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 DGMenJ delta 6113 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 diff --git a/application/src/test/resources/lwm2m/credentials/lwm2mtruststorechain.jks b/application/src/test/resources/lwm2m/credentials/lwm2mtruststorechain.jks index 2e46c718898093e907b646f2aa3051d7309d117c..b97f3629cda96a9c428c2556c4728310d0fbf431 100644 GIT binary patch delta 2678 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 delta 2678 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 /dev/null <<-CONFIG CONFIG echo "====================================================" -echo -e "Generate the root of certificates: \n-${CA_ROOT_KEY}-key.pem (certificate key)\n-${CA_ROOT_KEY}.pem (certificate)\n-${CA_ROOT_KEY}.csr (sign request)" +echo -e "Generate the root of certificates: \n-${CA_ROOT_CERT_KEY}-key.pem (certificate key)\n-${CA_ROOT_CERT_KEY}.pem (certificate)\n-${CA_ROOT_CERT_KEY}.csr (sign request)" echo "====================================================" cfssl genkey \ -initca \ @@ -286,14 +294,114 @@ keytool -importkeystore -deststorepass ${CLIENT_STORE_PWD} -destkeypass ${CLIENT done +#keytool -list -v -keystore ./${CLIENT_PATH}/lwm2mclient.jks -storepass client_ks_password -storetype PKCS12 + +echo "====================================================" +echo -e "Generate the root no trust in ${TRUST_NO_PATH} of certificates: \n-${CA_ROOT_CERT_KEY}-key.pem (certificate key)\n-${CA_ROOT_CERT_KEY}.pem (certificate)\n-${CA_ROOT_CERT_KEY}.csr (sign request)" +echo "====================================================" +cfssl genkey \ + -initca \ + - \ + <<-CONFIG | cfssljson -bare ./${TRUST_NO_PATH}/${CA_ROOT_CERT_KEY} +{ + "CN": "ROOT CA NO TRUST", + "key": { + "algo": "ecdsa", + "size": 256 + }, + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ], + "ca": { + "expiry": "131400h" + } +} +CONFIG + +CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${TRUST_NO_PATH}/${CA_ROOT_CERT_KEY}.pem) + +echo "====================================================" +echo -e "Generate and Signed the intermediates of our no trust in ${TRUST_NO_PATH} certificate: \n-${CA_INTERMEDIATE_CERT_KEY_PREF}?-key.pem (certificate key)\n-${CA_INTERMEDIATE_CERT_KEY_PREF}?.pem (certificate)\n-${CA_INTERMEDIATE_CERT_KEY_PREF}?.csr (sign request)" +echo "====================================================" + +CA_INTERMEDIATE_CERT_SIGN=${CA_ROOT_CERT_KEY} +CA_LIST_CERT_FOR_CAT="" +CA_INTERMEDIATE_NUMBER=0 +while [[ ${CA_INTERMEDIATE_NUMBER} -lt ${CA_INTERMEDIATE_FINISH} ]]; +do + CA_INTERMEDIATE_CERT_KEY=$(intermediate_common_name) + CA_INTERMEDIATE_NUMBER=$((${CA_INTERMEDIATE_NUMBER} + 1)) + + cfssl gencert \ + -ca ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_SIGN}.pem \ + -ca-key ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_SIGN}-key.pem \ + -config ./${TRUST_PATH}/ca-root-to-intermediate-config.json \ + -hostname "${SERVER_HOST_NAME},${SERVER_LOCAL_HOST_NAME}${SERVER_PUBLIC_HOST_NAMES:+, }${SERVER_PUBLIC_HOST_NAMES}" \ + - \ + <<-CONFIG | cfssljson -bare ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY} + { + "CN": "${CA_INTERMEDIATE_CERT_KEY}_TRUST_NO", + "names": [ + { + "C": "UK", + "ST": "Kyiv city", + "L": "Kyiv", + "O": "Thingsboard", + "OU": "DEVELOPER_TEST" + } + ] + } +CONFIG + #openssl x509 -in ${CA_INTERMEDIATE_CERT_KEY}.pem -text -noout + CA_LIST_CERT_FOR_CAT=$(set_list_sert_for_cat ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem) + CA_INTERMEDIATE_CERT_SIGN=${CA_INTERMEDIATE_CERT_KEY} +done + +echo "====================================================" +echo -e "Generate and Signed the client no trust of our certificate: \n-${CLIENT_CERT_TRUST_NO_KEY}-key.pem (certificate key)\n-${CLIENT_CERT_TRUST_NO_KEY}.pem (certificate)\n-${CLIENT_CERT_TRUST_NO_KEY}.csr (sign request)" +echo "====================================================" + + CLIENT_CERT_ALIAS=$(client_alias_name) + CLIENT_NUMBER=$((${CLIENT_NUMBER} + 1)) + + cfssl gencert \ + -ca ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem \ + -ca-key ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY}-key.pem \ + -config ./${TRUST_PATH}/ca-config.json \ + -profile client \ + -hostname "${CLIENT_HOST_NAME}" \ + - \ + <<-CONFIG | cfssljson -bare ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY} +{ + "CN": "${CLIENT_CERT_TRUST_NO_KEY}" +} +CONFIG + +echo "====================================================" +echo -e "Add the client certificate no trust (${CLIENT_CERT_TRUST_NO_KEY}.pem) to keystore: ${CLIENT_JKS_FOR_TEST}.jks" +echo "====================================================" +cat ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}.pem ${CA_LIST_CERT_FOR_CAT} > ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}_chain.pem +openssl pkcs12 -export -in ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}_chain.pem -inkey ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}-key.pem -out ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}.p12 -name ${CLIENT_CERT_ALIAS_TRUST_NO} -CAfile ./${TRUST_NO_PATH}/${CA_INTERMEDIATE_CERT_KEY}.pem -caname ${CA_ROOT_NO_ALIAS} -passin pass:${CLIENT_STORE_PWD} -passout pass:${CLIENT_STORE_PWD} +keytool -importkeystore -deststorepass ${CLIENT_STORE_PWD} -destkeypass ${CLIENT_STORE_PWD} -destkeystore ./${CLIENT_PATH}/${CLIENT_JKS_FOR_TEST}.jks -srckeystore ./${CLIENT_PATH}/${CLIENT_CERT_TRUST_NO_KEY}.p12 -srcstoretype PKCS12 -srcstorepass ${CLIENT_STORE_PWD} -alias ${CLIENT_CERT_ALIAS_TRUST_NO} + + + keytool -list -v -keystore ./${CLIENT_PATH}/lwm2mclient.jks -storepass client_ks_password -storetype PKCS12 -rm ./${TRUST_PATH}/*.p12 -rm ./${TRUST_PATH}/*.csr -rm ./${TRUST_PATH}/*.json -rm ./${TRUST_PATH}/${CA_ROOT_CERT_KEY}* -rm ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY_PREF}* +rm ./${TRUST_PATH}/*.p12 2> /dev/null +rm ./${TRUST_PATH}/*.csr 2> /dev/null +rm ./${TRUST_PATH}/*.json 2> /dev/null +rm ./${TRUST_PATH}/${CA_ROOT_CERT_KEY}* 2> /dev/null +rm ./${TRUST_PATH}/${CA_INTERMEDIATE_CERT_KEY_PREF}* 2> /dev/null + +rm -rf ${TRUST_NO_PATH} 2> /dev/null rm ./${CLIENT_PATH}/*.p12 2> /dev/null rm ./${CLIENT_PATH}/*.csr 2> /dev/null diff --git a/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_all_for_test.sh similarity index 78% rename from application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh rename to application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_all_for_test.sh index b3b114cb28..c869366ac2 100755 --- a/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_for_test_All.sh +++ b/application/src/test/resources/lwm2m/credentials/shell/lwm2m_cfssl_chain_all_for_test.sh @@ -27,11 +27,11 @@ Help() } if [ "$1" == "-h" ] ; then - echo -e "Usage 2: ./`basename $0` \"Information is not displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" - echo -e "Usage 1: ./`basename $0` true \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 1: ./`basename $0` \"Information is not displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" + echo -e "Usage 2: ./`basename $0` true \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are generated\"" echo -e "Usage 3: ./`basename $0` true false \"Information is displayed\" : \"Keys for the server are not generated\" : \"Keys for the clients and trusts are generated\"" echo -e "Usage 4: ./`basename $0` true false false \"Information is displayed\" : \"Keys for the server are not generated\" : \"Keys for the clients and trusts are not generated\"" - echo -e "Usage 4: ./`basename $0` true true false \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are not generated\"" + echo -e "Usage 5: ./`basename $0` true true false \"Information is displayed\" : \"Keys for the server are generated\" : \"Keys for the clients and trusts are not generated\"" echo "This Help File: ./`basename $0` -h" exit 0 fi @@ -53,13 +53,13 @@ if [ "$IS_IHFO" = false ] ; then ./lwm2m_cfssl_chain_server_for_test.sh > /dev/null 2>&1 & fi if [ "$IS_TRUST_CLIENT_CREATED_KEY" = true ] ; then - ./lwM2M_cfssl_chain_trusts_and_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} > /dev/null 2>&1 & + ./lwM2M_cfssl_chain_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} > /dev/null 2>&1 & fi else if [ "$IS_SERVER_CREATED_KEY" = true ] ; then ./lwm2m_cfssl_chain_server_for_test.sh fi if [ "$IS_TRUST_CLIENT_CREATED_KEY" = true ] ; then - ./lwM2M_cfssl_chain_trusts_and_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} + ./lwM2M_cfssl_chain_clients_for_test.sh ${INTERMEDIATE_START} ${INTERMEDIATE_FINISH} ${CLIENT_START} ${CLIENT_FINISH} fi fi \ No newline at end of file From be23dd2f7cfa43c1de39730d7a371208da06d454 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Sun, 9 Jan 2022 20:04:42 +0200 Subject: [PATCH 43/72] lwm2m tests add no trust and clear comments --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 8 -- .../transport/lwm2m/Lwm2mTestHelper.java | 13 ++- .../ota/sql/OtaLwM2MIntegrationTest.java | 4 - .../rpc/AbstractRpcLwM2MIntegrationTest.java | 9 +- .../AbstractSecurityLwM2MIntegrationTest.java | 89 ++----------------- .../sql/NoSecLwM2MIntegrationTest.java | 1 - .../security/sql/PskLwm2mIntegrationTest.java | 9 +- .../security/sql/RpkLwM2MIntegrationTest.java | 1 - .../sql/X509_NoTrustLwM2MIntegrationTest.java | 4 - application/src/test/resources/logback.xml | 1 - 10 files changed, 18 insertions(+), 121 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 0a95dfdbc9..09495402f3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -67,7 +67,6 @@ import java.util.concurrent.ScheduledExecutorService; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -@Slf4j @DaoSqlTest public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { @@ -135,7 +134,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest protected LwM2MTestClient client; private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; private String[] resources; -// protected String endpoint; public AbstractLwM2MIntegrationTest() { this.defaultBootstrapCredentials = new LwM2MBootstrapClientCredentials(); @@ -197,11 +195,9 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest wsClient.waitForReply(); wsClient.registerWaitForUpdate(); -// this.endpoint = endpoint; createNewClient(security, coapConfig, false, endpoint); String msg = wsClient.waitForUpdate(); - log.info("msg5555: [{}]", msg); EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); Assert.assertEquals(1, update.getCmdId()); List eData = update.getUpdate(); @@ -264,10 +260,6 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest this.resources = resources; } -// public void setEndpoint(String endpoint) { -// this.endpoint = endpoint; -// } - public void createNewClient(Security security, NetworkConfig coapConfig, boolean isRpc, String endpoint) throws Exception { clientDestroy(); client = new LwM2MTestClient(this.executor, endpoint); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 0ea700d46c..8dd44d25ae 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -25,18 +25,16 @@ public class Lwm2mTestHelper { // Server public static final int SECURE_PORT = 5686; public static final int SECURE_PORT_BS = 5688; - public static final String HOST = "localhost"; - public static final String HOST_BS = "localhost"; - public static final NetworkConfig SECURE_COAP_CONFIG = new NetworkConfig().setString("COAP_SECURE_PORT", Integer.toString(SECURE_PORT)); - public static final String ENDPOINT_SECURITY = "deviceAEndpoint"; - public static final String SECURE_URI = "coaps://localhost:" + SECURE_PORT; - public static final int PORT = 5685; public static final int PORT_BS = 5687; + public static final String HOST = "localhost"; + public static final String HOST_BS = "localhost"; public static final int SHORT_SERVER_ID = 123; public static final int SHORT_SERVER_ID_BS = 111; - public static final Security SECURITY = noSec("coap://localhost:" + PORT, SHORT_SERVER_ID); + public static final NetworkConfig SECURE_COAP_CONFIG = new NetworkConfig().setString("COAP_SECURE_PORT", Integer.toString(SECURE_PORT)); + public static final String SECURE_URI = "coaps://" + HOST + ":" + SECURE_PORT; + public static final Security SECURITY = noSec("coap://"+ HOST +":" + PORT, SHORT_SERVER_ID); public static final NetworkConfig COAP_CONFIG = new NetworkConfig().setString("COAP_PORT", Integer.toString(PORT)); // Models @@ -67,5 +65,4 @@ public class Lwm2mTestHelper { public static final String resourceIdName_3_14 = "UtfOffset"; public static final String resourceIdName_19_0_0 = "dataRead"; public static final String resourceIdName_19_1_0 = "dataWrite"; - } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 95a0a774a2..e3c538c928 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -138,8 +138,6 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile() throws Exception { -// String endpoint = "WithoutFirmwareInfoDevice"; -// setEndpoint(endpoint); createDeviceProfile(transportConfiguration); NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); final Device device = createDevice(credentials); @@ -165,8 +163,6 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateByObject5() throws Exception { -// String endpoint = "Ota5_Device"; -// setEndpoint(endpoint); createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA5); final Device device = createDevice(credentials); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 2310fe1659..4303c5e6eb 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -18,12 +18,10 @@ package org.thingsboard.server.transport.lwm2m.rpc; import org.junit.Before; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; -import org.thingsboard.server.controller.TbTestWebSocketClient; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; @@ -53,8 +51,6 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String RPC_TRANSPORT_CONFIGURATION; - protected ScheduledExecutorService executor; - protected TbTestWebSocketClient wsClient; protected String deviceId; public Set expectedObjects; public Set expectedObjectIdVers; @@ -73,7 +69,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String objectIdVer_50 = "/50"; protected String objectIdVer_3303; protected static AtomicInteger endpointSequence = new AtomicInteger(); - protected static String endpointRpcPref = "deviceEndpointRpc"; + protected static String DEVICE_ENDPOINT_RPC_PREF = "deviceEndpointRpc"; public AbstractRpcLwM2MIntegrationTest(){ setResources(resources); @@ -81,8 +77,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg @Before public void beforeTest() throws Exception { - String endpoint = endpointRpcPref + endpointSequence.incrementAndGet(); -// setEndpoint(endpoint); + String endpoint = DEVICE_ENDPOINT_RPC_PREF + endpointSequence.incrementAndGet(); init(); createNewClient (SECURITY, COAP_CONFIG, true, endpoint); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index c10eb46620..c9d2b556af 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -33,36 +33,16 @@ import java.security.cert.X509Certificate; public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { protected final String CREDENTIALS_PATH = "lwm2m/credentials/"; // client public key or id used for PSK - protected final String pskIdentity; // client public key or id used for PSK - protected final String pskKey; // client private/secret key used for PSK -// protected final PublicKey clientPublicKey; // client public key used for RPK -// protected final PrivateKey clientPrivateKey; // client private key used for RPK - - - -// // client certificate signed by rootCA but with bad CN (CN does not start by leshan_integration_test) -// protected final X509Certificate clientX509CertWithBadCN; -// // client certificate self-signed with a good CN (CN start by leshan_integration_test) -// protected final X509Certificate clientX509CertSelfSigned; -// // client certificate signed by another CA (not rootCA) with a good CN (CN start by leshan_integration_test) -// protected final X509Certificate clientX509CertNotTrusted; - - // self-signed server certificate -// protected final X509Certificate serverX509CertSelfSigned; -// // rootCA used by the server -// protected final X509Certificate rootCAX509Cert; - // certificates trustedby the server (should contain rootCA) + // Get keys PSK + protected final String CLIENT_PSK_IDENTITY = "SOME_PSK_ID"; // client public key or id used for PSK + protected final String CLIENT_PSK_KEY = "73656372657450534b73656372657450"; // client private/secret key used for PSK // Server protected static final String SERVER_JKS_FOR_TEST = "lwm2mserver"; protected static final String SERVER_STORE_PWD = "server_ks_password"; protected static final String SERVER_CERT_ALIAS = "server"; - protected final X509Certificate serverX509Cert; // server certificate signed by rootCA -// protected final PrivateKey serverPrivateKeyFromCert; // server private key used for RPK and X509 - protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK - -// // Server Trust -// protected final Certificate[] trustedCertificates = new Certificate[1]; +protected final X509Certificate serverX509Cert; // server certificate signed by rootCA + protected final PublicKey serverPublicKeyFromCert; // server public key used for RPK // Client protected LwM2MTestClient client; @@ -92,45 +72,18 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M public AbstractSecurityLwM2MIntegrationTest() { // create client credentials setResources(this.resources); -// setEndpoint(CLIENT_ENDPOINT_NO_TRUST); try { -// Get keys PSK - this.pskIdentity = "SOME_PSK_ID"; - this.pskKey = "73656372657450534b73656372657450"; - -// // Get point values -// byte[] publicX = Hex -// .decodeHex("89c048261979208666f2bfb188be1968fc9021c416ce12828c06f4e314c167b5".toCharArray()); -// byte[] publicY = Hex -// .decodeHex("cbf1eb7587f08e01688d9ada4be859137ca49f79394bad9179326b3090967b68".toCharArray()); -// byte[] privateS = Hex -// .decodeHex("e67b68d2aaeb6550f19d98cade3ad62b39532e02e6b422e1f7ea189dabaea5d2".toCharArray()); -// -// // Get Elliptic Curve Parameter spec for secp256r1 -// AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); -// algoParameters.init(new ECGenParameterSpec("secp256r1")); -// ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); -// -// // Create key specs -// KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), -// parameterSpec); -// KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); -// -// // Get keys RPK -// clientPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); -// clientPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); - // Get certificates from key store char[] clientKeyStorePwd = CLIENT_STORE_PWD.toCharArray(); KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); try (InputStream clientKeyStoreFile = this.getClass().getClassLoader().getResourceAsStream(CREDENTIALS_PATH + CLIENT_JKS_FOR_TEST + ".jks")) { clientKeyStore.load(clientKeyStoreFile, clientKeyStorePwd); } - + // Trust clientPrivateKeyFromCertTrust = (PrivateKey) clientKeyStore.getKey(CLIENT_ALIAS_CERT_TRUST, clientKeyStorePwd); clientX509CertTrust = (X509Certificate) clientKeyStore.getCertificate(CLIENT_ALIAS_CERT_TRUST); clientPublicKeyFromCertTrust = clientX509CertTrust != null ? clientX509CertTrust.getPublicKey() : null; - + // No trust clientPrivateKeyFromCertTrustNo = (PrivateKey) clientKeyStore.getKey(CLIENT_ALIAS_CERT_TRUST_NO, clientKeyStorePwd); clientX509CertTrustNo = (X509Certificate) clientKeyStore.getCertificate(CLIENT_ALIAS_CERT_TRUST_NO); clientPublicKeyFromCertTrustNo = clientX509CertTrustNo != null ? clientX509CertTrustNo.getPublicKey() : null; @@ -141,29 +94,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M // create server credentials try { -// // Get point values -// byte[] publicX = Hex -// .decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73".toCharArray()); -// byte[] publicY = Hex -// .decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a".toCharArray()); -// byte[] privateS = Hex -// .decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400".toCharArray()); -// -// // Get Elliptic Curve Parameter spec for secp256r1 -// AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC"); -// algoParameters.init(new ECGenParameterSpec("secp256r1")); -// ECParameterSpec parameterSpec = algoParameters.getParameterSpec(ECParameterSpec.class); -// -// // Create key specs -// KeySpec publicKeySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(publicX), new BigInteger(publicY)), -// parameterSpec); -// KeySpec privateKeySpec = new ECPrivateKeySpec(new BigInteger(privateS), parameterSpec); -// -// // Get keys -// serverPublicKey = KeyFactory.getInstance("EC").generatePublic(publicKeySpec); -// serverPrivateKey = KeyFactory.getInstance("EC").generatePrivate(privateKeySpec); - - // Get certificates from key store char[] serverKeyStorePwd = SERVER_STORE_PWD.toCharArray(); KeyStore serverKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); @@ -171,13 +101,8 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M serverKeyStore.load(serverKeyStoreFile, serverKeyStorePwd); } -// serverPrivateKeyFromCert = (PrivateKey) serverKeyStore.getKey("server", serverKeyStorePwd); serverX509Cert = (X509Certificate) serverKeyStore.getCertificate(SERVER_CERT_ALIAS); serverPublicKeyFromCert = serverX509Cert.getPublicKey(); -// rootCAX509Cert = (X509Certificate) serverKeyStore.getCertificate("rootCA"); - -// serverX509CertSelfSigned = (X509Certificate) serverKeyStore.getCertificate("server_self_signed"); -// trustedCertificates[0] = serverX509Cert; } catch (GeneralSecurityException | IOException e) { throw new RuntimeException(e); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 8331b99fff..c3af57ae71 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import lombok.extern.slf4j.Slf4j; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java index d7296fd47c..52a85ecf2a 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/PskLwm2mIntegrationTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.core.util.Hex; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; @@ -35,12 +34,12 @@ public class PskLwm2mIntegrationTest extends AbstractSecurityLwM2MIntegrationTes public void testConnectWithPSKAndObserveTelemetry() throws Exception { PSKClientCredential clientCredentials = new PSKClientCredential(); clientCredentials.setEndpoint(CLIENT_ENDPOINT_PSK); - clientCredentials.setKey(pskKey); - clientCredentials.setIdentity(pskIdentity); + clientCredentials.setKey(CLIENT_PSK_KEY); + clientCredentials.setIdentity(CLIENT_PSK_IDENTITY); Security security = psk(SECURE_URI, SHORT_SERVER_ID, - pskIdentity.getBytes(StandardCharsets.UTF_8), - Hex.decodeHex(pskKey.toCharArray())); + CLIENT_PSK_IDENTITY.getBytes(StandardCharsets.UTF_8), + Hex.decodeHex(CLIENT_PSK_KEY.toCharArray())); super.basicTestConnectionObserveTelemetry(security, clientCredentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_PSK); } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java index 0066014a9b..05933019bf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/RpkLwM2MIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.eclipse.leshan.client.object.Security; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredential; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java index b2ce6c470d..af7282bdb7 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509_NoTrustLwM2MIntegrationTest.java @@ -15,9 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.security.sql; -import org.apache.commons.codec.binary.Base64; import org.eclipse.leshan.client.object.Security; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredential; import org.thingsboard.server.common.transport.util.SslUtil; @@ -34,7 +32,6 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg public void testConnectWithCertAndObserveTelemetry() throws Exception { X509ClientCredential credentials = new X509ClientCredential(); credentials.setEndpoint(CLIENT_ENDPOINT_X509_TRUST_NO); -// rpkClientCredentials.setKey(new String(Base64.encodeBase64(clientPublicKeyFromCertTrust.getEncoded()))); credentials.setCert(SslUtil.getCertificateString(clientX509CertTrustNo)); Security security = x509(SECURE_URI, SHORT_SERVER_ID, @@ -43,5 +40,4 @@ public class X509_NoTrustLwM2MIntegrationTest extends AbstractSecurityLwM2MInteg serverX509Cert.getEncoded()); super.basicTestConnectionObserveTelemetry(security, credentials, SECURE_COAP_CONFIG, CLIENT_ENDPOINT_X509_TRUST_NO); } - } diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml index 175eda993c..d3301bf660 100644 --- a/application/src/test/resources/logback.xml +++ b/application/src/test/resources/logback.xml @@ -10,7 +10,6 @@ - From fa1cd21473e5fe6e2c21758b95846ae39685ea18 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Mon, 10 Jan 2022 15:21:44 +0200 Subject: [PATCH 44/72] UI: Fixed cancel event on color picker --- .../shared/components/dialog/color-picker-dialog.component.ts | 2 +- .../src/app/shared/components/json-form/json-form.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts index 2374749cbc..499f857df8 100644 --- a/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/dialog/color-picker-dialog.component.ts @@ -67,7 +67,7 @@ export class ColorPickerDialogComponent extends DialogComponent void) { this.dialogs.colorPicker(tinycolor(val).toRgbString()).subscribe((color) => { - if (colorSelectedFn) { + if (color && colorSelectedFn) { colorSelectedFn(tinycolor(color).toRgb()); } }); From d46575aeafddda85cf37b4de9b36103e660d5607 Mon Sep 17 00:00:00 2001 From: Nikitozin Date: Tue, 11 Jan 2022 11:49:28 +0200 Subject: [PATCH 45/72] [WIP][3.3.3] Add statement for support double type. --- .../engine/action/TbSaveToCustomCassandraTableNode.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index a56cb644be..bd3371edae 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -182,7 +182,11 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (dataAsObject.get(key).isJsonPrimitive()) { JsonPrimitive primitive = dataAsObject.get(key).getAsJsonPrimitive(); if (primitive.isNumber()) { - stmtBuilder.setLong(i.get(), dataAsObject.get(key).getAsLong()); + if (dataAsObject.get(key).getAsString().contains(".")) { + stmtBuilder.setDouble(i.get(), dataAsObject.get(key).getAsDouble()); + } else { + stmtBuilder.setLong(i.get(), dataAsObject.get(key).getAsLong()); + } } else if (primitive.isBoolean()) { stmtBuilder.setBoolean(i.get(), dataAsObject.get(key).getAsBoolean()); } else if (primitive.isString()) { From 3c7ec8b7d3de98e20753aa4d31f5e3ba913b3c09 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 11 Jan 2022 12:45:34 +0200 Subject: [PATCH 46/72] lwm2m tests refactoring by comments --- .../thingsboard/server/controller/TbTestWebSocketClient.java | 2 +- .../server/transport/lwm2m/AbstractLwM2MIntegrationTest.java | 1 - .../server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index 2bb68737ac..ff6b004405 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -74,7 +74,7 @@ public class TbTestWebSocketClient extends WebSocketClient { } public String waitForUpdate() { - return waitForUpdate(TimeUnit.SECONDS.toMillis(8)); + return waitForUpdate(TimeUnit.SECONDS.toMillis(3)); } public String waitForUpdate(long ms) { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 09495402f3..4fc9108862 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.lwm2m; import com.fasterxml.jackson.core.type.TypeReference; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.eclipse.californium.core.network.config.NetworkConfig; import org.eclipse.leshan.client.object.Security; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index e3c538c928..3b152ec728 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -200,8 +200,6 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { * */ @Test public void testSoftwareUpdateByObject9() throws Exception { -// String endpoint = "Ota9_Device"; -// setEndpoint(endpoint); createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9); final Device device = createDevice(credentials); From 6479160932d8a6f78e9e556a5349ef1c8946e8e9 Mon Sep 17 00:00:00 2001 From: Nikitozin Date: Tue, 11 Jan 2022 13:44:47 +0200 Subject: [PATCH 47/72] [WIP][3.3.3] Refactor variables. --- .../action/TbSaveToCustomCassandraTableNode.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index bd3371edae..66291f5245 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -179,18 +179,19 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (key.equals(ENTITY_ID)) { stmtBuilder.setUuid(i.get(), msg.getOriginator().getId()); } else if (dataAsObject.has(key)) { - if (dataAsObject.get(key).isJsonPrimitive()) { - JsonPrimitive primitive = dataAsObject.get(key).getAsJsonPrimitive(); + JsonElement dataKeyElement = dataAsObject.get(key); + if (dataKeyElement.isJsonPrimitive()) { + JsonPrimitive primitive = dataKeyElement.getAsJsonPrimitive(); if (primitive.isNumber()) { - if (dataAsObject.get(key).getAsString().contains(".")) { - stmtBuilder.setDouble(i.get(), dataAsObject.get(key).getAsDouble()); + if (primitive.getAsString().contains(".")) { + stmtBuilder.setDouble(i.get(), primitive.getAsDouble()); } else { - stmtBuilder.setLong(i.get(), dataAsObject.get(key).getAsLong()); + stmtBuilder.setLong(i.get(), primitive.getAsLong()); } } else if (primitive.isBoolean()) { - stmtBuilder.setBoolean(i.get(), dataAsObject.get(key).getAsBoolean()); + stmtBuilder.setBoolean(i.get(), primitive.getAsBoolean()); } else if (primitive.isString()) { - stmtBuilder.setString(i.get(), dataAsObject.get(key).getAsString()); + stmtBuilder.setString(i.get(), primitive.getAsString()); } else { stmtBuilder.setToNull(i.get()); } From 667045384649ac02f86e73418cf3daf07259c323 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 11 Jan 2022 14:59:49 +0200 Subject: [PATCH 48/72] UI: Added persistent page link for entities pages --- .../components/alarm/alarm-table-config.ts | 1 + .../audit-log/audit-log-table-config.ts | 4 +- .../audit-log/audit-log-table.component.ts | 8 +- .../entity/entities-table.component.ts | 96 ++++++++++++++----- .../components/event/event-table-config.ts | 1 + .../entity/entities-table-config.models.ts | 1 + .../entity/entity-table-component.models.ts | 1 + .../audit-log/audit-log-routing.module.ts | 3 +- 8 files changed, 88 insertions(+), 27 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 2bff69a53e..6c4e0a22e0 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -60,6 +60,7 @@ export class AlarmTableConfig extends EntityTableConfig this.loadDataOnInit = false; this.tableTitle = ''; this.useTimePageLink = true; + this.persistentPageLinkMode = false; this.defaultTimewindowInterval = historyInterval(DAY * 30); this.detailsPanelEnabled = false; this.selectionEnabled = false; diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts index f5c928f275..a61d0c54ab 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts @@ -52,11 +52,13 @@ export class AuditLogTableConfig extends EntityTableConfig) { + private store: Store, + private route: ActivatedRoute) { } ngOnInit() { @@ -117,6 +119,7 @@ export class AuditLogTableComponent implements OnInit { } updateOnInit = true; } + const persistentPageLinkMode = !!this.route.snapshot.data.isPage; this.auditLogTableConfig = new AuditLogTableConfig( this.auditLogService, this.translate, @@ -126,7 +129,8 @@ export class AuditLogTableComponent implements OnInit { this.entityIdValue, this.userIdValue, this.customerIdValue, - updateOnInit + updateOnInit, + persistentPageLinkMode ); } diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 33f1b2afe4..8fd2f8a976 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -41,9 +41,10 @@ import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { forkJoin, fromEvent, merge, Observable, of, Subscription } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; import { BaseData, HasId } from '@shared/models/base-data'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, QueryParamsHandling, Router } from '@angular/router'; import { - CellActionDescriptor, CellActionDescriptorType, + CellActionDescriptor, + CellActionDescriptorType, EntityActionTableColumn, EntityColumn, EntityTableColumn, @@ -55,14 +56,10 @@ import { EntityTypeTranslation } from '@shared/models/entity-type.models'; import { DialogService } from '@core/services/dialog.service'; import { AddEntityDialogComponent } from './add-entity-dialog.component'; import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models'; -import { - calculateIntervalStartEndTime, - HistoryWindowType, - Timewindow -} from '@shared/models/time/time.models'; +import { calculateIntervalStartEndTime, HistoryWindowType, Timewindow } from '@shared/models/time/time.models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; -import { isDefined, isUndefined } from '@core/utils'; +import { isDefined, isEmptyStr, isUndefined } from '@core/utils'; import { HasUUID } from '@shared/models/id/has-uuid'; import { IEntitiesTableComponent } from '@home/models/entity/entity-table-component.models'; @@ -101,6 +98,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa displayPagination = true; pageSizeOptions; pageLink: PageLink; + persistentPageLinkMode = true; textSearchMode = false; timewindow: Timewindow; dataSource: EntitiesDataSource>; @@ -117,7 +115,6 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; - private sortSubscription: Subscription; private updateDataSubscription: Subscription; private viewInited = false; @@ -128,6 +125,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa private dialogService: DialogService, private domSanitizer: DomSanitizer, private cd: ChangeDetectorRef, + private router: Router, private componentFactoryResolver: ComponentFactoryResolver) { super(store); } @@ -196,16 +194,19 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.columnsUpdated(); + const routerQueryParams = this.route.snapshot.queryParams; + let sortOrder: SortOrder = null; - if (this.entitiesTableConfig.defaultSortOrder) { + if (this.entitiesTableConfig.defaultSortOrder || routerQueryParams.hasOwnProperty('direction') || routerQueryParams.hasOwnProperty('property')) { sortOrder = { - property: this.entitiesTableConfig.defaultSortOrder.property, - direction: this.entitiesTableConfig.defaultSortOrder.direction + property: routerQueryParams?.property || this.entitiesTableConfig.defaultSortOrder.property, + direction: routerQueryParams?.direction || this.entitiesTableConfig.defaultSortOrder.direction }; } this.displayPagination = this.entitiesTableConfig.displayPagination; this.defaultPageSize = this.entitiesTableConfig.defaultPageSize; + this.persistentPageLinkMode = this.entitiesTableConfig.persistentPageLinkMode; this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; if (this.entitiesTableConfig.useTimePageLink) { @@ -217,6 +218,16 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.pageLink = new PageLink(10, 0, null, sortOrder); } this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : MAX_SAFE_PAGE_SIZE; + if (routerQueryParams.hasOwnProperty('page')) { + this.pageLink.page = routerQueryParams.page; + } + if (routerQueryParams.hasOwnProperty('pageSize')) { + this.pageLink.pageSize = routerQueryParams.pageSize; + } + if (routerQueryParams.hasOwnProperty('textSearch') && !isEmptyStr(routerQueryParams.textSearch)) { + this.textSearchMode = true; + this.pageLink.textSearch = decodeURI(routerQueryParams.textSearch); + } this.dataSource = this.entitiesTableConfig.dataSource(this.dataLoaded.bind(this)); if (this.entitiesTableConfig.onLoadAction) { this.entitiesTableConfig.onLoadAction(this.route); @@ -238,9 +249,14 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa debounceTime(150), distinctUntilChanged(), tap(() => { + const queryParams: any = { + textSearch: encodeURI(this.pageLink.textSearch) || null + }; if (this.displayPagination) { this.paginator.pageIndex = 0; + queryParams.page = null; } + this.updatedRouterQueryParams(queryParams); this.updateData(); }) ) @@ -251,23 +267,42 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } private updatePaginationSubscriptions() { - if (this.sortSubscription) { - this.sortSubscription.unsubscribe(); - this.sortSubscription = null; - } if (this.updateDataSubscription) { this.updateDataSubscription.unsubscribe(); this.updateDataSubscription = null; } + let paginatorSubscription$: Observable; + const sortSubscription$: Observable = this.sort.sortChange.asObservable().pipe( + map((data) => { + const direction = data.direction.toUpperCase(); + const queryParams: any = { + direction: this.entitiesTableConfig?.defaultSortOrder?.direction === direction ? null : direction, + property: this.entitiesTableConfig?.defaultSortOrder?.property === data.active ? null : data.active + }; + if (this.displayPagination) { + queryParams.page = null; + this.paginator.pageIndex = 0; + } + return queryParams; + }) + ); if (this.displayPagination) { - this.sortSubscription = this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0); + paginatorSubscription$ = this.paginator.page.asObservable().pipe( + map((data) => { + return { + page: data.pageIndex === 0 ? null : data.pageIndex, + pageSize: data.pageSize === this.defaultPageSize ? null : data.pageSize + }; + }) + ); } - this.updateDataSubscription = ((this.displayPagination ? merge(this.sort.sortChange, this.paginator.page) - : this.sort.sortChange) as Observable) - .pipe( - tap(() => this.updateData()) - ) - .subscribe(); + this.updateDataSubscription = ((this.displayPagination ? merge(sortSubscription$, paginatorSubscription$) + : sortSubscription$) as Observable).pipe( + tap((queryParams) => { + this.updatedRouterQueryParams(queryParams); + this.updateData(); + }) + ).subscribe(); } addEnabled() { @@ -451,9 +486,14 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa exitFilterMode() { this.textSearchMode = false; this.pageLink.textSearch = null; + const queryParams: any = { + textSearch: null + }; if (this.displayPagination) { this.paginator.pageIndex = 0; + queryParams.page = null; } + this.updatedRouterQueryParams(queryParams); this.updateData(); } @@ -468,6 +508,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property); this.sort.active = sortable.id; this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc'; + this.updatedRouterQueryParams({}, 'preserve'); if (update) { this.updateData(); } @@ -587,4 +628,13 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa return entity.id.id; } + protected updatedRouterQueryParams(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { + if (this.persistentPageLinkMode) { + this.router.navigate([], { + relativeTo: this.route, + queryParams, + queryParamsHandling + }); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index d407cf262b..d6eedb7ff6 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -92,6 +92,7 @@ export class EventTableConfig extends EntityTableConfig { this.searchEnabled = false; this.addEnabled = false; this.entitiesDeleteEnabled = false; + this.persistentPageLinkMode = false; this.headerComponent = EventTableHeaderComponent; diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index 93e68e40b7..d6ddf40735 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -160,6 +160,7 @@ export class EntityTableConfig, P extends PageLink = P addDialogStyle = {}; defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.DESC}; displayPagination = true; + persistentPageLinkMode = true; defaultPageSize = 10; columns: Array> = []; cellActionDescriptors: Array> = []; diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index 20b1d15b32..4cf41f0fed 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -50,6 +50,7 @@ export interface IEntitiesTableComponent { displayPagination: boolean; pageSizeOptions: number[]; pageLink: PageLink; + persistentPageLinkMode: boolean; textSearchMode: boolean; timewindow: Timewindow; dataSource: EntitiesDataSource>; diff --git a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts index 89d304e8f1..b471f3b540 100644 --- a/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts @@ -29,7 +29,8 @@ const routes: Routes = [ breadcrumb: { label: 'audit-log.audit-logs', icon: 'track_changes' - } + }, + isPage: true } } ]; From 9de9e6147fa5ef6db5d644b3cffd36bd6422a718 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 11 Jan 2022 17:46:06 +0200 Subject: [PATCH 49/72] lwm2m fix bug update profile if security mode == null --- .../3.3.2/schema_update_lwm2m_bootstrap.sql | 51 ++++++++++++------- .../install/SqlDatabaseUpgradeService.java | 2 + 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql b/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql index a182bc8117..8daa1b1e1e 100644 --- a/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql +++ b/application/src/main/data/upgrade/3.3.2/schema_update_lwm2m_bootstrap.sql @@ -14,6 +14,7 @@ -- limitations under the License. -- + CREATE OR REPLACE PROCEDURE update_profile_bootstrap() LANGUAGE plpgsql AS $$ @@ -25,9 +26,11 @@ BEGIN profile_data, '{transportConfiguration}', get_bootstrap( - profile_data::jsonb #> '{transportConfiguration}', - subquery.publickey_bs, - subquery.publickey_lw), + profile_data::jsonb #> '{transportConfiguration}', + subquery.publickey_bs, + subquery.publickey_lw, + profile_data::json #>> '{transportConfiguration, bootstrap, bootstrapServer, securityMode}', + profile_data::json #>> '{transportConfiguration, bootstrap, lwm2mServer, securityMode}'), true) FROM ( SELECT id, @@ -48,7 +51,8 @@ END; $$; CREATE OR REPLACE FUNCTION get_bootstrap(transport_configuration_in jsonb, publickey_bs text, - publickey_lw text) RETURNS jsonb AS + publickey_lw text, security_mode_bs text, + security_mode_lw text) RETURNS jsonb AS $$ DECLARE @@ -56,10 +60,19 @@ DECLARE bootstrap_in jsonb; BEGIN + + IF security_mode_lw IS NULL THEN + security_mode_lw := 'NO_SEC'; + END IF; + + IF security_mode_bs IS NULL THEN + security_mode_bs := 'NO_SEC'; + END IF; + bootstrap_in := transport_configuration_in::jsonb #> '{bootstrap}'; bootstrap_new := json_build_array( json_build_object('shortServerId', bootstrap_in::json #> '{bootstrapServer}' -> 'serverId', - 'securityMode', bootstrap_in::json #> '{bootstrapServer}' ->> 'securityMode', + 'securityMode', security_mode_bs, 'binding', bootstrap_in::json #> '{servers}' ->> 'binding', 'lifetime', bootstrap_in::json #> '{servers}' -> 'lifetime', 'notifIfDisabled', bootstrap_in::json #> '{servers}' -> 'notifIfDisabled', @@ -73,7 +86,7 @@ BEGIN bootstrap_in::json #> '{bootstrapServer}' -> 'bootstrapServerAccountTimeout' ), json_build_object('shortServerId', bootstrap_in::json #> '{lwm2mServer}' -> 'serverId', - 'securityMode', bootstrap_in::json #> '{lwm2mServer}' ->> 'securityMode', + 'securityMode', security_mode_lw, 'binding', bootstrap_in::json #> '{servers}' ->> 'binding', 'lifetime', bootstrap_in::json #> '{servers}' -> 'lifetime', 'notifIfDisabled', bootstrap_in::json #> '{servers}' -> 'notifIfDisabled', @@ -93,7 +106,7 @@ BEGIN bootstrap_new, true) || '{"bootstrapServerUpdateEnable": true}'; -END ; +END; $$ LANGUAGE plpgsql; CREATE OR REPLACE PROCEDURE update_device_credentials_to_base64_and_bootstrap() @@ -102,9 +115,9 @@ $$ BEGIN -UPDATE device_credentials -SET credentials_value = get_device_and_bootstrap(credentials_value::text) -WHERE credentials_type = 'LWM2M_CREDENTIALS'; + UPDATE device_credentials + SET credentials_value = get_device_and_bootstrap(credentials_value::text) + WHERE credentials_type = 'LWM2M_CREDENTIALS'; END; $$; @@ -112,7 +125,7 @@ CREATE OR REPLACE FUNCTION get_device_and_bootstrap(IN credentials_value text, O LANGUAGE plpgsql AS $$ DECLARE -client_secret_key text; + client_secret_key text; client_public_key_or_id text; client_key_value_object jsonb; client_bootstrap_server_value_object jsonb; @@ -130,7 +143,7 @@ BEGIN 'key', client_public_key_or_id); credentials_value_new := credentials_value_new::jsonb || json_build_object('client', client_key_value_object)::jsonb; -END IF; + END IF; IF credentials_value::jsonb #> '{client}' ->> 'securityConfigClientMode' = 'X509' AND NULLIF((credentials_value::jsonb #> '{client}' ->> 'cert' ~ '^[0-9a-fA-F]+$')::text, 'false') = 'true' THEN client_public_key_or_id := @@ -141,8 +154,8 @@ END IF; 'cert', client_public_key_or_id); credentials_value_new := credentials_value_new::jsonb || json_build_object('client', client_key_value_object)::jsonb; -END IF; - + END IF; + IF credentials_value::jsonb #> '{bootstrap,lwm2mServer}' ->> 'securityMode' = 'RPK' OR credentials_value::jsonb #> '{bootstrap,lwm2mServer}' ->> 'securityMode' = 'X509' THEN IF NULLIF((credentials_value::jsonb #> '{bootstrap,lwm2mServer}' ->> 'clientSecretKey' ~ '^[0-9a-fA-F]+$')::text, @@ -165,9 +178,9 @@ END IF; client_bootstrap_object := credentials_value_new::jsonb #> '{bootstrap}' || client_bootstrap_server_object::jsonb; credentials_value_new := jsonb_set(credentials_value_new::jsonb, '{bootstrap}', client_bootstrap_object::jsonb, false)::jsonb; -END IF; -END IF; - + END IF; + END IF; + IF credentials_value::jsonb #> '{bootstrap,bootstrapServer}' ->> 'securityMode' = 'RPK' OR credentials_value::jsonb #> '{bootstrap,bootstrapServer}' ->> 'securityMode' = 'X509' THEN IF NULLIF( @@ -193,8 +206,8 @@ END IF; client_bootstrap_object := credentials_value_new::jsonb #> '{bootstrap}' || client_bootstrap_server_object::jsonb; credentials_value_new := jsonb_set(credentials_value_new::jsonb, '{bootstrap}', client_bootstrap_object::jsonb, false)::jsonb; -END IF; -END IF; + END IF; + END IF; END; $$; \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index eef29026ba..c98e46d86e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -476,6 +476,8 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M..."); + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", "schema_update_lwm2m_bootstrap.sql"); + loadSql(schemaUpdateFile, conn); conn.createStatement().execute("call update_profile_bootstrap();"); log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated."); log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M..."); From b49c32e7b15117df4842386eebb02b6e0bedffd2 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 11 Jan 2022 18:19:36 +0200 Subject: [PATCH 50/72] lwm2m frefactoring by comments2 --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 4 +- .../transport/lwm2m/Lwm2mTestHelper.java | 42 +++++----- .../lwm2m/client/LwM2MTestClient.java | 25 ++---- .../client/LwM2mBinaryAppDataContainer.java | 11 +-- .../transport/lwm2m/client/LwM2mLocation.java | 4 - .../ota/AbstractOtaLwM2MIntegrationTest.java | 4 +- .../ota/sql/OtaLwM2MIntegrationTest.java | 2 +- .../rpc/AbstractRpcLwM2MIntegrationTest.java | 54 ++++++------- .../sql/RpcLwm2mIntegrationCreateTest.java | 30 ++++---- .../sql/RpcLwm2mIntegrationDeleteTest.java | 13 ++-- .../sql/RpcLwm2mIntegrationDiscoverTest.java | 8 +- .../sql/RpcLwm2mIntegrationExecuteTest.java | 30 ++++---- .../sql/RpcLwm2mIntegrationObserveTest.java | 26 +++---- .../rpc/sql/RpcLwm2mIntegrationReadTest.java | 68 ++++++++-------- ...pcLwm2mIntegrationWriteAttributesTest.java | 11 +-- .../rpc/sql/RpcLwm2mIntegrationWriteTest.java | 77 +++++++++---------- .../AbstractSecurityLwM2MIntegrationTest.java | 4 +- 17 files changed, 190 insertions(+), 223 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 4fc9108862..402e95b9ca 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -69,7 +69,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @DaoSqlTest public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { - protected String transportConfiguration = "{\n" + + protected final String TRANSPORT_CONFIGURATION = "{\n" + " \"type\": \"LWM2M\",\n" + " \"observeAttr\": {\n" + " \"keyName\": {\n" + @@ -176,7 +176,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest LwM2MClientCredential credentials, NetworkConfig coapConfig, String endpoint) throws Exception { - createDeviceProfile(transportConfiguration); + createDeviceProfile(TRANSPORT_CONFIGURATION); Device device = createDevice(credentials); SingleEntityFilter sef = new SingleEntityFilter(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java index 8dd44d25ae..19bde32467 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/Lwm2mTestHelper.java @@ -43,26 +43,26 @@ public class Lwm2mTestHelper { public static final int TEMPERATURE_SENSOR = 3303; // Ids in Client - public static final int objectId_0 = 0; - public static final int objectInstanceId_0 = 0; - public static final int objectInstanceId_1 = 1; - public static final int objectInstanceId_2 = 2; - public static final int objectInstanceId_12 = 12; - public static final int resourceId_0 = 0; - public static final int resourceId_1 = 1; - public static final int resourceId_2 = 2; - public static final int resourceId_3 = 3; - public static final int resourceId_4 = 4; - public static final int resourceId_7 = 7; - public static final int resourceId_8 = 8; - public static final int resourceId_9 = 9; - public static final int resourceId_11 = 11; - public static final int resourceId_14 = 14; - public static final int resourceId_15= 15; - public static final int resourceInstanceId_2 = 2; + public static final int OBJECT_ID_0 = 0; + public static final int OBJECT_INSTANCE_ID_0 = 0; + public static final int OBJECT_INSTANCE_ID_1 = 1; + public static final int OBJECT_INSTANCE_ID_2 = 2; + public static final int OBJECT_INSTANCE_ID_12 = 12; + public static final int RESOURCE_ID_0 = 0; + public static final int RESOURCE_ID_1 = 1; + public static final int RESOURCE_ID_2 = 2; + public static final int RESOURCE_ID_3 = 3; + public static final int RESOURCE_ID_4 = 4; + public static final int RESOURCE_ID_7 = 7; + public static final int RESOURCE_ID_8 = 8; + public static final int RESOURCE_ID_9 = 9; + public static final int RESOURCE_ID_11 = 11; + public static final int RESOURCE_ID_14 = 14; + public static final int RESOURCE_ID_15 = 15; + public static final int RESOURCE_INSTANCE_ID_2 = 2; - public static final String resourceIdName_3_9 = "batteryLevel"; - public static final String resourceIdName_3_14 = "UtfOffset"; - public static final String resourceIdName_19_0_0 = "dataRead"; - public static final String resourceIdName_19_1_0 = "dataWrite"; + public static final String RESOURCE_ID_NAME_3_9 = "batteryLevel"; + public static final String RESOURCE_ID_NAME_3_14 = "UtfOffset"; + public static final String RESOURCE_ID_NAME_19_0_0 = "dataRead"; + public static final String RESOURCE_ID_NAME_19_1_0 = "dataWrite"; } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 1c62b2b79d..af12168802 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -62,9 +62,9 @@ import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.eclipse.leshan.core.LwM2mId.SOFTWARE_MANAGEMENT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_12; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; @@ -99,12 +99,12 @@ public class LwM2MTestClient { initializer.setInstancesForObject(FIRMWARE, fwLwM2MDevice = new FwLwM2MDevice()); initializer.setInstancesForObject(SOFTWARE_MANAGEMENT, swLwM2MDevice = new SwLwM2MDevice()); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); - initializer.setInstancesForObject(BINARY_APP_DATA_CONTAINER, lwM2MBinaryAppDataContainer = new LwM2mBinaryAppDataContainer(executor, objectInstanceId_0), - new LwM2mBinaryAppDataContainer(executor, objectInstanceId_1)); + initializer.setInstancesForObject(BINARY_APP_DATA_CONTAINER, lwM2MBinaryAppDataContainer = new LwM2mBinaryAppDataContainer(executor, OBJECT_INSTANCE_ID_0), + new LwM2mBinaryAppDataContainer(executor, OBJECT_INSTANCE_ID_1)); locationParams = new LwM2MLocationParams(); locationParams.getPos(); - initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, objectInstanceId_0)); - initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2MTemperatureSensor = new LwM2mTemperatureSensor(executor, objectInstanceId_0), new LwM2mTemperatureSensor(executor, objectInstanceId_12)); + initializer.setInstancesForObject(LOCATION, new LwM2mLocation(locationParams.getLatitude(), locationParams.getLongitude(), locationParams.getScaleFactor(), executor, OBJECT_INSTANCE_ID_0)); + initializer.setInstancesForObject(TEMPERATURE_SENSOR, lwM2MTemperatureSensor = new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_0), new LwM2mTemperatureSensor(executor, OBJECT_INSTANCE_ID_12)); DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); dtlsConfig.setRecommendedCipherSuitesOnly(true); @@ -130,16 +130,6 @@ public class LwM2MTestClient { ObservationStore store) { CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); DtlsConnectorConfig.Builder dtlsConfigBuilder = new DtlsConnectorConfig.Builder(dtlsConfig); - - // tricks to be able to change psk information on the fly -// AdvancedPskStore pskStore = dtlsConfig.getAdvancedPskStore(); -// if (pskStore != null) { -// PskPublicInformation identity = pskStore.getIdentity(null, null); -// SecretKey key = pskStore -// .requestPskSecretResult(ConnectionId.EMPTY, null, identity, null, null, null).getSecret(); -// singlePSKStore = new SinglePSKStore(identity, key); -// dtlsConfigBuilder.setAdvancedPskStore(singlePSKStore); -// } builder.setConnector(new DTLSConnector(dtlsConfigBuilder.build())); builder.setNetworkConfig(coapConfig); return builder.build(); @@ -283,5 +273,4 @@ public class LwM2MTestClient { client.start(); } } - } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java index b7ac889649..7da7599119 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java @@ -66,8 +66,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements * "value":4 * }, */ -// private String data = "InNlcnZpY2VJZCI6Ik1ldGVyIiwNCiJzZXJ2aWNlRGF0YSI6ew0KImN1cnJlbnRSZWFkaW5nIjoiNDYuMyIsDQoic2lnbmFsU3RyZW5ndGgiOjE2LA0KImRhaWx5QWN0aXZpdHlUaW1lIjo1NzA2DQo="; -// private byte[] data; + Map data; private Integer priority = 0; private Time timestamp; @@ -83,7 +82,6 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements try { if (id != null) this.setId(id); executorService.scheduleWithFixedDelay(() -> -// fireResourcesChange(0, 2), 5000, 5000, TimeUnit.MILLISECONDS); fireResourcesChange(0, 2), 1800000, 1800000, TimeUnit.MILLISECONDS); // 30 MIN } catch (Throwable e) { log.error("[{}]Throwable", e.toString()); @@ -93,15 +91,11 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements @Override public ReadResponse read(ServerIdentity identity, int resourceId) { -// log.warn("Read on Location resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); try { switch (resourceId) { case 0: -// log.warn("Read on Location resource /[{}]/[{}]/[{}]", getModel().id, getId(), resourceId); ReadResponse response = ReadResponse.success(resourceId, getData(), ResourceModel.Type.OPAQUE); -// log.warn("Response [{}]", response); return response; - case 1: return ReadResponse.success(resourceId, getPriority()); case 2: @@ -168,7 +162,6 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements } private String getDataFormat() { -// return this.dataFormat == null ? "base64" : this.dataFormat; return this.dataFormat == null ? "OPAQUE" : this.dataFormat; } @@ -188,7 +181,6 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements return this.timestamp != null ? this.timestamp : new Time(new Date().getTime()); } -// fireResourcesChange(resourceId); private boolean setData(LwM2mResource value, boolean replace) { try { if (value instanceof LwM2mMultipleResource) { @@ -208,7 +200,6 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements } private Map getData() { -// this.data.put(23, new byte[]{0,0, 2,3}); return data; } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java index 079cd9fdac..b4dd8531f2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mLocation.java @@ -101,19 +101,15 @@ public class LwM2mLocation extends BaseInstanceEnabler implements Destroyable { switch (nextMove.charAt(0)) { case 'w': moveLatitude(1.0f); -// log.info("Move to North [{}]/[{}]", getLatitude(), getLongitude()); break; case 'a': moveLongitude(-1.0f); -// log.info("Move to East [{}]/[{}]", getLatitude(), getLongitude()); break; case 's': moveLatitude(-1.0f); -// log.info("Move to South [{}]/[{}]", getLatitude(), getLongitude()); break; case 'd': moveLongitude(1.0f); -// log.info("Move to West [{}]/[{}]", getLatitude(), getLongitude()); break; } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index 6cf35aeb94..a4e5e92db4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -29,13 +29,13 @@ import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; @DaoSqlTest public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { - private final String[] resources = new String[]{"3.xml", "5.xml", "9.xml"}; + private final String[] RESOURCES_OTA = new String[]{"3.xml", "5.xml", "9.xml"}; protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice"; protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device"; protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device"; public AbstractOtaLwM2MIntegrationTest() { - setResources(this.resources); + setResources(this.RESOURCES_OTA); } protected OtaPackageInfo createFirmware() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java index 3b152ec728..38db441e5d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/OtaLwM2MIntegrationTest.java @@ -138,7 +138,7 @@ public class OtaLwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @Test public void testFirmwareUpdateWithClientWithoutFirmwareOtaInfoFromProfile() throws Exception { - createDeviceProfile(transportConfiguration); + createDeviceProfile(TRANSPORT_CONFIGURATION); NoSecClientCredential credentials = createNoSecClientCredentials(this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); final Device device = createDevice(credentials); createNewClient(SECURITY, COAP_CONFIG, false, this.CLIENT_ENDPOINT_WITHOUT_FW_INFO); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index 4303c5e6eb..fba8109dbf 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -34,16 +34,16 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.COAP_CONFIG; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.SECURITY; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.TEMPERATURE_SENSOR; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_19_0_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_19_1_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_9; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resources; @DaoSqlTest @@ -60,13 +60,13 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg protected String objectInstanceIdVer_1; protected String objectIdVer_0; protected String objectIdVer_2; - private static final Predicate predicate_3 = path -> (!((String) path).contains("/" + TEMPERATURE_SENSOR) && ((String) path).contains("/" + DEVICE)); + private static final Predicate PREDICATE_3 = path -> (!((String) path).contains("/" + TEMPERATURE_SENSOR) && ((String) path).contains("/" + DEVICE)); protected String objectIdVer_3; protected String objectInstanceIdVer_3; protected String objectInstanceIdVer_5; protected String objectInstanceIdVer_9; protected String objectIdVer_19; - protected String objectIdVer_50 = "/50"; + protected final String OBJECT_ID_VER_50 = "/50"; protected String objectIdVer_3303; protected static AtomicInteger endpointSequence = new AtomicInteger(); protected static String DEVICE_ENDPOINT_RPC_PREF = "deviceEndpointRpc"; @@ -100,19 +100,19 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg }); } }); - String ver_Id_0 = client.getClient().getObjectTree().getModel().getObjectModel(objectId_0).version; + String ver_Id_0 = client.getClient().getObjectTree().getModel().getObjectModel(OBJECT_ID_0).version; if ("1.0".equals(ver_Id_0)) { - objectIdVer_0 = "/" + objectId_0; + objectIdVer_0 = "/" + OBJECT_ID_0; } else { - objectIdVer_0 = "/" + objectId_0 + "_" + ver_Id_0; + objectIdVer_0 = "/" + OBJECT_ID_0 + "_" + ver_Id_0; } objectIdVer_2 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).contains("/" + ACCESS_CONTROL)).findFirst().get(); - objectIdVer_3 = (String) expectedObjects.stream().filter(predicate_3).findFirst().get(); + objectIdVer_3 = (String) expectedObjects.stream().filter(PREDICATE_3).findFirst().get(); objectIdVer_19 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).contains("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get(); objectIdVer_3303 = (String) expectedObjectIdVers.stream().filter(path -> ((String) path).contains("/" + TEMPERATURE_SENSOR)).findFirst().get(); objectInstanceIdVer_1 = (String) expectedObjectIdVerInstances.stream().filter(path -> (!((String) path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String) path).contains("/" + SERVER))).findFirst().get(); - objectInstanceIdVer_3 = (String) expectedObjectIdVerInstances.stream().filter(predicate_3).findFirst().get(); + objectInstanceIdVer_3 = (String) expectedObjectIdVerInstances.stream().filter(PREDICATE_3).findFirst().get(); objectInstanceIdVer_5 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).contains("/" + FIRMWARE)).findFirst().get(); objectInstanceIdVer_9 = (String) expectedObjectIdVerInstances.stream().filter(path -> ((String) path).contains("/" + SOFTWARE_MANAGEMENT)).findFirst().get(); @@ -120,22 +120,22 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg " \"type\": \"LWM2M\",\n" + " \"observeAttr\": {\n" + " \"keyName\": {\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_9 + "\": \"" + resourceIdName_3_9 + "\",\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_14 + "\": \"" + resourceIdName_3_14 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_0 + "/" + resourceId_0 + "\": \"" + resourceIdName_19_0_0 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "\": \"" + resourceIdName_19_1_0 + "\"\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9 + "\": \"" + RESOURCE_ID_NAME_3_9 + "\",\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\": \"" + RESOURCE_ID_NAME_3_14 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_0_0 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\": \"" + RESOURCE_ID_NAME_19_1_0 + "\"\n" + " },\n" + " \"observe\": [\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_9 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_0 + "/" + resourceId_0 + "\"\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + "\"\n" + " ],\n" + " \"attribute\": [\n" + " ],\n" + " \"telemetry\": [\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_9 + "\",\n" + - " \"" + objectIdVer_3 + "/" + objectInstanceId_0 + "/" + resourceId_14 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_0 + "/" + resourceId_0 + "\",\n" + - " \"" + objectIdVer_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "\"\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_9 + "\",\n" + + " \"" + objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + "\",\n" + + " \"" + objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "\"\n" + " ],\n" + " \"attributeLwm2m\": {}\n" + " },\n" + diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java index 4ab0d68794..40d26bfc17 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java @@ -25,10 +25,10 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_12; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTest { @@ -43,8 +43,8 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdByIdKey_Result_CREATED() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_12; - String expectedValue = "{\"" + resourceId_0 + "\":{\"0\":\"00AC\"}, \"1\":1}"; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_12; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"0\":\"00AC\"}, \"1\":1}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CREATED.getName(), rpcActualResult.get("result").asText()); @@ -60,12 +60,12 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdAlreadyExistsById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_0; - String expectedValue = "{\"" + resourceId_0 + "\":{\"0\":\"00AC\"}, \"1\":1}"; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"0\":\"00AC\"}, \"1\":1}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); - String expected = "instance " + objectInstanceId_0 + " already exists"; + String expected = "instance " + OBJECT_INSTANCE_ID_0 + " already exists"; String actual = rpcActualResult.get("error").asText(); assertTrue(actual.equals(expected)); } @@ -77,8 +77,8 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdMandatorySingleObjectById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_3 + "/" + objectInstanceId_1; - String expectedValue = "{\"" + resourceId_0 + "\":{\"0\":\"00AC\"}}"; + String expectedPath = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_1; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"0\":\"00AC\"}}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -94,8 +94,8 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdSecurityObjectById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_0 + "/" + objectInstanceId_1; - String expectedValue = "{\"" + resourceId_0 + "\":{\"2\":4}}"; + String expectedPath = objectIdVer_0 + "/" + OBJECT_INSTANCE_ID_1; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"2\":4}}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -113,8 +113,8 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testCreateObjectInstanceWithInstanceIdAbsentObjectById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_50+ "/" + objectInstanceId_1; - String expectedValue = "{\"" + resourceId_0 + "\":{\"0\":\"00AC\"}}"; + String expectedPath = OBJECT_ID_VER_50 + "/" + OBJECT_INSTANCE_ID_1; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"0\":\"00AC\"}}"; String actualResult = sendRPCreateById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java index ebc0f6d783..099477d216 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDeleteTest.java @@ -24,10 +24,9 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_12; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_7; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTest { @@ -39,7 +38,7 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testDeleteObjectInstanceIsSuchByIdKey_Result_DELETED() throws Exception { - String expectedPath = objectIdVer_3303 + "/" + objectInstanceId_12; + String expectedPath = objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12; String actualResult = sendRPCDeleteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.DELETED.getName(), rpcActualResult.get("result").asText()); @@ -52,7 +51,7 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testDeleteObjectInstanceIsNotSuchByIdKey_Result_NOT_FOUND() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_12; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_12; String actualResult = sendRPCDeleteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); @@ -82,7 +81,7 @@ public class RpcLwm2mIntegrationDeleteTest extends AbstractRpcLwM2MIntegrationTe */ @Test public void testDeleteResourceByIdKey_Result_METHOD_NOT_ALLOWED() throws Exception { - String expectedPath = objectIdVer_3 + "/" + objectInstanceId_0 + resourceId_7; + String expectedPath = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + RESOURCE_ID_7; String actualResult = sendRPCDeleteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.METHOD_NOT_ALLOWED.getName(), rpcActualResult.get("result").asText()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java index b8ca7be61f..ede501c4a2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationDiscoverTest.java @@ -31,8 +31,8 @@ import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegrationTest { @@ -141,7 +141,7 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration */ @Test public void testDiscoverObjectInstanceAbsentInObject_Return_NOT_FOUND() throws Exception { - String expected = objectIdVer_2 + "/" + objectInstanceId_0; + String expected = objectIdVer_2 + "/" + OBJECT_INSTANCE_ID_0; String actualResult = sendDiscover(expected); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); @@ -152,7 +152,7 @@ public class RpcLwm2mIntegrationDiscoverTest extends AbstractRpcLwM2MIntegration */ @Test public void testDiscoverResourceAbsentInObject_Return_NOT_FOUND() throws Exception { - String expected = objectIdVer_2 + "/" + objectInstanceId_0 + "/" + resourceId_2; + String expected = objectIdVer_2 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_2; String actualResult = sendDiscover(expected); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java index 73ea0fb74c..4d13cbd386 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationExecuteTest.java @@ -25,12 +25,12 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_2; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_3; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_4; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_8; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_4; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_8; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationTest { @@ -43,7 +43,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteUpdateFWById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_5 + "/" + resourceId_2; + String expectedPath = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -56,7 +56,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteUpdateSWById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_9 + "/" + resourceId_4; + String expectedPath = objectInstanceIdVer_9 + "/" + RESOURCE_ID_4; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -69,7 +69,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteRebootById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_4; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_4; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -82,7 +82,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteRegistrationUpdateTriggerById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_1 + "/" + resourceId_8; + String expectedPath = objectInstanceIdVer_1 + "/" + RESOURCE_ID_8; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); @@ -96,7 +96,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteResourceWithParametersById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_4; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_4; Object expectedValue = 60; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -110,7 +110,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteBootstrapRequestTriggerById_Result_BAD_REQUEST_Error_NoBootstrapServerConfigured() throws Exception { - String expectedPath = objectInstanceIdVer_1 + "/" + resourceId_9; + String expectedPath = objectInstanceIdVer_1 + "/" + RESOURCE_ID_9; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -126,7 +126,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteResourceWithOperationNotExecuteById_Result_METHOD_NOT_ALLOWED() throws Exception { - String expectedPath = objectInstanceIdVer_5 + "/" + resourceId_3; + String expectedPath = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -143,7 +143,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteNonExistingResourceOnNonExistingObjectById_Result_BAD_REQUEST() throws Exception { - String expectedPath = objectIdVer_50 + "/" + objectInstanceId_0 + "/" + resourceId_3; + String expectedPath = OBJECT_ID_VER_50 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); @@ -161,7 +161,7 @@ public class RpcLwm2mIntegrationExecuteTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testExecuteSecurityObjectById_Result_NOT_FOUND() throws Exception { - String expectedPath = objectIdVer_0 + "/" + objectInstanceId_0 + "/" + resourceId_3; + String expectedPath = objectIdVer_0 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_3; String actualResult = sendRPCExecuteById(expectedPath); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 848a9563ed..f6b53c8c07 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -27,10 +27,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_3; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest { @@ -55,7 +55,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testObserveSingleResource_Result_CONTENT_Value_SingleResource() throws Exception { - String expectedIdVer = objectInstanceIdVer_3 + "/" + resourceId_9; + String expectedIdVer = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; String actualResult = sendObserve("Observe", expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); @@ -87,7 +87,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT @Test public void testObserveNoImplementedInstanceOnDevice_Result_NotFound() throws Exception { String objectInstanceIdVer = (String) expectedObjectIdVers.stream().filter(path -> ((String)path).contains("/" + ACCESS_CONTROL)).findFirst().get(); - String expected = objectInstanceIdVer + "/" + objectInstanceId_0; + String expected = objectInstanceIdVer + "/" + OBJECT_INSTANCE_ID_0; String actualResult = sendObserve("Observe", expected); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.NOT_FOUND.getName(), rpcActualResult.get("result").asText()); @@ -101,7 +101,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT @Test public void testObserveNoImplementedResourceOnDeviceValueNull_Result_BadRequest() throws Exception { String objectIdVer = (String) expectedObjectIdVers.stream().filter(path -> ((String)path).contains("/" + BINARY_APP_DATA_CONTAINER)).findFirst().get(); - String expected = objectIdVer + "/" + objectInstanceId_0 + "/" + resourceId_0; + String expected = objectIdVer + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; String actualResult = sendObserve("Observe", expected); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String expectedValue = "values MUST NOT be null"; @@ -116,7 +116,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testObserveRSourceNotRead_Result_METHOD_NOT_ALLOWED() throws Exception { - String expectedId = objectInstanceIdVer_5 + "/" + resourceId_0; + String expectedId = objectInstanceIdVer_5 + "/" + RESOURCE_ID_0; sendObserve("Observe", expectedId); String actualResult = sendObserve("Observe", expectedId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -130,7 +130,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT */ @Test public void testObserveRepeatedRequestObserveOnDevice_Result_BAD_REQUEST_ErrorMsg_AlreadyRegistered() throws Exception { - String expectedId = objectInstanceIdVer_3 + "/" + resourceId_0; + String expectedId = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; sendObserve("Observe", expectedId); String actualResult = sendObserve("Observe", expectedId); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -146,8 +146,8 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT @Test public void testObserveReadAll_Result_CONTENT_Value_Contains_Paths_Count_ObserveAll() throws Exception { sendObserve("ObserveCancelAll", null); - String expectedId_0 = objectInstanceIdVer_3 + "/" + resourceId_0; - String expectedId_9 = objectInstanceIdVer_3 + "/" + resourceId_9; + String expectedId_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; + String expectedId_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; sendObserve("Observe", expectedId_0); sendObserve("Observe", expectedId_9); String actualResult = sendObserve("ObserveReadAll", null); @@ -167,8 +167,8 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT @Test public void testObserveCancelOneResource_Result_CONTENT_Value_Count_1() throws Exception { sendObserve("ObserveCancelAll", null); - String expectedId_0 = objectInstanceIdVer_3 + "/" + resourceId_0; - String expectedId_3 = objectInstanceIdVer_5 + "/" + resourceId_3; + String expectedId_0 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_0; + String expectedId_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; sendObserve("Observe", expectedId_0); sendObserve("Observe", expectedId_3); String actualResult = sendObserve("ObserveCancel", expectedId_0); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java index 9116659bc3..aece59d119 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java @@ -27,18 +27,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_19_0_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_19_1_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_9; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_11; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_2; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest { @@ -96,11 +96,11 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadMultipleResourceById_Result_CONTENT_Value_IsLwM2mMultipleResource() throws Exception { - String expectedIdVer = objectInstanceIdVer_3 +"/" + resourceId_11 ; + String expectedIdVer = objectInstanceIdVer_3 +"/" + RESOURCE_ID_11; String actualResult = sendRPCById(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expected = "LwM2mMultipleResource [id=" + resourceId_11 + ", values={"; + String expected = "LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={"; assertTrue(rpcActualResult.get("value").asText().contains(expected)); } @@ -109,11 +109,11 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadSingleResourceById_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception { - String expectedIdVer = objectInstanceIdVer_3 +"/" + resourceId_14 ; + String expectedIdVer = objectInstanceIdVer_3 +"/" + RESOURCE_ID_14; String actualResult = sendRPCById(expectedIdVer); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value="; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; assertTrue(rpcActualResult.get("value").asText().contains(expected)); } @@ -122,11 +122,11 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadSingleResourceByKey_Result_CONTENT_Value_IsLwM2mSingleResource() throws Exception { - String expectedKey = resourceIdName_3_14 ; + String expectedKey = RESOURCE_ID_NAME_3_14; String actualResult = sendRPCByKey(expectedKey); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value="; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; assertTrue(rpcActualResult.get("value").asText().contains(expected)); } @@ -137,16 +137,16 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest public void testReadCompositeSingleResourceByIds_Result_CONTENT_Value_IsObjectIsLwM2mSingleResourceIsLwM2mMultipleResource() throws Exception { String expectedIdVer_1 = (String) expectedObjectIdVers.stream().filter(path -> (!((String)path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String)path).contains("/" + SERVER))).findFirst().get(); String objectId_1 = pathIdVerToObjectId(expectedIdVer_1); - String expectedIdVer3_0_1 = objectInstanceIdVer_3 + "/" + resourceId_1; - String expectedIdVer3_0_11 = objectInstanceIdVer_3 + "/" + resourceId_11; + String expectedIdVer3_0_1 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_1; + String expectedIdVer3_0_11 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11; String objectInstanceId_3 = pathIdVerToObjectId(objectInstanceIdVer_3); String expectedIds = "[\"" + expectedIdVer_1 + "\", \"" + expectedIdVer3_0_1 + "\", \"" + expectedIdVer3_0_11 + "\"]"; String actualResult = sendCompositeRPCByIds(expectedIds); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String expected1 = objectId_1 + "=LwM2mObject [id=" + new LwM2mPath(objectId_1).getObjectId() + ", instances={"; - String expected3_0_1 = objectInstanceId_3 + "/" + resourceId_1 + "=LwM2mSingleResource [id=" + resourceId_1 + ", value="; - String expected3_0_11 = objectInstanceId_3 + "/" + resourceId_11 + "=LwM2mMultipleResource [id=" + resourceId_11 + ", values={"; + String expected3_0_1 = objectInstanceId_3 + "/" + RESOURCE_ID_1 + "=LwM2mSingleResource [id=" + RESOURCE_ID_1 + ", value="; + String expected3_0_11 = objectInstanceId_3 + "/" + RESOURCE_ID_11 + "=LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={"; String actualValues = rpcActualResult.get("value").asText(); assertTrue(actualValues.contains(expected1)); assertTrue(actualValues.contains(expected3_0_1)); @@ -159,8 +159,8 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest @Test public void testReadCompositeSingleResourceByIds_Result_CONTENT_Value_IsObjectInstanceIsLwM2mSingleResource() throws Exception { String expectedIdVer3_0 = objectInstanceIdVer_3; - String expectedIdVer1_0_1 = objectInstanceIdVer_1 + "/" + resourceId_1; - String expectedIdVer1_0_2 = objectInstanceIdVer_1 + "/" + resourceId_2; + String expectedIdVer1_0_1 = objectInstanceIdVer_1 + "/" + RESOURCE_ID_1; + String expectedIdVer1_0_2 = objectInstanceIdVer_1 + "/" + RESOURCE_ID_2; String expectedIds = "[\"" + expectedIdVer1_0_1 + "\", \"" + expectedIdVer1_0_2 + "\", \"" + expectedIdVer3_0 + "\"]"; String actualResult = sendCompositeRPCByIds(expectedIds); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -169,8 +169,8 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest LwM2mPath path = new LwM2mPath(objectInstanceId_3); String expected3_0 = objectInstanceId_3 + "=LwM2mObjectInstance [id=" + path.getObjectInstanceId() + ", resources={"; String objectInstanceId_1 = pathIdVerToObjectId(objectInstanceIdVer_1); - String expected1_0_1 = objectInstanceId_1 + "/" + resourceId_1 + "=LwM2mSingleResource [id=" + resourceId_1 + ", value="; - String expected1_0_2 = objectInstanceId_1 + "/" + resourceId_2 + "=null"; + String expected1_0_1 = objectInstanceId_1 + "/" + RESOURCE_ID_1 + "=LwM2mSingleResource [id=" + RESOURCE_ID_1 + ", value="; + String expected1_0_2 = objectInstanceId_1 + "/" + RESOURCE_ID_2 + "=null"; String actualValues = rpcActualResult.get("value").asText(); assertTrue(actualValues.contains(expected3_0)); assertTrue(actualValues.contains(expected1_0_1)); @@ -182,20 +182,20 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest */ @Test public void testReadCompositeSingleResourceByKeys_Result_CONTENT_Value_3_0_IsLwM2mSingleResource_19_0_0_AND_19_0_1_Null() throws Exception { - String expectedKey3_0_9 = resourceIdName_3_9; - String expectedKey3_0_14 = resourceIdName_3_14; - String expectedKey19_0_0 = resourceIdName_19_0_0; - String expectedKey19_1_0 = resourceIdName_19_1_0; + String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; + String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; + String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0; String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]"; String actualResult = sendCompositeRPCByKeys(expectedKeys); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); String objectInstanceId_3 = pathIdVerToObjectId(objectInstanceIdVer_3); String objectId_19 = pathIdVerToObjectId(objectIdVer_19); - String expected3_0_9 = objectInstanceId_3 + "/" + resourceId_9 + "=LwM2mSingleResource [id=" + resourceId_9 + ", value="; - String expected3_0_14 = objectInstanceId_3 + "/" + resourceId_14 + "=LwM2mSingleResource [id=" + resourceId_14 + ", value="; - String expected19_0_0 = objectId_19 + "/" + objectInstanceId_0 + "/" + resourceId_0 + "=null"; - String expected19_1_0 = objectId_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "=null"; + String expected3_0_9 = objectInstanceId_3 + "/" + RESOURCE_ID_9 + "=LwM2mSingleResource [id=" + RESOURCE_ID_9 + ", value="; + String expected3_0_14 = objectInstanceId_3 + "/" + RESOURCE_ID_14 + "=LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value="; + String expected19_0_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0 + "=null"; + String expected19_1_0 = objectId_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "=null"; String actualValues = rpcActualResult.get("value").asText(); assertTrue(actualValues.contains(expected3_0_9)); assertTrue(actualValues.contains(expected3_0_14)); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java index ea23ab48b9..531fa33dc9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteAttributesTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.transport.lwm2m.rpc.sql; import com.fasterxml.jackson.databind.node.ObjectNode; import org.eclipse.leshan.core.ResponseCode; -import org.eclipse.leshan.core.node.LwM2mPath; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; @@ -25,13 +24,7 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_2; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_3; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_4; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_8; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { @@ -45,7 +38,7 @@ public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MInte */ @Test public void testWriteAttributesResourceWithParametersById_Result_INTERNAL_SERVER_ERROR() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_14; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; String expectedValue = "{\"pmax\":100, \"pmin\":10}"; String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java index 1a92831a36..f5f06631b9 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationWriteTest.java @@ -25,16 +25,15 @@ import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTes import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_1; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.objectInstanceId_2; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceIdName_3_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_0; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_14; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_15; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceId_9; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.resourceInstanceId_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_2; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_15; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_INSTANCE_ID_2; public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTest { @@ -46,7 +45,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteReplaceValueSingleResourceById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_14; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; String expectedValue = "+12"; String actualResult = sendRPCWriteStringById("WriteReplace", expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -54,7 +53,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes actualResult = sendRPCReadById(expectedPath); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue + ", type=STRING]"; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -65,7 +64,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteReplaceValueSingleResourceByKey_Result_CHANGED() throws Exception { - String expectedKey = resourceIdName_3_14; + String expectedKey = RESOURCE_ID_NAME_3_14; String expectedValue = "+09"; String actualResult = sendRPCWriteByKey("WriteReplace", expectedKey, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -73,7 +72,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes actualResult = sendRPCReadByKey(expectedKey); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue + ", type=STRING]"; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -85,7 +84,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteReplaceValueMultipleResource_Result_CHANGED_Value_Multi_Instance_Resource_must_in_Json_format() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_0 + "/" + resourceId_0; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; int resourceInstanceId0 = 0; int resourceInstanceId15 = 15; String expectedValue0 = "0000ad45675600"; @@ -115,7 +114,7 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteReplaceValueSingleResourceR_ById_Result_CHANGED() throws Exception { - String expectedPath = objectInstanceIdVer_3 + "/" + resourceId_9; + String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; Integer expectedValue = 90; String actualResult = sendRPCWriteObjectById("WriteReplace", expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); @@ -132,21 +131,21 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes String expectedPath = objectInstanceIdVer_3; String expectedValue14 = "+5"; String expectedValue15 = "Kiyv/Europe"; - String expectedValue = "{\"" + resourceId_14 + "\":\"" + expectedValue14 + "\",\"" + resourceId_15 + "\":\"" + expectedValue15 + "\"}"; + String expectedValue = "{\"" + RESOURCE_ID_14 + "\":\"" + expectedValue14 + "\",\"" + RESOURCE_ID_15 + "\":\"" + expectedValue15 + "\"}"; String actualResult = sendRPCWriteObjectById("WriteUpdate", expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); - String expectedPath14 = objectInstanceIdVer_3 + "/" + resourceId_14; - String expectedPath15 = objectInstanceIdVer_3 + "/" + resourceId_15; + String expectedPath14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; + String expectedPath15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; actualResult = sendRPCReadById(expectedPath14); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); - String expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue14 + ", type=STRING]"; + String expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue14 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadById(expectedPath15); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_15 + ", value=" + expectedValue15 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_15 + ", value=" + expectedValue15 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -157,17 +156,17 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteUpdateValueMultipleResourceById_Result_CHANGED() throws Exception { - String expectedPath = objectIdVer_19 + "/" + objectInstanceId_0; + String expectedPath = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0; int resourceInstanceId0 = 0; int resourceInstanceId25 = 25; String expectedValue0 = "00ad45675600"; String expectedValue25 = "25ad45675600cdef"; - String expectedValue = "{\"" + resourceId_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}"; + String expectedValue = "{\"" + RESOURCE_ID_0 + "\":{\"" + resourceInstanceId0 + "\":\"" + expectedValue0 + "\", \"" + resourceInstanceId25 + "\":\"" + expectedValue25 + "\"}}"; String actualResult = sendRPCWriteObjectById("WriteUpdate", expectedPath, expectedValue); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); - String expectedPath0 = expectedPath + "/" + resourceId_0 + "/" + resourceInstanceId0; - String expectedPath25 =expectedPath + "/" + resourceId_0 + "/" + resourceInstanceId25; + String expectedPath0 = expectedPath + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId0; + String expectedPath25 =expectedPath + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId25; actualResult = sendRPCReadById(expectedPath0); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); @@ -188,11 +187,11 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes @Test public void testWriteCompositeValueSingleResourceResourceInstanceByIdKey_Result_CHANGED() throws Exception { int resourceInstanceId2 = 2; - String expectedPath19_1_0_2 = objectIdVer_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "/" + resourceInstanceId2; + String expectedPath19_1_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + resourceInstanceId2; String expectedValue19_1_0_2 = "00001234"; - String expectedKey3_0_14 = resourceIdName_3_14; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedValue3_0_14 = "+04"; - String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + resourceId_15; + String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; String expectedValue3_0_15 = "Kiyv/Europe"; String nodes = "{\"" + expectedPath19_1_0_2 + "\":\"" + expectedValue19_1_0_2 + "\", \"" + expectedKey3_0_14 + "\":\"" + expectedValue3_0_14 + "\", \"" + expectedPath3_0_15 + "\":\"" + expectedValue3_0_15 + "\"}"; @@ -207,12 +206,12 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes actualResult = sendRPCReadByKey(expectedKey3_0_14); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadById(expectedPath3_0_15); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_15 + ", value=" + expectedValue3_0_15 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_15 + ", value=" + expectedValue3_0_15 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -246,11 +245,11 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteCompositeCreateResourceInstanceUpdateSingleResourceByIdKey_Result_CHANGED() throws Exception { - String expectedPath19_1_0_2 = objectIdVer_19 + "/" + objectInstanceId_1 + "/" + resourceId_0 + "/" + resourceInstanceId_2; + String expectedPath19_1_0_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_2; String expectedValue19_1_0_2 = "00001234"; - String expectedKey3_0_14 = resourceIdName_3_14; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedValue3_0_14 = "+04"; - String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + resourceId_15; + String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; String expectedValue3_0_15 = "Kiyv/Europe"; String nodes = "{\"" + expectedPath19_1_0_2 + "\":\"" + expectedValue19_1_0_2 + "\", \"" + expectedKey3_0_14 + "\":\"" + expectedValue3_0_14 + "\", \"" + expectedPath3_0_15 + "\":\"" + expectedValue3_0_15 + "\"}"; @@ -260,17 +259,17 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes actualResult = sendRPCReadById(expectedPath19_1_0_2); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); String actualValues = rpcActualResult.get("value").asText(); - String expected = "LwM2mResourceInstance [id=" + resourceInstanceId_2 + ", value=" + expectedValue19_1_0_2.length()/2 + "Bytes, type=OPAQUE]"; + String expected = "LwM2mResourceInstance [id=" + RESOURCE_INSTANCE_ID_2 + ", value=" + expectedValue19_1_0_2.length()/2 + "Bytes, type=OPAQUE]"; assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadByKey(expectedKey3_0_14); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_14 + ", value=" + expectedValue3_0_14 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); actualResult = sendRPCReadById(expectedPath3_0_15); rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); actualValues = rpcActualResult.get("value").asText(); - expected = "LwM2mSingleResource [id=" + resourceId_15 + ", value=" + expectedValue3_0_15 + ", type=STRING]"; + expected = "LwM2mSingleResource [id=" + RESOURCE_ID_15 + ", value=" + expectedValue3_0_15 + ", type=STRING]"; assertTrue(actualValues.contains(expected)); } @@ -285,11 +284,11 @@ public class RpcLwm2mIntegrationWriteTest extends AbstractRpcLwM2MIntegrationTes */ @Test public void testWriteCompositeCreateObjectInstanceUpdateSingleResourceByIdKey_Result_BAD_REQUEST() throws Exception { - String expectedPath19_1_2_2 = objectIdVer_19 + "/" + objectInstanceId_2 + "/" + resourceId_0 + "/" + resourceInstanceId_2; + String expectedPath19_1_2_2 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_2 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_2; String expectedValue19_1_0_2 = "00001234"; - String expectedKey3_0_14 = resourceIdName_3_14; + String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; String expectedValue3_0_14 = "+04"; - String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + resourceId_15; + String expectedPath3_0_15 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_15; String expectedValue3_0_15 = "Kiyv/Europe"; String nodes = "{\"" + expectedPath19_1_2_2 + "\":\"" + expectedValue19_1_0_2 + "\", \"" + expectedKey3_0_14 + "\":\"" + expectedValue3_0_14 + "\", \"" + expectedPath3_0_15 + "\":\"" + expectedValue3_0_15 + "\"}"; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index c9d2b556af..9108366395 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -62,7 +62,7 @@ protected final X509Certificate serverX509Cert; protected final X509Certificate clientX509CertTrustNo; // client certificate signed by intermediate, rootCA with a good CN ("host name") protected final PrivateKey clientPrivateKeyFromCertTrustNo; // client private key used for X509 and RPK protected final PublicKey clientPublicKeyFromCertTrustNo; // client public key used for RPK - private final String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; + private final String[] RESOURCES_SECURITY = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; @@ -71,7 +71,7 @@ protected final X509Certificate serverX509Cert; public AbstractSecurityLwM2MIntegrationTest() { // create client credentials - setResources(this.resources); + setResources(this.RESOURCES_SECURITY); try { // Get certificates from key store char[] clientKeyStorePwd = CLIENT_STORE_PWD.toCharArray(); From 1b488781d5690093e968fec4f6c6adb5f852af00 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 11 Jan 2022 19:07:33 +0200 Subject: [PATCH 51/72] lwm2m for profile bootstrap need input: X509 certificate (instead of X509 public key) --- .../server/service/lwm2m/LwM2MServiceImpl.java | 18 ++++++++++++++++++ .../bootstrap/LwM2MServerSecurityConfig.java | 14 ++++++++++---- .../dao/device/DeviceProfileServiceImpl.java | 6 +++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java index b5ec5cc654..7d0c623f96 100644 --- a/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServiceImpl.java @@ -63,6 +63,12 @@ public class LwM2MServiceImpl implements LwM2MService { } else { bsServ.setServerPublicKey(Base64.encodeBase64String(publicKeyBase64)); } + byte[] certificateBase64 = getCertificate(bsServerConfig); + if (certificateBase64 == null) { + bsServ.setServerCertificate(""); + } else { + bsServ.setServerCertificate(Base64.encodeBase64String(certificateBase64)); + } return bsServ; } @@ -77,5 +83,17 @@ public class LwM2MServiceImpl implements LwM2MService { } return null; } + + private byte[] getCertificate(LwM2MSecureServerConfig config) { + try { + SslCredentials sslCredentials = config.getSslCredentials(); + if (sslCredentials != null) { + return sslCredentials.getCertificateChain()[0].getEncoded(); + } + } catch (Exception e) { + log.trace("Failed to fetch certificate from key store!", e); + } + return null; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java index 12dd9c5a64..11bf049621 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MServerSecurityConfig.java @@ -42,17 +42,23 @@ public class LwM2MServerSecurityConfig { @ApiModelProperty(position = 8, value = "Server Public Key for 'Security' mode (DTLS): RPK or X509. Format: base64 encoded", example = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAZ0pSaGKHk/GrDaUDnQZpeEdGwX7m3Ws+U/kiVat\n" + "+44sgk3c8g0LotfMpLlZJPhPwJ6ipXV+O1r7IZUjBs3LNA==", readOnly = true) protected String serverPublicKey; - @ApiModelProperty(position = 9, value = "Bootstrap Server Account Timeout (If the value is set to 0, or if this resource is not instantiated, the Bootstrap-Server Account lifetime is infinite.)", example = "0", readOnly = true) + @ApiModelProperty(position = 9, value = "Server Public Key for 'Security' mode (DTLS): X509. Format: base64 encoded", example = "MMIICODCCAd6gAwIBAgIUI88U1zowOdrxDK/dOV+36gJxI2MwCgYIKoZIzj0EAwIwejELMAkGA1UEBhMCVUs\n" + + "xEjAQBgNVBAgTCUt5aXYgY2l0eTENMAsGA1UEBxMES3lpdjEUMBIGA1UEChMLVGhpbmdzYm9hcmQxFzAVBgNVBAsMDkRFVkVMT1BFUl9URVNUMRkwFwYDVQQDDBBpbnRlcm1lZGlhdGVfY2EwMB4XDTIyMDEwOTEzMDMwMFoXDTI3MDEwODEzMDMwMFowFDESMBAGA1UEAxM\n" + + "JbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUO3vBo/JTv0eooY7XHiKAIVDoWKFqtrU7C6q8AIKqpLcqhCdW+haFeBOH3PjY6EwaWkY04Bir4oanU0s7tz2uKOBpzCBpDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/\n" + + "BAIwADAdBgNVHQ4EFgQUEjc3Q4a0TxzP/3x3EV4fHxYUg0YwHwYDVR0jBBgwFoAUuSquGycMU6Q0SYNcbtSkSD3TfH0wLwYDVR0RBCgwJoIVbG9jYWxob3N0LmxvY2FsZG9tYWlugglsb2NhbGhvc3SCAiAtMAoGCCqGSM49BAMCA0gAMEUCIQD7dbZObyUaoDiNbX+9fUNp\n" + + "AWrD7N7XuJUwZ9FcN75R3gIgb2RNjDkHoyUyF1YajwkBk+7XmIXNClmizNJigj908mw=", readOnly = true) + protected String serverCertificate; + @ApiModelProperty(position = 10, value = "Bootstrap Server Account Timeout (If the value is set to 0, or if this resource is not instantiated, the Bootstrap-Server Account lifetime is infinite.)", example = "0", readOnly = true) Integer bootstrapServerAccountTimeout = 0; /** Config -> ObjectId = 1 'LwM2M Server' */ - @ApiModelProperty(position = 10, value = "Specify the lifetime of the registration in seconds.", example = "300", readOnly = true) + @ApiModelProperty(position = 11, value = "Specify the lifetime of the registration in seconds.", example = "300", readOnly = true) private Integer lifetime = 300; - @ApiModelProperty(position = 11, value = "The default value the LwM2M Client should use for the Minimum Period of an Observation in the absence of this parameter being included in an Observation. " + + @ApiModelProperty(position = 12, value = "The default value the LwM2M Client should use for the Minimum Period of an Observation in the absence of this parameter being included in an Observation. " + "If this Resource doesn’t exist, the default value is 0.", example = "1", readOnly = true) private Integer defaultMinPeriod = 1; /** ResourceID=6 'Notification Storing When Disabled or Offline' */ - @ApiModelProperty(position = 12, value = "If true, the LwM2M Client stores “Notify” operations to the LwM2M Server while the LwM2M Server account is disabled or the LwM2M Client is offline. After the LwM2M Server account is enabled or the LwM2M Client is online, the LwM2M Client reports the stored “Notify” operations to the Server. " + + @ApiModelProperty(position = 13, value = "If true, the LwM2M Client stores “Notify” operations to the LwM2M Server while the LwM2M Server account is disabled or the LwM2M Client is offline. After the LwM2M Server account is enabled or the LwM2M Client is online, the LwM2M Client reports the stored “Notify” operations to the Server. " + "If false, the LwM2M Client discards all the “Notify” operations or temporarily disables the Observe function while the LwM2M Server is disabled or the LwM2M Client is offline. " + "The default value is true.", example = "true", readOnly = true) private boolean notifIfDisabled = true; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 18a8a1ecde..b685fc7aa8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -765,15 +765,15 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D X509LwM2MBootstrapServerCredential x509ServerCredentials = (X509LwM2MBootstrapServerCredential) bootstrapServerConfig; server = x509ServerCredentials.isBootstrapServerIs() ? "Bootstrap Server" : "LwM2M Server"; if (StringUtils.isEmpty(x509ServerCredentials.getServerPublicKey())) { - throw new DeviceCredentialsValidationException(server + " X509 public key must be specified!"); + throw new DeviceCredentialsValidationException(server + " X509 certificate must be specified!"); } try { String certServer = EncryptionUtil.certTrimNewLines(x509ServerCredentials.getServerPublicKey()); x509ServerCredentials.setServerPublicKey(certServer); - SecurityUtil.publicKey.decode(x509ServerCredentials.getDecodedCServerPublicKey()); + SecurityUtil.certificate.decode(x509ServerCredentials.getDecodedCServerPublicKey()); } catch (Exception e) { - throw new DeviceCredentialsValidationException(server + " X509 public key must be in standard [RFC7250] and then encoded to Base64 format!"); + throw new DeviceCredentialsValidationException(server + " X509 certificate must be in DER-encoded X509v3 format and support only EC algorithm and then encoded to Base64 format!"); } break; } From e4579d21d788db8a0784a7dd9bf68383683cd216 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 12 Jan 2022 11:01:29 +0200 Subject: [PATCH 52/72] UI: Added persistent page link models and subscribe to router query params --- .../entity/entities-table.component.ts | 53 ++++++++++++------- .../src/app/shared/models/page/page-link.ts | 6 +++ 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 8fd2f8a976..11b0ea0488 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -31,12 +31,12 @@ import { import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { MAX_SAFE_PAGE_SIZE, PageLink, TimePageLink } from '@shared/models/page/page-link'; +import { MAX_SAFE_PAGE_SIZE, PageLink, PageQueryParam, TimePageLink } from '@shared/models/page/page-link'; import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; -import { MatSort } from '@angular/material/sort'; +import { MatSort, SortDirection } from '@angular/material/sort'; import { EntitiesDataSource } from '@home/models/datasource/entity-datasource'; -import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators'; +import { catchError, debounceTime, distinctUntilChanged, map, skip, tap } from 'rxjs/operators'; import { Direction, SortOrder } from '@shared/models/page/sort-order'; import { forkJoin, fromEvent, merge, Observable, of, Subscription } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; @@ -194,7 +194,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.columnsUpdated(); - const routerQueryParams = this.route.snapshot.queryParams; + const routerQueryParams: PageQueryParam = this.route.snapshot.queryParams; let sortOrder: SortOrder = null; if (this.entitiesTableConfig.defaultSortOrder || routerQueryParams.hasOwnProperty('direction') || routerQueryParams.hasOwnProperty('property')) { @@ -219,10 +219,10 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : MAX_SAFE_PAGE_SIZE; if (routerQueryParams.hasOwnProperty('page')) { - this.pageLink.page = routerQueryParams.page; + this.pageLink.page = Number(routerQueryParams.page); } if (routerQueryParams.hasOwnProperty('pageSize')) { - this.pageLink.pageSize = routerQueryParams.pageSize; + this.pageLink.pageSize = Number(routerQueryParams.pageSize); } if (routerQueryParams.hasOwnProperty('textSearch') && !isEmptyStr(routerQueryParams.textSearch)) { this.textSearchMode = true; @@ -249,19 +249,33 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa debounceTime(150), distinctUntilChanged(), tap(() => { - const queryParams: any = { + const queryParams: PageQueryParam = { textSearch: encodeURI(this.pageLink.textSearch) || null }; if (this.displayPagination) { this.paginator.pageIndex = 0; queryParams.page = null; } - this.updatedRouterQueryParams(queryParams); - this.updateData(); + this.updatedRouterParamsAndData(queryParams); }) ) .subscribe(); + this.route.queryParams.pipe(skip(1)).subscribe((params: PageQueryParam) => { + this.paginator.pageIndex = Number(params.page) || 0; + this.paginator.pageSize = Number(params.pageSize) || this.defaultPageSize; + this.sort.active = params.property || this.entitiesTableConfig.defaultSortOrder.property; + this.sort.direction = (params.direction || this.entitiesTableConfig.defaultSortOrder.direction).toLowerCase() as SortDirection; + if (params.hasOwnProperty('textSearch') && !isEmptyStr(params.textSearch)) { + this.textSearchMode = true; + this.pageLink.textSearch = decodeURI(params.textSearch); + } else { + this.textSearchMode = false; + this.pageLink.textSearch = null; + } + this.updateData(); + }); + this.updatePaginationSubscriptions(); this.viewInited = true; } @@ -275,8 +289,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa const sortSubscription$: Observable = this.sort.sortChange.asObservable().pipe( map((data) => { const direction = data.direction.toUpperCase(); - const queryParams: any = { - direction: this.entitiesTableConfig?.defaultSortOrder?.direction === direction ? null : direction, + const queryParams: PageQueryParam = { + direction: (this.entitiesTableConfig?.defaultSortOrder?.direction === direction ? null : direction) as Direction, property: this.entitiesTableConfig?.defaultSortOrder?.property === data.active ? null : data.active }; if (this.displayPagination) { @@ -297,10 +311,9 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa ); } this.updateDataSubscription = ((this.displayPagination ? merge(sortSubscription$, paginatorSubscription$) - : sortSubscription$) as Observable).pipe( + : sortSubscription$) as Observable).pipe( tap((queryParams) => { - this.updatedRouterQueryParams(queryParams); - this.updateData(); + this.updatedRouterParamsAndData(queryParams); }) ).subscribe(); } @@ -486,15 +499,14 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa exitFilterMode() { this.textSearchMode = false; this.pageLink.textSearch = null; - const queryParams: any = { + const queryParams: PageQueryParam = { textSearch: null }; if (this.displayPagination) { this.paginator.pageIndex = 0; queryParams.page = null; } - this.updatedRouterQueryParams(queryParams); - this.updateData(); + this.updatedRouterParamsAndData(queryParams); } resetSortAndFilter(update: boolean = true, preserveTimewindow: boolean = false) { @@ -508,9 +520,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa const sortable = this.sort.sortables.get(this.entitiesTableConfig.defaultSortOrder.property); this.sort.active = sortable.id; this.sort.direction = this.entitiesTableConfig.defaultSortOrder.direction === Direction.ASC ? 'asc' : 'desc'; - this.updatedRouterQueryParams({}, 'preserve'); if (update) { - this.updateData(); + this.updatedRouterParamsAndData({}, 'preserve'); } } @@ -628,13 +639,15 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa return entity.id.id; } - protected updatedRouterQueryParams(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { + protected updatedRouterParamsAndData(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { if (this.persistentPageLinkMode) { this.router.navigate([], { relativeTo: this.route, queryParams, queryParamsHandling }); + } else { + this.updateData(); } } } diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts index 92fe4634f5..2528b6ce99 100644 --- a/ui-ngx/src/app/shared/models/page/page-link.ts +++ b/ui-ngx/src/app/shared/models/page/page-link.ts @@ -23,6 +23,12 @@ export const MAX_SAFE_PAGE_SIZE = 2147483647; export type PageLinkSearchFunction = (entity: T, textSearch: string, searchProperty?: string) => boolean; +export interface PageQueryParam extends Partial{ + textSearch?: string; + pageSize?: number; + page?: number; +} + export function defaultPageLinkSearchFunction(searchProperty?: string): PageLinkSearchFunction { return (entity, textSearch) => defaultPageLinkSearch(entity, textSearch, searchProperty); } From 08997d682c0c69392cd268be8633408d6a2b325f Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Wed, 12 Jan 2022 12:23:43 +0200 Subject: [PATCH 53/72] Don't allow sysadmin to delete himself --- .../org/thingsboard/server/controller/UserController.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index e83bc8970c..c7369fa5b9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -302,6 +302,10 @@ public class UserController extends BaseController { UserId userId = new UserId(toUUID(strUserId)); User user = checkUserId(userId, Operation.DELETE); + if (user.getAuthority() == Authority.SYS_ADMIN && getCurrentUser().getId().equals(userId)) { + throw new ThingsboardException("Sysadmin is not allowed to delete himself", ThingsboardErrorCode.PERMISSION_DENIED); + } + List relatedEdgeIds = findRelatedEdgeIds(getTenantId(), userId); userService.deleteUser(getCurrentUser().getTenantId(), userId); From b84ab58d7b41f307d8d8a4cd99f160c5a73dc330 Mon Sep 17 00:00:00 2001 From: Sergey Tarnavskiy Date: Wed, 12 Jan 2022 12:56:02 +0200 Subject: [PATCH 54/72] LwM2M transport. Added serverCertificate value for X509-securityMode in Bootstrap-config. --- .../device/lwm2m/lwm2m-device-config-server.component.ts | 3 +++ .../profile/device/lwm2m/lwm2m-profile-config.models.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts index fa6d31153c..de75ea416b 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-config-server.component.ts @@ -112,6 +112,9 @@ export class Lwm2mDeviceConfigServerComponent implements OnInit, ControlValueAcc this.changeSecurityHostPortFields(serverSecurityConfig); } this.serverFormGroup.patchValue(serverSecurityConfig, {emitEvent: false}); + if (this.currentSecurityMode === Lwm2mSecurityType.X509) { + this.serverFormGroup.get('serverPublicKey').patchValue(serverSecurityConfig.serverCertificate, {emitEvent: false}); + } }); this.serverFormGroup.valueChanges.pipe( takeUntil(this.destroy$) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index 56ae00c5a9..ce2824fb12 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -129,6 +129,7 @@ export interface ServerSecurityConfig { securityHost?: string; securityPort?: number; serverPublicKey?: string; + serverCertificate?: string; clientHoldOffTime?: number; shortServerId?: number; bootstrapServerAccountTimeout: number; From ab76d5a7a6c84053a93013c01237a13e84a0d5d0 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 12 Jan 2022 13:59:13 +0200 Subject: [PATCH 55/72] fixed timescale agg by timezone --- .../dao/sqlts/timescale/AggregationRepository.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java index 6559ea2c9e..8628d3e160 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java @@ -38,15 +38,15 @@ public class AggregationRepository { public static final String FROM_WHERE_CLAUSE = "FROM ts_kv tskv WHERE tskv.entity_id = cast(:entityId AS uuid) AND tskv.key= cast(:entityKey AS int) AND tskv.ts > :startTs AND tskv.ts <= :endTs GROUP BY tskv.entity_id, tskv.key, tsBucket ORDER BY tskv.entity_id, tskv.key, tsBucket"; - public static final String FIND_AVG_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'AVG' AS aggType "; + public static final String FIND_AVG_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'AVG' AS aggType "; - public static final String FIND_MAX_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MAX(tskv.str_v) AS strValue, 'MAX' AS aggType "; + public static final String FIND_MAX_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MAX(tskv.str_v) AS strValue, 'MAX' AS aggType "; - public static final String FIND_MIN_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MIN(tskv.str_v) AS strValue, 'MIN' AS aggType "; + public static final String FIND_MIN_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MIN(tskv.str_v) AS strValue, 'MIN' AS aggType "; - public static final String FIND_SUM_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, null AS jsonValue, 'SUM' AS aggType "; + public static final String FIND_SUM_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, null AS jsonValue, 'SUM' AS aggType "; - public static final String FIND_COUNT_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts) AS tsBucket, :timeBucket AS interval, SUM(CASE WHEN tskv.bool_v IS NULL THEN 0 ELSE 1 END) AS booleanValueCount, SUM(CASE WHEN tskv.str_v IS NULL THEN 0 ELSE 1 END) AS strValueCount, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longValueCount, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleValueCount, SUM(CASE WHEN tskv.json_v IS NULL THEN 0 ELSE 1 END) AS jsonValueCount "; + public static final String FIND_COUNT_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(CASE WHEN tskv.bool_v IS NULL THEN 0 ELSE 1 END) AS booleanValueCount, SUM(CASE WHEN tskv.str_v IS NULL THEN 0 ELSE 1 END) AS strValueCount, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longValueCount, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleValueCount, SUM(CASE WHEN tskv.json_v IS NULL THEN 0 ELSE 1 END) AS jsonValueCount "; @PersistenceContext private EntityManager entityManager; From 6c166b90773092db02c702e24cd7739abddbec8e Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Wed, 12 Jan 2022 11:20:18 +0200 Subject: [PATCH 56/72] Restore alarm relations update script. Add Bootstrap upgrade script # Conflicts: # application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java --- .../install/SqlDatabaseUpgradeService.java | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index c98e46d86e..6f142bab4c 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2021 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 - * + *

+ * 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. @@ -475,14 +475,30 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.info("Updating schema ..."); schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); - log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M..."); - schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", "schema_update_lwm2m_bootstrap.sql"); - loadSql(schemaUpdateFile, conn); - conn.createStatement().execute("call update_profile_bootstrap();"); - log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated."); - log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M..."); - conn.createStatement().execute("call update_device_credentials_to_base64_and_bootstrap();"); - log.info("Client`s public key and secret key from HexDec to Base64 for LWM2M updated."); + try { + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + " select tenant_id, originator_id, created_time, type, customer_id, id from alarm;"); + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + " select a.tenant_id, r.from_id, created_time, type, customer_id, id" + + " from alarm a inner join relation r on r.relation_type_group = 'ALARM' and r.relation_type = 'ANY' and a.id = r.to_id ON CONFLICT DO NOTHING;"); + conn.createStatement().execute("delete from relation r where r.relation_type_group = 'ALARM';"); + } catch (Exception e) { + log.error("Failed to update alarm relations!!!", e); + } + + log.info("Updating lwm2m device profiles ..."); + try { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", "schema_update_lwm2m_bootstrap.sql"); + loadSql(schemaUpdateFile, conn); + log.info("Updating server`s public key from HexDec to Base64 in profile for LWM2M..."); + conn.createStatement().execute("call update_profile_bootstrap();"); + log.info("Server`s public key from HexDec to Base64 in profile for LWM2M updated."); + log.info("Updating client`s public key and secret key from HexDec to Base64 for LWM2M..."); + conn.createStatement().execute("call update_device_credentials_to_base64_and_bootstrap();"); + log.info("Client`s public key and secret key from HexDec to Base64 for LWM2M updated."); + } catch (Exception e) { + log.error("Failed to update lwm2m profiles!!!", e); + } log.info("Updating schema settings..."); conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003003;"); log.info("Schema updated."); From 69f060a48ce37cfc757ca93ca6bba4c975c1c9ad Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 12 Jan 2022 14:50:16 +0200 Subject: [PATCH 57/72] - license --- .../server/service/install/SqlDatabaseUpgradeService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 6f142bab4c..4a6d64caea 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2021 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 - *

+ * + * 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. From 926642bf8a5eb32a61d412adb833c8fddbfe37e9 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Wed, 12 Jan 2022 16:20:46 +0200 Subject: [PATCH 58/72] fixed column name in upgrade query --- .../server/service/install/SqlDatabaseUpgradeService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 4a6d64caea..51741fad5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -476,9 +476,9 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.3.2", SCHEMA_UPDATE_SQL); loadSql(schemaUpdateFile, conn); try { - conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, alarm_type, customer_id, alarm_id)" + " select tenant_id, originator_id, created_time, type, customer_id, id from alarm;"); - conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, type, customer_id, alarm_id)" + + conn.createStatement().execute("insert into entity_alarm(tenant_id, entity_id, created_time, alarm_type, customer_id, alarm_id)" + " select a.tenant_id, r.from_id, created_time, type, customer_id, id" + " from alarm a inner join relation r on r.relation_type_group = 'ALARM' and r.relation_type = 'ANY' and a.id = r.to_id ON CONFLICT DO NOTHING;"); conn.createStatement().execute("delete from relation r where r.relation_type_group = 'ALARM';"); From 7e71e909e39f669ea612dc9d4763fcd767c013a5 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 12 Jan 2022 17:31:30 +0200 Subject: [PATCH 59/72] UI: Rename variable persistentPageLinkMode --- .../app/modules/home/components/alarm/alarm-table-config.ts | 2 +- .../home/components/audit-log/audit-log-table-config.ts | 4 ++-- .../home/components/audit-log/audit-log-table.component.ts | 4 ++-- .../home/components/entity/entities-table.component.ts | 6 +++--- .../app/modules/home/components/event/event-table-config.ts | 2 +- .../home/models/entity/entities-table-config.models.ts | 2 +- .../home/models/entity/entity-table-component.models.ts | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts index 6c4e0a22e0..9a200b8a3d 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-table-config.ts @@ -60,7 +60,7 @@ export class AlarmTableConfig extends EntityTableConfig this.loadDataOnInit = false; this.tableTitle = ''; this.useTimePageLink = true; - this.persistentPageLinkMode = false; + this.pageMode = false; this.defaultTimewindowInterval = historyInterval(DAY * 30); this.detailsPanelEnabled = false; this.selectionEnabled = false; diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts index a61d0c54ab..2a5d339230 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts @@ -53,12 +53,12 @@ export class AuditLogTableConfig extends EntityTableConfig>; @@ -206,7 +206,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.displayPagination = this.entitiesTableConfig.displayPagination; this.defaultPageSize = this.entitiesTableConfig.defaultPageSize; - this.persistentPageLinkMode = this.entitiesTableConfig.persistentPageLinkMode; + this.pageMode = this.entitiesTableConfig.pageMode; this.pageSizeOptions = [this.defaultPageSize, this.defaultPageSize * 2, this.defaultPageSize * 3]; if (this.entitiesTableConfig.useTimePageLink) { @@ -640,7 +640,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa } protected updatedRouterParamsAndData(queryParams: object, queryParamsHandling: QueryParamsHandling = 'merge') { - if (this.persistentPageLinkMode) { + if (this.pageMode) { this.router.navigate([], { relativeTo: this.route, queryParams, diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index d6eedb7ff6..a10200060a 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -92,7 +92,7 @@ export class EventTableConfig extends EntityTableConfig { this.searchEnabled = false; this.addEnabled = false; this.entitiesDeleteEnabled = false; - this.persistentPageLinkMode = false; + this.pageMode = false; this.headerComponent = EventTableHeaderComponent; diff --git a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts index d6ddf40735..d5866cbd74 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts @@ -160,7 +160,7 @@ export class EntityTableConfig, P extends PageLink = P addDialogStyle = {}; defaultSortOrder: SortOrder = {property: 'createdTime', direction: Direction.DESC}; displayPagination = true; - persistentPageLinkMode = true; + pageMode = true; defaultPageSize = 10; columns: Array> = []; cellActionDescriptors: Array> = []; diff --git a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts index 4cf41f0fed..3c1a7428ab 100644 --- a/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/entity/entity-table-component.models.ts @@ -50,7 +50,7 @@ export interface IEntitiesTableComponent { displayPagination: boolean; pageSizeOptions: number[]; pageLink: PageLink; - persistentPageLinkMode: boolean; + pageMode: boolean; textSearchMode: boolean; timewindow: Timewindow; dataSource: EntitiesDataSource>; From 7820cb22de4c9cfb1d1b19b36945d69531428eb7 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 12 Jan 2022 17:51:27 +0200 Subject: [PATCH 60/72] lwm2m: preparing for certificate validation in bootstrap mode --- .../secure/TbLwM2MCertificateVerifier.java | 91 +++++++++++++++++++ .../TbLwM2MDtlsCertificateVerifier.java | 54 +---------- 2 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MCertificateVerifier.java diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MCertificateVerifier.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MCertificateVerifier.java new file mode 100644 index 0000000000..620d7243c4 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MCertificateVerifier.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2021 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.secure; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; +import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; +import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; + +@Slf4j +@Component +@TbLwM2mTransportComponent +@RequiredArgsConstructor +public class TbLwM2MCertificateVerifier { + + private final LwM2MTransportServerConfig config; + private final LwM2mCredentialsSecurityInfoValidator securityInfoValidator; + + public TbLwM2MSecurityInfo verifyCertificate(X509Certificate cert, String sha3Hash, LwM2mTypeServer lwM2mTypeServer) { + TbLwM2MSecurityInfo securityInfo = null; + // verify if trust + if (config.getTrustSslCredentials() != null && config.getTrustSslCredentials().getTrustedCertificates().length > 0) { + if (verifyTrust(cert, config.getTrustSslCredentials().getTrustedCertificates()) != null) { + String endpoint = config.getTrustSslCredentials().getValueFromSubjectNameByKey(cert.getSubjectX500Principal().getName(), "CN"); + securityInfo = StringUtils.isNotEmpty(endpoint) ? securityInfoValidator.getEndpointSecurityInfoByCredentialsId(endpoint, lwM2mTypeServer) : null; + } + } + // if not trust or cert trust securityInfo == null + if (securityInfo == null) { + try { + securityInfo = securityInfoValidator.getEndpointSecurityInfoByCredentialsId(sha3Hash, lwM2mTypeServer); + } catch (LwM2MAuthException e) { + log.trace("Failed find security info: {}", sha3Hash, e); + } + } + return securityInfo; + } + + private X509Certificate verifyTrust(X509Certificate certificate, X509Certificate[] certificates) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + CertPath cp = cf.generateCertPath(Arrays.asList(new X509Certificate[]{certificate})); + for (int index = 0; index < certificates.length; ++index) { + X509Certificate caCert = certificates[index]; + try { + TrustAnchor trustAnchor = new TrustAnchor(caCert, null); + CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); + PKIXParameters pkixParams = new PKIXParameters( + Collections.singleton(trustAnchor)); + pkixParams.setRevocationEnabled(false); + if (cpv.validate(cp, pkixParams) != null) return certificate; + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertPathValidatorException e) { + log.trace("[{}]. [{}]", certificate.getSubjectDN(), e.getMessage()); + } + } + } catch (CertificateException e) { + log.trace("[{}] certPath not valid. [{}]", certificate.getSubjectDN(), e.getMessage()); + } + return null; + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java index babf385bc8..5703f5b366 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java @@ -29,7 +29,6 @@ import org.eclipse.californium.scandium.dtls.HandshakeResultHandler; import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier; import org.eclipse.californium.scandium.dtls.x509.StaticCertificateVerifier; import org.eclipse.californium.scandium.util.ServerNames; -import org.eclipse.leshan.core.util.SecurityUtil; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -44,28 +43,18 @@ import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.queue.util.TbLwM2mTransportComponent; import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.secure.credentials.LwM2MClientCredentials; -import org.thingsboard.server.transport.lwm2m.server.client.LwM2MAuthException; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore; import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore; import javax.annotation.PostConstruct; import javax.security.auth.x500.X500Principal; -import java.security.InvalidAlgorithmParameterException; -import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.cert.CertPath; -import java.security.cert.CertPathValidator; -import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateFactory; import java.security.cert.CertificateNotYetValidException; -import java.security.cert.PKIXParameters; -import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.Arrays; -import java.util.Collections; import java.util.List; import static org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mTypeServer.CLIENT; @@ -80,6 +69,7 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer private final LwM2MTransportServerConfig config; private final LwM2mCredentialsSecurityInfoValidator securityInfoValidator; private final TbMainSecurityStore securityStore; + private final TbLwM2MCertificateVerifier certificateVerifier; @SuppressWarnings("deprecation") private StaticCertificateVerifier staticCertificateVerifier; @@ -124,26 +114,9 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer if (!skipValidityCheckForClientCert) { cert.checkValidity(); } - - - TbLwM2MSecurityInfo securityInfo = null; - // verify if trust - if (config.getTrustSslCredentials() != null && config.getTrustSslCredentials().getTrustedCertificates().length > 0) { - if (verifyTrust(cert, config.getTrustSslCredentials().getTrustedCertificates()) != null) { - String endpoint = config.getTrustSslCredentials().getValueFromSubjectNameByKey(cert.getSubjectX500Principal().getName(), "CN"); - securityInfo = StringUtils.isNotEmpty(endpoint) ? securityInfoValidator.getEndpointSecurityInfoByCredentialsId(endpoint, CLIENT) : null; - } - } - // if not trust or cert trust securityInfo == null String strCert = SslUtil.getCertificateString(cert); String sha3Hash = EncryptionUtil.getSha3Hash(strCert); - if (securityInfo == null) { - try { - securityInfo = securityInfoValidator.getEndpointSecurityInfoByCredentialsId(sha3Hash, CLIENT); - } catch (LwM2MAuthException e) { - log.trace("Failed find security info: {}", sha3Hash, e); - } - } + TbLwM2MSecurityInfo securityInfo = certificateVerifier.verifyCertificate(cert, sha3Hash, CLIENT); ValidateDeviceCredentialsResponse msg = securityInfo != null ? securityInfo.getMsg() : null; if (msg != null && org.thingsboard.server.common.data.StringUtils.isNotEmpty(msg.getCredentials())) { LwM2MClientCredentials credentials = JacksonUtil.fromString(msg.getCredentials(), LwM2MClientCredentials.class); @@ -201,27 +174,4 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer public void setResultHandler(HandshakeResultHandler resultHandler) { } - - private X509Certificate verifyTrust(X509Certificate certificate, X509Certificate[] certificates) { - try { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - CertPath cp = cf.generateCertPath(Arrays.asList(new X509Certificate[]{certificate})); - for (int index = 0; index < certificates.length; ++index) { - X509Certificate caCert = certificates[index]; - try { - TrustAnchor trustAnchor = new TrustAnchor(caCert, null); - CertPathValidator cpv = CertPathValidator.getInstance("PKIX"); - PKIXParameters pkixParams = new PKIXParameters( - Collections.singleton(trustAnchor)); - pkixParams.setRevocationEnabled(false); - if (cpv.validate(cp, pkixParams) != null) return certificate; - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertPathValidatorException e) { - log.trace("[{}]. [{}]", certificate.getSubjectDN(), e.getMessage()); - } - } - } catch (CertificateException e) { - log.trace("[{}] certPath not valid. [{}]", certificate.getSubjectDN(), e.getMessage()); - } - return null; - } } From 5bf75acccedca62b2f628d7a95ce203822c51a68 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 12 Jan 2022 19:01:47 +0200 Subject: [PATCH 61/72] Update material icons font --- ui-ngx/angular.json | 5 - ui-ngx/package.json | 1 - .../services/material-icons-codepoints.raw | 2142 +++++++++++++++++ ui-ngx/src/app/core/services/utils.service.ts | 2 +- .../assets/fonts/MaterialIcons-Regular.ttf | Bin 0 -> 337868 bytes ui-ngx/src/assets/fonts/material-icons.css | 33 + ui-ngx/src/index.html | 2 +- ui-ngx/yarn.lock | 5 - 8 files changed, 2177 insertions(+), 13 deletions(-) create mode 100644 ui-ngx/src/app/core/services/material-icons-codepoints.raw create mode 100644 ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf create mode 100644 ui-ngx/src/assets/fonts/material-icons.css diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index 7565ae770c..0691b25b4a 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -63,11 +63,6 @@ "glob": "marker-shadow.png", "input": "node_modules/leaflet/dist/images/", "output": "/" - }, - { - "glob": "**/*", - "input": "node_modules/material-design-icons/iconfont/", - "output": "assets/fonts" } ], "styles": [ diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 1153524a26..d3174bfc4d 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -61,7 +61,6 @@ "leaflet-providers": "^1.13.0", "leaflet.gridlayer.googlemutant": "^0.13.4", "leaflet.markercluster": "^1.5.3", - "material-design-icons": "^3.0.1", "messageformat": "^2.3.0", "moment": "^2.29.1", "moment-timezone": "^0.5.34", diff --git a/ui-ngx/src/app/core/services/material-icons-codepoints.raw b/ui-ngx/src/app/core/services/material-icons-codepoints.raw new file mode 100644 index 0000000000..a1f9b8bbc7 --- /dev/null +++ b/ui-ngx/src/app/core/services/material-icons-codepoints.raw @@ -0,0 +1,2142 @@ +10k e951 +10mp e952 +11mp e953 +123 eb8d +12mp e954 +13mp e955 +14mp e956 +15mp e957 +16mp e958 +17mp e959 +18mp e95a +19mp e95b +1k e95c +1k_plus e95d +1x_mobiledata efcd +20mp e95e +21mp e95f +22mp e960 +23mp e961 +24mp e962 +2k e963 +2k_plus e964 +2mp e965 +30fps efce +30fps_select efcf +360 e577 +3d_rotation e84d +3g_mobiledata efd0 +3k e966 +3k_plus e967 +3mp e968 +3p efd1 +4g_mobiledata efd2 +4g_plus_mobiledata efd3 +4k e072 +4k_plus e969 +4mp e96a +5g ef38 +5k e96b +5k_plus e96c +5mp e96d +60fps efd4 +60fps_select efd5 +6_ft_apart f21e +6k e96e +6k_plus e96f +6mp e970 +7k e971 +7k_plus e972 +7mp e973 +8k e974 +8k_plus e975 +8mp e976 +9k e977 +9k_plus e978 +9mp e979 +abc eb94 +ac_unit eb3b +access_alarm e190 +access_alarms e191 +access_time e192 +access_time_filled efd6 +accessibility e84e +accessibility_new e92c +accessible e914 +accessible_forward e934 +account_balance e84f +account_balance_wallet e850 +account_box e851 +account_circle e853 +account_tree e97a +ad_units ef39 +adb e60e +add e145 +add_a_photo e439 +add_alarm e193 +add_alert e003 +add_box e146 +add_business e729 +add_call e0e8 +add_card eb86 +add_chart e97b +add_circle e147 +add_circle_outline e148 +add_comment e266 +add_ic_call e97c +add_link e178 +add_location e567 +add_location_alt ef3a +add_moderator e97d +add_photo_alternate e43e +add_reaction e1d3 +add_road ef3b +add_shopping_cart e854 +add_task f23a +add_to_drive e65c +add_to_home_screen e1fe +add_to_photos e39d +add_to_queue e05c +addchart ef3c +adf_scanner eada +adjust e39e +admin_panel_settings ef3d +adobe ea96 +ads_click e762 +agriculture ea79 +air efd8 +airline_seat_flat e630 +airline_seat_flat_angled e631 +airline_seat_individual_suite e632 +airline_seat_legroom_extra e633 +airline_seat_legroom_normal e634 +airline_seat_legroom_reduced e635 +airline_seat_recline_extra e636 +airline_seat_recline_normal e637 +airline_stops e7d0 +airlines e7ca +airplane_ticket efd9 +airplanemode_active e195 +airplanemode_inactive e194 +airplanemode_off e194 +airplanemode_on e195 +airplay e055 +airport_shuttle eb3c +alarm e855 +alarm_add e856 +alarm_off e857 +alarm_on e858 +album e019 +align_horizontal_center e00f +align_horizontal_left e00d +align_horizontal_right e010 +align_vertical_bottom e015 +align_vertical_center e011 +align_vertical_top e00c +all_inbox e97f +all_inclusive eb3d +all_out e90b +alt_route f184 +alternate_email e0e6 +amp_stories ea13 +analytics ef3e +anchor f1cd +android e859 +animation e71c +announcement e85a +aod efda +apartment ea40 +api f1b7 +app_blocking ef3f +app_registration ef40 +app_settings_alt ef41 +app_shortcut eae4 +apple ea80 +approval e982 +apps e5c3 +apps_outage e7cc +architecture ea3b +archive e149 +area_chart e770 +arrow_back e5c4 +arrow_back_ios e5e0 +arrow_back_ios_new e2ea +arrow_circle_down f181 +arrow_circle_left eaa7 +arrow_circle_right eaaa +arrow_circle_up f182 +arrow_downward e5db +arrow_drop_down e5c5 +arrow_drop_down_circle e5c6 +arrow_drop_up e5c7 +arrow_forward e5c8 +arrow_forward_ios e5e1 +arrow_left e5de +arrow_right e5df +arrow_right_alt e941 +arrow_upward e5d8 +art_track e060 +article ef42 +aspect_ratio e85b +assessment e85c +assignment e85d +assignment_ind e85e +assignment_late e85f +assignment_return e860 +assignment_returned e861 +assignment_turned_in e862 +assistant e39f +assistant_direction e988 +assistant_navigation e989 +assistant_photo e3a0 +assured_workload eb6f +atm e573 +attach_email ea5e +attach_file e226 +attach_money e227 +attachment e2bc +attractions ea52 +attribution efdb +audio_file eb82 +audiotrack e3a1 +auto_awesome e65f +auto_awesome_mosaic e660 +auto_awesome_motion e661 +auto_delete ea4c +auto_fix_high e663 +auto_fix_normal e664 +auto_fix_off e665 +auto_graph e4fb +auto_stories e666 +autofps_select efdc +autorenew e863 +av_timer e01b +baby_changing_station f19b +back_hand e764 +backpack f19c +backspace e14a +backup e864 +backup_table ef43 +badge ea67 +bakery_dining ea53 +balance eaf6 +balcony e58f +ballot e172 +bar_chart e26b +batch_prediction f0f5 +bathroom efdd +bathtub ea41 +battery_0_bar ebdc +battery_1_bar ebd9 +battery_2_bar ebe0 +battery_3_bar ebdd +battery_4_bar ebe2 +battery_5_bar ebd4 +battery_6_bar ebd2 +battery_alert e19c +battery_charging_full e1a3 +battery_full e1a4 +battery_saver efde +battery_std e1a5 +battery_unknown e1a6 +beach_access eb3e +bed efdf +bedroom_baby efe0 +bedroom_child efe1 +bedroom_parent efe2 +bedtime ef44 +bedtime_off eb76 +beenhere e52d +bento f1f4 +bike_scooter ef45 +biotech ea3a +blender efe3 +block e14b +block_flipped ef46 +bloodtype efe4 +bluetooth e1a7 +bluetooth_audio e60f +bluetooth_connected e1a8 +bluetooth_disabled e1a9 +bluetooth_drive efe5 +bluetooth_searching e1aa +blur_circular e3a2 +blur_linear e3a3 +blur_off e3a4 +blur_on e3a5 +bolt ea0b +book e865 +book_online f217 +bookmark e866 +bookmark_add e598 +bookmark_added e599 +bookmark_border e867 +bookmark_outline e867 +bookmark_remove e59a +bookmarks e98b +border_all e228 +border_bottom e229 +border_clear e22a +border_color e22b +border_horizontal e22c +border_inner e22d +border_left e22e +border_outer e22f +border_right e230 +border_style e231 +border_top e232 +border_vertical e233 +boy eb67 +branding_watermark e06b +breakfast_dining ea54 +brightness_1 e3a6 +brightness_2 e3a7 +brightness_3 e3a8 +brightness_4 e3a9 +brightness_5 e3aa +brightness_6 e3ab +brightness_7 e3ac +brightness_auto e1ab +brightness_high e1ac +brightness_low e1ad +brightness_medium e1ae +broken_image e3ad +browse_gallery ebd1 +browser_not_supported ef47 +browser_updated e7cf +brunch_dining ea73 +brush e3ae +bubble_chart e6dd +bug_report e868 +build e869 +build_circle ef48 +bungalow e591 +burst_mode e43c +bus_alert e98f +business e0af +business_center eb3f +cabin e589 +cable efe6 +cached e86a +cake e7e9 +calculate ea5f +calendar_month ebcc +calendar_today e935 +calendar_view_day e936 +calendar_view_month efe7 +calendar_view_week efe8 +call e0b0 +call_end e0b1 +call_made e0b2 +call_merge e0b3 +call_missed e0b4 +call_missed_outgoing e0e4 +call_received e0b5 +call_split e0b6 +call_to_action e06c +camera e3af +camera_alt e3b0 +camera_enhance e8fc +camera_front e3b1 +camera_indoor efe9 +camera_outdoor efea +camera_rear e3b2 +camera_roll e3b3 +cameraswitch efeb +campaign ef49 +cancel e5c9 +cancel_presentation e0e9 +cancel_schedule_send ea39 +candlestick_chart ead4 +car_crash ebf2 +car_rental ea55 +car_repair ea56 +card_giftcard e8f6 +card_membership e8f7 +card_travel e8f8 +carpenter f1f8 +cases e992 +casino eb40 +cast e307 +cast_connected e308 +cast_for_education efec +castle eab1 +catching_pokemon e508 +category e574 +celebration ea65 +cell_tower ebba +cell_wifi e0ec +center_focus_strong e3b4 +center_focus_weak e3b5 +chair efed +chair_alt efee +chalet e585 +change_circle e2e7 +change_history e86b +charging_station f19d +chat e0b7 +chat_bubble e0ca +chat_bubble_outline e0cb +check e5ca +check_box e834 +check_box_outline_blank e835 +check_circle e86c +check_circle_outline e92d +checklist e6b1 +checklist_rtl e6b3 +checkroom f19e +chevron_left e5cb +chevron_right e5cc +child_care eb41 +child_friendly eb42 +chrome_reader_mode e86d +church eaae +circle ef4a +circle_notifications e994 +class e86e +clean_hands f21f +cleaning_services f0ff +clear e14c +clear_all e0b8 +close e5cd +close_fullscreen f1cf +closed_caption e01c +closed_caption_disabled f1dc +closed_caption_off e996 +cloud e2bd +cloud_circle e2be +cloud_done e2bf +cloud_download e2c0 +cloud_off e2c1 +cloud_queue e2c2 +cloud_sync eb5a +cloud_upload e2c3 +cloudy_snowing e810 +co2 e7b0 +co_present eaf0 +code e86f +code_off e4f3 +coffee efef +coffee_maker eff0 +collections e3b6 +collections_bookmark e431 +color_lens e3b7 +colorize e3b8 +comment e0b9 +comment_bank ea4e +comments_disabled e7a2 +commit eaf5 +commute e940 +compare e3b9 +compare_arrows e915 +compass_calibration e57c +compost e761 +compress e94d +computer e30a +confirmation_num e638 +confirmation_number e638 +connect_without_contact f223 +connected_tv e998 +connecting_airports e7c9 +construction ea3c +contact_mail e0d0 +contact_page f22e +contact_phone e0cf +contact_support e94c +contactless ea71 +contacts e0ba +content_copy e14d +content_cut e14e +content_paste e14f +content_paste_go ea8e +content_paste_off e4f8 +content_paste_search ea9b +contrast eb37 +control_camera e074 +control_point e3ba +control_point_duplicate e3bb +cookie eaac +copy_all e2ec +copyright e90c +coronavirus f221 +corporate_fare f1d0 +cottage e587 +countertops f1f7 +create e150 +create_new_folder e2cc +credit_card e870 +credit_card_off e4f4 +credit_score eff1 +crib e588 +crisis_alert ebe9 +crop e3be +crop_16_9 e3bc +crop_3_2 e3bd +crop_5_4 e3bf +crop_7_5 e3c0 +crop_din e3c1 +crop_free e3c2 +crop_landscape e3c3 +crop_original e3c4 +crop_portrait e3c5 +crop_rotate e437 +crop_square e3c6 +cruelty_free e799 +css eb93 +currency_bitcoin ebc5 +currency_exchange eb70 +currency_franc eafa +currency_lira eaef +currency_pound eaf1 +currency_ruble eaec +currency_rupee eaf7 +currency_yen eafb +currency_yuan eaf9 +cyclone ebd5 +dangerous e99a +dark_mode e51c +dashboard e871 +dashboard_customize e99b +data_array ead1 +data_exploration e76f +data_object ead3 +data_saver_off eff2 +data_saver_on eff3 +data_thresholding eb9f +data_usage e1af +date_range e916 +deblur eb77 +deck ea42 +dehaze e3c7 +delete e872 +delete_forever e92b +delete_outline e92e +delete_sweep e16c +delivery_dining ea72 +density_large eba9 +density_medium eb9e +density_small eba8 +departure_board e576 +description e873 +deselect ebb6 +design_services f10a +desktop_access_disabled e99d +desktop_mac e30b +desktop_windows e30c +details e3c8 +developer_board e30d +developer_board_off e4ff +developer_mode e1b0 +device_hub e335 +device_thermostat e1ff +device_unknown e339 +devices e1b1 +devices_fold ebde +devices_other e337 +dialer_sip e0bb +dialpad e0bc +diamond ead5 +difference eb7d +dining eff4 +dinner_dining ea57 +directions e52e +directions_bike e52f +directions_boat e532 +directions_boat_filled eff5 +directions_bus e530 +directions_bus_filled eff6 +directions_car e531 +directions_car_filled eff7 +directions_ferry e532 +directions_off f10f +directions_railway e534 +directions_railway_filled eff8 +directions_run e566 +directions_subway e533 +directions_subway_filled eff9 +directions_train e534 +directions_transit e535 +directions_transit_filled effa +directions_walk e536 +dirty_lens ef4b +disabled_by_default f230 +disabled_visible e76e +disc_full e610 +discord ea6c +discount ebc9 +display_settings eb97 +dnd_forwardslash e611 +dns e875 +do_disturb f08c +do_disturb_alt f08d +do_disturb_off f08e +do_disturb_on f08f +do_not_disturb e612 +do_not_disturb_alt e611 +do_not_disturb_off e643 +do_not_disturb_on e644 +do_not_disturb_on_total_silence effb +do_not_step f19f +do_not_touch f1b0 +dock e30e +document_scanner e5fa +domain e7ee +domain_add eb62 +domain_disabled e0ef +domain_verification ef4c +done e876 +done_all e877 +done_outline e92f +donut_large e917 +donut_small e918 +door_back effc +door_front effd +door_sliding effe +doorbell efff +double_arrow ea50 +downhill_skiing e509 +download f090 +download_done f091 +download_for_offline f000 +downloading f001 +drafts e151 +drag_handle e25d +drag_indicator e945 +draw e746 +drive_eta e613 +drive_file_move e675 +drive_file_move_outline e9a1 +drive_file_move_rtl e76d +drive_file_rename_outline e9a2 +drive_folder_upload e9a3 +dry f1b3 +dry_cleaning ea58 +duo e9a5 +dvr e1b2 +dynamic_feed ea14 +dynamic_form f1bf +e_mobiledata f002 +earbuds f003 +earbuds_battery f004 +east f1df +eco ea35 +edgesensor_high f005 +edgesensor_low f006 +edit e3c9 +edit_attributes e578 +edit_calendar e742 +edit_location e568 +edit_location_alt e1c5 +edit_note e745 +edit_notifications e525 +edit_off e950 +edit_road ef4d +egg eacc +egg_alt eac8 +eject e8fb +elderly f21a +elderly_woman eb69 +electric_bike eb1b +electric_car eb1c +electric_moped eb1d +electric_rickshaw eb1e +electric_scooter eb1f +electrical_services f102 +elevator f1a0 +email e0be +emergency e1eb +emergency_recording ebf4 +emergency_share ebf6 +emoji_emotions ea22 +emoji_events ea23 +emoji_flags ea1a +emoji_food_beverage ea1b +emoji_nature ea1c +emoji_objects ea24 +emoji_people ea1d +emoji_symbols ea1e +emoji_transportation ea1f +engineering ea3d +enhance_photo_translate e8fc +enhanced_encryption e63f +equalizer e01d +error e000 +error_outline e001 +escalator f1a1 +escalator_warning f1ac +euro ea15 +euro_symbol e926 +ev_station e56d +event e878 +event_available e614 +event_busy e615 +event_note e616 +event_repeat eb7b +event_seat e903 +exit_to_app e879 +expand e94f +expand_circle_down e7cd +expand_less e5ce +expand_more e5cf +explicit e01e +explore e87a +explore_off e9a8 +exposure e3ca +exposure_minus_1 e3cb +exposure_minus_2 e3cc +exposure_neg_1 e3cb +exposure_neg_2 e3cc +exposure_plus_1 e3cd +exposure_plus_2 e3ce +exposure_zero e3cf +extension e87b +extension_off e4f5 +face e87c +face_retouching_natural ef4e +face_retouching_off f007 +facebook f234 +fact_check f0c5 +factory ebbc +family_restroom f1a2 +fast_forward e01f +fast_rewind e020 +fastfood e57a +favorite e87d +favorite_border e87e +favorite_outline e87e +fax ead8 +featured_play_list e06d +featured_video e06e +feed f009 +feedback e87f +female e590 +fence f1f6 +festival ea68 +fiber_dvr e05d +fiber_manual_record e061 +fiber_new e05e +fiber_pin e06a +fiber_smart_record e062 +file_copy e173 +file_download e2c4 +file_download_done e9aa +file_download_off e4fe +file_open eaf3 +file_present ea0e +file_upload e2c6 +filter e3d3 +filter_1 e3d0 +filter_2 e3d1 +filter_3 e3d2 +filter_4 e3d4 +filter_5 e3d5 +filter_6 e3d6 +filter_7 e3d7 +filter_8 e3d8 +filter_9 e3d9 +filter_9_plus e3da +filter_alt ef4f +filter_alt_off eb32 +filter_b_and_w e3db +filter_center_focus e3dc +filter_drama e3dd +filter_frames e3de +filter_hdr e3df +filter_list e152 +filter_list_alt e94e +filter_list_off eb57 +filter_none e3e0 +filter_tilt_shift e3e2 +filter_vintage e3e3 +find_in_page e880 +find_replace e881 +fingerprint e90d +fire_extinguisher f1d8 +fire_hydrant f1a3 +fireplace ea43 +first_page e5dc +fit_screen ea10 +fitbit e82b +fitness_center eb43 +flag e153 +flag_circle eaf8 +flaky ef50 +flare e3e4 +flash_auto e3e5 +flash_off e3e6 +flash_on e3e7 +flashlight_off f00a +flashlight_on f00b +flatware f00c +flight e539 +flight_class e7cb +flight_land e904 +flight_takeoff e905 +flip e3e8 +flip_camera_android ea37 +flip_camera_ios ea38 +flip_to_back e882 +flip_to_front e883 +flood ebe6 +flourescent f00d +flutter_dash e00b +fmd_bad f00e +fmd_good f00f +foggy e818 +folder e2c7 +folder_copy ebbd +folder_delete eb34 +folder_off eb83 +folder_open e2c8 +folder_shared e2c9 +folder_special e617 +folder_zip eb2c +follow_the_signs f222 +font_download e167 +font_download_off e4f9 +food_bank f1f2 +forest ea99 +fork_left eba0 +fork_right ebac +format_align_center e234 +format_align_justify e235 +format_align_left e236 +format_align_right e237 +format_bold e238 +format_clear e239 +format_color_fill e23a +format_color_reset e23b +format_color_text e23c +format_indent_decrease e23d +format_indent_increase e23e +format_italic e23f +format_line_spacing e240 +format_list_bulleted e241 +format_list_numbered e242 +format_list_numbered_rtl e267 +format_overline eb65 +format_paint e243 +format_quote e244 +format_shapes e25e +format_size e245 +format_strikethrough e246 +format_textdirection_l_to_r e247 +format_textdirection_r_to_l e248 +format_underline e249 +format_underlined e249 +fort eaad +forum e0bf +forward e154 +forward_10 e056 +forward_30 e057 +forward_5 e058 +forward_to_inbox f187 +foundation f200 +free_breakfast eb44 +free_cancellation e748 +front_hand e769 +fullscreen e5d0 +fullscreen_exit e5d1 +functions e24a +g_mobiledata f010 +g_translate e927 +gamepad e30f +games e021 +garage f011 +gavel e90e +generating_tokens e749 +gesture e155 +get_app e884 +gif e908 +gif_box e7a3 +girl eb68 +gite e58b +goat 10fffd +golf_course eb45 +gpp_bad f012 +gpp_good f013 +gpp_maybe f014 +gps_fixed e1b3 +gps_not_fixed e1b4 +gps_off e1b5 +grade e885 +gradient e3e9 +grading ea4f +grain e3ea +graphic_eq e1b8 +grass f205 +grid_3x3 f015 +grid_4x4 f016 +grid_goldenratio f017 +grid_off e3eb +grid_on e3ec +grid_view e9b0 +group e7ef +group_add e7f0 +group_off e747 +group_remove e7ad +group_work e886 +groups f233 +h_mobiledata f018 +h_plus_mobiledata f019 +hail e9b1 +handshake ebcb +handyman f10b +hardware ea59 +hd e052 +hdr_auto f01a +hdr_auto_select f01b +hdr_enhanced_select ef51 +hdr_off e3ed +hdr_off_select f01c +hdr_on e3ee +hdr_on_select f01d +hdr_plus f01e +hdr_strong e3f1 +hdr_weak e3f2 +headphones f01f +headphones_battery f020 +headset e310 +headset_mic e311 +headset_off e33a +healing e3f3 +health_and_safety e1d5 +hearing e023 +hearing_disabled f104 +heart_broken eac2 +height ea16 +help e887 +help_center f1c0 +help_outline e8fd +hevc f021 +hexagon eb39 +hide_image f022 +hide_source f023 +high_quality e024 +highlight e25f +highlight_alt ef52 +highlight_off e888 +highlight_remove e888 +hiking e50a +history e889 +history_edu ea3e +history_toggle_off f17d +hive eaa6 +hls eb8a +hls_off eb8c +holiday_village e58a +home e88a +home_filled e9b2 +home_max f024 +home_mini f025 +home_repair_service f100 +home_work ea09 +horizontal_distribute e014 +horizontal_rule f108 +horizontal_split e947 +hot_tub eb46 +hotel e53a +hotel_class e743 +hourglass_bottom ea5c +hourglass_disabled ef53 +hourglass_empty e88b +hourglass_full e88c +hourglass_top ea5b +house ea44 +house_siding f202 +houseboat e584 +how_to_reg e174 +how_to_vote e175 +html eb7e +http e902 +https e88d +hub e9f4 +hvac f10e +ice_skating e50b +icecream ea69 +image e3f4 +image_aspect_ratio e3f5 +image_not_supported f116 +image_search e43f +imagesearch_roller e9b4 +import_contacts e0e0 +import_export e0c3 +important_devices e912 +inbox e156 +incomplete_circle e79b +indeterminate_check_box e909 +info e88e +info_outline e88f +input e890 +insert_chart e24b +insert_chart_outlined e26a +insert_comment e24c +insert_drive_file e24d +insert_emoticon e24e +insert_invitation e24f +insert_link e250 +insert_page_break eaca +insert_photo e251 +insights f092 +install_desktop eb71 +install_mobile eb72 +integration_instructions ef54 +interests e7c8 +interpreter_mode e83b +inventory e179 +inventory_2 e1a1 +invert_colors e891 +invert_colors_off e0c4 +invert_colors_on e891 +ios_share e6b8 +iron e583 +iso e3f6 +javascript eb7c +join_full eaeb +join_inner eaf4 +join_left eaf2 +join_right eaea +kayaking e50c +kebab_dining e842 +key e73c +key_off eb84 +keyboard e312 +keyboard_alt f028 +keyboard_arrow_down e313 +keyboard_arrow_left e314 +keyboard_arrow_right e315 +keyboard_arrow_up e316 +keyboard_backspace e317 +keyboard_capslock e318 +keyboard_command eae0 +keyboard_command_key eae7 +keyboard_control e5d3 +keyboard_control_key eae6 +keyboard_double_arrow_down ead0 +keyboard_double_arrow_left eac3 +keyboard_double_arrow_right eac9 +keyboard_double_arrow_up eacf +keyboard_hide e31a +keyboard_option eadf +keyboard_option_key eae8 +keyboard_return e31b +keyboard_tab e31c +keyboard_voice e31d +king_bed ea45 +kitchen eb47 +kitesurfing e50d +label e892 +label_important e937 +label_important_outline e948 +label_off e9b6 +label_outline e893 +lan eb2f +landscape e3f7 +landslide ebd7 +language e894 +laptop e31e +laptop_chromebook e31f +laptop_mac e320 +laptop_windows e321 +last_page e5dd +launch e895 +layers e53b +layers_clear e53c +leaderboard f20c +leak_add e3f8 +leak_remove e3f9 +leave_bags_at_home f21b +legend_toggle f11b +lens e3fa +lens_blur f029 +library_add e02e +library_add_check e9b7 +library_books e02f +library_music e030 +light f02a +light_mode e518 +lightbulb e0f0 +lightbulb_outline e90f +line_axis ea9a +line_style e919 +line_weight e91a +linear_scale e260 +link e157 +link_off e16f +linked_camera e438 +liquor ea60 +list e896 +list_alt e0ee +live_help e0c6 +live_tv e639 +living f02b +local_activity e53f +local_airport e53d +local_atm e53e +local_attraction e53f +local_bar e540 +local_cafe e541 +local_car_wash e542 +local_convenience_store e543 +local_dining e556 +local_drink e544 +local_fire_department ef55 +local_florist e545 +local_gas_station e546 +local_grocery_store e547 +local_hospital e548 +local_hotel e549 +local_laundry_service e54a +local_library e54b +local_mall e54c +local_movies e54d +local_offer e54e +local_parking e54f +local_pharmacy e550 +local_phone e551 +local_pizza e552 +local_play e553 +local_police ef56 +local_post_office e554 +local_print_shop e555 +local_printshop e555 +local_restaurant e556 +local_see e557 +local_shipping e558 +local_taxi e559 +location_city e7f1 +location_disabled e1b6 +location_history e55a +location_off e0c7 +location_on e0c8 +location_pin f1db +location_searching e1b7 +lock e897 +lock_clock ef57 +lock_open e898 +lock_outline e899 +lock_reset eade +login ea77 +logo_dev ead6 +logout e9ba +looks e3fc +looks_3 e3fb +looks_4 e3fd +looks_5 e3fe +looks_6 e3ff +looks_one e400 +looks_two e401 +loop e028 +loupe e402 +low_priority e16d +loyalty e89a +lte_mobiledata f02c +lte_plus_mobiledata f02d +luggage f235 +lunch_dining ea61 +mail e158 +mail_outline e0e1 +male e58e +man e4eb +manage_accounts f02e +manage_history ebe7 +manage_search f02f +map e55b +maps_home_work f030 +maps_ugc ef58 +margin e9bb +mark_as_unread e9bc +mark_chat_read f18b +mark_chat_unread f189 +mark_email_read f18c +mark_email_unread f18a +mark_unread_chat_alt eb9d +markunread e159 +markunread_mailbox e89b +masks f218 +maximize e930 +media_bluetooth_off f031 +media_bluetooth_on f032 +mediation efa7 +medical_information ebed +medical_services f109 +medication f033 +medication_liquid ea87 +meeting_room eb4f +memory e322 +menu e5d2 +menu_book ea19 +menu_open e9bd +merge eb98 +merge_type e252 +message e0c9 +messenger e0ca +messenger_outline e0cb +mic e029 +mic_external_off ef59 +mic_external_on ef5a +mic_none e02a +mic_off e02b +microwave f204 +military_tech ea3f +minimize e931 +minor_crash ebf1 +miscellaneous_services f10c +missed_video_call e073 +mms e618 +mobile_friendly e200 +mobile_off e201 +mobile_screen_share e0e7 +mobiledata_off f034 +mode f097 +mode_comment e253 +mode_edit e254 +mode_edit_outline f035 +mode_night f036 +mode_of_travel e7ce +mode_standby f037 +model_training f0cf +monetization_on e263 +money e57d +money_off e25c +money_off_csred f038 +monitor ef5b +monitor_heart eaa2 +monitor_weight f039 +monochrome_photos e403 +mood e7f2 +mood_bad e7f3 +moped eb28 +more e619 +more_horiz e5d3 +more_time ea5d +more_vert e5d4 +mosque eab2 +motion_photos_auto f03a +motion_photos_off e9c0 +motion_photos_on e9c1 +motion_photos_pause f227 +motion_photos_paused e9c2 +motorcycle e91b +mouse e323 +move_down eb61 +move_to_inbox e168 +move_up eb64 +movie e02c +movie_creation e404 +movie_filter e43a +moving e501 +mp e9c3 +multiline_chart e6df +multiple_stop f1b9 +multitrack_audio e1b8 +museum ea36 +music_note e405 +music_off e440 +music_video e063 +my_library_add e02e +my_library_books e02f +my_library_music e030 +my_location e55c +nat ef5c +nature e406 +nature_people e407 +navigate_before e408 +navigate_next e409 +navigation e55d +near_me e569 +near_me_disabled f1ef +nearby_error f03b +nearby_off f03c +network_cell e1b9 +network_check e640 +network_locked e61a +network_ping ebca +network_wifi e1ba +network_wifi_1_bar ebe4 +network_wifi_2_bar ebd6 +network_wifi_3_bar ebe1 +new_label e609 +new_releases e031 +newspaper eb81 +next_plan ef5d +next_week e16a +nfc e1bb +night_shelter f1f1 +nightlife ea62 +nightlight f03d +nightlight_round ef5e +nights_stay ea46 +no_accounts f03e +no_backpack f237 +no_cell f1a4 +no_crash ebf0 +no_drinks f1a5 +no_encryption e641 +no_encryption_gmailerrorred f03f +no_flash f1a6 +no_food f1a7 +no_luggage f23b +no_meals f1d6 +no_meals_ouline f229 +no_meeting_room eb4e +no_photography f1a8 +no_sim e0cc +no_stroller f1af +no_transfer f1d5 +noise_aware ebec +noise_control_off ebf3 +nordic_walking e50e +north f1e0 +north_east f1e1 +north_west f1e2 +not_accessible f0fe +not_interested e033 +not_listed_location e575 +not_started f0d1 +note e06f +note_add e89c +note_alt f040 +notes e26c +notification_add e399 +notification_important e004 +notifications e7f4 +notifications_active e7f7 +notifications_none e7f5 +notifications_off e7f6 +notifications_on e7f7 +notifications_paused e7f8 +now_wallpaper e1bc +now_widgets e1bd +numbers eac7 +offline_bolt e932 +offline_pin e90a +offline_share e9c5 +ondemand_video e63a +online_prediction f0eb +opacity e91c +open_in_browser e89d +open_in_full f1ce +open_in_new e89e +open_in_new_off e4f6 +open_with e89f +other_houses e58c +outbond f228 +outbound e1ca +outbox ef5f +outdoor_grill ea47 +outgoing_mail f0d2 +outlet f1d4 +outlined_flag e16e +output ebbe +padding e9c8 +pages e7f9 +pageview e8a0 +paid f041 +palette e40a +pan_tool e925 +pan_tool_alt ebb9 +panorama e40b +panorama_fish_eye e40c +panorama_fisheye e40c +panorama_horizontal e40d +panorama_horizontal_select ef60 +panorama_photosphere e9c9 +panorama_photosphere_select e9ca +panorama_vertical e40e +panorama_vertical_select ef61 +panorama_wide_angle e40f +panorama_wide_angle_select ef62 +paragliding e50f +park ea63 +party_mode e7fa +password f042 +pattern f043 +pause e034 +pause_circle e1a2 +pause_circle_filled e035 +pause_circle_outline e036 +pause_presentation e0ea +payment e8a1 +payments ef63 +paypal ea8d +pedal_bike eb29 +pending ef64 +pending_actions f1bb +pentagon eb50 +people e7fb +people_alt ea21 +people_outline e7fc +percent eb58 +perm_camera_mic e8a2 +perm_contact_cal e8a3 +perm_contact_calendar e8a3 +perm_data_setting e8a4 +perm_device_info e8a5 +perm_device_information e8a5 +perm_identity e8a6 +perm_media e8a7 +perm_phone_msg e8a8 +perm_scan_wifi e8a9 +person e7fd +person_add e7fe +person_add_alt ea4d +person_add_alt_1 ef65 +person_add_disabled e9cb +person_off e510 +person_outline e7ff +person_pin e55a +person_pin_circle e56a +person_remove ef66 +person_remove_alt_1 ef67 +person_search f106 +personal_injury e6da +personal_video e63b +pest_control f0fa +pest_control_rodent f0fd +pets e91d +phishing ead7 +phone e0cd +phone_android e324 +phone_bluetooth_speaker e61b +phone_callback e649 +phone_disabled e9cc +phone_enabled e9cd +phone_forwarded e61c +phone_in_talk e61d +phone_iphone e325 +phone_locked e61e +phone_missed e61f +phone_paused e620 +phonelink e326 +phonelink_erase e0db +phonelink_lock e0dc +phonelink_off e327 +phonelink_ring e0dd +phonelink_setup e0de +photo e410 +photo_album e411 +photo_camera e412 +photo_camera_back ef68 +photo_camera_front ef69 +photo_filter e43b +photo_library e413 +photo_size_select_actual e432 +photo_size_select_large e433 +photo_size_select_small e434 +php eb8f +piano e521 +piano_off e520 +picture_as_pdf e415 +picture_in_picture e8aa +picture_in_picture_alt e911 +pie_chart e6c4 +pie_chart_outline f044 +pie_chart_outlined e6c5 +pin f045 +pin_drop e55e +pin_end e767 +pin_invoke e763 +pinch eb38 +pivot_table_chart e9ce +pix eaa3 +place e55f +plagiarism ea5a +play_arrow e037 +play_circle e1c4 +play_circle_fill e038 +play_circle_filled e038 +play_circle_outline e039 +play_disabled ef6a +play_for_work e906 +play_lesson f047 +playlist_add e03b +playlist_add_check e065 +playlist_add_check_circle e7e6 +playlist_add_circle e7e5 +playlist_play e05f +playlist_remove eb80 +plumbing f107 +plus_one e800 +podcasts f048 +point_of_sale f17e +policy ea17 +poll e801 +polyline ebbb +polymer e8ab +pool eb48 +portable_wifi_off e0ce +portrait e416 +post_add ea20 +power e63c +power_input e336 +power_off e646 +power_settings_new e8ac +precision_manufacturing f049 +pregnant_woman e91e +present_to_all e0df +preview f1c5 +price_change f04a +price_check f04b +print e8ad +print_disabled e9cf +priority_high e645 +privacy_tip f0dc +private_connectivity e744 +production_quantity_limits e1d1 +psychology ea4a +public e80b +public_off f1ca +publish e255 +published_with_changes f232 +punch_clock eaa8 +push_pin f10d +qr_code ef6b +qr_code_2 e00a +qr_code_scanner f206 +query_builder e8ae +query_stats e4fc +question_answer e8af +question_mark eb8b +queue e03c +queue_music e03d +queue_play_next e066 +quick_contacts_dialer e0cf +quick_contacts_mail e0d0 +quickreply ef6c +quiz f04c +quora ea98 +r_mobiledata f04d +radar f04e +radio e03e +radio_button_checked e837 +radio_button_off e836 +radio_button_on e837 +radio_button_unchecked e836 +railway_alert e9d1 +ramen_dining ea64 +ramp_left eb9c +ramp_right eb96 +rate_review e560 +raw_off f04f +raw_on f050 +read_more ef6d +real_estate_agent e73a +receipt e8b0 +receipt_long ef6e +recent_actors e03f +recommend e9d2 +record_voice_over e91f +rectangle eb54 +recycling e760 +reddit eaa0 +redeem e8b1 +redo e15a +reduce_capacity f21c +refresh e5d5 +remember_me f051 +remove e15b +remove_circle e15c +remove_circle_outline e15d +remove_done e9d3 +remove_from_queue e067 +remove_moderator e9d4 +remove_red_eye e417 +remove_shopping_cart e928 +reorder e8fe +repeat e040 +repeat_on e9d6 +repeat_one e041 +repeat_one_on e9d7 +replay e042 +replay_10 e059 +replay_30 e05a +replay_5 e05b +replay_circle_filled e9d8 +reply e15e +reply_all e15f +report e160 +report_gmailerrorred f052 +report_off e170 +report_problem e8b2 +request_page f22c +request_quote f1b6 +reset_tv e9d9 +restart_alt f053 +restaurant e56c +restaurant_menu e561 +restore e8b3 +restore_from_trash e938 +restore_page e929 +reviews f054 +rice_bowl f1f5 +ring_volume e0d1 +rocket eba5 +rocket_launch eb9b +roller_skating ebcd +roofing f201 +room e8b4 +room_preferences f1b8 +room_service eb49 +rotate_90_degrees_ccw e418 +rotate_90_degrees_cw eaab +rotate_left e419 +rotate_right e41a +roundabout_left eb99 +roundabout_right eba3 +rounded_corner e920 +route eacd +router e328 +rowing e921 +rss_feed e0e5 +rsvp f055 +rtt e9ad +rule f1c2 +rule_folder f1c9 +run_circle ef6f +running_with_errors e51d +rv_hookup e642 +safety_check ebef +safety_divider e1cc +sailing e502 +sanitizer f21d +satellite e562 +satellite_alt eb3a +save e161 +save_alt e171 +save_as eb60 +saved_search ea11 +savings e2eb +scale eb5f +scanner e329 +scatter_plot e268 +schedule e8b5 +schedule_send ea0a +schema e4fd +school e80c +science ea4b +score e269 +scoreboard ebd0 +screen_lock_landscape e1be +screen_lock_portrait e1bf +screen_lock_rotation e1c0 +screen_rotation e1c1 +screen_rotation_alt ebee +screen_search_desktop ef70 +screen_share e0e2 +screenshot f056 +scuba_diving ebce +sd e9dd +sd_card e623 +sd_card_alert f057 +sd_storage e1c2 +search e8b6 +search_off ea76 +security e32a +security_update f058 +security_update_good f059 +security_update_warning f05a +segment e94b +select_all e162 +self_improvement ea78 +sell f05b +send e163 +send_and_archive ea0c +send_time_extension eadb +send_to_mobile f05c +sensor_door f1b5 +sensor_window f1b4 +sensors e51e +sensors_off e51f +sentiment_dissatisfied e811 +sentiment_neutral e812 +sentiment_satisfied e813 +sentiment_satisfied_alt e0ed +sentiment_very_dissatisfied e814 +sentiment_very_satisfied e815 +set_meal f1ea +settings e8b8 +settings_accessibility f05d +settings_applications e8b9 +settings_backup_restore e8ba +settings_bluetooth e8bb +settings_brightness e8bd +settings_cell e8bc +settings_display e8bd +settings_ethernet e8be +settings_input_antenna e8bf +settings_input_component e8c0 +settings_input_composite e8c1 +settings_input_hdmi e8c2 +settings_input_svideo e8c3 +settings_overscan e8c4 +settings_phone e8c5 +settings_power e8c6 +settings_remote e8c7 +settings_suggest f05e +settings_system_daydream e1c3 +settings_voice e8c8 +severe_cold ebd3 +share e80d +share_arrival_time e524 +share_location f05f +shield e9e0 +shield_moon eaa9 +shop e8c9 +shop_2 e19e +shop_two e8ca +shopify ea9d +shopping_bag f1cc +shopping_basket e8cb +shopping_cart e8cc +shopping_cart_checkout eb88 +short_text e261 +shortcut f060 +show_chart e6e1 +shower f061 +shuffle e043 +shuffle_on e9e1 +shutter_speed e43d +sick f220 +sign_language ebe5 +signal_cellular_0_bar f0a8 +signal_cellular_4_bar e1c8 +signal_cellular_alt e202 +signal_cellular_alt_1_bar ebdf +signal_cellular_alt_2_bar ebe3 +signal_cellular_connected_no_internet_0_bar f0ac +signal_cellular_connected_no_internet_4_bar e1cd +signal_cellular_no_sim e1ce +signal_cellular_nodata f062 +signal_cellular_null e1cf +signal_cellular_off e1d0 +signal_wifi_0_bar f0b0 +signal_wifi_4_bar e1d8 +signal_wifi_4_bar_lock e1d9 +signal_wifi_bad f063 +signal_wifi_connected_no_internet_4 f064 +signal_wifi_off e1da +signal_wifi_statusbar_4_bar f065 +signal_wifi_statusbar_connected_no_internet_4 f066 +signal_wifi_statusbar_null f067 +signpost eb91 +sim_card e32b +sim_card_alert e624 +sim_card_download f068 +single_bed ea48 +sip f069 +skateboarding e511 +skip_next e044 +skip_previous e045 +sledding e512 +slideshow e41b +slow_motion_video e068 +smart_button f1c1 +smart_display f06a +smart_screen f06b +smart_toy f06c +smartphone e32c +smoke_free eb4a +smoking_rooms eb4b +sms e625 +sms_failed e626 +snapchat ea6e +snippet_folder f1c7 +snooze e046 +snowboarding e513 +snowing e80f +snowmobile e503 +snowshoeing e514 +soap f1b2 +social_distance e1cb +sort e164 +sort_by_alpha e053 +sos ebf7 +soup_kitchen e7d3 +source f1c4 +south f1e3 +south_america e7e4 +south_east f1e4 +south_west f1e5 +spa eb4c +space_bar e256 +space_dashboard e66b +spatial_audio ebeb +spatial_audio_off ebe8 +spatial_tracking ebea +speaker e32d +speaker_group e32e +speaker_notes e8cd +speaker_notes_off e92a +speaker_phone e0d2 +speed e9e4 +spellcheck e8ce +splitscreen f06d +spoke e9a7 +sports ea30 +sports_bar f1f3 +sports_baseball ea51 +sports_basketball ea26 +sports_cricket ea27 +sports_esports ea28 +sports_football ea29 +sports_golf ea2a +sports_gymnastics ebc4 +sports_handball ea33 +sports_hockey ea2b +sports_kabaddi ea34 +sports_martial_arts eae9 +sports_mma ea2c +sports_motorsports ea2d +sports_rugby ea2e +sports_score f06e +sports_soccer ea2f +sports_tennis ea32 +sports_volleyball ea31 +square eb36 +square_foot ea49 +ssid_chart eb66 +stacked_bar_chart e9e6 +stacked_line_chart f22b +stadium eb90 +stairs f1a9 +star e838 +star_border e83a +star_border_purple500 f099 +star_half e839 +star_outline f06f +star_purple500 f09a +star_rate f0ec +stars e8d0 +start e089 +stay_current_landscape e0d3 +stay_current_portrait e0d4 +stay_primary_landscape e0d5 +stay_primary_portrait e0d6 +sticky_note_2 f1fc +stop e047 +stop_circle ef71 +stop_screen_share e0e3 +storage e1db +store e8d1 +store_mall_directory e563 +storefront ea12 +storm f070 +straight eb95 +straighten e41c +stream e9e9 +streetview e56e +strikethrough_s e257 +stroller f1ae +style e41d +subdirectory_arrow_left e5d9 +subdirectory_arrow_right e5da +subject e8d2 +subscript f111 +subscriptions e064 +subtitles e048 +subtitles_off ef72 +subway e56f +summarize f071 +sunny e81a +sunny_snowing e819 +superscript f112 +supervised_user_circle e939 +supervisor_account e8d3 +support ef73 +support_agent f0e2 +surfing e515 +surround_sound e049 +swap_calls e0d7 +swap_horiz e8d4 +swap_horizontal_circle e933 +swap_vert e8d5 +swap_vert_circle e8d6 +swap_vertical_circle e8d6 +swipe e9ec +swipe_down eb53 +swipe_down_alt eb30 +swipe_left eb59 +swipe_left_alt eb33 +swipe_right eb52 +swipe_right_alt eb56 +swipe_up eb2e +swipe_up_alt eb35 +swipe_vertical eb51 +switch_access_shortcut e7e1 +switch_access_shortcut_add e7e2 +switch_account e9ed +switch_camera e41e +switch_left f1d1 +switch_right f1d2 +switch_video e41f +synagogue eab0 +sync e627 +sync_alt ea18 +sync_disabled e628 +sync_lock eaee +sync_problem e629 +system_security_update f072 +system_security_update_good f073 +system_security_update_warning f074 +system_update e62a +system_update_alt e8d7 +system_update_tv e8d7 +tab e8d8 +tab_unselected e8d9 +table_bar ead2 +table_chart e265 +table_restaurant eac6 +table_rows f101 +table_view f1be +tablet e32f +tablet_android e330 +tablet_mac e331 +tag e9ef +tag_faces e420 +takeout_dining ea74 +tap_and_play e62b +tapas f1e9 +task f075 +task_alt e2e6 +taxi_alert ef74 +telegram ea6b +temple_buddhist eab3 +temple_hindu eaaf +terminal eb8e +terrain e564 +text_decrease eadd +text_fields e262 +text_format e165 +text_increase eae2 +text_rotate_up e93a +text_rotate_vertical e93b +text_rotation_angledown e93c +text_rotation_angleup e93d +text_rotation_down e93e +text_rotation_none e93f +text_snippet f1c6 +textsms e0d8 +texture e421 +theater_comedy ea66 +theaters e8da +thermostat f076 +thermostat_auto f077 +thumb_down e8db +thumb_down_alt e816 +thumb_down_off_alt e9f2 +thumb_up e8dc +thumb_up_alt e817 +thumb_up_off_alt e9f3 +thumbs_up_down e8dd +thunderstorm ebdb +tiktok ea7e +time_to_leave e62c +timelapse e422 +timeline e922 +timer e425 +timer_10 e423 +timer_10_select f07a +timer_3 e424 +timer_3_select f07b +timer_off e426 +tips_and_updates e79a +tire_repair ebc8 +title e264 +toc e8de +today e8df +toggle_off e9f5 +toggle_on e9f6 +token ea25 +toll e8e0 +tonality e427 +topic f1c8 +touch_app e913 +tour ef75 +toys e332 +track_changes e8e1 +traffic e565 +train e570 +tram e571 +transfer_within_a_station e572 +transform e428 +transgender e58d +transit_enterexit e579 +translate e8e2 +travel_explore e2db +trending_down e8e3 +trending_flat e8e4 +trending_neutral e8e4 +trending_up e8e5 +trip_origin e57b +try f07c +tsunami ebd8 +tty f1aa +tune e429 +tungsten f07d +turn_left eba6 +turn_right ebab +turn_sharp_left eba7 +turn_sharp_right ebaa +turn_slight_left eba4 +turn_slight_right eb9a +turned_in e8e6 +turned_in_not e8e7 +tv e333 +tv_off e647 +two_wheeler e9f9 +u_turn_left eba1 +u_turn_right eba2 +umbrella f1ad +unarchive e169 +undo e166 +unfold_less e5d6 +unfold_more e5d7 +unpublished f236 +unsubscribe e0eb +upcoming f07e +update e923 +update_disabled e075 +upgrade f0fb +upload f09b +upload_file e9fc +usb e1e0 +usb_off e4fa +vaccines e138 +vape_free ebc6 +vaping_rooms ebcf +verified ef76 +verified_user e8e8 +vertical_align_bottom e258 +vertical_align_center e259 +vertical_align_top e25a +vertical_distribute e076 +vertical_split e949 +vibration e62d +video_call e070 +video_camera_back f07f +video_camera_front f080 +video_collection e04a +video_file eb87 +video_label e071 +video_library e04a +video_settings ea75 +video_stable f081 +videocam e04b +videocam_off e04c +videogame_asset e338 +videogame_asset_off e500 +view_agenda e8e9 +view_array e8ea +view_carousel e8eb +view_column e8ec +view_comfortable e42a +view_comfy e42a +view_comfy_alt eb73 +view_compact e42b +view_compact_alt eb74 +view_cozy eb75 +view_day e8ed +view_headline e8ee +view_in_ar e9fe +view_kanban eb7f +view_list e8ef +view_module e8f0 +view_quilt e8f1 +view_sidebar f114 +view_stream e8f2 +view_timeline eb85 +view_week e8f3 +vignette e435 +villa e586 +visibility e8f4 +visibility_off e8f5 +voice_chat e62e +voice_over_off e94a +voicemail e0d9 +volcano ebda +volume_down e04d +volume_down_alt e79c +volume_mute e04e +volume_off e04f +volume_up e050 +volunteer_activism ea70 +vpn_key e0da +vpn_key_off eb7a +vpn_lock e62f +vrpano f082 +wallet_giftcard e8f6 +wallet_membership e8f7 +wallet_travel e8f8 +wallpaper e1bc +warehouse ebb8 +warning e002 +warning_amber f083 +wash f1b1 +watch e334 +watch_later e924 +watch_off eae3 +water f084 +water_damage f203 +water_drop e798 +waterfall_chart ea00 +waves e176 +waving_hand e766 +wb_auto e42c +wb_cloudy e42d +wb_incandescent e42e +wb_iridescent e436 +wb_shade ea01 +wb_sunny e430 +wb_twighlight ea02 +wb_twilight e1c6 +wc e63d +web e051 +web_asset e069 +web_asset_off e4f7 +web_stories e595 +webhook eb92 +wechat ea81 +weekend e16b +west f1e6 +whatsapp ea9c +whatshot e80e +wheelchair_pickup f1ab +where_to_vote e177 +widgets e1bd +wifi e63e +wifi_1_bar e4ca +wifi_2_bar e4d9 +wifi_calling ef77 +wifi_calling_3 f085 +wifi_channel eb6a +wifi_find eb31 +wifi_lock e1e1 +wifi_off e648 +wifi_password eb6b +wifi_protected_setup f0fc +wifi_tethering e1e2 +wifi_tethering_error ead9 +wifi_tethering_error_rounded f086 +wifi_tethering_off f087 +window f088 +wine_bar f1e8 +woman e13e +woo_commerce ea6d +wordpress ea9f +work e8f9 +work_off e942 +work_outline e943 +workspace_premium e7af +workspaces e1a0 +workspaces_filled ea0d +workspaces_outline ea0f +wrap_text e25b +wrong_location ef78 +wysiwyg f1c3 +yard f089 +youtube_searched_for e8fa +zoom_in e8ff +zoom_in_map eb2d +zoom_out e900 +zoom_out_map e56b diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index f8a4ad279b..bb74a5f2e3 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -41,7 +41,7 @@ import { alarmFields } from '@shared/models/alarm.models'; import { materialColors } from '@app/shared/models/material.models'; import { WidgetInfo } from '@home/models/widget-component.models'; import jsonSchemaDefaults from 'json-schema-defaults'; -import materialIconsCodepoints from '!raw-loader!material-design-icons/iconfont/codepoints'; +import materialIconsCodepoints from '!raw-loader!./material-icons-codepoints.raw'; import { Observable, of, ReplaySubject } from 'rxjs'; const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g'); diff --git a/ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf b/ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..be4be29c8664ae8199bd0fa6c8df9e8e140d354f GIT binary patch literal 337868 zcmb@v2Y3}#*SEiB=Iq%+Z%Kgk(3Bzyf>JElP_Ro;5d{P*h$xB`#fG4$h=7P8mTd(BP+pZEFR|Mgwhmvi0u&FtA_?X}kInKS1E#1oMk zGC~3wa_-sZbT*Zwn~0Z0Y5KXBT-;5Pr84^w`&G~F*5l0N%l14hazhJ|f_fKsJHB=0 zp$+}}5R@f<(tMhb$%euDiW>^HjcmOJh)x@^xCBEefC zHRlbwwST|rkIsEcB&n}R&W*S9yL(79$GepJCs03oaKBrwODk(TLd3^j@7^J|4!z^> zNNLUXC3?KvHV>Qp*rdwLmD$GIRZ;!%c?EI|WydBCxO&l*UbfULKdQBLI-R6tT*B=1 z=kOys&VTu#t{)fsa`;fZ9XsQwnnB#3m*}W`SpIffKN_}}$$1>&Cam=f&4d9=j5pu%e!xgs0rAJ$q zEyP8v^uilnq7vJEWl{OIC+RY+eA}1mGO>I+kjsR~#`P0N)iIJP&C1I|wnQ$JesYIg zC%4N@Yz>kw(qC?s!7@~iqmS$42Dy{cejM)^*BVMaJ)SMC@cRk;K3O`)C3nPnJ1wr` zXzTcBwsc_oKgY;EY7|$aEu)Yfs|HsJxTl=#Zdx+c<+kb=H$Xs-6wW}Rl=W_H` zEET|Kz7RQB9Uk z(gW=jt8TabXK$bkq*QCUn$2ZIyfyQmGCit2XuBR9)#I*?_Sl^E6XK(~*MOQ@tKyZ+ zeslH|x2=68<0IFrZflKB{MHfFhEB}8hunn@j9XTeYA-sPt2Z#-Yfd!vxcj+^UIxZ! zO^N%|R?XPbQS`@oLVMA&PSTz7PU6!}%PPj#aoo0+IZIA0Ki*t&{yD1s>C4qQ(n3db zeN@cL)pEV+v5MCOyyspzeh;Wo{!*Xm9FDwnoG5Oo*6U0?>bS8^q&%6UmEsz@HmFS# zI6JecIYAz0PhV25Ztv*d6KJ8HcQr=Pt}P`!Xsxw076Xa9+R!8JTl>E&-qK@gyL(-Z zCd>J8J?%l;>uj}7+wwiN?#Pe&zQP|mTQ_T$WjeNQ&6JPh(MQBJb@mlMIMcorNQ?v3cwp?NQ5gd}oETC|PQY zMnwCm7uV3^I*%*S%(%q$ND+0kmkNt7i0x6Ej<&+J)*f{&8}lCa^sLZO%HZ zd83h5PiY@LZ2m4)HNJg6mJWHTx^d)H)r-mAm#@=JuU8owY}7uGshD zTFwp~uemggd)E=P^vFxI&YeL$xU##tI+nK4+39#)SXW$M75%GSdc@7&m1z< zxHkG*YrAq+s&(B65|1I&Q9oBfD~>qt4P?I)*K9qmkxb?~>AX5HR_(i290ga}T-wAf zbTnt3D_6fe?{-49Z$0kLauR~Huf6JdnoM1_Lyzcht*icbedrq2i5|6;*6+kBIxt=bI&t^Yg?+W42PK-{6?Ujr zc-NKvzx-V>1829h*_G*bCoEPwwM6Hx+ZAzkwX{UbbgnvEUEQ>m`Y(#hbj$UnTW

?Ybv9RcK)s_@ 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 G{l5W@A9e8n literal 0 HcmV?d00001 diff --git a/ui-ngx/src/assets/fonts/material-icons.css b/ui-ngx/src/assets/fonts/material-icons.css new file mode 100644 index 0000000000..fe6f64850d --- /dev/null +++ b/ui-ngx/src/assets/fonts/material-icons.css @@ -0,0 +1,33 @@ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + font-weight: 400; + src: local('Material Icons'), + local('MaterialIcons-Regular'), + url(MaterialIcons-Regular.ttf) format('truetype'); +} + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; /* Preferred icon size */ + display: inline-block; + line-height: 1; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + + /* Support for all WebKit browsers. */ + -webkit-font-smoothing: antialiased; + /* Support for Safari and Chrome. */ + text-rendering: optimizeLegibility; + + /* Support for Firefox. */ + -moz-osx-font-smoothing: grayscale; + + /* Support for IE. */ + font-feature-settings: 'liga'; +} diff --git a/ui-ngx/src/index.html b/ui-ngx/src/index.html index 946d96f1b7..da8d9f6568 100644 --- a/ui-ngx/src/index.html +++ b/ui-ngx/src/index.html @@ -24,7 +24,7 @@ - +