Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard into edge/refactoring

pull/4410/head
Volodymyr Babak 6 years ago
parent
commit
c78823332b
  1. 18
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  2. 2
      application/src/main/java/org/thingsboard/server/config/WebConfig.java
  3. 2
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  4. 7
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java
  5. 5
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java
  6. 21
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java
  7. 19
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java
  8. 5
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java
  9. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java
  10. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java
  11. 151
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  12. 1
      msa/black-box-tests/README.md
  13. 9
      msa/tb/docker-cassandra/Dockerfile
  14. 9
      msa/tb/docker-postgres/Dockerfile
  15. 2
      msa/tb/pom.xml
  16. 2
      pom.xml
  17. 2
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  18. 23
      ui-ngx/src/app/core/http/resource.service.ts
  19. 2
      ui-ngx/src/app/core/services/time.service.ts
  20. 4
      ui-ngx/src/app/core/utils.ts
  21. 13
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  22. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  23. 2
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  24. 36
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts
  25. 8
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts
  26. 18
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts
  27. 84
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts
  28. 29
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts
  29. 16
      ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts
  30. 33
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts
  31. 31
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts
  32. 69
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html
  33. 36
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.scss
  34. 227
      ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts
  35. 25
      ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts
  36. 15
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts
  37. 3
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  38. 4
      ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts
  39. 4
      ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts
  40. 4
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  41. 59
      ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts
  42. 24
      ui-ngx/src/app/modules/home/pages/edge/edge.component.html
  43. 4
      ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts
  44. 4
      ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts
  45. 4
      ui-ngx/src/app/modules/home/pages/profile/profile.component.html
  46. 9
      ui-ngx/src/app/modules/home/pages/profile/profile.component.ts
  47. 53
      ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts
  48. 10
      ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html
  49. 23
      ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts
  50. 14
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html
  51. 82
      ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts
  52. 4
      ui-ngx/src/app/shared/components/file-input.component.html
  53. 107
      ui-ngx/src/app/shared/components/file-input.component.ts
  54. 8
      ui-ngx/src/app/shared/components/json-object-edit.component.ts
  55. 8
      ui-ngx/src/app/shared/components/time/datetime-period.component.html
  56. 1
      ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts
  57. 39
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.html
  58. 24
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss
  59. 5
      ui-ngx/src/app/shared/models/resource.models.ts
  60. 5
      ui-ngx/src/app/shared/shared.module.ts
  61. 2
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json

18
application/src/main/data/json/system/widget_bundles/input_widgets.json

@ -455,6 +455,24 @@
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Photo camera input\",\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"displayTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
} }
},
{
"alias": "update_json_attribute",
"name": "Update JSON attribute",
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADhCAMAAACZWwyuAAAB71BMVEX////w8PDt7e3z8/Pg4ODAwMDc3Nz19fUZhJQ4lKK9vb17e3v9/f2mpqbj4+P4+Pi+vr58fHzT09P7+/uTxcz2+/ucy9GpqakCAgK93OHNzc0eh5b09PSw1dusrKwvkZ+ampqysrIzMzPV1dXCwsKurq4iiZnZ2dnX19fIyMgrj50ABsiKiopmZmbo8/XQ5+ro6OjPz89JnqqCgoLX6u2cnJwmjJv39/ff39+12N7ExMQ2NjaioqKYmJjs7OzD4OTS0tLKysrh8PLq6uq6uroCCMjDw8OOjo6hzdO0tLQuj56UlJS4uvDZ7O7l5eVvsrw0k6EchZWEhIRwcHDU6ezG4eaGv8d1tb+3t7dCmqeIiIj4+/3j8fO62+AKD8tWpbGgoKBqamouLi7u7u47lqTP0PWr0tgXHM1/u8N6uMFqsLpfqrVNoKxFRUXU1faYyM92dnY7Ozvz+Pry8vK5ubm4uLh+fn5sbGzGx/OipOtzduG42t8uM9KNw8pTo690dHT6+vrt9viOkefb29um0NYfHx/p6vu9vvGqrO2YmulXW9uhztRnrrgmJiYQEBDt7fzk5frf4PjL4+eMj+a/3eJpbN9MUNk7P9SQxMuNwslfX19TU1OcnurI4uY9QdYyN9ImK9BVVVVPT097fuNYWFjSJwPyAAALvElEQVR42uzbTWviQBjA8ad5qgNONI05FCSIshUNKERCSFlM2WIxSKCH5FI8RfoFCrvVYwv7xTfG4susxqgpqJkfHoLM6c9MTCYROI7jOC4doiIRoqrEkhQRuIOJEsEVROI1Dw2pIkPlMQ8PyWMez8ItLOD2QnArAlxyIsEYhK/x5CSMJQGX0Cfu8AmXTCEqbkWUPQaBiDtd8hJXMJ6SbNByeWd3iROMRxIPAhEx09MSd1ATDwIJMdPTEndJNIid4D/kkLk4yMbFZTop2fXdonSZktb2XuFKpwRnBzeoGoeklNZS1tjj+BVu2hDKfYhxKXUXTtnXHIqgXd+esk/pNSK+GrQcjZsbuotBVrKUFmzSGs++b1djZqVV1m01D6crqlSpVOldpYKFSmzKWUCN0nI0bq5bWAwiuHquNNnj+JOlSDWA144OJW86KUcp38oAtQZIPZ/mICQEBvWGcLpw7i6acu0iCkXDtQ1hcos4MtdTOt4VYs2h5dk42y4azj3ejhYpVUyEwEZ1F+BpBJKviQVfWUkZuOTZz0e5ba8FJ4xNWZcLPw0De97vezpYTzmUK1iiL18p5T+aEaymxGRU2OhhTMAbgFISATvCMqU0LgEUo4U/KOreKV9LsSlHAeKtgY+09STr6ylvzABfRuWvlC5i4DCz8oiU4Az6Uwug25blzsMyZb8zDpkQkgj04YSxKSf16FwpTOquzZwrnx5lvT3sLlP21lKSoxY4aM4wANCn10DmKemvaFZ2HuBMMCnDT5QSe205x6R8ufICWqowKQ/92WFJ/lsTID+9vhp29FlKpye1/AbUXELqTTgDbMq60S2PDMRHKutsSqzTGrIpD74YYvUohBr+x1D+O0tZMqaO0wASfPjmWdwjbfgFd0wDUZjY+F/Ke6ptSRl/iZ6Nm3Dc4sq7Se/GMRtbQ7hZvjrR+XZGKimrhoZ7pOSbbKntV/Kt39R20fkDiZSf7Yi4i3rBkzLVJ46g4M7mHH+l4Bjpv+hiXfTyTr9lVk+U38Diqzs1n1l8XeibiJaKLP7O74FEiYdMT/RfE0RVJZICe8txSSRKmU9KEIRm8V0QnitNIWvST6m93whhy3/sm/lTEmEYx7/SOLPbdFgjCDGEWm0FBUVEGRRF0WEmlCgxeFSaeKHpNF1ajWW3V4cddh9/aLs078PqCMOCxjrs55dn3mff9/3hM7x7fGfYrSs1ll9leUWFrmrK+ERXahSk0uc9srTKJ7c31+hKjQJUHnnorc2g0lijK9eVGgWovHF1fUaV5zSVyg54RUaVNzWVearcK5FWWWPcqanMU2VjU1NTI6kcNjbpNJX5HvDGq43pA7510/Q+TaVylcyl/F65z1iuqVSi8vgpX23VqaUfO5c1lUpUrq+V2LXUh6PxnPbhuExxhnHrfi3OKFylbseV8ic/tz/WlRoroFK8V9ZMGad0pUZuKstyBSWMpnJ1qfS0tXmk6hhFHmRfR/sXnQJUVswPJy8dy0Vlvd9fL1WDFXmRfR3tv5hn39oT9+JU5TyyNwBQi8rOz3vWVnurc1HZ5XR2rZhK2n8x68buNbwNPqIq43lCVSrLpB/k1aZcVD5wODoAXOQk9ND3hD39DrzhzR39QgiIhs22Xj1YH4EQL5hjQIQ3C5OO9DrbWaCSZ9dpPu2PjdeuHZK7BPD+JdU0g4nrQVWplPizJxeVAaAVgLvF5na7gT6+rnlgAJZ6p6ujWfCD72uesEWpDxP3qvlrGzA+6hwNh2gdU8mus/m0PzYYjYexkO/3qBLxsbtQnUpf8vDCRi4HNcBFADcXsPDoDcF1FnwlYOFaWV9UpQdjiGfrSCW7TvPTVB/FQl4knlIlfr+H6lRWDZ8uU66yjjOLcM2Syj64DCmV41wd65PK1sl6m9WWUSXNz8zd9jtUiaftg6pTuXX4VFkeKk2c84KIXlTZE2IqRzgT65PKrgF/pEem0jpEKuX7ZDPZQDXNC3swGLSnnkNqUbnfW6PwFb1bgEgr1w0JUSXAVJq4AOszVSNcHTBho3VwRUkl7bOQg9WQMdj+nOo/vnwBMDMoEnwxA0AtKm/MbxE5qUBlhGt5Z9AjKtw3WSwylTFLd7gfrM9Uge/wO3ts6XWfhFf+PlJJ84nFj52XH66LxKli1m5/DQl2wM9MX1aFyl//8koFKvHRY3Y5sO5+2Gxtkau0CpMjYH1SdaFDMNf3p9fpo8LF+hBdp/nEopehH3aJ11QRHxuLL1D5y3hbFSqXAvnAV6JIbJuu1lQuC0eStRs1lctC1aU16njsaCFb8VRW5pCxvWnBauW/5ZXZDvKQJ0K6ednYMarGXHIFVFZ99ib36BTklRlVRjmTXCWNDdaccsmgXaIBSlGPSt9BMa/0KcgrM6rUOyFTSWNSmT2XxB2RucQsFKMalRLztxTklbxVEKJ6FktQXhngOK4OIiOTgjXGszHllA5p1Mo1L5FLErPtd6EcFansXDt8TkFeyff7J4RPTCXllXG3+2JKXX/YYhFVsjHllLFewCLEs+WSc2MoOoWoXFtbu6kzg8pM74+9LlJJeSWQUhfg/Kk+G9MBb+GB3g5kySWfioFF0SlE5b4TV4YvKVQ5ZCWVLK9k6tq4kaVVOjk3YkPIwtuECv4gV+C90uc9pkzlWRvljiyvZOpMmVSiq2WciyALH+ZQfApUuV65SsodKRkCPN2p/NGS6rMx5ZQYCk+Es+SSmAk+Q/EpQOX5mgpf8mGZYpUsd5SpfDBgMugR8hikPhtTTolxwdqDzLkkntlHUHzyV9l5I+lNHmhUrpLljjKVzpg55kC8x2a2hsDGlFMCvVwEmXNJPEpABayOOMPggvpZBSrHnRaPCepnBVRuOFFefmIDlg3TX/buXqVhKIzD+FuonEEJJQ4eKogEOrjFQRRSisEiuIl3YPYOvQId7Kg34OSdGgd1SaD5aPmH8zwQsv8g5Jx3OOf08c0GUP+UUezMXDzA8/3lKHNnZS630Oqf8sAK//5QvkKrG+VVsqygXEQvGyibUc5WlZTl44+gbEJ5u5pm1ZT3/hjKBpSLr5NRDeWFf4WyyR78eVRHeecnUG5PebOOaynt0K8ttNpTzpMsy5LkvIry+uNpZqHVnjK6LMvm4yrKqXd84A2X6PzBd0mZux/KERvHfsYZE884o5ch2/LscxOe5C4orfDFxMJrAFP0oQQllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBCaVeUEKpF5RQ6gUllHpBuWdK2qLwDj8lIiIiIiIKLpdG45ZFqTP6y407heV/aTfK1Oi3qBtlgNdmfLdrxypyw0AAhifJ2AeDZtcohUAEgZGFrdJVjIsgk2DY5urt9xFu9wn2xWMFLklzV3gb45u/GHD7YSML5s3wwUASSqHcbkIplNtLKIVyewll7onaZR7oB/yfJ/UYZfvLmbMXyscpy2CKxplOKB+mtBwRNRcflLJJvhh/Anybir4nBV9sMeJKyonnZaYO/cm4GX1+JC6xr02dPgAlpaFpFMTq7onUsRkx0WEd5RD42ucP3YWJeMJQI4YTJj7H2tz3TzkBlPR0oQEgkUK6AEzz2mPnxhx6LPsW0RA2XHYc8ebKwXPcP2UPgPTynAk9KV2pTHtcRZkxZ8Ma9dkZvqJmO3KLjnPFTimP1EEm/PyP8tMfSp8p7/R9FWWyy9BMrTnp1lwRA53zNx70UrtTSmjiMhIdXykvdADQpDCT2katoiTWiC1Xli0OmbJyZkQ8m4XR7/XYASQ7JNLwSglxfh4qUl+bETUNsIqyM66YAyfPdaz5hpiYuzzDWLHeKyUcYhEH9Zcy/wzZRApebDUOa2873dWZ2iJG54oQENHVuGRrE+Ju30q5g28qoRTK7SWUQrm9hFIot5dQCuX2kvUrWQrcXrKqKgvUkiRJkiRJkiS9328dj4CaN1dagAAAAABJRU5ErkJggg==",
"description": "Simple form to input new JSON value for pre-defined attribute/timeseries key.",
"descriptor": {
"type": "latest",
"sizeX": 7.5,
"sizeY": 3,
"resources": [],
"templateHtml": "<tb-json-input-widget \n [ctx]=\"ctx\">\n</tb-json-input-widget>",
"templateCss": ".attribute-update-form {\n overflow: hidden;\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.attribute-update-form__grid {\n display: flex;\n}\n.grid__element:first-child {\n flex: 1;\n}\n.grid__element:last-child {\n margin-top: 19px;\n margin-left: 7px;\n}\n.grid__element {\n display: flex;\n}\n\n.attribute-update-form .mat-button.mat-icon-button {\n width: 32px;\n min-width: 32px;\n height: 32px;\n min-height: 32px;\n padding: 0 !important;\n margin: 0 !important;\n line-height: 20px;\n}\n\n.attribute-update-form .mat-icon-button mat-icon {\n width: 20px;\n min-width: 20px;\n height: 20px;\n min-height: 20px;\n font-size: 20px;\n}\n\n.tb-toast {\n font-size: 14px!important;\n}",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.jsonInputWidget.onDataUpdated();\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true\n }\n}\n\nself.onDestroy = function() {\n}",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"AdvancedSettings\",\n \"properties\": {\n \"widgetTitle\": {\n \"title\": \"Widget title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"widgetMode\": {\n \"title\": \"Widget mode\",\n \"type\": \"string\",\n \"default\": \"ATTRIBUTE\"\n },\n \"attributeScope\": {\n \"title\": \"Attribute scope\",\n \"type\": \"string\",\n \"default\": \"SERVER_SCOPE\"\n },\n \"showLabel\":{\n \"title\": \"Show label\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelValue\": {\n \"title\": \"Label\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"attributeRequired\": {\n \"title\": \"Value required\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showResultMessage\": {\n \"title\": \"Show result message\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"widgetTitle\",\n {\n \"key\": \"widgetMode\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"ATTRIBUTE\",\n \"label\": \"Update attribute\"\n },\n {\n \"value\": \"TIME_SERIES\",\n \"label\": \"Update timeseries\"\n }\n ]\n },\n {\n \"key\": \"attributeScope\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"condition\": \"model.widgetMode === 'ATTRIBUTE'\",\n \"items\": [\n {\n \"value\": \"SERVER_SCOPE\",\n \"label\": \"Server attribute\"\n },\n {\n \"value\": \"SHARED_SCOPE\",\n \"label\": \"Shared attribute\"\n }\n ]\n },\n \"showLabel\",\n {\n \"key\": \"labelValue\",\n \"condition\": \"model.showLabel\"\n },\n \"attributeRequired\",\n \"showResultMessage\"\n ]\n}",
"dataKeySettingsSchema": "{}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"attributeScope\":\"SERVER_SCOPE\",\"showLabel\":true,\"attributeRequired\":true,\"showResultMessage\":true},\"title\":\"Update JSON attribute\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
}
} }
] ]
} }

2
application/src/main/java/org/thingsboard/server/config/WebConfig.java

@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
@Controller @Controller
public class WebConfig { public class WebConfig {
@RequestMapping(value = "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**") @RequestMapping(value = {"/assets", "/assets/", "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**"})
public String redirect() { public String redirect() {
return "forward:/index.html"; return "forward:/index.html";
} }

2
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -17,8 +17,10 @@ package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

7
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapSecurityStore.java

@ -40,7 +40,7 @@ import org.thingsboard.server.transport.lwm2m.utils.TypeServer;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.Arrays; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -70,8 +70,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
@Override @Override
public List<SecurityInfo> getAllByEndpoint(String endPoint) { public List<SecurityInfo> getAllByEndpoint(String endPoint) {
String endPointKey = endPoint; ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPoint, TypeServer.BOOTSTRAP);
ReadResultSecurityStore store = lwM2MCredentialsSecurityInfoValidator.createAndValidateCredentialsSecurityInfo(endPointKey, TypeServer.BOOTSTRAP);
if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) { if (store.getBootstrapJsonCredential() != null && store.getSecurityMode() < LwM2MSecurityMode.DEFAULT_MODE.code) {
/** add value to store from BootstrapJson */ /** add value to store from BootstrapJson */
this.setBootstrapConfigScurityInfo(store); this.setBootstrapConfigScurityInfo(store);
@ -87,7 +86,7 @@ public class LwM2MBootstrapSecurityStore implements BootstrapSecurityStore {
} catch (InvalidConfigurationException e) { } catch (InvalidConfigurationException e) {
log.error("", e); log.error("", e);
} }
return store.getSecurityInfo() == null ? null : Arrays.asList(store.getSecurityInfo()); return store.getSecurityInfo() == null ? null : Collections.singletonList(store.getSecurityInfo());
} }
} }
return null; return null;

5
ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss → common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mQueuedRequest.java

@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.transport.lwm2m.server;
:host { public interface LwM2mQueuedRequest {
min-width: 364px; void send();
} }

21
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportRequest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.transport.lwm2m.server; package org.thingsboard.server.transport.lwm2m.server;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Response; import org.eclipse.californium.core.coap.Response;
import org.eclipse.leshan.core.attributes.Attribute; import org.eclipse.leshan.core.attributes.Attribute;
import org.eclipse.leshan.core.attributes.AttributeSet; import org.eclipse.leshan.core.attributes.AttributeSet;
@ -34,6 +35,7 @@ import org.eclipse.leshan.core.request.ObserveRequest;
import org.eclipse.leshan.core.request.ReadRequest; import org.eclipse.leshan.core.request.ReadRequest;
import org.eclipse.leshan.core.request.WriteAttributesRequest; import org.eclipse.leshan.core.request.WriteAttributesRequest;
import org.eclipse.leshan.core.request.WriteRequest; import org.eclipse.leshan.core.request.WriteRequest;
import org.eclipse.leshan.core.request.exception.ClientSleepingException;
import org.eclipse.leshan.core.response.CancelObservationResponse; import org.eclipse.leshan.core.response.CancelObservationResponse;
import org.eclipse.leshan.core.response.DeleteResponse; import org.eclipse.leshan.core.response.DeleteResponse;
import org.eclipse.leshan.core.response.DiscoverResponse; import org.eclipse.leshan.core.response.DiscoverResponse;
@ -58,7 +60,6 @@ import java.util.Date;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import static org.eclipse.californium.core.coap.CoAP.ResponseCode.isSuccess;
import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD; import static org.eclipse.leshan.core.attributes.Attribute.MINIMUM_PERIOD;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEFAULT_TIMEOUT; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.DEFAULT_TIMEOUT;
import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER; import static org.thingsboard.server.transport.lwm2m.server.LwM2mTransportHandler.GET_TYPE_OPER_DISCOVER;
@ -215,13 +216,19 @@ public class LwM2mTransportRequest {
request = new WriteAttributesRequest(resultIds.getObjectId(), attrSet); request = new WriteAttributesRequest(resultIds.getObjectId(), attrSet);
} }
break; break;
default:
} }
if (request != null) { if (request != null) {
this.sendRequest(registration, lwM2MClient, request, timeoutInMs); try {
} this.sendRequest(registration, lwM2MClient, request, timeoutInMs);
else { } catch (ClientSleepingException e) {
DownlinkRequest finalRequest = request;
long finalTimeoutInMs = timeoutInMs;
lwM2MClient.getQueuedRequests().add(() -> sendRequest(registration, lwM2MClient, finalRequest, finalTimeoutInMs));
} catch (Exception e) {
log.error("[{}] [{}] [{}] Failed to send downlink.", registration.getEndpoint(), targetIdVer, typeOper, e);
}
} else {
log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer); log.error("[{}], [{}] - [{}] error SendRequest", registration.getEndpoint(), typeOper, targetIdVer);
} }
} }
@ -240,7 +247,7 @@ public class LwM2mTransportRequest {
if (!lwM2MClient.isInit()) { if (!lwM2MClient.isInit()) {
lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration)); lwM2MClient.initValue(this.serviceImpl, convertToIdVerFromObjectId(request.getPath().toString(), registration));
} }
if (isSuccess(((Response) response.getCoapResponse()).getCode())) { if (CoAP.ResponseCode.isSuccess(((Response) response.getCoapResponse()).getCode())) {
this.handleResponse(registration, request.getPath().toString(), response, request); this.handleResponse(registration, request.getPath().toString(), response, request);
if (request instanceof WriteRequest && ((WriteRequest) request).isReplaceRequest()) { if (request instanceof WriteRequest && ((WriteRequest) request).isReplaceRequest()) {
String msg = String.format("%s: sendRequest Replace: CoapCde - %s Lwm2m code - %d name - %s Resource path - %s value - %s SendRequest to Client", String msg = String.format("%s: sendRequest Replace: CoapCde - %s Lwm2m code - %d name - %s Resource path - %s value - %s SendRequest to Client",
@ -283,7 +290,7 @@ public class LwM2mTransportRequest {
case FLOAT: // Double case FLOAT: // Double
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString())); return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Double.parseDouble(value.toString())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Double.parseDouble(value.toString()));
case TIME: // Date case TIME: // Date
Date date = new Date(Long.decode(value.toString())); Date date = new Date(Long.decode(value.toString()));
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date); return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, date) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, date);
case OPAQUE: // byte[] value, base64 case OPAQUE: // byte[] value, base64
return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())); return (contentFormat == null) ? new WriteRequest(objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray())) : new WriteRequest(contentFormat, objectId, instanceId, resourceId, Hex.decodeHex(value.toString().toCharArray()));

19
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2mTransportServiceImpl.java

@ -146,9 +146,9 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
* Next -> Create new LwM2MClient for current session -> setModelClient... * Next -> Create new LwM2MClient for current session -> setModelClient...
* *
* @param registration - Registration LwM2M Client * @param registration - Registration LwM2M Client
* @param previousObsersations - may be null * @param previousObservations - may be null
*/ */
public void onRegistered(Registration registration, Collection<Observation> previousObsersations) { public void onRegistered(Registration registration, Collection<Observation> previousObservations) {
executorRegistered.submit(() -> { executorRegistered.submit(() -> {
try { try {
log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId()); log.warn("[{}] [{{}] Client: create after Registration", registration.getEndpoint(), registration.getId());
@ -188,6 +188,13 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2MClient(sessionInfo); LwM2mClient lwM2MClient = this.lwM2mClientContext.getLwM2MClient(sessionInfo);
if (lwM2MClient.getDeviceId() == null && lwM2MClient.getProfileId() == null) { if (lwM2MClient.getDeviceId() == null && lwM2MClient.getProfileId() == null) {
initLwM2mClient(lwM2MClient, sessionInfo); initLwM2mClient(lwM2MClient, sessionInfo);
} else {
if (registration.getBindingMode().useQueueMode()) {
LwM2mQueuedRequest request;
while ((request = lwM2MClient.getQueuedRequests().poll()) != null) {
request.send();
}
}
} }
log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType()); log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType());
@ -206,7 +213,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
* !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect * !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect
*/ */
public void unReg(Registration registration, Collection<Observation> observations) { public void unReg(Registration registration, Collection<Observation> observations) {
executorUnRegistered.submit(() -> { executorUnRegistered.submit(() -> {
try { try {
this.setCancelObservations(registration); this.setCancelObservations(registration);
this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration); this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration);
@ -239,8 +246,11 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
} }
} }
@Override
public void onSleepingDev(Registration registration) { public void onSleepingDev(Registration registration) {
log.info("[{}] [{}] Received endpoint Sleeping version event", registration.getId(), registration.getEndpoint()); log.info("[{}] [{}] Received endpoint Sleeping version event", registration.getId(), registration.getEndpoint());
this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is sleeping!", registration);
//TODO: associate endpointId with device information. //TODO: associate endpointId with device information.
} }
@ -417,6 +427,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
*/ */
protected void onAwakeDev(Registration registration) { protected void onAwakeDev(Registration registration) {
log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint()); log.info("[{}] [{}] Received endpoint Awake version event", registration.getId(), registration.getEndpoint());
this.sendLogsToThingsboard(LOG_LW2M_INFO + ": Client is awake!", registration);
//TODO: associate endpointId with device information. //TODO: associate endpointId with device information.
} }
@ -612,7 +623,7 @@ public class LwM2mTransportServiceImpl implements LwM2mTransportService {
if (GET_TYPE_OPER_READ.equals(typeOper)) { if (GET_TYPE_OPER_READ.equals(typeOper)) {
result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() { result = JacksonUtil.fromString(lwM2MClientProfile.getPostAttributeProfile().toString(), new TypeReference<>() {
}); });
result.addAll(JacksonUtil.fromString(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() { result.addAll(JacksonUtil.convertValue(lwM2MClientProfile.getPostTelemetryProfile().toString(), new TypeReference<>() {
})); }));
} else { } else {
result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() { result = JacksonUtil.fromString(lwM2MClientProfile.getPostObserveProfile().toString(), new TypeReference<>() {

5
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java

@ -25,13 +25,16 @@ import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.security.SecurityInfo; import org.eclipse.leshan.server.security.SecurityInfo;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg;
import org.thingsboard.server.transport.lwm2m.server.LwM2mQueuedRequest;
import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServiceImpl;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -54,6 +57,7 @@ public class LwM2mClient implements Cloneable {
private final Map<String, ResourceValue> resources; private final Map<String, ResourceValue> resources;
private final Map<String, TransportProtos.TsKvProto> delayedRequests; private final Map<String, TransportProtos.TsKvProto> delayedRequests;
private final List<String> pendingRequests; private final List<String> pendingRequests;
private final Queue<LwM2mQueuedRequest> queuedRequests;
private boolean init; private boolean init;
public Object clone() throws CloneNotSupportedException { public Object clone() throws CloneNotSupportedException {
@ -71,6 +75,7 @@ public class LwM2mClient implements Cloneable {
this.profileId = profileId; this.profileId = profileId;
this.sessionId = sessionId; this.sessionId = sessionId;
this.init = false; this.init = false;
this.queuedRequests = new ConcurrentLinkedQueue<>();
} }
public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) { public boolean saveResourceValue(String pathRez, LwM2mResource rez, LwM2mModelProvider modelProvider) {

2
dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java

@ -51,11 +51,11 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com
TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
try { try {
componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); componentDescriptorEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
transactionManager.commit(transaction);
} catch (Throwable th) { } catch (Throwable th) {
log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType()); log.trace("Could not execute the update statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType());
transactionManager.rollback(transaction); transactionManager.rollback(transaction);
} }
transactionManager.commit(transaction);
} else { } else {
log.trace("Could not execute the insert statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType()); log.trace("Could not execute the insert statement for Component Descriptor with id {}, name {} and entityType {}", entity.getUuid(), entity.getName(), entity.getType());
} }

2
dao/src/main/java/org/thingsboard/server/dao/sql/event/AbstractEventInsertRepository.java

@ -51,11 +51,11 @@ public abstract class AbstractEventInsertRepository implements EventInsertReposi
TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW); TransactionStatus transaction = getTransactionStatus(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
try { try {
eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict); eventEntity = processSaveOrUpdate(entity, insertOrUpdateOnUniqueKeyConflict);
transactionManager.commit(transaction);
} catch (Throwable th) { } catch (Throwable th) {
log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); log.trace("Could not execute the update statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
transactionManager.rollback(transaction); transactionManager.rollback(transaction);
} }
transactionManager.commit(transaction);
} else { } else {
log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType()); log.trace("Could not execute the insert statement for Entity with entityId {} and entityType {}", entity.getEventUid(), entity.getEventType());
} }

151
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -62,6 +62,8 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
@ -107,6 +109,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement[] fetchStmtsDesc; private PreparedStatement[] fetchStmtsDesc;
private PreparedStatement deleteStmt; private PreparedStatement deleteStmt;
private PreparedStatement deletePartitionStmt; private PreparedStatement deletePartitionStmt;
private final Lock stmtCreationLock = new ReentrantLock();
private boolean isInstall() { private boolean isInstall() {
return environment.acceptsProfiles(Profiles.of("install")); return environment.acceptsProfiles(Profiles.of("install"));
@ -545,13 +548,20 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement getDeleteStmt() { private PreparedStatement getDeleteStmt() {
if (deleteStmt == null) { if (deleteStmt == null) {
deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF + stmtCreationLock.lock();
" WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM try {
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM if (deleteStmt == null) {
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM deleteStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_CF +
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " >= ? " + "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " < ?"); + "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.TS_COLUMN + " >= ? "
+ "AND " + ModelConstants.TS_COLUMN + " < ?");
}
} finally {
stmtCreationLock.unlock();
}
} }
return deleteStmt; return deleteStmt;
} }
@ -585,27 +595,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement getDeletePartitionStmt() { private PreparedStatement getDeletePartitionStmt() {
if (deletePartitionStmt == null) { if (deletePartitionStmt == null) {
deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF + stmtCreationLock.lock();
" WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM try {
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM if (deletePartitionStmt == null) {
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM deletePartitionStmt = prepare("DELETE FROM " + ModelConstants.TS_KV_PARTITIONS_CF +
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM); " WHERE " + ModelConstants.ENTITY_TYPE_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.ENTITY_ID_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.PARTITION_COLUMN + EQUALS_PARAM
+ "AND " + ModelConstants.KEY_COLUMN + EQUALS_PARAM);
}
} finally {
stmtCreationLock.unlock();
}
} }
return deletePartitionStmt; return deletePartitionStmt;
} }
private PreparedStatement getSaveStmt(DataType dataType) { private PreparedStatement getSaveStmt(DataType dataType) {
if (saveStmts == null) { if (saveStmts == null) {
saveStmts = new PreparedStatement[DataType.values().length]; stmtCreationLock.lock();
for (DataType type : DataType.values()) { try {
saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + if (saveStmts == null) {
"(" + ModelConstants.ENTITY_TYPE_COLUMN + saveStmts = new PreparedStatement[DataType.values().length];
"," + ModelConstants.ENTITY_ID_COLUMN + for (DataType type : DataType.values()) {
"," + ModelConstants.KEY_COLUMN + saveStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"," + ModelConstants.PARTITION_COLUMN + "(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.TS_COLUMN + "," + ModelConstants.ENTITY_ID_COLUMN +
"," + getColumnName(type) + ")" + "," + ModelConstants.KEY_COLUMN +
" VALUES(?, ?, ?, ?, ?, ?)"); "," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + getColumnName(type) + ")" +
" VALUES(?, ?, ?, ?, ?, ?)");
}
}
} finally {
stmtCreationLock.unlock();
} }
} }
return saveStmts[dataType.ordinal()]; return saveStmts[dataType.ordinal()];
@ -613,16 +637,23 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement getSaveTtlStmt(DataType dataType) { private PreparedStatement getSaveTtlStmt(DataType dataType) {
if (saveTtlStmts == null) { if (saveTtlStmts == null) {
saveTtlStmts = new PreparedStatement[DataType.values().length]; stmtCreationLock.lock();
for (DataType type : DataType.values()) { try {
saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF + if (saveTtlStmts == null) {
"(" + ModelConstants.ENTITY_TYPE_COLUMN + saveTtlStmts = new PreparedStatement[DataType.values().length];
"," + ModelConstants.ENTITY_ID_COLUMN + for (DataType type : DataType.values()) {
"," + ModelConstants.KEY_COLUMN + saveTtlStmts[type.ordinal()] = prepare(INSERT_INTO + ModelConstants.TS_KV_CF +
"," + ModelConstants.PARTITION_COLUMN + "(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.TS_COLUMN + "," + ModelConstants.ENTITY_ID_COLUMN +
"," + getColumnName(type) + ")" + "," + ModelConstants.KEY_COLUMN +
" VALUES(?, ?, ?, ?, ?, ?) USING TTL ?"); "," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + getColumnName(type) + ")" +
" VALUES(?, ?, ?, ?, ?, ?) USING TTL ?");
}
}
} finally {
stmtCreationLock.unlock();
} }
} }
return saveTtlStmts[dataType.ordinal()]; return saveTtlStmts[dataType.ordinal()];
@ -630,24 +661,38 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement getPartitionInsertStmt() { private PreparedStatement getPartitionInsertStmt() {
if (partitionInsertStmt == null) { if (partitionInsertStmt == null) {
partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + stmtCreationLock.lock();
"(" + ModelConstants.ENTITY_TYPE_COLUMN + try {
"," + ModelConstants.ENTITY_ID_COLUMN + if (partitionInsertStmt == null) {
"," + ModelConstants.PARTITION_COLUMN + partitionInsertStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"," + ModelConstants.KEY_COLUMN + ")" + "(" + ModelConstants.ENTITY_TYPE_COLUMN +
" VALUES(?, ?, ?, ?)"); "," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?)");
}
} finally {
stmtCreationLock.unlock();
}
} }
return partitionInsertStmt; return partitionInsertStmt;
} }
private PreparedStatement getPartitionInsertTtlStmt() { private PreparedStatement getPartitionInsertTtlStmt() {
if (partitionInsertTtlStmt == null) { if (partitionInsertTtlStmt == null) {
partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF + stmtCreationLock.lock();
"(" + ModelConstants.ENTITY_TYPE_COLUMN + try {
"," + ModelConstants.ENTITY_ID_COLUMN + if (partitionInsertTtlStmt == null) {
"," + ModelConstants.PARTITION_COLUMN + partitionInsertTtlStmt = prepare(INSERT_INTO + ModelConstants.TS_KV_PARTITIONS_CF +
"," + ModelConstants.KEY_COLUMN + ")" + "(" + ModelConstants.ENTITY_TYPE_COLUMN +
" VALUES(?, ?, ?, ?) USING TTL ?"); "," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.KEY_COLUMN + ")" +
" VALUES(?, ?, ?, ?) USING TTL ?");
}
} finally {
stmtCreationLock.unlock();
}
} }
return partitionInsertTtlStmt; return partitionInsertTtlStmt;
} }
@ -713,12 +758,26 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
switch (orderBy) { switch (orderBy) {
case ASC_ORDER: case ASC_ORDER:
if (fetchStmtsAsc == null) { if (fetchStmtsAsc == null) {
fetchStmtsAsc = initFetchStmt(orderBy); stmtCreationLock.lock();
try {
if (fetchStmtsAsc == null) {
fetchStmtsAsc = initFetchStmt(orderBy);
}
} finally {
stmtCreationLock.unlock();
}
} }
return fetchStmtsAsc[aggType.ordinal()]; return fetchStmtsAsc[aggType.ordinal()];
case DESC_ORDER: case DESC_ORDER:
if (fetchStmtsDesc == null) { if (fetchStmtsDesc == null) {
fetchStmtsDesc = initFetchStmt(orderBy); stmtCreationLock.lock();
try {
if (fetchStmtsDesc == null) {
fetchStmtsDesc = initFetchStmt(orderBy);
}
} finally {
stmtCreationLock.unlock();
}
} }
return fetchStmtsDesc[aggType.ordinal()]; return fetchStmtsDesc[aggType.ordinal()];
default: default:

1
msa/black-box-tests/README.md

@ -10,6 +10,7 @@ To run the black box tests with using Docker, the local Docker images of Thingsb
As result, in REPOSITORY column, next images should be present: As result, in REPOSITORY column, next images should be present:
thingsboard/tb-coap-transport thingsboard/tb-coap-transport
thingsboard/tb-lwm2m-transport
thingsboard/tb-http-transport thingsboard/tb-http-transport
thingsboard/tb-mqtt-transport thingsboard/tb-mqtt-transport
thingsboard/tb-node thingsboard/tb-node

9
msa/tb/docker-cassandra/Dockerfile

@ -17,15 +17,14 @@
FROM thingsboard/openjdk11 FROM thingsboard/openjdk11
RUN apt-get update RUN apt-get update
RUN apt-get install -y curl nmap procps RUN apt-get install -y curl nmap procps gnupg2
RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null RUN echo 'deb http://downloads.apache.org/cassandra/debian 40x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null
RUN curl -L https://downloads.apache.org/cassandra/KEYS | apt-key add - RUN curl -L https://downloads.apache.org/cassandra/KEYS | apt-key add -
ENV PG_MAJOR=11 ENV PG_MAJOR=12
RUN apt-get update RUN apt-get update
RUN apt-get install -y cassandra cassandra-tools postgresql-11 RUN apt-get install -y cassandra cassandra-tools postgresql-12
RUN update-rc.d cassandra disable RUN update-rc.d cassandra disable
RUN update-rc.d postgresql disable RUN update-rc.d postgresql disable
RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra RUN sed -i.old '/ulimit/d' /etc/init.d/cassandra

9
msa/tb/docker-postgres/Dockerfile

@ -17,13 +17,12 @@
FROM thingsboard/openjdk11 FROM thingsboard/openjdk11
RUN apt-get update RUN apt-get update
RUN apt-get install -y curl RUN apt-get install -y curl gnupg2
RUN echo 'deb http://ftp.us.debian.org/debian sid main' | tee --append /etc/apt/sources.list.d/debian.list > /dev/null RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ sid-pgdg main' | tee --append /etc/apt/sources.list.d/pgdg.list > /dev/null
RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - RUN curl -L https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
ENV PG_MAJOR 11 ENV PG_MAJOR 12
RUN apt-get update RUN apt-get update
RUN apt-get install -y postgresql-11 RUN apt-get install -y postgresql-12
RUN update-rc.d postgresql disable RUN update-rc.d postgresql disable
COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/ COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/

2
msa/tb/pom.xml

@ -39,7 +39,7 @@
<tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name> <tb-postgres.docker.name>tb-postgres</tb-postgres.docker.name>
<tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name> <tb-cassandra.docker.name>tb-cassandra</tb-cassandra.docker.name>
<pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder> <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
<pkg.upgradeVersion>2.4.2</pkg.upgradeVersion> <pkg.upgradeVersion>3.3.0</pkg.upgradeVersion>
</properties> </properties>
<dependencies> <dependencies>

2
pom.xml

@ -99,7 +99,7 @@
org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
</sonar.exclusions> </sonar.exclusions>
<elasticsearch.version>5.0.2</elasticsearch.version> <elasticsearch.version>5.0.2</elasticsearch.version>
<delight-nashorn-sandbox.version>0.1.31</delight-nashorn-sandbox.version> <delight-nashorn-sandbox.version>0.1.16</delight-nashorn-sandbox.version>
<kafka.version>2.6.0</kafka.version> <kafka.version>2.6.0</kafka.version>
<bucket4j.version>4.1.1</bucket4j.version> <bucket4j.version>4.1.1</bucket4j.version>
<fst.version>2.57</fst.version> <fst.version>2.57</fst.version>

2
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -1338,7 +1338,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody(); HttpEntity.EMPTY, DeviceProfile.class, deviceProfileId).getBody();
} }
public PageData<DeviceProfile> getTenantDevices(PageLink pageLink) { public PageData<DeviceProfile> getDeviceProfiles(PageLink pageLink) {
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
addPageLinkToParam(params, pageLink); addPageLinkToParam(params, pageLink);
return restTemplate.exchange( return restTemplate.exchange(

23
ui-ngx/src/app/core/http/resource.service.ts

@ -18,10 +18,10 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link'; import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs'; import { forkJoin, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { Resource, ResourceInfo } from '@shared/models/resource.models'; import { Resource, ResourceInfo } from '@shared/models/resource.models';
import { map } from 'rxjs/operators'; import { catchError, map, mergeMap } from 'rxjs/operators';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -70,6 +70,25 @@ export class ResourceService {
); );
} }
public saveResources(resources: Resource[], config?: RequestConfig): Observable<Resource[]> {
let partSize = 100;
partSize = resources.length > partSize ? partSize : resources.length;
const resourceObservables: Observable<Resource>[] = [];
for (let i = 0; i < partSize; i++) {
resourceObservables.push(this.saveResource(resources[i], config).pipe(catchError(() => of({} as Resource))));
}
return forkJoin(resourceObservables).pipe(
mergeMap((resource) => {
resources.splice(0, partSize);
if (resources.length) {
return this.saveResources(resources, config);
} else {
return of(resource);
}
})
);
}
public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> { public saveResource(resource: Resource, config?: RequestConfig): Observable<Resource> {
return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config)); return this.http.post<Resource>('/api/resource', resource, defaultHttpOptionsFromConfig(config));
} }

2
ui-ngx/src/app/core/services/time.service.ts

@ -79,7 +79,7 @@ export class TimeService {
public boundMinInterval(min: number): number { public boundMinInterval(min: number): number {
if (isDefined(min)) { if (isDefined(min)) {
min = Math.floor(min / 1000) * 1000; min = Math.ceil(min / 1000) * 1000;
} }
return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL); return this.toBound(min, MIN_INTERVAL, MAX_INTERVAL, MIN_INTERVAL);
} }

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

@ -127,6 +127,10 @@ export function isEmpty(obj: any): boolean {
return true; return true;
} }
export function isLiteralObject(value: any) {
return (!!value) && (value.constructor === Object);
}
export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined { export function formatValue(value: any, dec?: number, units?: string, showZeroDecimals?: boolean): string | undefined {
if (isDefinedAndNotNull(value) && isNumeric(value) && if (isDefinedAndNotNull(value) && isNumeric(value) &&
(isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) { (isDefinedAndNotNull(dec) || isDefinedAndNotNull(units) || Number(value).toString() === value)) {

13
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -90,7 +90,6 @@ import {
} from '@home/components/alias/entity-aliases-dialog.component'; } from '@home/components/alias/entity-aliases-dialog.component';
import { EntityAliases } from '@app/shared/models/alias.models'; import { EntityAliases } from '@app/shared/models/alias.models';
import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component'; import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { import {
AddWidgetDialogComponent, AddWidgetDialogComponent,
AddWidgetDialogData AddWidgetDialogData
@ -118,8 +117,7 @@ import { ComponentPortal } from '@angular/cdk/portal';
import { import {
DISPLAY_WIDGET_TYPES_PANEL_DATA, DISPLAY_WIDGET_TYPES_PANEL_DATA,
DisplayWidgetTypesPanelComponent, DisplayWidgetTypesPanelComponent,
DisplayWidgetTypesPanelData, DisplayWidgetTypesPanelData
WidgetTypes
} from '@home/components/dashboard-page/widget-types-panel.component'; } from '@home/components/dashboard-page/widget-types-panel.component';
import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component'; import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component';
import {AliasEntityType, EntityType} from "@shared/models/entity-type.models"; import {AliasEntityType, EntityType} from "@shared/models/entity-type.models";
@ -1189,13 +1187,16 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
overlayRef.dispose(); overlayRef.dispose();
}); });
const filterWidgetTypes = this.dashboardWidgetSelectComponent.filterWidgetTypes;
const widgetTypesList = Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => {
return {type, display: filterWidgetTypes === null ? true : filterWidgetTypes.includes(type)};
});
const providers: StaticProvider[] = [ const providers: StaticProvider[] = [
{ {
provide: DISPLAY_WIDGET_TYPES_PANEL_DATA, provide: DISPLAY_WIDGET_TYPES_PANEL_DATA,
useValue: { useValue: {
types: Array.from(this.dashboardWidgetSelectComponent.widgetTypes.values()).map(type => { types: widgetTypesList,
return {type, display: true};
}),
typesUpdated: (newTypes) => { typesUpdated: (newTypes) => {
this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type); this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type);
} }

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts

@ -77,6 +77,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
this.filterWidgetTypes$.next(widgetTypes); this.filterWidgetTypes$.next(widgetTypes);
} }
get filterWidgetTypes(): Array<widgetType> {
return this.filterWidgetTypes$.value;
}
@Output() @Output()
widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>(); widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>();

2
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html

@ -80,7 +80,7 @@
(mousedown)="widgetMouseDown($event, widget)" (mousedown)="widgetMouseDown($event, widget)"
(click)="widgetClicked($event, widget)" (click)="widgetClicked($event, widget)"
(contextmenu)="openWidgetContextMenu($event, widget)"> (contextmenu)="openWidgetContextMenu($event, widget)">
<div *ngIf="widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start"> <div *ngIf="!!widgetComponent.widgetContext?.inited" fxLayout="row" fxLayoutAlign="space-between start">
<div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel"> <div class="tb-widget-title" fxLayout="column" fxLayoutAlign="center start" *ngIf="widget.showWidgetTitlePanel">
<span *ngIf="widget.showTitle" <span *ngIf="widget.showTitle"
[ngClass]="{'single-row': widget.hasTimewindow}" [ngClass]="{'single-row': widget.hasTimewindow}"

36
ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.ts

@ -15,11 +15,21 @@
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { import {
BooleanFilterPredicate, BooleanFilterPredicate,
BooleanOperation, BooleanOperation,
booleanOperationTranslationMap, EntityKeyValueType, booleanOperationTranslationMap,
EntityKeyValueType,
FilterPredicateType FilterPredicateType
} from '@shared/models/query/query.models'; } from '@shared/models/query/query.models';
@ -32,10 +42,15 @@ import {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BooleanFilterPredicateComponent), useExisting: forwardRef(() => BooleanFilterPredicateComponent),
multi: true multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => BooleanFilterPredicateComponent),
multi: true
} }
] ]
}) })
export class BooleanFilterPredicateComponent implements ControlValueAccessor, OnInit { export class BooleanFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean; @Input() disabled: boolean;
@ -73,7 +88,7 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
} }
setDisabledState?(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
this.booleanFilterPredicateFormGroup.disable({emitEvent: false}); this.booleanFilterPredicateFormGroup.disable({emitEvent: false});
@ -82,17 +97,20 @@ export class BooleanFilterPredicateComponent implements ControlValueAccessor, On
} }
} }
validate(): ValidationErrors | null {
return this.booleanFilterPredicateFormGroup ? null : {
booleanFilterPredicate: {valid: false}
};
}
writeValue(predicate: BooleanFilterPredicate): void { writeValue(predicate: BooleanFilterPredicate): void {
this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); this.booleanFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
this.booleanFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); this.booleanFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false});
} }
private updateModel() { private updateModel() {
let predicate: BooleanFilterPredicate = null; const predicate: BooleanFilterPredicate = this.booleanFilterPredicateFormGroup.getRawValue();
if (this.booleanFilterPredicateFormGroup.valid) { predicate.type = FilterPredicateType.BOOLEAN;
predicate = this.booleanFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.BOOLEAN;
}
this.propagateChange(predicate); this.propagateChange(predicate);
} }

8
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate.component.ts

@ -16,11 +16,7 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { import { ComplexFilterPredicateInfo, EntityKeyValueType } from '@shared/models/query/query.models';
ComplexFilterPredicate,
ComplexFilterPredicateInfo,
EntityKeyValueType
} from '@shared/models/query/query.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { import {
ComplexFilterPredicateDialogComponent, ComplexFilterPredicateDialogComponent,
@ -71,7 +67,7 @@ export class ComplexFilterPredicateComponent implements ControlValueAccessor, On
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
} }
setDisabledState?(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
} }

18
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.ts

@ -21,7 +21,10 @@ import {
FormArray, FormArray,
FormBuilder, FormBuilder,
FormGroup, FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of, Subscription } from 'rxjs';
@ -49,10 +52,15 @@ import { map } from 'rxjs/operators';
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterPredicateListComponent), useExisting: forwardRef(() => FilterPredicateListComponent),
multi: true multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FilterPredicateListComponent),
multi: true
} }
] ]
}) })
export class FilterPredicateListComponent implements ControlValueAccessor, OnInit { export class FilterPredicateListComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean; @Input() disabled: boolean;
@ -108,6 +116,12 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
} }
} }
validate(control: AbstractControl): ValidationErrors | null {
return this.filterListFormGroup.valid ? null : {
filterList: {valid: false}
};
}
writeValue(predicates: Array<KeyFilterPredicateInfo>): void { writeValue(predicates: Array<KeyFilterPredicateInfo>): void {
if (this.valueChangeSubscription) { if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe(); this.valueChangeSubscription.unsubscribe();
@ -178,7 +192,7 @@ export class FilterPredicateListComponent implements ControlValueAccessor, OnIni
private updateModel() { private updateModel() {
const predicates: Array<KeyFilterPredicateInfo> = this.filterListFormGroup.getRawValue().predicates; const predicates: Array<KeyFilterPredicateInfo> = this.filterListFormGroup.getRawValue().predicates;
if (this.filterListFormGroup.valid && predicates.length) { if (predicates.length) {
this.propagateChange(predicates); this.propagateChange(predicates);
} else { } else {
this.propagateChange(null); this.propagateChange(null);

84
ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.ts

@ -19,7 +19,10 @@ import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
FormGroup, FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
ValidatorFn, ValidatorFn,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
@ -39,10 +42,15 @@ import {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterPredicateValueComponent), useExisting: forwardRef(() => FilterPredicateValueComponent),
multi: true multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FilterPredicateValueComponent),
multi: true
} }
] ]
}) })
export class FilterPredicateValueComponent implements ControlValueAccessor, OnInit { export class FilterPredicateValueComponent implements ControlValueAccessor, Validator, OnInit {
private readonly inheritModeForSources: DynamicValueSourceType[] = [ private readonly inheritModeForSources: DynamicValueSourceType[] = [
DynamicValueSourceType.CURRENT_CUSTOMER, DynamicValueSourceType.CURRENT_CUSTOMER,
@ -62,7 +70,22 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
} }
} }
@Input() onlyUserDynamicSource = false; private onlyUserDynamicSourceValue = false;
@Input()
set onlyUserDynamicSource(dynamicMode: boolean) {
this.onlyUserDynamicSourceValue = dynamicMode;
if (this.filterPredicateValueFormGroup) {
this.updateValidationDynamicMode();
setTimeout(() => {
this.updateModel();
}, 0);
}
}
get onlyUserDynamicSource(): boolean {
return this.onlyUserDynamicSourceValue;
}
@Input() @Input()
valueType: EntityKeyValueType; valueType: EntityKeyValueType;
@ -83,6 +106,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
allow = true; allow = true;
private propagateChange = null; private propagateChange = null;
private propagateChangePending = false;
constructor(private fb: FormBuilder) { constructor(private fb: FormBuilder) {
} }
@ -126,6 +150,7 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
this.updateShowInheritMode(sourceType); this.updateShowInheritMode(sourceType);
} }
); );
this.updateValidationDynamicMode();
this.filterPredicateValueFormGroup.valueChanges.subscribe(() => { this.filterPredicateValueFormGroup.valueChanges.subscribe(() => {
this.updateModel(); this.updateModel();
}); });
@ -133,12 +158,18 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.propagateChange = fn; this.propagateChange = fn;
if (this.propagateChangePending) {
this.propagateChangePending = false;
setTimeout(() => {
this.updateModel();
}, 0);
}
} }
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
} }
setDisabledState?(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
this.filterPredicateValueFormGroup.disable({emitEvent: false}); this.filterPredicateValueFormGroup.disable({emitEvent: false});
@ -147,28 +178,35 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
} }
} }
validate(): ValidationErrors | null {
return this.filterPredicateValueFormGroup.valid ? null : {
filterPredicateValue: {valid: false}
};
}
writeValue(predicateValue: FilterPredicateValue<string | number | boolean>): void { writeValue(predicateValue: FilterPredicateValue<string | number | boolean>): void {
this.propagateChangePending = false;
this.filterPredicateValueFormGroup.get('defaultValue').patchValue(predicateValue.defaultValue, {emitEvent: false}); this.filterPredicateValueFormGroup.get('defaultValue').patchValue(predicateValue.defaultValue, {emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').patchValue(predicateValue.dynamicValue ? this.filterPredicateValueFormGroup.get('dynamicValue').patchValue({
predicateValue.dynamicValue.sourceType : null, {emitEvent: false}); sourceType: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceType : null,
this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').patchValue(predicateValue.dynamicValue ? sourceAttribute: predicateValue.dynamicValue ? predicateValue.dynamicValue.sourceAttribute : null,
predicateValue.dynamicValue.sourceAttribute : null, {emitEvent: false}); inherit: predicateValue.dynamicValue ? predicateValue.dynamicValue.inherit : false
this.filterPredicateValueFormGroup.get('dynamicValue.inherit').patchValue(predicateValue.dynamicValue ? }, {emitEvent: this.onlyUserDynamicSource});
predicateValue.dynamicValue.inherit : false, {emitEvent: false});
this.updateShowInheritMode(predicateValue?.dynamicValue?.sourceType); this.updateShowInheritMode(predicateValue?.dynamicValue?.sourceType);
} }
private updateModel() { private updateModel() {
let predicateValue: FilterPredicateValue<string | number | boolean> = null; const predicateValue: FilterPredicateValue<string | number | boolean> = this.filterPredicateValueFormGroup.getRawValue();
if (this.filterPredicateValueFormGroup.valid) { if (predicateValue.dynamicValue) {
predicateValue = this.filterPredicateValueFormGroup.getRawValue(); if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) {
if (predicateValue.dynamicValue) { predicateValue.dynamicValue = null;
if (!predicateValue.dynamicValue.sourceType || !predicateValue.dynamicValue.sourceAttribute) {
predicateValue.dynamicValue = null;
}
} }
} }
this.propagateChange(predicateValue); if (this.propagateChange) {
this.propagateChange(predicateValue);
} else {
this.propagateChangePending = true;
}
} }
private updateShowInheritMode(sourceType: DynamicValueSourceType) { private updateShowInheritMode(sourceType: DynamicValueSourceType) {
@ -179,4 +217,16 @@ export class FilterPredicateValueComponent implements ControlValueAccessor, OnIn
this.inheritMode = false; this.inheritMode = false;
} }
} }
private updateValidationDynamicMode() {
if (this.onlyUserDynamicSource) {
this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').setValidators(Validators.required);
this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').setValidators(Validators.required);
} else {
this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').clearValidators();
this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').clearValidators();
}
this.filterPredicateValueFormGroup.get('dynamicValue.sourceType').updateValueAndValidity({emitEvent: false});
this.filterPredicateValueFormGroup.get('dynamicValue.sourceAttribute').updateValueAndValidity({emitEvent: false});
}
} }

29
ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.ts

@ -15,11 +15,17 @@
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { import {
EntityKeyValueType, ControlValueAccessor,
FilterPredicateType, KeyFilterPredicate, KeyFilterPredicateInfo FormBuilder,
} from '@shared/models/query/query.models'; FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { EntityKeyValueType, FilterPredicateType, KeyFilterPredicateInfo } from '@shared/models/query/query.models';
@Component({ @Component({
selector: 'tb-filter-predicate', selector: 'tb-filter-predicate',
@ -30,10 +36,15 @@ import {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FilterPredicateComponent), useExisting: forwardRef(() => FilterPredicateComponent),
multi: true multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FilterPredicateComponent),
multi: true
} }
] ]
}) })
export class FilterPredicateComponent implements ControlValueAccessor, OnInit { export class FilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean; @Input() disabled: boolean;
@ -75,7 +86,7 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
} }
setDisabledState?(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
this.filterPredicateFormGroup.disable({emitEvent: false}); this.filterPredicateFormGroup.disable({emitEvent: false});
@ -84,6 +95,12 @@ export class FilterPredicateComponent implements ControlValueAccessor, OnInit {
} }
} }
validate(): ValidationErrors | null {
return this.filterPredicateFormGroup.valid ? null : {
filterPredicate: {valid: false}
};
}
writeValue(predicate: KeyFilterPredicateInfo): void { writeValue(predicate: KeyFilterPredicateInfo): void {
this.type = predicate.keyFilterPredicate.type; this.type = predicate.keyFilterPredicate.type;
this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false}); this.filterPredicateFormGroup.get('predicate').patchValue(predicate.keyFilterPredicate, {emitEvent: false});

16
ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.ts

@ -22,7 +22,10 @@ import {
FormBuilder, FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
@ -46,10 +49,15 @@ import { EntityId } from '@shared/models/id/entity-id';
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => KeyFilterListComponent), useExisting: forwardRef(() => KeyFilterListComponent),
multi: true multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => KeyFilterListComponent),
multi: true
} }
] ]
}) })
export class KeyFilterListComponent implements ControlValueAccessor, OnInit { export class KeyFilterListComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean; @Input() disabled: boolean;
@ -104,6 +112,12 @@ export class KeyFilterListComponent implements ControlValueAccessor, OnInit {
} }
} }
validate(): ValidationErrors | null {
return this.keyFilterListFormGroup.valid && this.keyFiltersControl.valid ? null : {
keyFilterList: {valid: false}
};
}
writeValue(keyFilters: Array<KeyFilterInfo>): void { writeValue(keyFilters: Array<KeyFilterInfo>): void {
if (this.valueChangeSubscription) { if (this.valueChangeSubscription) {
this.valueChangeSubscription.unsubscribe(); this.valueChangeSubscription.unsubscribe();

33
ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.ts

@ -15,7 +15,16 @@
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { import {
EntityKeyValueType, EntityKeyValueType,
FilterPredicateType, FilterPredicateType,
@ -33,10 +42,15 @@ import {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumericFilterPredicateComponent), useExisting: forwardRef(() => NumericFilterPredicateComponent),
multi: true multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => NumericFilterPredicateComponent),
multi: true
} }
] ]
}) })
export class NumericFilterPredicateComponent implements ControlValueAccessor, OnInit { export class NumericFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean; @Input() disabled: boolean;
@ -76,7 +90,7 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
} }
setDisabledState?(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) { if (this.disabled) {
this.numericFilterPredicateFormGroup.disable({emitEvent: false}); this.numericFilterPredicateFormGroup.disable({emitEvent: false});
@ -85,17 +99,20 @@ export class NumericFilterPredicateComponent implements ControlValueAccessor, On
} }
} }
validate(): ValidationErrors | null {
return this.numericFilterPredicateFormGroup.valid ? null : {
numericFilterPredicate: {valid: false}
};
}
writeValue(predicate: NumericFilterPredicate): void { writeValue(predicate: NumericFilterPredicate): void {
this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false}); this.numericFilterPredicateFormGroup.get('operation').patchValue(predicate.operation, {emitEvent: false});
this.numericFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false}); this.numericFilterPredicateFormGroup.get('value').patchValue(predicate.value, {emitEvent: false});
} }
private updateModel() { private updateModel() {
let predicate: NumericFilterPredicate = null; const predicate: NumericFilterPredicate = this.numericFilterPredicateFormGroup.getRawValue();
if (this.numericFilterPredicateFormGroup.valid) { predicate.type = FilterPredicateType.NUMERIC;
predicate = this.numericFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.NUMERIC;
}
this.propagateChange(predicate); this.propagateChange(predicate);
} }

31
ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.ts

@ -15,7 +15,16 @@
/// ///
import { Component, forwardRef, Input, OnInit } from '@angular/core'; import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import {
ControlValueAccessor,
FormBuilder,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
Validators
} from '@angular/forms';
import { import {
EntityKeyValueType, EntityKeyValueType,
FilterPredicateType, FilterPredicateType,
@ -33,10 +42,15 @@ import {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => StringFilterPredicateComponent), useExisting: forwardRef(() => StringFilterPredicateComponent),
multi: true multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => StringFilterPredicateComponent),
multi: true
} }
] ]
}) })
export class StringFilterPredicateComponent implements ControlValueAccessor, OnInit { export class StringFilterPredicateComponent implements ControlValueAccessor, Validator, OnInit {
@Input() disabled: boolean; @Input() disabled: boolean;
@ -90,12 +104,15 @@ export class StringFilterPredicateComponent implements ControlValueAccessor, OnI
this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false}); this.stringFilterPredicateFormGroup.get('ignoreCase').patchValue(predicate.ignoreCase, {emitEvent: false});
} }
validate(c): ValidationErrors {
return this.stringFilterPredicateFormGroup.valid ? null : {
stringFilterPredicate: {valid: false}
};
}
private updateModel() { private updateModel() {
let predicate: StringFilterPredicate = null; const predicate: StringFilterPredicate = this.stringFilterPredicateFormGroup.getRawValue();
if (this.stringFilterPredicateFormGroup.valid) { predicate.type = FilterPredicateType.STRING;
predicate = this.stringFilterPredicateFormGroup.getRawValue();
predicate.type = FilterPredicateType.STRING;
}
this.propagateChange(predicate); this.propagateChange(predicate);
} }

69
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.html

@ -0,0 +1,69 @@
<!--
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.
-->
<div class="tb-json-input" tb-toast toastTarget="{{ toastTargetId }}">
<form *ngIf="attributeUpdateFormGroup"
fxLayout="column"
class="tb-json-input__form"
[formGroup]="attributeUpdateFormGroup"
(ngSubmit)="save()">
<div fxLayout="column" fxLayoutGap="10px" fxFlex *ngIf="entityDetected && isValidParameter && dataKeyDetected">
<fieldset fxFlex>
<tb-json-object-edit
[editorStyle]="{minHeight: '100px'}"
fillHeight="true"
[required]="settings.attributeRequired"
label="{{ settings.showLabel ? labelValue : '' }}"
formControlName="currentValue"
(focusin)="isFocused = true;"
(focusout)="isFocused = false;"
></tb-json-object-edit>
</fieldset>
<div class="tb-json-input-form__actions" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="20px">
<button mat-button color="primary"
type="button"
[disabled]="!attributeUpdateFormGroup.dirty"
(click)="discard()"
matTooltip="{{ 'widgets.input-widgets.discard-changes' | translate }}"
matTooltipPosition="above">
{{ "action.undo" | translate }}
</button>
<button mat-button mat-raised-button color="primary"
type="submit"
[disabled]="attributeUpdateFormGroup.invalid || !attributeUpdateFormGroup.dirty">
{{ "action.save" | translate }}
</button>
</div>
</div>
<div fxLayout="column" fxLayoutAlign="center center" fxFlex *ngIf="!entityDetected || !dataKeyDetected || !isValidParameter">
<div class="tb-json-input__error"
*ngIf="!entityDetected">
{{ 'widgets.input-widgets.no-entity-selected' | translate }}
</div>
<div class="tb-json-input__error"
*ngIf="entityDetected && !dataKeyDetected">
{{ 'widgets.input-widgets.no-datakey-selected' | translate }}
</div>
<div class="tb-json-input__error"
*ngIf="dataKeyDetected && !isValidParameter">
{{ errorMessage | translate }}
</div>
</div>
</form>
</div>

36
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.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.
*/
.tb-json-input {
width: 100%;
height: 100%;
padding: 5px;
&__form {
overflow: auto;
height: 100%;
}
&__error {
text-align: center;
font-size: 18px;
color: #a0a0a0;
}
}
.tb-toast {
font-size: 14px!important;
}

227
ui-ngx/src/app/modules/home/components/widget/lib/json-input-widget.component.ts

@ -0,0 +1,227 @@
///
/// 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, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { Datasource, DatasourceData, DatasourceType, WidgetConfig } from '@shared/models/widget.models';
import { IWidgetSubscription } from '@core/api/widget-api.models';
import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { AttributeService } from '@core/http/attribute.service';
import { AttributeData, AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
import { createLabelFromDatasource } from '@core/utils';
import { Observable } from 'rxjs';
enum JsonInputWidgetMode {
ATTRIBUTE = 'ATTRIBUTE',
TIME_SERIES = 'TIME_SERIES',
}
interface JsonInputWidgetSettings {
widgetTitle: string;
widgetMode: JsonInputWidgetMode;
attributeScope?: AttributeScope;
showLabel: boolean;
labelValue?: string;
attributeRequired: boolean;
showResultMessage: boolean;
}
@Component({
selector: 'tb-json-input-widget ',
templateUrl: './json-input-widget.component.html',
styleUrls: ['./json-input-widget.component.scss']
})
export class JsonInputWidgetComponent extends PageComponent implements OnInit {
@Input()
ctx: WidgetContext;
public settings: JsonInputWidgetSettings;
private widgetConfig: WidgetConfig;
private subscription: IWidgetSubscription;
private datasource: Datasource;
labelValue: string;
entityDetected = false;
dataKeyDetected = false;
isValidParameter = false;
errorMessage: string;
isFocused: boolean;
originalValue: any;
attributeUpdateFormGroup: FormGroup;
toastTargetId = 'json-input-widget' + this.utils.guid();
constructor(protected store: Store<AppState>,
private utils: UtilsService,
private fb: FormBuilder,
private attributeService: AttributeService,
private translate: TranslateService) {
super(store);
}
ngOnInit(): void {
this.ctx.$scope.jsonInputWidget = this;
this.settings = this.ctx.settings;
this.widgetConfig = this.ctx.widgetConfig;
this.subscription = this.ctx.defaultSubscription;
this.datasource = this.subscription.datasources[0];
this.initializeConfig();
this.validateDatasources();
this.buildForm();
this.ctx.updateWidgetParams();
}
private initializeConfig() {
if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
const title = createLabelFromDatasource(this.datasource, this.settings.widgetTitle);
this.ctx.widgetTitle = this.utils.customTranslation(title, title);
} else {
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
}
if (this.settings.labelValue && this.settings.labelValue.length) {
const label = createLabelFromDatasource(this.datasource, this.settings.labelValue);
this.labelValue = this.utils.customTranslation(label, label);
} else {
this.labelValue = this.translate.instant('widgets.input-widgets.value');
}
}
private validateDatasources() {
if (this.datasource?.type === DatasourceType.entity) {
this.entityDetected = true;
if (this.datasource.dataKeys.length) {
this.dataKeyDetected = true;
if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) {
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
if (this.settings.attributeScope === AttributeScope.SERVER_SCOPE || this.datasource.entityType === EntityType.DEVICE) {
this.isValidParameter = true;
} else {
this.errorMessage = 'widgets.input-widgets.not-allowed-entity';
}
} else {
this.errorMessage = 'widgets.input-widgets.no-attribute-selected';
}
} else {
if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
this.isValidParameter = true;
} else {
this.errorMessage = 'widgets.input-widgets.no-timeseries-selected';
}
}
}
}
}
private buildForm() {
const validators: ValidatorFn[] = [];
if (this.settings.attributeRequired) {
validators.push(Validators.required);
}
this.attributeUpdateFormGroup = this.fb.group({
currentValue: [{}, validators]
});
this.attributeUpdateFormGroup.valueChanges.subscribe( () => {
this.ctx.detectChanges();
});
}
private updateWidgetData(data: Array<DatasourceData>) {
if (this.isValidParameter) {
let value = {};
if (data[0].data[0][1] !== '') {
try {
value = JSON.parse(data[0].data[0][1]);
} catch (e) {
value = data[0].data[0][1];
}
}
this.originalValue = value;
if (!this.isFocused) {
this.attributeUpdateFormGroup.get('currentValue').patchValue(this.originalValue);
this.ctx.detectChanges();
}
}
}
public onDataUpdated() {
this.updateWidgetData(this.subscription.data);
}
public save() {
this.isFocused = false;
const attributeToSave: AttributeData = {
key: this.datasource.dataKeys[0].name,
value: this.attributeUpdateFormGroup.get('currentValue').value
};
const entityId: EntityId = {
entityType: this.datasource.entityType,
id: this.datasource.entityId
};
let saveAttributeObservable: Observable<any>;
if (this.settings.widgetMode === JsonInputWidgetMode.ATTRIBUTE) {
saveAttributeObservable = this.attributeService.saveEntityAttributes(
entityId,
this.settings.attributeScope,
[ attributeToSave ],
{}
);
} else {
saveAttributeObservable = this.attributeService.saveEntityTimeseries(
entityId,
LatestTelemetry.LATEST_TELEMETRY,
[ attributeToSave ],
{}
);
}
saveAttributeObservable.subscribe(
() => {
this.attributeUpdateFormGroup.markAsPristine();
this.ctx.detectChanges();
if (this.settings.showResultMessage) {
this.ctx.showSuccessToast(this.translate.instant('widgets.input-widgets.update-successful'),
1000, 'bottom', 'left', this.toastTargetId);
}
},
() => {
if (this.settings.showResultMessage) {
this.ctx.showErrorToast(this.translate.instant('widgets.input-widgets.update-failed'),
'bottom', 'left', this.toastTargetId);
}
});
}
public discard() {
this.attributeUpdateFormGroup.reset({currentValue: this.originalValue}, {emitEvent: false});
this.attributeUpdateFormGroup.markAsPristine();
this.isFocused = false;
}
}

25
ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts

@ -19,7 +19,8 @@ import {
createLabelFromDatasource, createLabelFromDatasource,
hashCode, hashCode,
isDefined, isDefined,
isDefinedAndNotNull, isFunction, isDefinedAndNotNull,
isFunction,
isNumber, isNumber,
isUndefined, isUndefined,
padValue padValue
@ -30,7 +31,7 @@ import { Datasource, DatasourceData } from '@shared/models/widget.models';
import _ from 'lodash'; import _ from 'lodash';
import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes'; import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes';
import { addCondition, mergeSchemes } from '@core/schema-utils'; import { addCondition, mergeSchemes } from '@core/schema-utils';
import L, {Projection} from "leaflet"; import L from 'leaflet';
export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) { export function getProviderSchema(mapProvider: MapProviders, ignoreImageMap = false) {
const providerSchema = _.cloneDeep(mapProviderSchema); const providerSchema = _.cloneDeep(mapProviderSchema);
@ -318,22 +319,24 @@ export const parseWithTranslation = {
}; };
export function parseData(input: DatasourceData[]): FormattedData[] { export function parseData(input: DatasourceData[]): FormattedData[] {
return _(input).groupBy(el => el?.datasource?.entityName) return _(input).groupBy(el => el?.datasource.entityId + el?.datasource.entityType)
.values().value().map((entityArray, i) => { .values().value().map((entityArray, i) => {
const obj: FormattedData = { const obj: FormattedData = {
entityName: entityArray[0]?.datasource?.entityName, entityName: entityArray[0]?.datasource?.entityName,
entityId: entityArray[0]?.datasource?.entityId, entityId: entityArray[0].datasource.entityId,
entityType: entityArray[0]?.datasource?.entityType, entityType: entityArray[0].datasource.entityType,
$datasource: entityArray[0]?.datasource, $datasource: entityArray[0].datasource,
dsIndex: i, dsIndex: i,
deviceType: null deviceType: null
}; };
entityArray.filter(el => el.data.length).forEach(el => { entityArray.filter(el => el.data.length).forEach(el => {
const indexDate = el?.data?.length ? el.data.length - 1 : 0; const indexDate = el.data.length ? el.data.length - 1 : 0;
obj[el?.dataKey?.label] = el?.data[indexDate][1]; if (!obj.hasOwnProperty(el.dataKey.label) || el.data[indexDate][1] !== '') {
obj[el?.dataKey?.label + '|ts'] = el?.data[indexDate][0]; obj[el.dataKey.label] = el.data[indexDate][1];
if (el?.dataKey?.label === 'type') { obj[el.dataKey.label + '|ts'] = el.data[indexDate][0];
obj.deviceType = el?.data[indexDate][1]; if (el.dataKey.label === 'type') {
obj.deviceType = el.data[indexDate][1];
}
} }
}); });
return obj; return obj;

15
ui-ngx/src/app/modules/home/components/widget/lib/rpc/switch.component.ts

@ -121,7 +121,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
this.switchResize$ = new ResizeObserver(() => { this.switchResize$ = new ResizeObserver(() => {
this.resize(); this.resize();
}) });
this.switchResize$.observe(this.switchContainerRef.nativeElement); this.switchResize$.observe(this.switchContainerRef.nativeElement);
this.init(); this.init();
} }
@ -202,13 +202,13 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
let width = this.switchContainer.width(); let width = this.switchContainer.width();
let height = this.switchContainer.height(); let height = this.switchContainer.height();
if (this.showOnOffLabels) { if (this.showOnOffLabels) {
height = height*2/3; height = height * 2 / 3;
} }
const ratio = width/height; const ratio = width / height;
if (ratio > switchAspectRation) { if (ratio > switchAspectRation) {
width = height*switchAspectRation; width = height * switchAspectRation;
} else { } else {
height = width/switchAspectRation; height = width / switchAspectRation;
} }
this.switchElement.css({width, height}); this.switchElement.css({width, height});
this.matSlideToggle.css({width, height, minWidth: width}); this.matSlideToggle.css({width, height, minWidth: width});
@ -232,11 +232,11 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
fontSize--; fontSize--;
textWidth = this.measureTextWidth(text, fontSize); textWidth = this.measureTextWidth(text, fontSize);
} }
element.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); element.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'});
} }
private measureTextWidth(text: string, fontSize: number): number { private measureTextWidth(text: string, fontSize: number): number {
this.textMeasure.css({fontSize: fontSize+'px', lineHeight: fontSize+'px'}); this.textMeasure.css({fontSize: fontSize + 'px', lineHeight: fontSize + 'px'});
this.textMeasure.text(text); this.textMeasure.text(text);
return this.textMeasure.width(); return this.textMeasure.width();
} }
@ -260,6 +260,7 @@ export class SwitchComponent extends PageComponent implements OnInit, OnDestroy
this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe( this.ctx.controlApi.sendTwoWayCommand(this.getValueMethod, null, this.requestTimeout).subscribe(
(responseBody) => { (responseBody) => {
this.setValue(this.parseValueFunction(responseBody)); this.setValue(this.parseValueFunction(responseBody));
this.ctx.detectChanges();
}, },
() => { () => {
const errorText = this.ctx.defaultSubscription.rpcErrorText; const errorText = this.ctx.defaultSubscription.rpcErrorText;

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

@ -38,6 +38,7 @@ import { ImportExportService } from '@home/components/import-export/import-expor
import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component'; import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component'; import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component'; import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-overview-widget.component';
import { JsonInputWidgetComponent } from '@home/components/widget/lib/json-input-widget.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -51,6 +52,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
EdgesOverviewWidgetComponent, EdgesOverviewWidgetComponent,
DateRangeNavigatorWidgetComponent, DateRangeNavigatorWidgetComponent,
DateRangeNavigatorPanelComponent, DateRangeNavigatorPanelComponent,
JsonInputWidgetComponent,
MultipleInputWidgetComponent, MultipleInputWidgetComponent,
TripAnimationComponent, TripAnimationComponent,
PhotoCameraInputWidgetComponent, PhotoCameraInputWidgetComponent,
@ -72,6 +74,7 @@ import { EdgesOverviewWidgetComponent } from '@home/components/widget/lib/edges-
EdgesOverviewWidgetComponent, EdgesOverviewWidgetComponent,
RpcWidgetsModule, RpcWidgetsModule,
DateRangeNavigatorWidgetComponent, DateRangeNavigatorWidgetComponent,
JsonInputWidgetComponent,
MultipleInputWidgetComponent, MultipleInputWidgetComponent,
TripAnimationComponent, TripAnimationComponent,
PhotoCameraInputWidgetComponent, PhotoCameraInputWidgetComponent,

4
ui-ngx/src/app/modules/home/pages/asset/assets-table-config.resolver.ts

@ -104,7 +104,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
)); ));
}; };
this.config.onEntityAction = action => this.onAssetAction(action); this.config.onEntityAction = action => this.onAssetAction(action);
this.config.detailsReadonly = () => this.config.componentsData.assetScope === 'customer_user'; this.config.detailsReadonly = () => (this.config.componentsData.assetScope === 'customer_user' || this.config.componentsData.assetScope === 'edge_customer_user');
this.config.headerComponent = AssetTableHeaderComponent; this.config.headerComponent = AssetTableHeaderComponent;
@ -151,7 +151,7 @@ export class AssetsTableConfigResolver implements Resolve<EntityTableConfig<Asse
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.assetScope); this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.assetScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.assetScope); this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.assetScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.assetScope); this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.assetScope);
this.config.addEnabled = this.config.componentsData.assetScope !== 'customer_user'; this.config.addEnabled = !(this.config.componentsData.assetScope === 'customer_user' || this.config.componentsData.assetScope === 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.assetScope === 'tenant'; this.config.entitiesDeleteEnabled = this.config.componentsData.assetScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.assetScope === 'tenant'; this.config.deleteEnabled = () => this.config.componentsData.assetScope === 'tenant';
return this.config; return this.config;

4
ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts

@ -103,7 +103,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
return this.dashboardService.saveDashboard(dashboard as Dashboard); return this.dashboardService.saveDashboard(dashboard as Dashboard);
}; };
this.config.onEntityAction = action => this.onDashboardAction(action); this.config.onEntityAction = action => this.onDashboardAction(action);
this.config.detailsReadonly = () => this.config.componentsData.dashboardScope === 'customer_user'; this.config.detailsReadonly = () => (this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user');
} }
resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DashboardInfo | Dashboard>> { resolve(route: ActivatedRouteSnapshot): Observable<EntityTableConfig<DashboardInfo | Dashboard>> {
@ -147,7 +147,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig<
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.dashboardScope); this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.dashboardScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.dashboardScope); this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.dashboardScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.dashboardScope); this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.dashboardScope);
this.config.addEnabled = this.config.componentsData.dashboardScope !== 'customer_user'; this.config.addEnabled = !(this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.dashboardScope === 'tenant'; this.config.entitiesDeleteEnabled = this.config.componentsData.dashboardScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.dashboardScope === 'tenant'; this.config.deleteEnabled = () => this.config.componentsData.dashboardScope === 'tenant';
return this.config; return this.config;

4
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -112,7 +112,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
)); ));
}; };
this.config.onEntityAction = action => this.onDeviceAction(action); this.config.onEntityAction = action => this.onDeviceAction(action);
this.config.detailsReadonly = () => this.config.componentsData.deviceScope === 'customer_user'; this.config.detailsReadonly = () => (this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.headerComponent = DeviceTableHeaderComponent; this.config.headerComponent = DeviceTableHeaderComponent;
@ -161,7 +161,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope); this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.deviceScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope); this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.deviceScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope); this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.deviceScope);
this.config.addEnabled = this.config.componentsData.deviceScope !== 'customer_user'; this.config.addEnabled = !(this.config.componentsData.deviceScope === 'customer_user' || this.config.componentsData.deviceScope === 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant'; this.config.entitiesDeleteEnabled = this.config.componentsData.deviceScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant'; this.config.deleteEnabled = () => this.config.componentsData.deviceScope === 'tenant';
return this.config; return this.config;

59
ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts

@ -58,21 +58,6 @@ const routes: Routes = [
entitiesTableConfig: EdgesTableConfigResolver entitiesTableConfig: EdgesTableConfigResolver
} }
}, },
{
path: ':edgeId/ruleChains',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
ruleChainsType: 'edge',
breadcrumb: {
label: 'edge.edge-rulechains',
icon: 'settings_ethernet'
},
},
resolve: {
entitiesTableConfig: RuleChainsTableConfigResolver
}
},
{ {
path: ':edgeId/assets', path: ':edgeId/assets',
component: EntitiesTableComponent, component: EntitiesTableComponent,
@ -156,6 +141,50 @@ const routes: Routes = [
} }
] ]
}, },
{
path: ':edgeId/ruleChains',
data: {
breadcrumb: {
label: 'edge.edge-rulechains',
icon: 'settings_ethernet'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.TENANT_ADMIN],
title: 'edge.rulechains',
ruleChainsType: 'edge'
},
resolve: {
entitiesTableConfig: RuleChainsTableConfigResolver
}
},
{
path: ':ruleChainId',
component: RuleChainPageComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
breadcrumb: {
labelFunction: ruleChainBreadcumbLabelFunction,
icon: 'settings_ethernet'
} as BreadCrumbConfig<RuleChainPageComponent>,
auth: [Authority.TENANT_ADMIN],
title: 'rulechain.edge-rulechain',
import: false,
ruleChainType: RuleChainType.EDGE
},
resolve: {
ruleChain: RuleChainResolver,
ruleChainMetaData: ResolvedRuleChainMetaDataResolver,
ruleNodeComponents: RuleNodeComponentsResolver,
tooltipster: TooltipsterResolver
}
}
]
},
{ {
path: 'ruleChains', path: 'ruleChains',
data: { data: {

24
ui-ngx/src/app/modules/home/pages/edge/edge.component.html

@ -44,31 +44,31 @@
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeAssets')" (click)="onEntityAction($event, 'openEdgeAssets')"
[fxShow]="!isEdit"> [fxShow]="!isEdit && edgeScope !== 'customer'">
{{'edge.manage-edge-assets' | translate }} {{'edge.manage-edge-assets' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeDevices')" (click)="onEntityAction($event, 'openEdgeDevices')"
[fxShow]="!isEdit"> [fxShow]="!isEdit && edgeScope !== 'customer'">
{{'edge.manage-edge-devices' | translate }} {{'edge.manage-edge-devices' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeEntityViews')" (click)="onEntityAction($event, 'openEdgeEntityViews')"
[fxShow]="!isEdit"> [fxShow]="!isEdit && edgeScope !== 'customer'">
{{'edge.manage-edge-entity-views' | translate }} {{'edge.manage-edge-entity-views' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeDashboards')" (click)="onEntityAction($event, 'openEdgeDashboards')"
[fxShow]="!isEdit"> [fxShow]="!isEdit && edgeScope !== 'customer'">
{{'edge.manage-edge-dashboards' | translate }} {{'edge.manage-edge-dashboards' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'openEdgeRuleChains')" (click)="onEntityAction($event, 'openEdgeRuleChains')"
[fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> [fxShow]="!isEdit && edgeScope === 'tenant'">
{{'edge.manage-edge-rulechains' | translate }} {{'edge.manage-edge-rulechains' | translate }}
</button> </button>
</div> </div>
@ -85,7 +85,7 @@
ngxClipboard ngxClipboard
(cbOnSuccess)="onEdgeInfoCopied('key')" (cbOnSuccess)="onEdgeInfoCopied('key')"
[cbContent]="entity?.routingKey" [cbContent]="entity?.routingKey"
[fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> [fxShow]="!isEdit && edgeScope !== 'customer_user'">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>edge.copy-edge-key</span> <span translate>edge.copy-edge-key</span>
</button> </button>
@ -93,7 +93,7 @@
ngxClipboard ngxClipboard
(cbOnSuccess)="onEdgeInfoCopied('secret')" (cbOnSuccess)="onEdgeInfoCopied('secret')"
[cbContent]="entity?.secret" [cbContent]="entity?.secret"
[fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> [fxShow]="!isEdit && edgeScope !== 'customer_user'">
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
<span translate>edge.copy-edge-secret</span> <span translate>edge.copy-edge-secret</span>
</button> </button>
@ -101,7 +101,7 @@
ngxClipboard ngxClipboard
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'syncEdge')" (click)="onEntityAction($event, 'syncEdge')"
[fxShow]="!isEdit && (edgeScope === 'tenant' || edgeScope === 'customer')"> [fxShow]="!isEdit && edgeScope !== 'customer_user'">
<mat-icon svgIcon="mdi:sync"></mat-icon> <mat-icon svgIcon="mdi:sync"></mat-icon>
<span translate>edge.sync</span> <span translate>edge.sync</span>
</button> </button>
@ -132,7 +132,7 @@
[required]="true" [required]="true"
[entityType]="entityType.EDGE"> [entityType]="entityType.EDGE">
</tb-entity-subtype-autocomplete> </tb-entity-subtype-autocomplete>
<div [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')"> <div [fxShow]="edgeScope !== 'customer_user'">
<div class="tb-hint" [innerHTML]="'edge.edge-license-key-hint' | translate"></div> <div class="tb-hint" [innerHTML]="'edge.edge-license-key-hint' | translate"></div>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>edge.edge-license-key</mat-label> <mat-label translate>edge.edge-license-key</mat-label>
@ -142,7 +142,7 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<div [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')"> <div [fxShow]="edgeScope !== 'customer_user'">
<div translate class="tb-hint">edge.cloud-endpoint-hint</div> <div translate class="tb-hint">edge.cloud-endpoint-hint</div>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>edge.cloud-endpoint</mat-label> <mat-label translate>edge.cloud-endpoint</mat-label>
@ -153,7 +153,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
</fieldset> </fieldset>
<div fxLayout="row" [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')"> <div fxLayout="row" [fxShow]="edgeScope !== 'customer_user'">
<mat-form-field class="mat-block" fxFlex> <mat-form-field class="mat-block" fxFlex>
<mat-label translate>edge.edge-key</mat-label> <mat-label translate>edge.edge-key</mat-label>
<input matInput formControlName="routingKey"> <input matInput formControlName="routingKey">
@ -164,7 +164,7 @@
<mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button> </button>
</div> </div>
<div fxLayout="row" [fxShow]="(edgeScope === 'tenant' || edgeScope === 'customer')"> <div fxLayout="row" [fxShow]="edgeScope !== 'customer_user'">
<mat-form-field class="mat-block" fxFlex> <mat-form-field class="mat-block" fxFlex>
<mat-label translate>edge.edge-secret</mat-label> <mat-label translate>edge.edge-secret</mat-label>
<input matInput formControlName="secret"> <input matInput formControlName="secret">

4
ui-ngx/src/app/modules/home/pages/edge/edges-table-config.resolver.ts

@ -477,8 +477,8 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI
$event.stopPropagation(); $event.stopPropagation();
} }
this.dialogService.confirm( this.dialogService.confirm(
this.translate.instant('edge.unassign-edge-title', {count: edges.length}), this.translate.instant('edge.unassign-edges-title', {count: edges.length}),
this.translate.instant('edge.unassign-edge-text'), this.translate.instant('edge.unassign-edges-text'),
this.translate.instant('action.no'), this.translate.instant('action.no'),
this.translate.instant('action.yes'), this.translate.instant('action.yes'),
true true

4
ui-ngx/src/app/modules/home/pages/entity-view/entity-views-table-config.resolver.ts

@ -104,7 +104,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
)); ));
}; };
this.config.onEntityAction = action => this.onEntityViewAction(action); this.config.onEntityAction = action => this.onEntityViewAction(action);
this.config.detailsReadonly = () => this.config.componentsData.entityViewScope === 'customer_user'; this.config.detailsReadonly = () => (this.config.componentsData.entityViewScope === 'customer_user' || this.config.componentsData.entityViewScope === 'edge_customer_user');
this.config.headerComponent = EntityViewTableHeaderComponent; this.config.headerComponent = EntityViewTableHeaderComponent;
@ -152,7 +152,7 @@ export class EntityViewsTableConfigResolver implements Resolve<EntityTableConfig
this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.entityViewScope); this.config.cellActionDescriptors = this.configureCellActions(this.config.componentsData.entityViewScope);
this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.entityViewScope); this.config.groupActionDescriptors = this.configureGroupActions(this.config.componentsData.entityViewScope);
this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.entityViewScope); this.config.addActionDescriptors = this.configureAddActions(this.config.componentsData.entityViewScope);
this.config.addEnabled = this.config.componentsData.entityViewScope !== 'customer_user'; this.config.addEnabled = !(this.config.componentsData.entityViewScope === 'customer_user' || this.config.componentsData.entityViewScope !== 'edge_customer_user');
this.config.entitiesDeleteEnabled = this.config.componentsData.entityViewScope === 'tenant'; this.config.entitiesDeleteEnabled = this.config.componentsData.entityViewScope === 'tenant';
this.config.deleteEnabled = () => this.config.componentsData.entityViewScope === 'tenant'; this.config.deleteEnabled = () => this.config.componentsData.entityViewScope === 'tenant';
return this.config; return this.config;

4
ui-ngx/src/app/modules/home/pages/profile/profile.component.html

@ -57,13 +57,13 @@
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>language.language</mat-label> <mat-label translate>language.language</mat-label>
<mat-select matInput formControlName="language"> <mat-select formControlName="language">
<mat-option *ngFor="let lang of languageList" [value]="lang"> <mat-option *ngFor="let lang of languageList" [value]="lang">
{{ lang ? ('language.locales.' + lang | translate) : ''}} {{ lang ? ('language.locales.' + lang | translate) : ''}}
</mat-option> </mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row"> <section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row" *ngIf="!isSysAdmin()">
<tb-dashboard-autocomplete <tb-dashboard-autocomplete
fxFlex fxFlex
placeholder="{{ 'dashboard.home-dashboard' | translate }}" placeholder="{{ 'dashboard.home-dashboard' | translate }}"

9
ui-ngx/src/app/modules/home/pages/profile/profile.component.ts

@ -16,7 +16,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { UserService } from '@core/http/user.service'; import { UserService } from '@core/http/user.service';
import { User } from '@shared/models/user.model'; import { AuthUser, User } from '@shared/models/user.model';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -33,6 +33,7 @@ import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service'; import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
@Component({ @Component({
selector: 'tb-profile', selector: 'tb-profile',
@ -45,6 +46,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
profile: FormGroup; profile: FormGroup;
user: User; user: User;
languageList = env.supportedLangs; languageList = env.supportedLangs;
private readonly authUser: AuthUser;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -55,6 +57,7 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
public dialogService: DialogService, public dialogService: DialogService,
public fb: FormBuilder) { public fb: FormBuilder) {
super(store); super(store);
this.authUser = getCurrentAuthUser(this.store);
} }
ngOnInit() { ngOnInit() {
@ -134,4 +137,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
return this.profile; return this.profile;
} }
isSysAdmin(): boolean {
return this.authUser.authority === Authority.SYS_ADMIN;
}
} }

53
ui-ngx/src/app/modules/home/pages/resource/resources-library-table-config.resolve.ts

@ -24,7 +24,6 @@ import {
import { Resolve } from '@angular/router'; import { Resolve } from '@angular/router';
import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models'; import { Resource, ResourceInfo, ResourceTypeTranslationMap } from '@shared/models/resource.models';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { Direction } from '@shared/models/page/sort-order';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -34,13 +33,14 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { ResourcesLibraryComponent } from '@home/pages/resource/resources-library.component'; import { ResourcesLibraryComponent } from '@home/pages/resource/resources-library.component';
import { Observable } from 'rxjs/internal/Observable'; import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { EntityAction } from '@home/models/entity/entity-component.models';
import { map } from 'rxjs/operators';
@Injectable() @Injectable()
export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource>> { export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource, PageLink, ResourceInfo>> {
private readonly config: EntityTableConfig<Resource> = new EntityTableConfig<Resource>(); private readonly config: EntityTableConfig<Resource, PageLink, ResourceInfo> = new EntityTableConfig<Resource, PageLink, ResourceInfo>();
private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; private readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
@ -52,17 +52,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.entityComponent = ResourcesLibraryComponent; this.config.entityComponent = ResourcesLibraryComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE); this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
this.config.defaultSortOrder = {property: 'title', direction: Direction.ASC};
this.config.entityTitle = (resource) => resource ? this.config.entityTitle = (resource) => resource ?
resource.title : ''; resource.title : '';
this.config.columns.push( this.config.columns.push(
new DateEntityTableColumn<ResourceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'), new DateEntityTableColumn<ResourceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<ResourceInfo>('title', 'widgets-bundle.title', '60%'), new EntityTableColumn<ResourceInfo>('title', 'resource.title', '60%'),
new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%', new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%',
entity => this.resourceTypesTranslationMap.get(entity.resourceType)), entity => this.resourceTypesTranslationMap.get(entity.resourceType)),
new EntityTableColumn<ResourceInfo>('tenantId', 'widgets-bundle.system', '60px', new EntityTableColumn<ResourceInfo>('tenantId', 'resource.system', '60px',
entity => { entity => {
return checkBoxCell(entity.tenantId.id === NULL_UUID); return checkBoxCell(entity.tenantId.id === NULL_UUID);
}), }),
@ -83,13 +82,34 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count}); this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text'); this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink) as Observable<PageData<Resource>>; this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink);
this.config.loadEntity = id => this.resourceService.getResource(id.id); this.config.loadEntity = id => this.resourceService.getResource(id.id);
this.config.saveEntity = resource => this.resourceService.saveResource(resource); this.config.saveEntity = resource => this.saveResource(resource);
this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
this.config.onEntityAction = action => this.onResourceAction(action);
}
saveResource(resource) {
if (Array.isArray(resource.data)) {
const resources = [];
resource.data.forEach((data, index) => {
resources.push({
resourceType: resource.resourceType,
data,
fileName: resource.fileName[index],
title: resource.title
});
});
return this.resourceService.saveResources(resources, {resendRequest: true}).pipe(
map((response) => response[0])
);
} else {
return this.resourceService.saveResource(resource);
}
} }
resolve(): EntityTableConfig<Resource> { resolve(): EntityTableConfig<Resource, PageLink, ResourceInfo> {
this.config.tableTitle = this.translate.instant('resource.resources-library'); this.config.tableTitle = this.translate.instant('resource.resources-library');
const authUser = getCurrentAuthUser(this.store); const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority); this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
@ -105,7 +125,16 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.resourceService.downloadResource(resource.id.id).subscribe(); this.resourceService.downloadResource(resource.id.id).subscribe();
} }
private isResourceEditable(resource: Resource, authority: Authority): boolean { onResourceAction(action: EntityAction<ResourceInfo>): boolean {
switch (action.action) {
case 'uploadResource':
this.exportResource(action.event, action.entity);
return true;
}
return false;
}
private isResourceEditable(resource: ResourceInfo, authority: Authority): boolean {
if (authority === Authority.TENANT_ADMIN) { if (authority === Authority.TENANT_ADMIN) {
return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID; return resource && resource.tenantId && resource.tenantId.id !== NULL_UUID;
} else { } else {

10
ui-ngx/src/app/modules/home/pages/resource/resources-library.component.html

@ -16,6 +16,12 @@
--> -->
<div class="tb-details-buttons" fxLayout.xs="column"> <div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'uploadResource')"
[fxShow]="!isEdit">
{{'resource.export' | translate }}
</button>
<button mat-raised-button color="primary" fxFlex.xs <button mat-raised-button color="primary" fxFlex.xs
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (click)="onEntityAction($event, 'delete')"
@ -44,9 +50,11 @@
<tb-file-input <tb-file-input
formControlName="data" formControlName="data"
required required
[convertToBase64]="true" [readAsBinary]="true"
[allowedExtensions]="getAllowedExtensions()" [allowedExtensions]="getAllowedExtensions()"
[contentConvertFunction]="convertToBase64File"
[accept]="getAcceptType()" [accept]="getAcceptType()"
[multipleFile]="entityForm.get('resourceType').value === resourceType.LWM2M_MODEL"
dropLabel="{{'resource.drop-file' | translate}}" dropLabel="{{'resource.drop-file' | translate}}"
[existingFileName]="entityForm.get('fileName')?.value" [existingFileName]="entityForm.get('fileName')?.value"
(fileNameChanged)="entityForm?.get('fileName').patchValue($event)"> (fileNameChanged)="entityForm?.get('fileName').patchValue($event)">

23
ui-ngx/src/app/modules/home/pages/resource/resources-library.component.ts

@ -29,7 +29,7 @@ import {
ResourceTypeMIMETypes, ResourceTypeMIMETypes,
ResourceTypeTranslationMap ResourceTypeTranslationMap
} from '@shared/models/resource.models'; } from '@shared/models/resource.models';
import { distinctUntilChanged, takeUntil } from 'rxjs/operators'; import { pairwise, startWith, takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'tb-resources-library', selector: 'tb-resources-library',
@ -54,15 +54,22 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
this.entityForm.get('resourceType').valueChanges.pipe( this.entityForm.get('resourceType').valueChanges.pipe(
distinctUntilChanged((oldValue, newValue) => [oldValue, newValue].includes(this.resourceType.LWM2M_MODEL)), startWith(ResourceType.LWM2M_MODEL),
pairwise(),
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe((type) => { ).subscribe(([previousType, type]) => {
if (previousType === this.resourceType.LWM2M_MODEL) {
this.entityForm.get('title').setValidators(Validators.required);
this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
}
if (type === this.resourceType.LWM2M_MODEL) { if (type === this.resourceType.LWM2M_MODEL) {
this.entityForm.get('title').clearValidators(); this.entityForm.get('title').clearValidators();
} else { this.entityForm.get('title').updateValueAndValidity({emitEvent: false});
this.entityForm.get('title').setValidators(Validators.required);
} }
this.entityForm.get('title').updateValueAndValidity({emitEvent: false}); this.entityForm.patchValue({
data: null,
fileName: null
}, {emitEvent: false});
}); });
} }
@ -119,4 +126,8 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
return '*/*'; return '*/*';
} }
} }
convertToBase64File(data: string): string {
return window.btoa(data);
}
} }

14
ui-ngx/src/app/modules/home/pages/rulechain/rulechain.component.html

@ -19,19 +19,19 @@
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')" (click)="onEntityAction($event, 'open')"
[fxShow]="!isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')"> [fxShow]="!isEdit">
{{'rulechain.open-rulechain' | translate }} {{'rulechain.open-rulechain' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')" (click)="onEntityAction($event, 'export')"
[fxShow]="!isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')"> [fxShow]="!isEdit">
{{'rulechain.export' | translate }} {{'rulechain.export' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setRoot')" (click)="onEntityAction($event, 'setRoot')"
[fxShow]="!isEdit && !entity?.root && ruleChainScope === 'tenant'"> [fxShow]="!isEdit && ((!entity?.root && ruleChainScope === 'tenant') || (!isEdgeRootRuleChain() && ruleChainScope === 'edge'))">
{{'rulechain.set-root' | translate }} {{'rulechain.set-root' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
@ -52,12 +52,6 @@
[fxShow]="!isEdit && isAutoAssignToEdgeRuleChain() && ruleChainScope === 'edges'"> [fxShow]="!isEdit && isAutoAssignToEdgeRuleChain() && ruleChainScope === 'edges'">
{{'rulechain.unset-auto-assign-to-edge' | translate }} {{'rulechain.unset-auto-assign-to-edge' | translate }}
</button> </button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'setRoot')"
[fxShow]="!isEdit && !isEdgeRootRuleChain() && ruleChainScope === 'edge'">
{{'rulechain.set-root' | translate }}
</button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'unassignFromEdge')" (click)="onEntityAction($event, 'unassignFromEdge')"
@ -67,7 +61,7 @@
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')" (click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit && (ruleChainScope === 'tenant' || ruleChainScope === 'edges')"> [fxShow]="!hideDelete() && !isEdit && ruleChainScope !== 'edge'">
{{'rulechain.delete' | translate }} {{'rulechain.delete' | translate }}
</button> </button>
<div fxLayout="row"> <div fxLayout="row">

82
ui-ngx/src/app/modules/home/pages/rulechain/rulechains-table-config.resolver.ts

@ -211,53 +211,51 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
configureCellActions(ruleChainScope: string): Array<CellActionDescriptor<RuleChain>> { configureCellActions(ruleChainScope: string): Array<CellActionDescriptor<RuleChain>> {
const actions: Array<CellActionDescriptor<RuleChain>> = []; const actions: Array<CellActionDescriptor<RuleChain>> = [];
if (ruleChainScope === 'tenant' || ruleChainScope === 'edges') { actions.push(
{
name: this.translate.instant('rulechain.open-rulechain'),
icon: 'settings_ethernet',
isEnabled: () => true,
onAction: ($event, entity) => this.openRuleChain($event, entity)
},
{
name: this.translate.instant('rulechain.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: ($event, entity) => this.exportRuleChain($event, entity)
}
);
if (ruleChainScope === 'tenant') {
actions.push( actions.push(
{ {
name: this.translate.instant('rulechain.open-rulechain'), name: this.translate.instant('rulechain.set-root'),
icon: 'settings_ethernet', icon: 'flag',
isEnabled: () => true, isEnabled: (entity) => this.isNonRootRuleChain(entity),
onAction: ($event, entity) => this.openRuleChain($event, entity) onAction: ($event, entity) => this.setRootRuleChain($event, entity)
}
);
}
if (ruleChainScope === 'edges') {
actions.push(
{
name: this.translate.instant('rulechain.set-edge-template-root-rulechain'),
icon: 'flag',
isEnabled: (entity) => this.isNonRootRuleChain(entity),
onAction: ($event, entity) => this.setEdgeTemplateRootRuleChain($event, entity)
}, },
{ {
name: this.translate.instant('rulechain.export'), name: this.translate.instant('rulechain.set-auto-assign-to-edge'),
icon: 'file_download', icon: 'bookmark_outline',
isEnabled: () => true, isEnabled: (entity) => this.isNotAutoAssignToEdgeRuleChain(entity),
onAction: ($event, entity) => this.exportRuleChain($event, entity) onAction: ($event, entity) => this.setAutoAssignToEdgeRuleChain($event, entity)
},
{
name: this.translate.instant('rulechain.unset-auto-assign-to-edge'),
icon: 'bookmark',
isEnabled: (entity) => this.isAutoAssignToEdgeRuleChain(entity),
onAction: ($event, entity) => this.unsetAutoAssignToEdgeRuleChain($event, entity)
} }
); );
if (ruleChainScope === 'tenant') {
actions.push(
{
name: this.translate.instant('rulechain.set-root'),
icon: 'flag',
isEnabled: (entity) => this.isNonRootRuleChain(entity),
onAction: ($event, entity) => this.setRootRuleChain($event, entity)
}
);
}
if (ruleChainScope === 'edges') {
actions.push(
{
name: this.translate.instant('rulechain.set-edge-template-root-rulechain'),
icon: 'flag',
isEnabled: (entity) => this.isNonRootRuleChain(entity),
onAction: ($event, entity) => this.setEdgeTemplateRootRuleChain($event, entity)
},
{
name: this.translate.instant('rulechain.set-auto-assign-to-edge'),
icon: 'bookmark_outline',
isEnabled: (entity) => this.isNotAutoAssignToEdgeRuleChain(entity),
onAction: ($event, entity) => this.setAutoAssignToEdgeRuleChain($event, entity)
},
{
name: this.translate.instant('rulechain.unset-auto-assign-to-edge'),
icon: 'bookmark',
isEnabled: (entity) => this.isAutoAssignToEdgeRuleChain(entity),
onAction: ($event, entity) => this.unsetAutoAssignToEdgeRuleChain($event, entity)
}
);
}
} }
if (ruleChainScope === 'edge') { if (ruleChainScope === 'edge') {
actions.push( actions.push(
@ -301,6 +299,8 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig<
} }
if (this.config.componentsData.ruleChainScope === 'edges') { if (this.config.componentsData.ruleChainScope === 'edges') {
this.router.navigateByUrl(`edges/ruleChains/${ruleChain.id.id}`); this.router.navigateByUrl(`edges/ruleChains/${ruleChain.id.id}`);
} else if (this.config.componentsData.ruleChainScope === 'edge') {
this.router.navigateByUrl(`edges/${this.config.componentsData.edgeId}/ruleChains/${ruleChain.id.id}`);
} else { } else {
this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`); this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`);
} }

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

@ -18,7 +18,7 @@
<div class="tb-container"> <div class="tb-container">
<label class="tb-title">{{ label }}</label> <label class="tb-title">{{ label }}</label>
<ng-container #flow="flow" <ng-container #flow="flow"
[flowConfig]="{singleFile: true, allowDuplicateUploads: true}"> [flowConfig]="{allowDuplicateUploads: true}">
<div class="tb-file-select-container"> <div class="tb-file-select-container">
<div class="tb-file-clear-container"> <div class="tb-file-clear-container">
<button mat-button mat-icon-button color="primary" <button mat-button mat-icon-button color="primary"
@ -34,7 +34,7 @@
flowDrop flowDrop
[flow]="flow.flowJs"> [flow]="flow.flowJs">
<label for="{{inputId}}">{{ dropLabel }}</label> <label for="{{inputId}}">{{ dropLabel }}</label>
<input class="file-input" flowButton type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}"> <input class="file-input" flowButton #flowInput type="file" [flow]="flow.flowJs" [flowAttributes]="{accept: accept}" id="{{inputId}}">
</div> </div>
</div> </div>
</ng-container> </ng-container>

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

@ -17,6 +17,7 @@
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
ElementRef,
EventEmitter, EventEmitter,
forwardRef, forwardRef,
Input, Input,
@ -102,17 +103,34 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
existingFileName: string; existingFileName: string;
@Input() @Input()
convertToBase64 = false; readAsBinary = false;
private multipleFileValue = false;
@Input()
set multipleFile(value: boolean) {
this.multipleFileValue = value;
if (this.flow?.flowJs) {
this.updateMultipleFileMode(this.multipleFile);
}
}
get multipleFile(): boolean {
return this.multipleFileValue;
}
@Output() @Output()
fileNameChanged = new EventEmitter<string>(); fileNameChanged = new EventEmitter<string|string[]>();
fileName: string; fileName: string | string[];
fileContent: any; fileContent: any;
@ViewChild('flow', {static: true}) @ViewChild('flow', {static: true})
flow: FlowDirective; flow: FlowDirective;
@ViewChild('flowInput', {static: true})
flowInput: ElementRef;
autoUploadSubscription: Subscription; autoUploadSubscription: Subscription;
private propagateChange = null; private propagateChange = null;
@ -125,34 +143,60 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
ngAfterViewInit() { ngAfterViewInit() {
this.autoUploadSubscription = this.flow.events$.subscribe(event => { this.autoUploadSubscription = this.flow.events$.subscribe(event => {
if (event.type === 'fileAdded') { if (event.type === 'filesAdded') {
const file = event.event[0] as flowjs.FlowFile; const readers = [];
if (this.filterFile(file)) { (event.event[0] as flowjs.FlowFile[]).forEach(file => {
const reader = new FileReader(); if (this.filterFile(file)) {
reader.onload = (loadEvent) => { readers.push(this.readerAsFile(file));
if (typeof reader.result === 'string') { }
const fileContent = this.convertToBase64 ? window.btoa(reader.result) : reader.result; });
if (fileContent && fileContent.length > 0) { if (readers.length) {
if (this.contentConvertFunction) { Promise.all(readers).then((filesContent) => {
this.fileContent = this.contentConvertFunction(fileContent); filesContent = filesContent.filter(content => content.fileContent != null);
} else { if (filesContent.length === 1) {
this.fileContent = fileContent; this.fileContent = filesContent[0].fileContent;
} this.fileName = filesContent[0].fileName;
if (this.fileContent) { this.updateModel();
this.fileName = file.name; } else if (filesContent.length > 1) {
} else { this.fileContent = filesContent.map(content => content.fileContent);
this.fileName = null; this.fileName = filesContent.map(content => content.fileName);
} this.updateModel();
this.updateModel(); }
} });
}
}
});
if (!this.multipleFile) {
this.updateMultipleFileMode(this.multipleFile);
}
}
private readerAsFile(file: flowjs.FlowFile): Promise<any> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = () => {
let fileName = null;
let fileContent = null;
if (typeof reader.result === 'string') {
fileContent = reader.result;
if (fileContent && fileContent.length > 0) {
if (this.contentConvertFunction) {
fileContent = this.contentConvertFunction(fileContent);
}
if (fileContent) {
fileName = file.name;
} }
};
if (this.convertToBase64) {
reader.readAsBinaryString(file.file);
} else {
reader.readAsText(file.file);
} }
} }
resolve({fileContent, fileName});
};
reader.onerror = () => {
resolve({fileContent: null, fileName: null});
};
if (this.readAsBinary) {
reader.readAsBinaryString(file.file);
} else {
reader.readAsText(file.file);
} }
}); });
} }
@ -207,4 +251,11 @@ export class FileInputComponent extends PageComponent implements AfterViewInit,
this.fileContent = null; this.fileContent = null;
this.updateModel(); this.updateModel();
} }
private updateMultipleFileMode(multiple: boolean) {
this.flow.flowJs.opts.singleFile = !multiple;
if (!multiple) {
this.flowInput.nativeElement.removeAttribute('multiple');
}
}
} }

8
ui-ngx/src/app/shared/components/json-object-edit.component.ts

@ -22,7 +22,7 @@ import { ActionNotificationHide, ActionNotificationShow } from '@core/notificati
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { guid, isUndefined } from '@core/utils'; import { guid, isUndefined, isDefinedAndNotNull, isLiteralObject } from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { getAce } from '@shared/models/ace/ace.models'; import { getAce } from '@shared/models/ace/ace.models';
@ -230,8 +230,7 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
this.contentValue = ''; this.contentValue = '';
this.objectValid = false; this.objectValid = false;
try { try {
if (isDefinedAndNotNull(this.modelValue)) {
if (this.modelValue) {
this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined : this.contentValue = JSON.stringify(this.modelValue, isUndefined(this.sort) ? undefined :
(key, objectValue) => { (key, objectValue) => {
return this.sort(key, objectValue); return this.sort(key, objectValue);
@ -260,6 +259,9 @@ export class JsonObjectEditComponent implements OnInit, ControlValueAccessor, Va
if (this.contentValue && this.contentValue.length > 0) { if (this.contentValue && this.contentValue.length > 0) {
try { try {
data = JSON.parse(this.contentValue); data = JSON.parse(this.contentValue);
if (!isLiteralObject(data)) {
throw new TypeError(`Value is not a valid JSON`);
}
this.objectValid = true; this.objectValid = true;
this.validationError = ''; this.validationError = '';
} catch (ex) { } catch (ex) {

8
ui-ngx/src/app/shared/components/time/datetime-period.component.html

@ -15,8 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<section fxLayout="column" fxLayoutAlign="start start"> <section fxLayout="column" fxLayoutAlign="start stretch">
<section fxLayout="row" fxLayout.xs="row wrap" fxLayoutAlign="start start" fxLayoutGap="16px"> <section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"
fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0">
<mat-form-field> <mat-form-field>
<mat-placeholder translate>datetime.date-from</mat-placeholder> <mat-placeholder translate>datetime.date-from</mat-placeholder>
<mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="startDatePicker" matPrefix></mat-datetimepicker-toggle>
@ -30,7 +31,8 @@
<input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()"> <input matInput [disabled]="disabled" [(ngModel)]="startDate" [matDatetimepicker]="startTimePicker" (ngModelChange)="onStartDateChange()">
</mat-form-field> </mat-form-field>
</section> </section>
<section fxLayout="row" fxLayout.xs="row wrap" fxLayoutAlign="start start" fxLayoutGap="16px"> <section fxLayout="row" fxLayoutAlign="start start" fxLayoutGap="16px"
fxLayout.xs="column" fxLayoutAlign.xs="start stretch" fxLayoutGap.xs="0">
<mat-form-field> <mat-form-field>
<mat-placeholder translate>datetime.date-to</mat-placeholder> <mat-placeholder translate>datetime.date-to</mat-placeholder>
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>

1
ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts

@ -21,7 +21,6 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode
@Component({ @Component({
selector: 'tb-quick-time-interval', selector: 'tb-quick-time-interval',
templateUrl: './quick-time-interval.component.html', templateUrl: './quick-time-interval.component.html',
styleUrls: ['./quick-time-interval.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

39
ui-ngx/src/app/shared/components/time/timewindow-panel.component.html

@ -51,7 +51,7 @@
[fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" [fxShow]="timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.REALTIME && [required]="timewindow.selectedTab === timewindowTypes.REALTIME &&
timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL" timewindowForm.get('realtime.realtimeType').value === realtimeTypes.INTERVAL"
style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval> style="padding-top: 8px"></tb-quick-time-interval>
</section> </section>
</mat-radio-button> </mat-radio-button>
</mat-radio-group> </mat-radio-group>
@ -75,6 +75,7 @@
<tb-timeinterval <tb-timeinterval
formControlName="timewindowMs" formControlName="timewindowMs"
predefinedName="timewindow.last" predefinedName="timewindow.last"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL" [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY && [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL" timewindowForm.get('history.historyType').value === historyTypes.LAST_INTERVAL"
@ -86,6 +87,7 @@
<span translate>timewindow.time-period</span> <span translate>timewindow.time-period</span>
<tb-datetime-period <tb-datetime-period
formControlName="fixedTimewindow" formControlName="fixedTimewindow"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED" [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.FIXED"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY && [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.FIXED" timewindowForm.get('history.historyType').value === historyTypes.FIXED"
@ -97,10 +99,11 @@
<span translate>timewindow.interval</span> <span translate>timewindow.interval</span>
<tb-quick-time-interval <tb-quick-time-interval
formControlName="quickInterval" formControlName="quickInterval"
class="history-time-input"
[fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" [fxShow]="timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
[required]="timewindow.selectedTab === timewindowTypes.HISTORY && [required]="timewindow.selectedTab === timewindowTypes.HISTORY &&
timewindowForm.get('history.historyType').value === historyTypes.INTERVAL" timewindowForm.get('history.historyType').value === historyTypes.INTERVAL"
style="padding-top: 8px; min-width: 364px"></tb-quick-time-interval> style="padding-top: 8px"></tb-quick-time-interval>
</section> </section>
</mat-radio-button> </mat-radio-button>
</mat-radio-group> </mat-radio-group>
@ -134,21 +137,23 @@
(ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox> (ngModelChange)="onHideAggIntervalChanged()"></mat-checkbox>
</section> </section>
<section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval"> <section fxLayout="column" fxFlex [fxShow]="isEdit || !timewindow.hideAggInterval">
<div class="limit-slider-container" <div class="limit-slider-container" fxLayout="row" fxLayoutAlign="start center"
fxLayout="row" fxLayoutAlign="start center"> fxLayout.xs="column" fxLayoutAlign.xs="stretch">
<span translate>aggregation.limit</span> <label translate>aggregation.limit</label>
<mat-slider fxFlex formControlName="limit" <div fxLayout="row" fxLayoutAlign="start center" fxFlex>
thumbLabel <mat-slider fxFlex formControlName="limit"
[value]="timewindowForm.get('aggregation.limit').value" thumbLabel
min="{{minDatapointsLimit()}}" [value]="timewindowForm.get('aggregation.limit').value"
max="{{maxDatapointsLimit()}}"> min="{{minDatapointsLimit()}}"
</mat-slider> max="{{maxDatapointsLimit()}}">
<mat-form-field style="max-width: 80px;"> </mat-slider>
<input matInput formControlName="limit" type="number" step="1" <mat-form-field class="limit-slider-value">
[value]="timewindowForm.get('aggregation.limit').value" <input matInput formControlName="limit" type="number" step="1"
min="{{minDatapointsLimit()}}" [value]="timewindowForm.get('aggregation.limit').value"
max="{{maxDatapointsLimit()}}"/> min="{{minDatapointsLimit()}}"
</mat-form-field> max="{{maxDatapointsLimit()}}"/>
</mat-form-field>
</div>
</div> </div>
</section> </section>
</section> </section>

24
ui-ngx/src/app/shared/components/time/timewindow-panel.component.scss

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import "../../../../scss/constants";
:host { :host {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -40,21 +42,29 @@
} }
.limit-slider-container { .limit-slider-container {
>:first-child { .limit-slider-value {
margin-right: 16px;
}
>:last-child {
margin-left: 16px; margin-left: 16px;
}
>:first-child, >:last-child {
min-width: 25px; min-width: 25px;
max-width: 42px; max-width: 80px;
} }
mat-form-field input[type=number] { mat-form-field input[type=number] {
text-align: center; text-align: center;
} }
} }
@media #{$mat-gt-sm} {
.history-time-input {
min-width: 364px;
}
.limit-slider-container {
> label {
margin-right: 16px;
width: min-content;
max-width: 40%;
}
}
}
} }
:host ::ng-deep { :host ::ng-deep {

5
ui-ngx/src/app/shared/models/resource.models.ts

@ -59,3 +59,8 @@ export interface Resource extends ResourceInfo {
data: string; data: string;
fileName: string; fileName: string;
} }
export interface Resources extends ResourceInfo {
data: string|string[];
fileName: string|string[];
}

5
ui-ngx/src/app/shared/shared.module.ts

@ -141,6 +141,7 @@ import { FileSizePipe } from '@shared/pipe/file-size.pipe';
import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component'; import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component';
import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe'; import { SelectableColumnsPipe } from '@shared/pipe/selectable-columns.pipe';
import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component'; import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-interval.component';
import { MAT_DATE_LOCALE } from '@angular/material/core';
@NgModule({ @NgModule({
providers: [ providers: [
@ -154,6 +155,10 @@ import { QuickTimeIntervalComponent } from '@shared/components/time/quick-time-i
{ {
provide: FlowInjectionToken, provide: FlowInjectionToken,
useValue: Flow useValue: Flow
},
{
provide: MAT_DATE_LOCALE,
useValue: 'en-GB'
} }
], ],
declarations: [ declarations: [

2
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -1943,7 +1943,7 @@
"selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }", "selected-rulechains": "已选择 { count, plural, 1 {# 个规则链} other {# 个规则链} }",
"set-root": "设置为根规则链", "set-root": "设置为根规则链",
"set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。", "set-root-rulechain-text": "确认之后,规则链将变为根规格链,并将处理所有传入的传输消息。",
"set-root-rulechain-title": "您确定要生成规则链'{{RuleChainName}}'根吗?", "set-root-rulechain-title": "您确定要生成规则链'{{ruleChainName}}'根吗?",
"system": "系统" "system": "系统"
}, },
"rulenode": { "rulenode": {

Loading…
Cancel
Save