Browse Source

Merge remote-tracking branch 'upstream/master' into develop/3.3

pull/4084/head
Vladyslav_Prykhodko 6 years ago
parent
commit
9b4309c61a
  1. 52
      application/src/main/data/json/system/widget_bundles/charts.json
  2. 41
      application/src/main/data/json/system/widget_bundles/navigation_widgets.json
  3. 107
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  4. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 3
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  6. 25
      application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java
  8. 21
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  9. 4
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java
  11. 85
      application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java
  12. 155
      application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java
  13. 30
      common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboard.java
  14. 27
      common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboardInfo.java
  15. 6
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  16. 40
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  17. 9
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java
  18. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java
  19. 2
      dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java
  20. 18
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  21. 2
      dao/src/main/resources/sql/schema-entities-hsql.sql
  22. 56
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
  23. 9
      dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java
  24. 2
      msa/tb/docker-cassandra/Dockerfile
  25. 2
      msa/tb/docker-postgres/Dockerfile
  26. 2
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  27. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
  28. 4
      ui-ngx/src/app/core/api/alias-controller.ts
  29. 12
      ui-ngx/src/app/core/guards/auth.guard.ts
  30. 15
      ui-ngx/src/app/core/http/dashboard.service.ts
  31. 9
      ui-ngx/src/app/core/services/menu.service.ts
  32. 25
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts
  33. 3
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  34. 24
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  35. 7
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  36. 10
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  37. 5
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html
  38. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html
  39. 46
      ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.component.ts
  40. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts
  41. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts
  42. 2
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  43. 3
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss
  44. 3
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  45. 3
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  46. 4
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts
  47. 25
      ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts
  48. 42
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html
  49. 3
      ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html
  50. 27
      ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts
  51. 3
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html
  52. 60
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  53. 34
      ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts
  54. 3
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html
  55. 32
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  56. 4
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  57. 6
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  58. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts
  59. 1
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  60. 14
      ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts
  61. 21
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html
  62. 60
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss
  63. 66
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.ts
  64. 37
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html
  65. 85
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss
  66. 114
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts
  67. 3
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  68. 2
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  69. 1
      ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts
  70. 10
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  71. 25
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  72. 4
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  73. 54
      ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html
  74. 36
      ui-ngx/src/app/modules/home/pages/admin/home-settings.component.scss
  75. 84
      ui-ngx/src/app/modules/home/pages/admin/home-settings.component.ts
  76. 15
      ui-ngx/src/app/modules/home/pages/customer/customer.component.html
  77. 35
      ui-ngx/src/app/modules/home/pages/customer/customer.component.scss
  78. 14
      ui-ngx/src/app/modules/home/pages/customer/customer.component.ts
  79. 26
      ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts
  80. 43
      ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html
  81. 5
      ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss
  82. 17
      ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts
  83. 2
      ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts
  84. 14
      ui-ngx/src/app/modules/home/pages/profile/profile.component.html
  85. 16
      ui-ngx/src/app/modules/home/pages/profile/profile.component.scss
  86. 24
      ui-ngx/src/app/modules/home/pages/profile/profile.component.ts
  87. 15
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html
  88. 35
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.scss
  89. 13
      ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts
  90. 14
      ui-ngx/src/app/modules/home/pages/user/user.component.html
  91. 10
      ui-ngx/src/app/modules/home/pages/user/user.component.ts
  92. 1
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html
  93. 26
      ui-ngx/src/app/shared/components/dashboard-select.component.ts
  94. 7
      ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx
  95. 16
      ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx
  96. 27
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  97. 11
      ui-ngx/src/app/shared/components/toast.directive.ts
  98. 9
      ui-ngx/src/app/shared/models/dashboard.models.ts
  99. 574
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  100. 5
      ui-ngx/src/assets/locale/locale.constant-en_US.json

52
application/src/main/data/json/system/widget_bundles/charts.json

@ -25,22 +25,6 @@
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Bars - Chart.js\"}"
}
},
{
"alias": "basic_timeseries",
"name": "Timeseries - Flot",
"descriptor": {
"type": "timeseries",
"sizeX": 8,
"sizeY": 5,
"resources": [],
"templateHtml": "",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"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;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"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\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
}
},
{
"alias": "doughnut_chart_js",
"name": "Doughnut - Chart.js",
@ -71,7 +55,7 @@
"resources": [],
"templateHtml": "",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.pie-label {\n font-size: 12px;\n font-family: 'Roboto';\n font-weight: bold;\n text-align: center;\n padding: 2px;\n color: white;\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'pie'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.pieSettingsSchema();\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.pieDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\nself.actionSources = function() {\n return {\n 'sliceClick': {\n name: 'widget-action.pie-slice-click',\n multiple: false\n }\n };\n}\n",
"settingsSchema": "{}\n",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.6114638304362894,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.9955906536344441,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.9430835931647599,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"radius\":1,\"fontColor\":\"#545454\",\"fontSize\":10,\"decimals\":1,\"legend\":{\"show\":true,\"position\":\"nw\",\"labelBoxBorderColor\":\"#CCCCCC\",\"backgroundColor\":\"#F0F0F0\",\"backgroundOpacity\":0.85},\"innerRadius\":0,\"showLabels\":true,\"showPercentages\":true,\"stroke\":{\"width\":5},\"tilt\":1,\"animatedPie\":false},\"title\":\"Pie - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
@ -138,8 +122,8 @@
}
},
{
"alias": "timeseries_bars_flot",
"name": "Timeseries Bars - Flot",
"alias": "state_chart",
"name": "State Chart",
"descriptor": {
"type": "timeseries",
"sizeX": 8,
@ -147,15 +131,15 @@
"resources": [],
"templateHtml": "",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
}
},
{
"alias": "state_chart",
"name": "State Chart",
"alias": "basic_timeseries",
"name": "Timeseries - Flot",
"descriptor": {
"type": "timeseries",
"sizeX": 8,
@ -163,11 +147,27 @@
"resources": [],
"templateHtml": "",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false},\"_hash\":0.8587686344902596,\"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;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"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\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"legend\":{\"show\":true,\"position\":\"nw\",\"backgroundColor\":\"#f0f0f0\",\"backgroundOpacity\":0.85,\"labelBoxBorderColor\":\"rgba(1, 1, 1, 0.45)\"},\"decimals\":1,\"stack\":false,\"tooltipIndividual\":false},\"title\":\"Timeseries - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
}
},
{
"alias": "timeseries_bars_flot",
"name": "Timeseries Bars - Flot",
"descriptor": {
"type": "timeseries",
"sizeX": 8,
"sizeY": 5,
"resources": [],
"templateHtml": "",
"templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n",
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'bar'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('bar');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(false, 'bar');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":false,\"fillLines\":false,\"showPoints\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000},\"aggregation\":{\"limit\":200,\"type\":\"AVG\"}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":true,\"tooltipIndividual\":false,\"defaultBarWidth\":600},\"title\":\"Timeseries Bars - Flot\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
}
}
]
}
}

41
application/src/main/data/json/system/widget_bundles/navigation_widgets.json

@ -0,0 +1,41 @@
{
"widgetsBundle": {
"alias": "navigation_widgets",
"title": "Navigation widgets",
"image": null
},
"widgetTypes": [
{
"alias": "navigation_cards",
"name": "Navigation cards",
"descriptor": {
"type": "static",
"sizeX": 7,
"sizeY": 6,
"resources": [],
"templateHtml": "<tb-navigation-cards-widget [ctx]=\"ctx\"></tb-navigation-cards-widget>",
"templateCss": "/*#widget-container {\n overflow-y: auto;\n box-sizing: content-box !important;\n cursor: auto;\n}*/\n\n#widget-container #container {\n overflow-y: auto;\n box-sizing: content-box;\n cursor: auto;\n}",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onResize = function() {\n self.ctx.$scope.navigationCardsWidget.resize();\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"filterType\": {\n \"title\": \"Filter type\",\n \"type\": \"string\",\n \"default\": \"all\"\n },\n \"filter\": {\n \"title\": \"Items\",\n \"type\": \"array\"\n }\n },\n \"required\": []\n },\n \"form\": [\n {\n \"key\": \"filterType\",\n \"type\": \"radios\",\n \"direction\": \"row\",\n \"titleMap\": [\n {\n \"value\": \"all\",\n \"name\": \"All items\"\n },\n {\n \"value\": \"include\",\n \"name\": \"Include items\"\n },\n {\n \"value\": \"exclude\",\n \"name\": \"Exclude items\"\n }\n ]\n },\n {\n \"key\": \"filter\",\n \"type\": \"rc-select\",\n \"condition\": \"model.filterType !== 'all'\",\n \"tags\": true,\n \"placeholder\": \"Enter urls to filter\",\n \"items\": [{\"value\": \"/devices\", \"label\": \"/devices\"}, {\"value\": \"/assets\", \"label\": \"/assets\"}, {\"value\": \"/deviceProfies\", \"label\": \"/deviceProfies\"}]\n }\n ]\n}\n",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"filterType\":\"all\"},\"title\":\"Navigation cards\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
}
},
{
"alias": "navigation_card",
"name": "Navigation card",
"descriptor": {
"type": "static",
"sizeX": 2.5,
"sizeY": 2,
"resources": [],
"templateHtml": "<tb-navigation-card-widget [ctx]=\"ctx\"></tb-navigation-card-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n\n}\n\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"name\": {\n \"title\": \"Title\",\n \"type\": \"string\",\n \"default\": \"{i18n:device.devices}\"\n },\n \"icon\": {\n \"title\": \"icon\",\n \"type\": \"string\",\n \"default\": \"devices_other\"\n },\n \"path\": {\n \"title\": \"Navigation path\",\n \"type\": \"string\",\n \"default\": \"/devices\"\n }\n },\n \"required\": [\"name\", \"icon\", \"path\"]\n },\n \"form\": [\n \"name\",\n {\n \"key\": \"icon\",\n \"type\": \"icon\"\n },\n \"path\"\n ]\n}\n",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255,255,255,0)\",\"color\":\"rgba(255,255,255,0.87)\",\"padding\":\"8px\",\"settings\":{\"name\":\"{i18n:device.devices}\",\"icon\":\"devices_other\",\"path\":\"/devices\"},\"title\":\"Navigation card\",\"dropShadow\":false,\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"enableFullscreen\":false,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}"
}
}
]
}

107
application/src/main/java/org/thingsboard/server/controller/DashboardController.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
@ -30,7 +32,11 @@ import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HomeDashboard;
import org.thingsboard.server.common.data.HomeDashboardInfo;
import org.thingsboard.server.common.data.ShortCustomerInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
@ -39,7 +45,9 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -53,6 +61,9 @@ public class DashboardController extends BaseController {
public static final String DASHBOARD_ID = "dashboardId";
private static final String HOME_DASHBOARD_ID = "homeDashboardId";
private static final String HOME_DASHBOARD_HIDE_TOOLBAR = "homeDashboardHideToolbar";
@Value("${dashboard.max_datapoints_limit}")
private long maxDatapointsLimit;
@ -472,4 +483,100 @@ public class DashboardController extends BaseController {
throw handleException(e);
}
}
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/dashboard/home", method = RequestMethod.GET)
@ResponseBody
public HomeDashboard getHomeDashboard() throws ThingsboardException {
try {
SecurityUser securityUser = getCurrentUser();
if (securityUser.isSystemAdmin()) {
return null;
}
User user = userService.findUserById(securityUser.getTenantId(), securityUser.getId());
JsonNode additionalInfo = user.getAdditionalInfo();
HomeDashboard homeDashboard;
homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
if (homeDashboard == null) {
if (securityUser.isCustomerUser()) {
Customer customer = customerService.findCustomerById(securityUser.getTenantId(), securityUser.getCustomerId());
additionalInfo = customer.getAdditionalInfo();
homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
}
if (homeDashboard == null) {
Tenant tenant = tenantService.findTenantById(securityUser.getTenantId());
additionalInfo = tenant.getAdditionalInfo();
homeDashboard = extractHomeDashboardFromAdditionalInfo(additionalInfo);
}
}
return homeDashboard;
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.GET)
@ResponseBody
public HomeDashboardInfo getTenantHomeDashboardInfo() throws ThingsboardException {
try {
Tenant tenant = tenantService.findTenantById(getTenantId());
JsonNode additionalInfo = tenant.getAdditionalInfo();
DashboardId dashboardId = null;
boolean hideDashboardToolbar = true;
if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {
String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
dashboardId = new DashboardId(toUUID(strDashboardId));
if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
}
}
return new HomeDashboardInfo(dashboardId, hideDashboardToolbar);
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/dashboard/home/info", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void setTenantHomeDashboardInfo(@RequestBody HomeDashboardInfo homeDashboardInfo) throws ThingsboardException {
try {
if (homeDashboardInfo.getDashboardId() != null) {
checkDashboardId(homeDashboardInfo.getDashboardId(), Operation.READ);
}
Tenant tenant = tenantService.findTenantById(getTenantId());
JsonNode additionalInfo = tenant.getAdditionalInfo();
if (additionalInfo == null || !(additionalInfo instanceof ObjectNode)) {
additionalInfo = JacksonUtil.OBJECT_MAPPER.createObjectNode();
}
if (homeDashboardInfo.getDashboardId() != null) {
((ObjectNode) additionalInfo).put(HOME_DASHBOARD_ID, homeDashboardInfo.getDashboardId().getId().toString());
((ObjectNode) additionalInfo).put(HOME_DASHBOARD_HIDE_TOOLBAR, homeDashboardInfo.isHideDashboardToolbar());
} else {
((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_ID);
((ObjectNode) additionalInfo).remove(HOME_DASHBOARD_HIDE_TOOLBAR);
}
tenant.setAdditionalInfo(additionalInfo);
tenantService.saveTenant(tenant);
} catch (Exception e) {
throw handleException(e);
}
}
private HomeDashboard extractHomeDashboardFromAdditionalInfo(JsonNode additionalInfo) {
try {
if (additionalInfo != null && additionalInfo.has(HOME_DASHBOARD_ID) && !additionalInfo.get(HOME_DASHBOARD_ID).isNull()) {
String strDashboardId = additionalInfo.get(HOME_DASHBOARD_ID).asText();
DashboardId dashboardId = new DashboardId(toUUID(strDashboardId));
Dashboard dashboard = checkDashboardId(dashboardId, Operation.READ);
boolean hideDashboardToolbar = true;
if (additionalInfo.has(HOME_DASHBOARD_HIDE_TOOLBAR)) {
hideDashboardToolbar = additionalInfo.get(HOME_DASHBOARD_HIDE_TOOLBAR).asBoolean();
}
return new HomeDashboard(dashboard, hideDashboardToolbar);
}
} catch (Exception e) {}
return null;
}
}

2
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -185,6 +185,8 @@ public class ThingsboardInstallService {
case "3.2.0":
log.info("Upgrading ThingsBoard from version 3.2.0 to 3.2.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.2.0");
case "3.2.1":
log.info("Upgrading ThingsBoard from version 3.2.1 to 3.3.0 ...");
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
break;

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

@ -197,7 +197,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
generalSettings.setKey("general");
ObjectNode node = objectMapper.createObjectNode();
node.put("baseUrl", "http://localhost:8080");
node.put("prohibitDifferentUrl", true);
node.put("prohibitDifferentUrl", false);
generalSettings.setJsonValue(node);
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, generalSettings);
@ -438,6 +438,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
this.deleteSystemWidgetBundle("input_widgets");
this.deleteSystemWidgetBundle("date");
this.deleteSystemWidgetBundle("entity_admin_widgets");
this.deleteSystemWidgetBundle("navigation_widgets");
installScripts.loadSystemWidgets();
}

25
application/src/main/java/org/thingsboard/server/service/install/HsqlEntityDatabaseSchemaService.java

@ -15,11 +15,20 @@
*/
package org.thingsboard.server.service.install;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.util.HsqlDao;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DriverManager;
@Service
@Slf4j
@HsqlDao
@Profile("install")
public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService
@ -27,5 +36,21 @@ public class HsqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSe
protected HsqlEntityDatabaseSchemaService() {
super("schema-entities-hsql.sql", "schema-entities-idx.sql");
}
private final String schemaTypesSql = "schema-types-hsql.sql";
@Override
public void createDatabaseSchema(boolean createIndexes) throws Exception {
log.info("Installing SQL DataBase types part: " + schemaTypesSql);
Path schemaFile = Paths.get(installScripts.getDataDir(), SQL_DIR, schemaTypesSql);
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
String sql = new String(Files.readAllBytes(schemaFile), Charset.forName("UTF-8"));
conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to load initial thingsboard database schema
}
super.createDatabaseSchema(createIndexes);
}
}

4
application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java

@ -30,7 +30,7 @@ import java.sql.SQLException;
@Slf4j
public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchemaService {
private static final String SQL_DIR = "sql";
protected static final String SQL_DIR = "sql";
@Value("${spring.datasource.url}")
protected String dbUrl;
@ -42,7 +42,7 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema
protected String dbPassword;
@Autowired
private InstallScripts installScripts;
protected InstallScripts installScripts;
private final String schemaSql;
private final String schemaIdxSql;

21
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -59,6 +59,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.utils.EventDeduplicationExecutor;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
@ -126,13 +127,13 @@ public class DefaultDeviceStateService implements DeviceStateService {
@Getter
private int initFetchPackSize;
private volatile boolean clusterUpdatePending = false;
private ListeningScheduledExecutorService queueExecutor;
private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, Long> deviceLastReportedActivity = new ConcurrentHashMap<>();
private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>();
private volatile EventDeduplicationExecutor<Set<TopicPartitionInfo>> deduplicationExecutor;
public DefaultDeviceStateService(TenantService tenantService, DeviceService deviceService,
AttributesService attributesService, TimeseriesService tsService,
@ -155,6 +156,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
// Should be always single threaded due to absence of locks.
queueExecutor = MoreExecutors.listeningDecorator(Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("device-state")));
queueExecutor.scheduleAtFixedRate(this::updateState, new Random().nextInt(defaultStateCheckIntervalInSec), defaultStateCheckIntervalInSec, TimeUnit.SECONDS);
deduplicationExecutor = new EventDeduplicationExecutor<>(DefaultDeviceStateService.class.getSimpleName(), queueExecutor, this::initStateFromDB);
}
@PreDestroy
@ -292,25 +294,14 @@ public class DefaultDeviceStateService implements DeviceStateService {
}
}
volatile Set<TopicPartitionInfo> pendingPartitions;
@Override
public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) {
if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) {
synchronized (this) {
pendingPartitions = partitionChangeEvent.getPartitions();
if (!clusterUpdatePending) {
clusterUpdatePending = true;
queueExecutor.submit(() -> {
clusterUpdatePending = false;
initStateFromDB();
});
}
}
deduplicationExecutor.submit(partitionChangeEvent.getPartitions());
}
}
private void initStateFromDB() {
private void initStateFromDB(Set<TopicPartitionInfo> pendingPartitions) {
try {
log.info("CURRENT PARTITIONS: {}", partitionedDevices.keySet());
log.info("NEW PARTITIONS: {}", pendingPartitions);

4
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -302,7 +302,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription);
ctx.setAndResolveQuery(cmd.getQuery());
if (cmd.getQuery() != null) {
ctx.setAndResolveQuery(cmd.getQuery());
}
sessionSubs.put(cmd.getCmdId(), ctx);
return ctx;
}

2
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java

@ -107,7 +107,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
public void setAndResolveQuery(T query) {
dynamicValues.clear();
this.query = query;
if (query.getKeyFilters() != null) {
if (query != null && query.getKeyFilters() != null) {
for (KeyFilter filter : query.getKeyFilters()) {
registerDynamicValues(filter.getPredicate());
}

85
application/src/main/java/org/thingsboard/server/utils/EventDeduplicationExecutor.java

@ -0,0 +1,85 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.utils;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.function.Consumer;
/**
* This class deduplicate executions of the specified function.
* Useful in cluster mode, when you get event about partition change multiple times.
* Assuming that the function execution is expensive, we should execute it immediately when first time event occurs and
* later, once the processing of first event is done, process last pending task.
*
* @param <P> parameters of the function
*/
@Slf4j
public class EventDeduplicationExecutor<P> {
private final String name;
private final ExecutorService executor;
private final Consumer<P> function;
private P pendingTask;
private boolean busy;
public EventDeduplicationExecutor(String name, ExecutorService executor, Consumer<P> function) {
this.name = name;
this.executor = executor;
this.function = function;
}
public void submit(P params) {
log.info("[{}] Going to submit: {}", name, params);
synchronized (EventDeduplicationExecutor.this) {
if (!busy) {
busy = true;
pendingTask = null;
try {
log.info("[{}] Submitting task: {}", name, params);
executor.submit(() -> {
try {
log.info("[{}] Executing task: {}", name, params);
function.accept(params);
} catch (Throwable e) {
log.warn("[{}] Failed to process task with parameters: {}", name, params, e);
throw e;
} finally {
unlockAndProcessIfAny();
}
});
} catch (Throwable e) {
log.warn("[{}] Failed to submit task with parameters: {}", name, params, e);
unlockAndProcessIfAny();
throw e;
}
} else {
log.info("[{}] Task is already in progress. {} pending task: {}", name, pendingTask == null ? "adding" : "updating", params);
pendingTask = params;
}
}
}
private void unlockAndProcessIfAny() {
synchronized (EventDeduplicationExecutor.this) {
busy = false;
if (pendingTask != null) {
submit(pendingTask);
}
}
}
}

155
application/src/test/java/org/thingsboard/server/util/EventDeduplicationExecutorTest.java

@ -0,0 +1,155 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.util;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.thingsboard.server.utils.EventDeduplicationExecutor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class EventDeduplicationExecutorTest {
@Test
public void testSimpleFlowSameThread() throws InterruptedException {
simpleFlow(MoreExecutors.newDirectExecutorService());
}
@Test
public void testPeriodicFlowSameThread() throws InterruptedException {
periodicFlow(MoreExecutors.newDirectExecutorService());
}
@Test
public void testExceptionFlowSameThread() throws InterruptedException {
exceptionFlow(MoreExecutors.newDirectExecutorService());
}
@Test
public void testSimpleFlowSingleThread() throws InterruptedException {
simpleFlow(Executors.newSingleThreadExecutor());
}
@Test
public void testPeriodicFlowSingleThread() throws InterruptedException {
periodicFlow(Executors.newSingleThreadExecutor());
}
@Test
public void testExceptionFlowSingleThread() throws InterruptedException {
exceptionFlow(Executors.newSingleThreadExecutor());
}
@Test
public void testSimpleFlowMultiThread() throws InterruptedException {
simpleFlow(Executors.newFixedThreadPool(3));
}
@Test
public void testPeriodicFlowMultiThread() throws InterruptedException {
periodicFlow(Executors.newFixedThreadPool(3));
}
@Test
public void testExceptionFlowMultiThread() throws InterruptedException {
exceptionFlow(Executors.newFixedThreadPool(3));
}
private void simpleFlow(ExecutorService executorService) throws InterruptedException {
try {
Consumer<String> function = Mockito.spy(StringConsumer.class);
EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
String params1 = "params1";
String params2 = "params2";
String params3 = "params3";
executor.submit(params1);
executor.submit(params2);
executor.submit(params3);
Thread.sleep(500);
Mockito.verify(function).accept(params1);
Mockito.verify(function).accept(params3);
} finally {
executorService.shutdownNow();
}
}
private void periodicFlow(ExecutorService executorService) throws InterruptedException {
try {
Consumer<String> function = Mockito.spy(StringConsumer.class);
EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
String params1 = "params1";
String params2 = "params2";
String params3 = "params3";
executor.submit(params1);
Thread.sleep(500);
executor.submit(params2);
Thread.sleep(500);
executor.submit(params3);
Thread.sleep(500);
Mockito.verify(function).accept(params1);
Mockito.verify(function).accept(params2);
Mockito.verify(function).accept(params3);
} finally {
executorService.shutdownNow();
}
}
private void exceptionFlow(ExecutorService executorService) throws InterruptedException {
try {
Consumer<String> function = Mockito.spy(StringConsumer.class);
EventDeduplicationExecutor<String> executor = new EventDeduplicationExecutor<>(EventDeduplicationExecutorTest.class.getSimpleName(), executorService, function);
String params1 = "params1";
String params2 = "params2";
String params3 = "params3";
Mockito.doThrow(new RuntimeException()).when(function).accept("params1");
executor.submit(params1);
executor.submit(params2);
Thread.sleep(500);
executor.submit(params3);
Thread.sleep(500);
Mockito.verify(function).accept(params2);
Mockito.verify(function).accept(params3);
} finally {
executorService.shutdownNow();
}
}
public static class StringConsumer implements Consumer<String> {
@Override
public void accept(String s) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

30
common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboard.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
import lombok.Data;
@Data
public class HomeDashboard extends Dashboard {
private boolean hideDashboardToolbar;
public HomeDashboard(Dashboard dashboard, boolean hideDashboardToolbar) {
super(dashboard);
this.hideDashboardToolbar = hideDashboardToolbar;
}
}

27
common/data/src/main/java/org/thingsboard/server/common/data/HomeDashboardInfo.java

@ -0,0 +1,27 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.thingsboard.server.common.data.id.DashboardId;
@Data
@AllArgsConstructor
public class HomeDashboardInfo {
private DashboardId dashboardId;
private boolean hideDashboardToolbar;
}

6
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java

@ -19,16 +19,18 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataPageLink;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.dao.Dao;
import java.util.Collection;
import java.util.Set;
import java.util.UUID;
/**
@ -48,4 +50,6 @@ public interface AlarmDao extends Dao<Alarm> {
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId,
AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status);
}

40
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -39,10 +39,10 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.query.AlarmData;
import org.thingsboard.server.common.data.query.AlarmDataPageLink;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.DeviceTypeFilter;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@ -60,7 +60,6 @@ import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@ -316,37 +315,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
@Override
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus) {
TimePageLink nextPageLink = new TimePageLink(100);
boolean hasNext = true;
AlarmSeverity highestSeverity = null;
AlarmQuery query;
while (hasNext && AlarmSeverity.CRITICAL != highestSeverity) {
query = new AlarmQuery(entityId, nextPageLink, alarmSearchStatus, alarmStatus, false, null);
PageData<AlarmInfo> alarms = alarmDao.findAlarms(tenantId, query);
if (alarms.hasNext()) {
nextPageLink = nextPageLink.nextPageLink();
}
AlarmSeverity severity = detectHighestSeverity(alarms.getData());
if (severity == null) {
continue;
}
if (severity == AlarmSeverity.CRITICAL || highestSeverity == null) {
highestSeverity = severity;
} else {
highestSeverity = highestSeverity.compareTo(severity) < 0 ? highestSeverity : severity;
}
Set<AlarmStatus> statusList = null;
if (alarmSearchStatus != null) {
statusList = alarmSearchStatus.getStatuses();
} else if (alarmStatus != null) {
statusList = Collections.singleton(alarmStatus);
}
return highestSeverity;
}
private AlarmSeverity detectHighestSeverity(List<AlarmInfo> alarms) {
if (!alarms.isEmpty()) {
List<AlarmInfo> sorted = new ArrayList(alarms);
sorted.sort(Comparator.comparing(Alarm::getSeverity));
return sorted.get(0).getSeverity();
} else {
return null;
}
Set<AlarmSeverity> alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, statusList);
return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null);
}
private void deleteRelation(TenantId tenantId, EntityRelation alarmRelation) {

9
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java

@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.dao.model.sql.AlarmEntity;
import org.thingsboard.server.dao.model.sql.AlarmInfoEntity;
@ -75,4 +76,12 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT alarm.severity FROM AlarmEntity alarm" +
" WHERE alarm.tenantId = :tenantId" +
" AND alarm.originatorId = :entityId" +
" AND ((:status) IS NULL OR alarm.status in (:status))")
Set<AlarmSeverity> findAlarmSeverities(@Param("tenantId") UUID tenantId,
@Param("entityId") UUID entityId,
@Param("status") Set<AlarmStatus> status);
}

6
dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java

@ -24,6 +24,7 @@ import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
@ -120,4 +121,9 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
public PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, CustomerId customerId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds) {
return alarmQueryRepository.findAlarmDataByQueryForEntities(tenantId, customerId, query, orderedEntityIds);
}
@Override
public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status) {
return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), status);
}
}

2
dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java

@ -83,7 +83,7 @@ public class EntityDataAdapter {
if (value != null) {
String strVal = value.toString();
// check number
if (strVal.length() > 0 && NumberUtils.isParsable(strVal)) {
if (NumberUtils.isNumber(strVal)) {
try {
long longVal = Long.parseLong(strVal);
return Long.toString(longVal);

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

@ -116,16 +116,16 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
super.startExecutor();
if (!isInstall()) {
getFetchStmt(Aggregation.NONE, DESC_ORDER);
Optional<NoSqlTsPartitionDate> partition = NoSqlTsPartitionDate.parse(partitioning);
if (partition.isPresent()) {
tsFormat = partition.get();
if (!isFixedPartitioning() && partitionsCacheSize > 0) {
cassandraTsPartitionsCache = new CassandraTsPartitionsCache(partitionsCacheSize);
}
} else {
log.warn("Incorrect configuration of partitioning {}", partitioning);
throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
}
Optional<NoSqlTsPartitionDate> partition = NoSqlTsPartitionDate.parse(partitioning);
if (partition.isPresent()) {
tsFormat = partition.get();
if (!isFixedPartitioning() && partitionsCacheSize > 0) {
cassandraTsPartitionsCache = new CassandraTsPartitionsCache(partitionsCacheSize);
}
} else {
log.warn("Incorrect configuration of partitioning {}", partitioning);
throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!");
}
}

2
dao/src/main/resources/sql/schema-entities-hsql.sql

@ -114,7 +114,7 @@ CREATE TABLE IF NOT EXISTS customer (
CREATE TABLE IF NOT EXISTS dashboard (
id uuid NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY,
created_time bigint NOT NULL,
configuration varchar,
configuration varchar(10000000),
assigned_customers varchar(1000000),
search_text varchar(255),
tenant_id uuid,

56
dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java

@ -354,6 +354,62 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
return new AlarmDataQuery(new DeviceTypeFilter(), pageLink, null, null, null, alarmFields);
}
@Test
public void testFindHighestAlarmSeverity() throws ExecutionException, InterruptedException {
Customer customer = new Customer();
customer.setTitle("TestCustomer");
customer.setTenantId(tenantId);
customer = customerService.saveCustomer(customer);
Device customerDevice = new Device();
customerDevice.setName("TestCustomerDevice");
customerDevice.setType("default");
customerDevice.setTenantId(tenantId);
customerDevice.setCustomerId(customer.getId());
customerDevice = deviceService.saveDevice(customerDevice);
// no one alarms was created
Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null));
Alarm alarm1 = Alarm.builder()
.tenantId(tenantId)
.originator(customerDevice.getId())
.type(TEST_ALARM)
.severity(AlarmSeverity.MAJOR)
.status(AlarmStatus.ACTIVE_UNACK)
.startTs(System.currentTimeMillis())
.build();
alarm1 = alarmService.createOrUpdateAlarm(alarm1).getAlarm();
alarmService.clearAlarm(tenantId, alarm1.getId(), null, System.currentTimeMillis()).get();
Alarm alarm2 = Alarm.builder()
.tenantId(tenantId)
.originator(customerDevice.getId())
.type(TEST_ALARM)
.severity(AlarmSeverity.MINOR)
.status(AlarmStatus.ACTIVE_ACK)
.startTs(System.currentTimeMillis())
.build();
alarm2 = alarmService.createOrUpdateAlarm(alarm2).getAlarm();
alarmService.clearAlarm(tenantId, alarm2.getId(), null, System.currentTimeMillis()).get();
Alarm alarm3 = Alarm.builder()
.tenantId(tenantId)
.originator(customerDevice.getId())
.type(TEST_ALARM)
.severity(AlarmSeverity.CRITICAL)
.status(AlarmStatus.ACTIVE_ACK)
.startTs(System.currentTimeMillis())
.build();
alarm3 = alarmService.createOrUpdateAlarm(alarm3).getAlarm();
Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null));
Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null));
Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK));
Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null));
Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK));
}
@Test
public void testFindAlarmUsingAlarmDataQuery() throws ExecutionException, InterruptedException {
AssetId parentId = new AssetId(Uuids.timeBased());

9
dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java

@ -827,10 +827,7 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue());
}
List<String> deviceTemperatures = temperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList());
if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) {
// in H2 double values are stored with E0 in the end of the string
loadedTemperatures = loadedTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList());
}
Assert.assertEquals(deviceTemperatures, loadedTemperatures);
pageLink = new EntityDataPageLink(10, 0, null, sortOrder);
@ -858,10 +855,6 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest {
entityData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).collect(Collectors.toList());
List<String> deviceHighTemperatures = highTemperatures.stream().map(aDouble -> Double.toString(aDouble)).collect(Collectors.toList());
if (DaoTestUtil.getSqlDbType(template) == SqlDbType.H2) {
// in H2 double values are stored with E0 in the end of the string
loadedHighTemperatures = loadedHighTemperatures.stream().map(s -> s.substring(0, s.length() - 2)).collect(Collectors.toList());
}
Assert.assertEquals(deviceHighTemperatures, loadedHighTemperatures);
deviceService.deleteDevicesByTenantId(tenantId);

2
msa/tb/docker-cassandra/Dockerfile

@ -18,7 +18,7 @@ FROM thingsboard/openjdk8
RUN apt-get update
RUN apt-get install -y curl nmap procps
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-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 echo 'deb http://www.apache.org/dist/cassandra/debian 311x main' | tee --append /etc/apt/sources.list.d/cassandra.list > /dev/null
RUN curl -L https://www.apache.org/dist/cassandra/KEYS | apt-key add -

2
msa/tb/docker-postgres/Dockerfile

@ -18,7 +18,7 @@ FROM thingsboard/openjdk8
RUN apt-get update
RUN apt-get install -y curl
RUN echo 'deb http://apt.postgresql.org/pub/repos/apt/ stretch-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 -
ENV PG_MAJOR 11
RUN apt-get update

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

@ -1189,7 +1189,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
public List<EntitySubtype> getDeviceTypes() {
return restTemplate.exchange(
baseURL + "/api/devices",
baseURL + "/api/device/types",
HttpMethod.GET,
HttpEntity.EMPTY,
new ParameterizedTypeReference<List<EntitySubtype>>() {

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java

@ -61,4 +61,12 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
configuration.setCredentials(new AnonymousCredentials());
return configuration;
}
public ClientCredentials getCredentials() {
if (this.credentials == null) {
return new AnonymousCredentials();
} else {
return this.credentials;
}
}
}

4
ui-ngx/src/app/core/api/alias-controller.ts

@ -53,8 +53,8 @@ export class AliasController implements IAliasController {
private stateControllerHolder: StateControllerHolder,
private origEntityAliases: EntityAliases,
private origFilters: Filters) {
this.entityAliases = deepClone(this.origEntityAliases);
this.filters = deepClone(this.origFilters);
this.entityAliases = deepClone(this.origEntityAliases) || {};
this.filters = deepClone(this.origFilters) || {};
this.userFilters = {};
}

12
ui-ngx/src/app/core/guards/auth.guard.ts

@ -15,7 +15,7 @@
///
import { Injectable, NgZone } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
import { AuthService } from '../auth/auth.service';
import { select, Store } from '@ngrx/store';
import { AppState } from '../core.state';
@ -28,6 +28,7 @@ import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { UtilsService } from '@core/services/utils.service';
import { isObject } from '@core/utils';
@Injectable({
providedIn: 'root'
@ -35,6 +36,7 @@ import { UtilsService } from '@core/services/utils.service';
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private store: Store<AppState>,
private router: Router,
private authService: AuthService,
private dialogService: DialogService,
private utils: UtilsService,
@ -115,6 +117,14 @@ export class AuthGuard implements CanActivate, CanActivateChild {
if (data.auth && data.auth.indexOf(authority) === -1) {
this.dialogService.forbidden();
return of(false);
} else if (data.redirectTo) {
let redirect;
if (isObject(data.redirectTo)) {
redirect = data.redirectTo[authority];
} else {
redirect = data.redirectTo;
}
return of(this.router.parseUrl(redirect));
} else {
return of(true);
}

15
ui-ngx/src/app/core/http/dashboard.service.ts

@ -20,7 +20,7 @@ import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { Dashboard, DashboardInfo } from '@shared/models/dashboard.models';
import { Dashboard, DashboardInfo, HomeDashboard, HomeDashboardInfo } from '@shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, publishReplay, refCount } from 'rxjs/operators';
@ -122,6 +122,19 @@ export class DashboardService {
defaultHttpOptionsFromConfig(config));
}
public getHomeDashboard(config?: RequestConfig): Observable<HomeDashboard> {
return this.http.get<HomeDashboard>('/api/dashboard/home', defaultHttpOptionsFromConfig(config));
}
public getTenantHomeDashboardInfo(config?: RequestConfig): Observable<HomeDashboardInfo> {
return this.http.get<HomeDashboardInfo>('/api/tenant/dashboard/home/info', defaultHttpOptionsFromConfig(config));
}
public setTenantHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo, config?: RequestConfig): Observable<any> {
return this.http.post<any>('/api/tenant/dashboard/home/info', homeDashboardInfo,
defaultHttpOptionsFromConfig(config));
}
public getPublicDashboardLink(dashboard: DashboardInfo): string | null {
if (dashboard && dashboard.assignedCustomers && dashboard.assignedCustomers.length > 0) {
const publicCustomers = dashboard.assignedCustomers

9
ui-ngx/src/app/core/services/menu.service.ts

@ -223,6 +223,7 @@ export class MenuService {
name: 'home.home',
type: 'link',
path: '/home',
notExact: true,
icon: 'home'
},
{
@ -282,6 +283,13 @@ export class MenuService {
path: '/dashboards',
icon: 'dashboards'
},
{
id: guid(),
name: 'admin.home-settings',
type: 'link',
path: '/settings/home',
icon: 'settings_applications'
},
{
id: guid(),
name: 'audit-log.audit-logs',
@ -402,6 +410,7 @@ export class MenuService {
name: 'home.home',
type: 'link',
path: '/home',
notExact: true,
icon: 'home'
},
{

25
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.ts

@ -14,14 +14,23 @@
/// limitations under the License.
///
import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import {
Component,
Injector,
Input,
OnDestroy,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { TooltipPosition } from '@angular/material/tooltip';
import { AliasInfo, IAliasController } from '@core/api/widget-api.models';
import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { BreakpointObserver } from '@angular/cdk/layout';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import {
ALIASES_ENTITY_SELECT_PANEL_DATA,
AliasesEntitySelectPanelComponent,
@ -136,12 +145,12 @@ export class AliasesEntitySelectComponent implements OnInit, OnDestroy {
overlayRef.attach(new ComponentPortal(AliasesEntitySelectPanelComponent, this.viewContainerRef, injector));
}
private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): PortalInjector {
const injectionTokens = new WeakMap<any, any>([
[ALIASES_ENTITY_SELECT_PANEL_DATA, data],
[OverlayRef, overlayRef]
]);
return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
private _createAliasesEntitySelectPanelInjector(overlayRef: OverlayRef, data: AliasesEntitySelectPanelData): Injector {
const providers: StaticProvider[] = [
{provide: ALIASES_ENTITY_SELECT_PANEL_DATA, useValue: data},
{provide: OverlayRef, useValue: overlayRef}
];
return Injector.create({parent: this.viewContainerRef.injector, providers});
}
private updateDisplayValue() {

3
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -195,7 +195,8 @@
[length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"></mat-paginator>
[pageSizeOptions]="[10, 20, 30]"
showFirstLastButtons></mat-paginator>
<ngx-hm-carousel fxFlex *ngIf="mode === 'widget' && widgetsList.length > 0"
#carousel
[(ngModel)]="widgetsCarouselIndex"

24
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -19,9 +19,11 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
Injector,
Input,
NgZone,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
@ -62,7 +64,7 @@ import {
EditAttributeValuePanelComponent,
EditAttributeValuePanelData
} from './edit-attribute-value-panel.component';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import { TelemetryWebsocketService } from '@core/ws/telemetry-websocket.service';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { DataKey, Datasource, DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
@ -319,13 +321,19 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injectionTokens = new WeakMap<any, any>([
[EDIT_ATTRIBUTE_VALUE_PANEL_DATA, {
attributeValue: attribute.value
} as EditAttributeValuePanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
const providers: StaticProvider[] = [
{
provide: EDIT_ATTRIBUTE_VALUE_PANEL_DATA,
useValue: {
attributeValue: attribute.value
} as EditAttributeValuePanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(EditAttributeValuePanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => {

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

@ -49,10 +49,11 @@
<tb-states-component fxFlex.lt-md
[statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
[dashboardCtrl]="this"
[dashboardId]="dashboard.id ? dashboard.id.id : ''"
[dashboardId]="(!embedded && dashboard.id) ? dashboard.id.id : ''"
[isMobile]="isMobile"
[state]="dashboardCtx.state"
[currentState]="currentState"
[syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states">
</tb-states-component>
</div>
@ -78,7 +79,7 @@
(click)="isFullscreen = !isFullscreen">
<mat-icon>{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
<button [fxShow]="isEdit || displayExport()" mat-icon-button
<button [fxShow]="currentDashboardId && (isEdit || displayExport())" mat-icon-button
matTooltip="{{'dashboard.export' | translate}}"
matTooltipPosition="below"
(click)="exportDashboard($event)">
@ -118,7 +119,7 @@
(click)="openDashboardSettings($event)">
<mat-icon>settings</mat-icon>
</button>
<tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && displayDashboardsSelect()"
<tb-dashboard-select [fxShow]="!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()"
[(ngModel)]="currentDashboardId"
(ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
[customerId]="currentCustomerId"

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

@ -123,6 +123,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
@Input()
hideToolbar: boolean;
@Input()
syncStateWithQueryParam = true;
@Input()
dashboard: Dashboard;
dashboardConfiguration: DashboardConfiguration;
@ -266,9 +269,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.rxSubscriptions.push(this.route.data.subscribe(
(data) => {
if (this.embedded) {
data.dashboard = this.dashboard;
data.dashboard = this.dashboardUtils.validateAndUpdateDashboard(this.dashboard);
data.currentDashboardId = this.dashboard.id ? this.dashboard.id.id : null;
data.widgetEditMode = false;
data.singlePageMode = false;
} else {
data.currentDashboardId = this.route.snapshot.params.dashboardId;
}
this.init(data);
this.runChangeDetection();
@ -286,7 +292,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.reset();
this.currentDashboardId = this.route.snapshot.params.dashboardId;
this.currentDashboardId = data.currentDashboardId;
if (this.route.snapshot.params.customerId) {
this.currentCustomerId = this.route.snapshot.params.customerId;

5
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html

@ -23,6 +23,7 @@
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
@ -34,6 +35,7 @@
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
@ -45,6 +47,7 @@
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
@ -56,6 +59,7 @@
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
@ -67,6 +71,7 @@
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"

1
ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html

@ -53,6 +53,7 @@
[mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
[isMobile]="isMobile"
[isMobileDisabled]="widgetEditMode"
[disableWidgetInteraction]="isEdit"
[isEditActionEnabled]="isEdit"
[isExportActionEnabled]="isEdit && !widgetEditMode"
[isRemoveActionEnabled]="isEdit && !widgetEditMode"

46
ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.component.ts

@ -88,6 +88,8 @@ export abstract class StateControllerComponent implements IStateControllerCompon
currentState: string;
syncStateWithQueryParam: boolean;
private rxSubscriptions = new Array<Subscription>();
private inited = false;
@ -99,18 +101,20 @@ export abstract class StateControllerComponent implements IStateControllerCompon
}
ngOnInit(): void {
this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => {
const dashboardId = this.route.snapshot.params.dashboardId || '';
if (this.dashboardId === dashboardId) {
const newState = this.decodeStateParam(paramMap.get('state'));
if (this.currentState !== newState) {
this.currentState = newState;
if (this.inited) {
this.onStateChanged();
if (this.syncStateWithQueryParam) {
this.rxSubscriptions.push(this.route.queryParamMap.subscribe((paramMap) => {
const dashboardId = this.route.snapshot.params.dashboardId || '';
if (this.dashboardId === dashboardId) {
const newState = this.decodeStateParam(paramMap.get('state'));
if (this.currentState !== newState) {
this.currentState = newState;
if (this.inited) {
this.onStateChanged();
}
}
}
}
}));
}));
}
this.init();
this.inited = true;
}
@ -124,16 +128,18 @@ export abstract class StateControllerComponent implements IStateControllerCompon
protected updateStateParam(newState: string) {
this.currentState = newState;
const queryParams: Params = { state: this.currentState };
this.ngZone.run(() => {
this.router.navigate(
[],
{
relativeTo: this.route,
queryParams,
queryParamsHandling: 'merge',
});
});
if (this.syncStateWithQueryParam) {
const queryParams: Params = {state: this.currentState};
this.ngZone.run(() => {
this.router.navigate(
[],
{
relativeTo: this.route,
queryParams,
queryParamsHandling: 'merge',
});
});
}
}
public openRightLayout(): void {

1
ui-ngx/src/app/modules/home/components/dashboard-page/states/state-controller.models.ts

@ -24,6 +24,7 @@ export interface IStateControllerComponent extends IStateController {
stateControllerInstanceId: string;
state: string;
currentState: string;
syncStateWithQueryParam: boolean;
isMobile: boolean;
states: {[id: string]: DashboardState };
dashboardId: string;

6
ui-ngx/src/app/modules/home/components/dashboard-page/states/states-component.directive.ts

@ -53,6 +53,9 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
@Input()
currentState: string;
@Input()
syncStateWithQueryParam: boolean;
@Input()
isMobile: boolean;
@ -89,6 +92,8 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
this.stateControllerComponent.state = this.state;
} else if (propName === 'currentState') {
this.stateControllerComponent.currentState = this.currentState;
} else if (propName === 'syncStateWithQueryParam') {
this.stateControllerComponent.syncStateWithQueryParam = this.syncStateWithQueryParam;
}
}
}
@ -119,6 +124,7 @@ export class StatesComponentDirective implements OnInit, OnDestroy, OnChanges {
this.stateControllerComponent.stateControllerInstanceId = stateControllerInstanceId;
this.stateControllerComponent.state = this.state;
this.stateControllerComponent.currentState = this.currentState;
this.stateControllerComponent.syncStateWithQueryParam = this.syncStateWithQueryParam;
this.stateControllerComponent.isMobile = this.isMobile;
this.stateControllerComponent.states = this.states;
this.stateControllerComponent.dashboardId = this.dashboardId;

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

@ -150,7 +150,7 @@
</button>
</div>
</div>
<div fxFlex fxLayout="column" class="tb-widget-content">
<div fxFlex fxLayout="column" class="tb-widget-content" [ngClass]="{'tb-no-interaction': disableWidgetInteraction}">
<tb-widget fxFlex
#widgetComponent
[dashboardWidget]="widget"

3
ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.scss

@ -128,6 +128,9 @@ div.tb-widget {
}
.tb-widget-content {
&.tb-no-interaction {
pointer-events: none;
}
tb-widget {
position: relative;
width: 100%;

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

@ -113,6 +113,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
@Input()
isRemoveActionEnabled: boolean;
@Input()
disableWidgetInteraction = false;
@Input()
dashboardStyle: {[klass: string]: any};

3
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -238,7 +238,8 @@
[length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"></mat-paginator>
[pageSizeOptions]="pageSizeOptions"
showFirstLastButtons></mat-paginator>
</div>
</div>
</mat-drawer-content>

4
ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts

@ -44,7 +44,7 @@ import { EntityAction } from '@home/models/entity/entity-component.models';
import { Subscription } from 'rxjs';
import { MatTab, MatTabGroup } from '@angular/material/tabs';
import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component';
import { deepClone } from '@core/utils';
import { deepClone, mergeDeep } from '@core/utils';
@Component({
selector: 'tb-entity-details-panel',
@ -280,7 +280,7 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
saveEntity() {
if (this.detailsForm.valid) {
const editingEntity = {...this.editingEntity, ...this.entityComponent.entityFormValue()};
const editingEntity = mergeDeep(this.editingEntity, this.entityComponent.entityFormValue());
this.entitiesTableConfig.saveEntity(editingEntity).subscribe(
(entity) => {
this.entity = entity;

25
ui-ngx/src/app/modules/home/components/filter/filters-edit.component.ts

@ -14,7 +14,16 @@
/// limitations under the License.
///
import { Component, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import {
Component,
Injector,
Input,
OnDestroy,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { TooltipPosition } from '@angular/material/tooltip';
import { IAliasController } from '@core/api/widget-api.models';
import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
@ -28,7 +37,7 @@ import {
FiltersEditPanelComponent,
FiltersEditPanelData
} from '@home/components/filter/filters-edit-panel.component';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import { UserFilterDialogComponent, UserFilterDialogData } from '@home/components/filter/user-filter-dialog.component';
import { MatDialog } from '@angular/material/dialog';
@ -153,12 +162,12 @@ export class FiltersEditComponent implements OnInit, OnDestroy {
}
}
private _createFiltersEditPanelInjector(overlayRef: OverlayRef, data: FiltersEditPanelData): PortalInjector {
const injectionTokens = new WeakMap<any, any>([
[FILTER_EDIT_PANEL_DATA, data],
[OverlayRef, overlayRef]
]);
return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
private _createFiltersEditPanelInjector(overlayRef: OverlayRef, data: FiltersEditPanelData): Injector {
const providers: StaticProvider[] = [
{provide: FILTER_EDIT_PANEL_DATA, useValue: data},
{provide: OverlayRef, useValue: overlayRef}
];
return Injector.create({parent: this.viewContainerRef.injector, providers});
}
private updateFiltersInfo() {

42
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html

@ -67,15 +67,13 @@
</mat-form-field>
<section fxLayout="column" [formGroup]="actionTypeFormGroup" [ngSwitch]="widgetActionFormGroup.get('type').value">
<ng-template [ngSwitchCase]="widgetActionType.openDashboard">
<div fxLayout="column">
<div class="mat-caption tb-required"
style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
<tb-dashboard-autocomplete
formControlName="targetDashboardId"
required
[selectFirstDashboard]="true"
></tb-dashboard-autocomplete>
</div>
<div class="mat-caption tb-required"
style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>widget-action.target-dashboard</div>
<tb-dashboard-autocomplete
formControlName="targetDashboardId"
required
[selectFirstDashboard]="true"
></tb-dashboard-autocomplete>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
@ -122,26 +120,24 @@
widgetActionFormGroup.get('type').value === widgetActionType.updateDashboardState ||
widgetActionFormGroup.get('type').value === widgetActionType.openDashboard ?
widgetActionFormGroup.get('type').value : ''">
<div fxFlex fxLayout="column">
<mat-checkbox formControlName="setEntityId">
{{ 'widget-action.set-entity-from-widget' | translate }}
</mat-checkbox>
<mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value"
floatLabel="always"
class="mat-block">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
formControlName="stateEntityParamName">
</mat-form-field>
</div>
<mat-checkbox formControlName="setEntityId">
{{ 'widget-action.set-entity-from-widget' | translate }}
</mat-checkbox>
<mat-form-field *ngIf="actionTypeFormGroup.get('setEntityId').value"
floatLabel="always"
class="mat-block">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}"
formControlName="stateEntityParamName">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="widgetActionFormGroup.get('type').value === widgetActionType.openDashboardState ?
widgetActionFormGroup.get('type').value : ''">
<mat-checkbox formControlName="openInSeparateDialog">
{{ 'widget-action.open-in-separate-dialog' | translate }}
</mat-checkbox>
<section fxLayout="column" *ngIf="actionTypeFormGroup.get('openInSeparateDialog').value">
<section *ngIf="actionTypeFormGroup.get('openInSeparateDialog').value">
<mat-form-field class="mat-block">
<mat-label translate>widget-action.dialog-title</mat-label>
<input matInput formControlName="dialogTitle">

3
ui-ngx/src/app/modules/home/components/widget/dialog/embed-dashboard-dialog.component.html

@ -27,7 +27,8 @@
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div class="dashboard-state-dialog-content" mat-dialog-content fxFlex fxLayout="column" style="padding: 8px;">
<tb-dashboard-page [embedded]="true" [hideToolbar]="hideToolbar" [currentState]="state" [dashboard]="dashboard" style="width: 100%; height: 100%;"></tb-dashboard-page>
<tb-dashboard-page [embedded]="true" [syncStateWithQueryParam]="false" [hideToolbar]="hideToolbar"
[currentState]="state" [dashboard]="dashboard" style="width: 100%; height: 100%;"></tb-dashboard-page>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"

27
ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts

@ -14,11 +14,22 @@
/// limitations under the License.
///
import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import {
Component,
forwardRef,
Inject,
Injector,
Input,
OnDestroy,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DOCUMENT } from '@angular/common';
import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import { MediaBreakpoints } from '@shared/models/constants';
import { BreakpointObserver } from '@angular/cdk/layout';
import { WINDOW } from '@core/services/window.service';
@ -140,12 +151,12 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector));
}
private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): PortalInjector {
const injectionTokens = new WeakMap<any, any>([
[LEGEND_CONFIG_PANEL_DATA, data],
[OverlayRef, overlayRef]
]);
return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): Injector {
const providers: StaticProvider[] = [
{provide: LEGEND_CONFIG_PANEL_DATA, useValue: data},
{provide: OverlayRef, useValue: overlayRef}
];
return Injector.create({parent: this.viewContainerRef.injector, providers});
}
registerOnChange(fn: any): void {

3
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.html

@ -140,6 +140,7 @@
[length]="alarmsDatasource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"></mat-paginator>
[pageSizeOptions]="pageSizeOptions"
showFirstLastButtons></mat-paginator>
</div>
</div>

60
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -19,9 +19,11 @@ import {
Component,
ElementRef,
EventEmitter,
Injector,
Input,
NgZone,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
@ -64,7 +66,7 @@ import {
widthStyle
} from '@home/components/widget/lib/table-widget.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import {
DISPLAY_COLUMNS_PANEL_DATA,
DisplayColumnsPanelComponent,
@ -452,20 +454,26 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
};
});
const injectionTokens = new WeakMap<any, any>([
[DISPLAY_COLUMNS_PANEL_DATA, {
columns,
columnsUpdated: (newColumns) => {
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
if (this.enableSelection) {
this.displayedColumns.unshift('select');
const providers: StaticProvider[] = [
{
provide: DISPLAY_COLUMNS_PANEL_DATA,
useValue: {
columns,
columnsUpdated: (newColumns) => {
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
if (this.enableSelection) {
this.displayedColumns.unshift('select');
}
this.displayedColumns.push('actions');
}
this.displayedColumns.push('actions');
}
} as DisplayColumnsPanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
} as DisplayColumnsPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector));
this.ctx.detectChanges();
@ -492,15 +500,21 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injectionTokens = new WeakMap<any, any>([
[ALARM_FILTER_PANEL_DATA, {
statusList: this.pageLink.statusList,
severityList: this.pageLink.severityList,
typeList: this.pageLink.typeList
} as AlarmFilterPanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
const providers: StaticProvider[] = [
{
provide: ALARM_FILTER_PANEL_DATA,
useValue: {
statusList: this.pageLink.statusList,
severityList: this.pageLink.severityList,
typeList: this.pageLink.typeList
} as AlarmFilterPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
const componentRef = overlayRef.attach(new ComponentPortal(AlarmFilterPanelComponent,
this.viewContainerRef, injector));
componentRef.onDestroy(() => {

34
ui-ngx/src/app/modules/home/components/widget/lib/date-range-navigator/date-range-navigator.component.ts

@ -18,9 +18,11 @@ import {
Component,
Inject,
InjectionToken,
Injector,
Input,
OnDestroy,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef,
ViewEncapsulation
@ -41,7 +43,7 @@ import {
import { KeyValue } from '@angular/common';
import * as _moment from 'moment';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import { MatSelect } from '@angular/material/select';
import { Subscription } from 'rxjs';
import { HistoryWindowType, TimewindowType } from '@shared/models/time/time.models';
@ -142,18 +144,24 @@ export class DateRangeNavigatorWidgetComponent extends PageComponent implements
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injectionTokens = new WeakMap<any, any>([
[DATE_RANGE_NAVIGATOR_PANEL_DATA, {
model: cloneDateRangeNavigatorModel(this.advancedModel),
settings: this.settings,
onChange: model => {
this.advancedModel = model;
this.triggerChange();
}
} as DateRangeNavigatorPanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
const providers: StaticProvider[] = [
{
provide: DATE_RANGE_NAVIGATOR_PANEL_DATA,
useValue: {
model: cloneDateRangeNavigatorModel(this.advancedModel),
settings: this.settings,
onChange: model => {
this.advancedModel = model;
this.triggerChange();
}
} as DateRangeNavigatorPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DateRangeNavigatorPanelComponent,
this.viewContainerRef, injector));
this.ctx.detectChanges();

3
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.html

@ -99,6 +99,7 @@
[length]="entityDatasource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"></mat-paginator>
[pageSizeOptions]="pageSizeOptions"
showFirstLastButtons></mat-paginator>
</div>
</div>

32
ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts

@ -18,9 +18,11 @@ import {
AfterViewInit,
Component,
ElementRef,
Injector,
Input,
NgZone,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
@ -70,7 +72,7 @@ import {
widthStyle
} from '@home/components/widget/lib/table-widget.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import {
DISPLAY_COLUMNS_PANEL_DATA,
DisplayColumnsPanelComponent,
@ -422,17 +424,23 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni
};
});
const injectionTokens = new WeakMap<any, any>([
[DISPLAY_COLUMNS_PANEL_DATA, {
columns,
columnsUpdated: (newColumns) => {
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
this.displayedColumns.push('actions');
}
} as DisplayColumnsPanelData],
[OverlayRef, overlayRef]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
const providers: StaticProvider[] = [
{
provide: DISPLAY_COLUMNS_PANEL_DATA,
useValue: {
columns,
columnsUpdated: (newColumns) => {
this.displayedColumns = newColumns.filter(column => column.display).map(column => column.def);
this.displayedColumns.push('actions');
}
} as DisplayColumnsPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector));
this.ctx.detectChanges();

4
ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts

@ -175,7 +175,7 @@ export class TbFlot {
autoHighlight: this.tooltipIndividual === true,
markings: []
},
selection : { mode : ctx.isMobile ? null : 'x' },
selection : { mode : 'x' },
legend : {
show: false
}
@ -702,7 +702,7 @@ export class TbFlot {
}
public checkMouseEvents() {
const enabled = !this.ctx.isMobile && !this.ctx.isEdit;
const enabled = !this.ctx.isEdit;
if (isUndefined(this.mouseEventsEnabled) || this.mouseEventsEnabled !== enabled) {
this.mouseEventsEnabled = enabled;
if (this.$element) {

6
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts

@ -626,10 +626,14 @@ export default abstract class LeafletMap {
}
this.points = new FeatureGroup();
}
let pointColor = this.options.pointColor;
for (const pointsList of pointsData) {
pointsList.filter(pdata => !!this.convertPosition(pdata)).forEach(data => {
if (this.options.useColorPointFunction) {
pointColor = safeExecute(this.options.colorPointFunction, [data, pointsData, data.dsIndex]);
}
const point = L.circleMarker(this.convertPosition(data), {
color: this.options.pointColor,
color: pointColor,
radius: this.options.pointSize
});
if (!this.options.pointTooltipOnRightPanel) {

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts

@ -201,6 +201,8 @@ export type TripAnimationSettings = {
pointAsAnchorFunction: GenericFunction;
tooltipFunction: GenericFunction;
labelFunction: GenericFunction;
useColorPointFunction: boolean;
colorPointFunction: GenericFunction;
};
export type actionsHandler = ($event: Event, datasource: Datasource) => void;

1
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -301,6 +301,7 @@ export class MapWidgetController implements MapWidgetInterface {
labelFunction: parseFunction(settings.labelFunction, functionParams),
tooltipFunction: parseFunction(settings.tooltipFunction, functionParams),
colorFunction: parseFunction(settings.colorFunction, functionParams),
colorPointFunction: parseFunction(settings.colorPointFunction, functionParams),
polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams),
polygonTooltipFunction: parseFunction(settings.polygonTooltipFunction, functionParams),
markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']),

14
ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts

@ -871,6 +871,15 @@ export const pointSchema =
title: 'Point color',
type: 'string'
},
useColorPointFunction: {
title: 'Use color point function',
type: 'boolean',
default: false
},
colorPointFunction: {
title: 'Color point function: f(data, dsData, dsIndex)',
type: 'string'
},
pointSize: {
title: 'Point size (px)',
type: 'number',
@ -899,6 +908,11 @@ export const pointSchema =
key: 'pointColor',
type: 'color'
},
'useColorPointFunction',
{
key: 'colorPointFunction',
type: 'javascript'
},
'pointSize',
'usePointAsAnchor',
{

21
ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html

@ -0,0 +1,21 @@
<!--
Copyright © 2016-2021 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<a mat-raised-button color="primary" class="tb-nav-button" href="{{settings.path}}" (click)="navigate($event, settings.path)">
<mat-icon class="material-icons tb-mat-96">{{settings.icon}}</mat-icon>
<span>{{translatedName}}</span>
</a>

60
ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.scss

@ -0,0 +1,60 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
width: 100%;
height: 100%;
}
:host ::ng-deep {
.tb-nav-button {
width: 100%;
height: 100%;
&:hover {
border-bottom: none;
}
&:focus {
border-bottom: none;
}
.mat-button-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
mat-icon {
margin: auto;
}
span {
height: 18px;
min-height: 36px;
max-height: 36px;
padding: 0 0 20px 0;
margin: auto;
font-size: 18px;
font-weight: 400;
line-height: 18px;
white-space: normal;
}
}
&.mat-raised-button.mat-primary {
.mat-ripple-element {
opacity: 0.3;
background-color: rgba(255, 255, 255, 0.3);
}
}
}
}

66
ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.ts

@ -0,0 +1,66 @@
///
/// 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 { PageComponent } from '@shared/components/page.component';
import { Component, Input, NgZone, OnInit } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { UtilsService } from '@core/services/utils.service';
interface NavigationCardWidgetSettings {
name: string;
icon: string;
path: string;
}
@Component({
selector: 'tb-navigation-card-widget',
templateUrl: './navigation-card-widget.component.html',
styleUrls: ['./navigation-card-widget.component.scss']
})
export class NavigationCardWidgetComponent extends PageComponent implements OnInit {
settings: NavigationCardWidgetSettings;
translatedName: string;
@Input()
ctx: WidgetContext;
constructor(protected store: Store<AppState>,
private utils: UtilsService,
private ngZone: NgZone,
private router: Router) {
super(store);
}
ngOnInit(): void {
this.ctx.$scope.navigationCardWidget = this;
this.settings = this.ctx.settings;
this.translatedName = this.utils.customTranslation(this.settings.name, this.settings.name);
}
navigate($event: Event, path: string) {
$event.preventDefault();
this.ngZone.run(() => {
this.router.navigateByUrl(path);
});
}
}

37
ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html

@ -0,0 +1,37 @@
<!--
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.
-->
<mat-grid-list *ngIf="cols" class="tb-navigation-cards" [cols]="cols" rowHeight="280px">
<mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of showHomeSections$| async">
<mat-card style="width: 100%;">
<mat-card-title>
<span translate class="mat-headline">{{section.name}}</span>
</mat-card-title>
<mat-card-content>
<mat-grid-list rowHeight="170px" [cols]="sectionPlaces(section).length">
<mat-grid-tile *ngFor="let place of sectionPlaces(section)">
<a mat-raised-button color="primary" class="tb-card-button" href="{{place.path}}" (click)="navigate($event, place.path)">
<mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
<mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
<span translate>{{place.name}}</span>
</a>
</mat-grid-tile>
</mat-grid-list>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>

85
ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss

@ -0,0 +1,85 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../scss/constants';
:host {
width: 100%;
height: 100%;
}
:host ::ng-deep {
.tb-navigation-cards {
.mat-headline {
font-size: 20px;
@media #{$mat-gt-xmd} {
font-size: 24px;
}
}
mat-card {
padding: 0;
margin: 8px;
mat-card-title {
margin: 0;
padding: 24px 16px 16px;
}
mat-card-title+mat-card-content {
padding-top: 0;
}
mat-card-content {
padding: 16px;
}
}
.tb-card-button {
width: 100%;
height: 100%;
max-width: 240px;
&:hover {
border-bottom: none;
}
&:focus {
border-bottom: none;
}
.mat-button-wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
mat-icon {
margin: auto;
}
span {
height: 18px;
min-height: 36px;
max-height: 36px;
padding: 0 0 20px 0;
margin: auto;
font-size: 18px;
font-weight: 400;
line-height: 18px;
white-space: normal;
}
}
&.mat-raised-button.mat-primary {
.mat-ripple-element {
opacity: 0.3;
background-color: rgba(255, 255, 255, 0.3);
}
}
}
}
}

114
ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts

@ -0,0 +1,114 @@
///
/// 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 { PageComponent } from '@shared/components/page.component';
import { Component, Input, NgZone, OnInit } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { MenuService } from '@core/services/menu.service';
import { HomeSection, HomeSectionPlace } from '@core/services/menu.models';
import { Router } from '@angular/router';
import { map } from 'rxjs/operators';
interface NavigationCardsWidgetSettings {
filterType: 'all' | 'include' | 'exclude';
filter: string[];
}
@Component({
selector: 'tb-navigation-cards-widget',
templateUrl: './navigation-cards-widget.component.html',
styleUrls: ['./navigation-cards-widget.component.scss']
})
export class NavigationCardsWidgetComponent extends PageComponent implements OnInit {
homeSections$ = this.menuService.homeSections();
showHomeSections$ = this.homeSections$.pipe(
map((sections) => {
return sections.filter((section) => this.sectionPlaces(section).length > 0);
})
);
cols = null;
settings: NavigationCardsWidgetSettings;
@Input()
ctx: WidgetContext;
constructor(protected store: Store<AppState>,
private menuService: MenuService,
private ngZone: NgZone,
private router: Router) {
super(store);
}
ngOnInit(): void {
this.ctx.$scope.navigationCardsWidget = this;
this.settings = this.ctx.settings;
}
resize() {
this.updateColumnCount();
}
private updateColumnCount() {
this.cols = 2;
const width = this.ctx.width;
if (width >= 1280) {
this.cols = 3;
if (width >= 1920) {
this.cols = 4;
}
}
this.ctx.detectChanges();
}
navigate($event: Event, path: string) {
$event.preventDefault();
this.ngZone.run(() => {
this.router.navigateByUrl(path);
});
}
sectionPlaces(section: HomeSection): HomeSectionPlace[] {
return section && section.places ? section.places.filter((place) => this.filterPlace(place)) : [];
}
private filterPlace(place: HomeSectionPlace): boolean {
if (this.settings.filterType === 'include') {
return this.settings.filter.includes(place.path);
} else if (this.settings.filterType === 'exclude') {
return !this.settings.filter.includes(place.path);
}
return true;
}
sectionColspan(section: HomeSection): number {
if (this.ctx.width >= 960) {
let colspan = this.cols;
const places = this.sectionPlaces(section);
if (places.length <= colspan) {
colspan = places.length;
}
return colspan;
} else {
return 2;
}
}
}

3
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html

@ -104,7 +104,8 @@
[length]="source.timeseriesDatasource.total() | async"
[pageIndex]="source.pageLink.page"
[pageSize]="source.pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"></mat-paginator>
[pageSizeOptions]="pageSizeOptions"
showFirstLastButtons></mat-paginator>
</mat-tab>
</mat-tab-group>
</div>

2
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -210,7 +210,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.hideEmptyLines = isDefined(this.settings.hideEmptyLines) ? this.settings.hideEmptyLines : false;
this.showTimestamp = this.settings.showTimestamp !== false;
this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.sss';
this.dateFormatFilter = (this.settings.showMilliseconds !== true) ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd HH:mm:ss.SSS';
const pageSize = this.settings.defaultPageSize;
if (isDefined(pageSize) && isNumber(pageSize) && pageSize > 0) {

1
ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts

@ -112,6 +112,7 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']);
this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']);
this.settings.labelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']);
this.settings.colorPointFunction = parseFunction(this.settings.colorPointFunction, ['data', 'dsData', 'dsIndex']);
this.normalizationStep = this.settings.normalizationStep;
const subscription = this.ctx.defaultSubscription;
subscription.callbacks.onDataUpdated = () => {

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

@ -35,6 +35,8 @@ import { TripAnimationComponent } from './trip-animation/trip-animation.componen
import { PhotoCameraInputWidgetComponent } from './lib/photo-camera-input.component';
import { GatewayFormComponent } from './lib/gateway/gateway-form.component';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { NavigationCardsWidgetComponent } from '@home/components/widget/lib/navigation-cards-widget.component';
import { NavigationCardWidgetComponent } from '@home/components/widget/lib/navigation-card-widget.component';
@NgModule({
declarations:
@ -50,7 +52,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,
GatewayFormComponent
GatewayFormComponent,
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent
],
imports: [
CommonModule,
@ -68,7 +72,9 @@ import { ImportExportService } from '@home/components/import-export/import-expor
MultipleInputWidgetComponent,
TripAnimationComponent,
PhotoCameraInputWidgetComponent,
GatewayFormComponent
GatewayFormComponent,
NavigationCardsWidgetComponent,
NavigationCardWidgetComponent
],
providers: [
CustomDialogService,

25
ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts

@ -32,6 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { OAuth2Service } from '@core/http/oauth2.service';
import { UserProfileResolver } from '@home/pages/profile/profile-routing.module';
import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
@Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -48,7 +49,7 @@ const routes: Routes = [
{
path: 'settings',
data: {
auth: [Authority.SYS_ADMIN],
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: {
label: 'admin.system-settings',
icon: 'settings'
@ -57,8 +58,13 @@ const routes: Routes = [
children: [
{
path: '',
redirectTo: 'general',
pathMatch: 'full'
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
redirectTo: {
SYS_ADMIN: '/settings/general',
TENANT_ADMIN: '/settings/home'
}
}
},
{
path: 'general',
@ -127,6 +133,19 @@ const routes: Routes = [
resolve: {
loginProcessingUrl: OAuth2LoginProcessingUrlResolver
}
},
{
path: 'home',
component: HomeSettingsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.TENANT_ADMIN],
title: 'admin.home-settings',
breadcrumb: {
label: 'admin.home-settings',
icon: 'settings_applications'
}
}
}
]
}

4
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -26,6 +26,7 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m
import { OAuth2SettingsComponent } from '@modules/home/pages/admin/oauth2-settings.component';
import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
@NgModule({
declarations:
@ -35,7 +36,8 @@ import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dial
SmsProviderComponent,
SendTestSmsDialogComponent,
SecuritySettingsComponent,
OAuth2SettingsComponent
OAuth2SettingsComponent,
HomeSettingsComponent
],
imports: [
CommonModule,

54
ui-ngx/src/app/modules/home/pages/admin/home-settings.component.html

@ -0,0 +1,54 @@
<!--
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>
<mat-card class="settings-card">
<mat-card-title>
<div fxLayout="row">
<span class="mat-headline" translate>admin.home-settings</span>
</div>
</mat-card-title>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<mat-card-content style="padding-top: 16px;">
<form [formGroup]="homeSettings" (ngSubmit)="save()">
<fieldset [disabled]="isLoading$ | async">
<section class="tb-default-dashboard" fxFlex fxLayout="column">
<section fxFlex fxLayout="column" fxLayout.gt-sm="row">
<tb-dashboard-autocomplete
fxFlex
placeholder="{{ 'dashboard.home-dashboard' | translate }}"
formControlName="dashboardId"
[dashboardsScope]="'tenant'"
[selectFirstDashboard]="false"
></tb-dashboard-autocomplete>
<mat-checkbox fxFlex formControlName="hideDashboardToolbar">
{{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
</mat-checkbox>
</section>
</section>
<div fxLayout="row" fxLayoutAlign="end center" style="width: 100%;" class="layout-wrap">
<button mat-button mat-raised-button color="primary" [disabled]="(isLoading$ | async) || homeSettings.invalid || !homeSettings.dirty"
type="submit">{{'action.save' | translate}}
</button>
</div>
</fieldset>
</form>
</mat-card-content>
</mat-card>
</div>

36
ui-ngx/src/app/modules/home/pages/admin/home-settings.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.
*/
@import "../../../../../scss/constants";
:host {
.tb-default-dashboard {
tb-dashboard-autocomplete {
@media #{$mat-gt-sm} {
padding-right: 12px;
}
@media #{$mat-lt-md} {
padding-bottom: 12px;
}
}
mat-checkbox {
@media #{$mat-gt-sm} {
margin-top: 16px;
}
}
}
}

84
ui-ngx/src/app/modules/home/pages/admin/home-settings.component.ts

@ -0,0 +1,84 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup } from '@angular/forms';
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { DashboardService } from '@core/http/dashboard.service';
import { HomeDashboardInfo } from '@shared/models/dashboard.models';
import { isDefinedAndNotNull } from '@core/utils';
import { DashboardId } from '@shared/models/id/dashboard-id';
@Component({
selector: 'tb-home-settings',
templateUrl: './home-settings.component.html',
styleUrls: ['./home-settings.component.scss', './settings-card.scss']
})
export class HomeSettingsComponent extends PageComponent implements OnInit, HasConfirmForm {
homeSettings: FormGroup;
constructor(protected store: Store<AppState>,
private router: Router,
private dashboardService: DashboardService,
public fb: FormBuilder) {
super(store);
}
ngOnInit() {
this.homeSettings = this.fb.group({
dashboardId: [null],
hideDashboardToolbar: [true]
});
this.dashboardService.getTenantHomeDashboardInfo().subscribe(
(homeDashboardInfo) => {
this.setHomeDashboardInfo(homeDashboardInfo);
}
);
}
save(): void {
const strDashboardId = this.homeSettings.get('dashboardId').value;
const dashboardId: DashboardId = strDashboardId ? new DashboardId(strDashboardId) : null;
const hideDashboardToolbar = this.homeSettings.get('hideDashboardToolbar').value;
const homeDashboardInfo: HomeDashboardInfo = {
dashboardId,
hideDashboardToolbar
};
this.dashboardService.setTenantHomeDashboardInfo(homeDashboardInfo).subscribe(
() => {
this.setHomeDashboardInfo(homeDashboardInfo);
}
);
}
confirmForm(): FormGroup {
return this.homeSettings;
}
private setHomeDashboardInfo(homeDashboardInfo: HomeDashboardInfo) {
this.homeSettings.reset({
dashboardId: homeDashboardInfo?.dashboardId?.id,
hideDashboardToolbar: isDefinedAndNotNull(homeDashboardInfo?.hideDashboardToolbar) ?
homeDashboardInfo?.hideDashboardToolbar : true
});
}
}

15
ui-ngx/src/app/modules/home/pages/customer/customer.component.html

@ -72,6 +72,21 @@
<mat-label translate>customer.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
<section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
<section fxFlex fxLayout="column" fxLayout.gt-sm="row">
<tb-dashboard-autocomplete
fxFlex
placeholder="{{ 'dashboard.home-dashboard' | translate }}"
formControlName="homeDashboardId"
[dashboardsScope]="'customer'"
[customerId]="entity?.id.id"
[selectFirstDashboard]="false"
></tb-dashboard-autocomplete>
<mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
{{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
</mat-checkbox>
</section>
</section>
</div>
<tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
</fieldset>

35
ui-ngx/src/app/modules/home/pages/customer/customer.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
.tb-default-dashboard {
tb-dashboard-autocomplete {
@media #{$mat-gt-sm} {
padding-right: 12px;
}
@media #{$mat-lt-md} {
padding-bottom: 12px;
}
}
mat-checkbox {
@media #{$mat-gt-sm} {
margin-top: 16px;
}
}
}
}

14
ui-ngx/src/app/modules/home/pages/customer/customer.component.ts

@ -23,10 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
import { TranslateService } from '@ngx-translate/core';
import { ContactBasedComponent } from '../../components/entity/contact-based.component';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-customer',
templateUrl: './customer.component.html'
templateUrl: './customer.component.html',
styleUrls: ['./customer.component.scss']
})
export class CustomerComponent extends ContactBasedComponent<Customer> {
@ -54,7 +56,10 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
title: [entity ? entity.title : '', [Validators.required]],
additionalInfo: this.fb.group(
{
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : '']
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
homeDashboardHideToolbar: [entity && entity.additionalInfo &&
isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
}
)
}
@ -65,6 +70,11 @@ export class CustomerComponent extends ContactBasedComponent<Customer> {
this.isPublic = entity.additionalInfo && entity.additionalInfo.isPublic;
this.entityForm.patchValue({title: entity.title});
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
this.entityForm.patchValue({additionalInfo:
{homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
this.entityForm.patchValue({additionalInfo:
{homeDashboardHideToolbar: entity.additionalInfo &&
isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
}
onCustomerIdCopied(event) {

26
ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts

@ -14,11 +14,25 @@
/// limitations under the License.
///
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Injectable, NgModule } from '@angular/core';
import { Resolve, RouterModule, Routes } from '@angular/router';
import { HomeLinksComponent } from './home-links.component';
import { Authority } from '@shared/models/authority.enum';
import { Observable } from 'rxjs';
import { HomeDashboard } from '@shared/models/dashboard.models';
import { DashboardService } from '@core/http/dashboard.service';
@Injectable()
export class HomeDashboardResolver implements Resolve<HomeDashboard> {
constructor(private dashboardService: DashboardService) {
}
resolve(): Observable<HomeDashboard> {
return this.dashboardService.getHomeDashboard();
}
}
const routes: Routes = [
{
@ -31,12 +45,18 @@ const routes: Routes = [
label: 'home.home',
icon: 'home'
}
},
resolve: {
homeDashboard: HomeDashboardResolver
}
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
exports: [RouterModule],
providers: [
HomeDashboardResolver
]
})
export class HomeLinksRoutingModule { }

43
ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html

@ -15,23 +15,26 @@
limitations under the License.
-->
<mat-grid-list class="tb-home-links" [cols]="cols" rowHeight="280px">
<mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of homeSections$| async">
<mat-card style="width: 100%;">
<mat-card-title>
<span translate class="mat-headline">{{section.name}}</span>
</mat-card-title>
<mat-card-content>
<mat-grid-list rowHeight="170px" [cols]="section.places.length">
<mat-grid-tile *ngFor="let place of section.places">
<a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">
<mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
<mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
<span translate>{{place.name}}</span>
</a>
</mat-grid-tile>
</mat-grid-list>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
<tb-dashboard-page *ngIf="homeDashboard; else homeLinks" [embedded]="true" [dashboard]="homeDashboard" [hideToolbar]="homeDashboard.hideDashboardToolbar"></tb-dashboard-page>
<ng-template #homeLinks>
<mat-grid-list class="tb-home-links" [cols]="cols" rowHeight="280px">
<mat-grid-tile [colspan]="sectionColspan(section)" *ngFor="let section of homeSections$| async">
<mat-card style="width: 100%;">
<mat-card-title>
<span translate class="mat-headline">{{section.name}}</span>
</mat-card-title>
<mat-card-content>
<mat-grid-list rowHeight="170px" [cols]="section.places.length">
<mat-grid-tile *ngFor="let place of section.places">
<a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">
<mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
<mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
<span translate>{{place.name}}</span>
</a>
</mat-grid-tile>
</mat-grid-list>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</ng-template>

5
ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss

@ -15,6 +15,11 @@
*/
@import '../../../../../scss/constants';
:host {
width: 100%;
height: 100%;
}
:host ::ng-deep {
.tb-home-links {
.mat-headline {

17
ui-ngx/src/app/modules/home/pages/home-links/home-links.component.ts

@ -19,6 +19,8 @@ import { MenuService } from '@core/services/menu.service';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { HomeSection } from '@core/services/menu.models';
import { ActivatedRoute } from '@angular/router';
import { HomeDashboard } from '@shared/models/dashboard.models';
@Component({
selector: 'tb-home-links',
@ -31,15 +33,20 @@ export class HomeLinksComponent implements OnInit {
cols = 2;
homeDashboard: HomeDashboard = this.route.snapshot.data.homeDashboard;
constructor(private menuService: MenuService,
public breakpointObserver: BreakpointObserver) {
public breakpointObserver: BreakpointObserver,
private route: ActivatedRoute) {
}
ngOnInit() {
this.updateColumnCount();
this.breakpointObserver
.observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']])
.subscribe((state: BreakpointState) => this.updateColumnCount());
if (!this.homeDashboard) {
this.updateColumnCount();
this.breakpointObserver
.observe([MediaBreakpoints.lg, MediaBreakpoints['gt-lg']])
.subscribe((state: BreakpointState) => this.updateColumnCount());
}
}
private updateColumnCount() {

2
ui-ngx/src/app/modules/home/pages/home-links/home-links.module.ts

@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common';
import { HomeLinksRoutingModule } from './home-links-routing.module';
import { HomeLinksComponent } from './home-links.component';
import { SharedModule } from '@app/shared/shared.module';
import { HomeComponentsModule } from '@home/components/home-components.module';
@NgModule({
declarations:
@ -29,6 +30,7 @@ import { SharedModule } from '@app/shared/shared.module';
imports: [
CommonModule,
SharedModule,
HomeComponentsModule,
HomeLinksRoutingModule
]
})

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

@ -63,6 +63,20 @@
</mat-option>
</mat-select>
</mat-form-field>
<section class="tb-home-dashboard" fxFlex fxLayout="column" fxLayout.gt-sm="row">
<tb-dashboard-autocomplete
fxFlex
placeholder="{{ 'dashboard.home-dashboard' | translate }}"
formControlName="homeDashboardId"
[dashboardsScope]="user?.authority === authorities.TENANT_ADMIN ? 'tenant' : 'customer'"
[tenantId]="user?.tenantId?.id"
[customerId]="user?.customerId?.id"
[selectFirstDashboard]="false"
></tb-dashboard-autocomplete>
<mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
{{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
</mat-checkbox>
</section>
<div fxLayout="row" style="padding-bottom: 16px;">
<button mat-button mat-raised-button color="primary"
type="button"

16
ui-ngx/src/app/modules/home/pages/profile/profile.component.scss

@ -38,5 +38,21 @@
font-size: 16px;
font-weight: 400;
}
.tb-home-dashboard {
tb-dashboard-autocomplete {
@media #{$mat-gt-sm} {
padding-right: 12px;
}
@media #{$mat-lt-md} {
padding-bottom: 12px;
}
}
mat-checkbox {
@media #{$mat-gt-sm} {
margin-top: 16px;
}
}
}
}
}

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

@ -32,6 +32,7 @@ import { MatDialog } from '@angular/material/dialog';
import { DialogService } from '@core/services/dialog.service';
import { AuthService } from '@core/auth/auth.service';
import { ActivatedRoute } from '@angular/router';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-profile',
@ -66,7 +67,9 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
email: ['', [Validators.required, Validators.email]],
firstName: [''],
lastName: [''],
language: ['']
language: [''],
homeDashboardId: [null],
homeDashboardHideToolbar: [true]
});
}
@ -76,6 +79,8 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
this.user.additionalInfo = {};
}
this.user.additionalInfo.lang = this.profile.get('language').value;
this.user.additionalInfo.homeDashboardId = this.profile.get('homeDashboardId').value;
this.user.additionalInfo.homeDashboardHideToolbar = this.profile.get('homeDashboardHideToolbar').value;
this.userService.saveUser(this.user).subscribe(
(user) => {
this.userLoaded(user);
@ -106,12 +111,23 @@ export class ProfileComponent extends PageComponent implements OnInit, HasConfir
this.user = user;
this.profile.reset(user);
let lang;
if (user.additionalInfo && user.additionalInfo.lang) {
lang = user.additionalInfo.lang;
} else {
let homeDashboardId;
let homeDashboardHideToolbar = true;
if (user.additionalInfo) {
if (user.additionalInfo.lang) {
lang = user.additionalInfo.lang;
}
homeDashboardId = user.additionalInfo.homeDashboardId;
if (isDefinedAndNotNull(user.additionalInfo.homeDashboardHideToolbar)) {
homeDashboardHideToolbar = user.additionalInfo.homeDashboardHideToolbar;
}
}
if (!lang) {
lang = this.translate.currentLang;
}
this.profile.get('language').setValue(lang);
this.profile.get('homeDashboardId').setValue(homeDashboardId);
this.profile.get('homeDashboardHideToolbar').setValue(homeDashboardHideToolbar);
}
confirmForm(): FormGroup {

15
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.html

@ -60,6 +60,21 @@
<mat-label translate>tenant.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
<section class="tb-default-dashboard" fxFlex fxLayout="column" *ngIf="entity?.id">
<section fxFlex fxLayout="column" fxLayout.gt-sm="row">
<tb-dashboard-autocomplete
fxFlex
placeholder="{{ 'dashboard.home-dashboard' | translate }}"
formControlName="homeDashboardId"
[dashboardsScope]="'tenant'"
[tenantId]="entity?.id.id"
[selectFirstDashboard]="false"
></tb-dashboard-autocomplete>
<mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
{{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
</mat-checkbox>
</section>
</section>
</div>
<tb-contact [parentForm]="entityForm" [isEdit]="isEdit"></tb-contact>
</fieldset>

35
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.scss

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2021 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import "../../../../../scss/constants";
:host {
.tb-default-dashboard {
tb-dashboard-autocomplete {
@media #{$mat-gt-sm} {
padding-right: 12px;
}
@media #{$mat-lt-md} {
padding-bottom: 12px;
}
}
mat-checkbox {
@media #{$mat-gt-sm} {
margin-top: 16px;
}
}
}
}

13
ui-ngx/src/app/modules/home/pages/tenant/tenant.component.ts

@ -23,11 +23,12 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
import { TranslateService } from '@ngx-translate/core';
import { ContactBasedComponent } from '../../components/entity/contact-based.component';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-tenant',
templateUrl: './tenant.component.html',
styleUrls: []
styleUrls: ['./tenant.component.scss']
})
export class TenantComponent extends ContactBasedComponent<TenantInfo> {
@ -54,7 +55,10 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
tenantProfileId: [entity ? entity.tenantProfileId : null, [Validators.required]],
additionalInfo: this.fb.group(
{
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : '']
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
homeDashboardHideToolbar: [entity && entity.additionalInfo &&
isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
}
)
}
@ -65,6 +69,11 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> {
this.entityForm.patchValue({title: entity.title});
this.entityForm.patchValue({tenantProfileId: entity.tenantProfileId});
this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}});
this.entityForm.patchValue({additionalInfo:
{homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
this.entityForm.patchValue({additionalInfo:
{homeDashboardHideToolbar: entity.additionalInfo &&
isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
}
updateFormState() {

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

@ -95,6 +95,20 @@
{{ 'user.always-fullscreen' | translate }}
</mat-checkbox>
</section>
<section fxFlex fxLayout="column" fxLayout.gt-sm="row">
<tb-dashboard-autocomplete
fxFlex
placeholder="{{ 'dashboard.home-dashboard' | translate }}"
formControlName="homeDashboardId"
[dashboardsScope]="entity?.authority === authority.TENANT_ADMIN ? 'tenant' : 'customer'"
[tenantId]="entity?.tenantId?.id"
[customerId]="entity?.customerId?.id"
[selectFirstDashboard]="false"
></tb-dashboard-autocomplete>
<mat-checkbox fxFlex formControlName="homeDashboardHideToolbar">
{{ 'dashboard.home-dashboard-hide-toolbar' | translate }}
</mat-checkbox>
</section>
</section>
</div>
</fieldset>

10
ui-ngx/src/app/modules/home/pages/user/user.component.ts

@ -23,7 +23,7 @@ import { User } from '@shared/models/user.model';
import { selectAuth } from '@core/auth/auth.selectors';
import { map } from 'rxjs/operators';
import { Authority } from '@shared/models/authority.enum';
import { isUndefined } from '@core/utils';
import { isDefinedAndNotNull, isUndefined } from '@core/utils';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
@Component({
@ -74,6 +74,9 @@ export class UserComponent extends EntityComponent<User> {
description: [entity && entity.additionalInfo ? entity.additionalInfo.description : ''],
defaultDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null],
defaultDashboardFullscreen: [entity && entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false],
homeDashboardId: [entity && entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null],
homeDashboardHideToolbar: [entity && entity.additionalInfo &&
isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true]
}
)
}
@ -89,6 +92,11 @@ export class UserComponent extends EntityComponent<User> {
{defaultDashboardId: entity.additionalInfo ? entity.additionalInfo.defaultDashboardId : null}});
this.entityForm.patchValue({additionalInfo:
{defaultDashboardFullscreen: entity.additionalInfo ? entity.additionalInfo.defaultDashboardFullscreen : false}});
this.entityForm.patchValue({additionalInfo:
{homeDashboardId: entity.additionalInfo ? entity.additionalInfo.homeDashboardId : null}});
this.entityForm.patchValue({additionalInfo:
{homeDashboardHideToolbar: entity.additionalInfo &&
isDefinedAndNotNull(entity.additionalInfo.homeDashboardHideToolbar) ? entity.additionalInfo.homeDashboardHideToolbar : true}});
}
}

1
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html

@ -35,6 +35,7 @@
[isEditActionEnabled]="true"
[isExportActionEnabled]="true"
[isRemoveActionEnabled]="!isReadOnly"
[disableWidgetInteraction]="true"
[callbacks]="dashboardCallbacks"></tb-dashboard>
<tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
</tb-footer-fab-buttons>

26
ui-ngx/src/app/shared/components/dashboard-select.component.ts

@ -14,7 +14,17 @@
/// limitations under the License.
///
import { Component, forwardRef, Inject, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import {
Component,
forwardRef,
Inject,
Injector,
Input,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
@ -32,7 +42,7 @@ import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef
import { BreakpointObserver } from '@angular/cdk/layout';
import { DOCUMENT } from '@angular/common';
import { WINDOW } from '@core/services/window.service';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import {
DASHBOARD_SELECT_PANEL_DATA,
DashboardSelectPanelComponent,
@ -186,12 +196,12 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit {
overlayRef.attach(new ComponentPortal(DashboardSelectPanelComponent, this.viewContainerRef, injector));
}
private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): PortalInjector {
const injectionTokens = new WeakMap<any, any>([
[DASHBOARD_SELECT_PANEL_DATA, data],
[OverlayRef, overlayRef]
]);
return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
private _createDashboardSelectPanelInjector(overlayRef: OverlayRef, data: DashboardSelectPanelData): Injector {
const providers: StaticProvider[] = [
{provide: DASHBOARD_SELECT_PANEL_DATA, useValue: data},
{provide: OverlayRef, useValue: overlayRef}
];
return Injector.create({parent: this.viewContainerRef.injector, providers});
}
private updateView() {

7
ui-ngx/src/app/shared/components/json-form/react/json-form-radios.tsx

@ -28,12 +28,17 @@ class ThingsboardRadios extends React.Component<JsonFormFieldProps, JsonFormFiel
);
});
let row = false;
if (this.props.form.direction === 'row') {
row = true;
}
return (
<FormControl component='fieldset'
className={this.props.form.htmlClass}
disabled={this.props.form.readonly}>
<FormLabel component='legend'>{this.props.form.title}</FormLabel>
<RadioGroup name={this.props.form.title} value={this.props.value} onChange={(e) => {
<RadioGroup row={row} name={this.props.form.title} value={this.props.value} onChange={(e) => {
this.props.onChangeValidate(e);
}}>
{items}

16
ui-ngx/src/app/shared/components/json-form/react/json-form-rc-select.tsx

@ -22,6 +22,7 @@ import {
KeyLabelItem
} from '@shared/components/json-form/react/json-form.models';
import { Mode } from 'rc-select/lib/interface';
import { deepClone } from '@core/utils';
interface ThingsboardRcSelectState extends JsonFormFieldState {
currentValue: KeyLabelItem | KeyLabelItem[];
@ -151,10 +152,14 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
labelClass += ' tb-focused';
}
let mode: Mode;
if (this.props.form.tags) {
mode = 'tags';
} else if (this.props.form.multiple) {
mode = 'multiple';
let value = this.state.currentValue;
if (this.props.form.tags || this.props.form.multiple) {
value = deepClone(value);
if (this.props.form.tags) {
mode = 'tags';
} else if (this.props.form.multiple) {
mode = 'multiple';
}
}
const dropdownStyle = {...this.props.form.dropdownStyle, ...{zIndex: 100001}};
@ -176,12 +181,13 @@ class ThingsboardRcSelect extends React.Component<JsonFormFieldProps, Thingsboar
maxTagTextLength={this.props.form.maxTagTextLength}
disabled={this.props.form.readonly}
optionLabelProp='children'
value={this.state.currentValue}
value={value}
labelInValue={true}
onSelect={this.onSelect}
onDeselect={this.onDeselect}
onFocus={this.onFocus}
onBlur={this.onBlur}
placeholder={this.props.form.placeholder}
style={this.props.form.style || {width: '100%'}}>
{options}
</Select>

27
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -14,7 +14,18 @@
/// limitations under the License.
///
import { Component, forwardRef, Inject, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import {
Component,
forwardRef,
Inject,
Injector,
Input,
OnDestroy,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { MillisecondsToTimeStringPipe } from '@shared/pipe/milliseconds-to-time-string.pipe';
@ -32,7 +43,7 @@ import {
TimewindowPanelComponent,
TimewindowPanelData
} from '@shared/components/time/timewindow-panel.component';
import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
import { ComponentPortal } from '@angular/cdk/portal';
import { MediaBreakpoints } from '@shared/models/constants';
import { BreakpointObserver } from '@angular/cdk/layout';
import { WINDOW } from '@core/services/window.service';
@ -229,12 +240,12 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces
});
}
private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): PortalInjector {
const injectionTokens = new WeakMap<any, any>([
[TIMEWINDOW_PANEL_DATA, data],
[OverlayRef, overlayRef]
]);
return new PortalInjector(this.viewContainerRef.injector, injectionTokens);
private _createTimewindowPanelInjector(overlayRef: OverlayRef, data: TimewindowPanelData): Injector {
const providers: StaticProvider[] = [
{provide: TIMEWINDOW_PANEL_DATA, useValue: data},
{provide: OverlayRef, useValue: overlayRef}
];
return Injector.create({parent: this.viewContainerRef.injector, providers});
}
registerOnChange(fn: any): void {

11
ui-ngx/src/app/shared/components/toast.directive.ts

@ -20,9 +20,11 @@ import {
Directive,
ElementRef, HostBinding,
Inject,
Injector,
Input,
NgZone,
OnDestroy, Optional,
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
@ -34,7 +36,6 @@ import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { MatButton } from '@angular/material/button';
import Timeout = NodeJS.Timeout;
import { PortalInjector } from '@angular/cdk/portal';
@Directive({
selector: '[tb-toast]'
@ -138,10 +139,10 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
this.toastComponentRef.destroy();
}
};
const injectionTokens = new WeakMap<any, any>([
[MAT_SNACK_BAR_DATA, data]
]);
const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
const providers: StaticProvider[] = [
{provide: MAT_SNACK_BAR_DATA, useValue: data}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
this.toastComponentRef = this.viewContainerRef.createComponent(componentFactory, 0, injector);
this.cd.detectChanges();

9
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -106,6 +106,15 @@ export interface Dashboard extends DashboardInfo {
configuration?: DashboardConfiguration;
}
export interface HomeDashboard extends Dashboard {
hideDashboardToolbar: boolean;
}
export interface HomeDashboardInfo {
dashboardId: DashboardId;
hideDashboardToolbar: boolean;
}
export function isPublicDashboard(dashboard: DashboardInfo): boolean {
if (dashboard && dashboard.assignedCustomers) {
return dashboard.assignedCustomers

574
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -54,7 +54,10 @@
"share-via": "Sdílet přes {{provider}}",
"continue": "Pokračovat",
"discard-changes": "Zahodit změny",
"download": "Stáhnout"
"download": "Stáhnout",
"next-with-label": "Další: {{label}}",
"read-more": "Zobrazit více",
"hide": "Skrýt"
},
"aggregation": {
"aggregation": "Agregace",
@ -77,6 +80,8 @@
"test-mail-sent": "Testovací zpráva byla úspěšně odeslána!",
"base-url": "Základní URL",
"base-url-required": "Hodnota Základní URL je povinná.",
"prohibit-different-url": "Zakázat použití názvu hosta z hlaviček požadavku klienta",
"prohibit-different-url-hint": "Toto nastavení by mělo být povoleno v produkčních prostředích. Pokud je zakázáno, může způsobit bezpečnostní problémy",
"mail-from": "Email od",
"mail-from-required": "Hodnota Email od je povinná.",
"smtp-protocol": "SMTP protokol",
@ -99,6 +104,33 @@
"proxy-user": "Uživatel proxy",
"proxy-password": "Heslo proxy",
"send-test-mail": "Odeslat testovací zprávu",
"sms-provider": "Poskytovatel SMS",
"sms-provider-settings": "Nastavení poskytovatele SMS",
"sms-provider-type": "Typ poskytovatele SMS",
"sms-provider-type-required": "Typ poskytovatele SMS je povinný.",
"sms-provider-type-aws-sns": "Amazon SNS",
"sms-provider-type-twilio": "Twilio",
"aws-access-key-id": "AWS Access Key ID",
"aws-access-key-id-required": "AWS Access Key ID je povinný",
"aws-secret-access-key": "AWS Secret Access Key",
"aws-secret-access-key-required": "AWS Secret Access Key je povinný",
"aws-region": "AWS Region",
"aws-region-required": "AWS Region je povinný",
"number-from": "Telefonní číslo odesílatele",
"number-from-required": "Telefonní číslo Odesílatele je povinné.",
"number-to": "Telefonní číslo příjemce",
"number-to-required": "Telefonní číslo příjemce je povinné.",
"phone-number-hint": "Telefonní číslo ve formátu E.164, např. +19995550123",
"phone-number-pattern": "Neplatné telefonní číslo. Mělo by odpovídat formátu E.164, např. +19995550123.",
"sms-message": "SMS zpráva",
"sms-message-required": "SMS zpráva je povinná.",
"sms-message-max-length": "SMS zpráva nemůže být delší než 1600 znaků",
"twilio-account-sid": "Twilio Account SID",
"twilio-account-sid-required": "Twilio Account SID je povinné",
"twilio-account-token": "Twilio Account Token",
"twilio-account-token-required": "Twilio Account Token je povinný",
"send-test-sms": "Odeslat testovací SMS",
"test-sms-sent": "Testovací SMS úspěšně odeslána!",
"security-settings": "Bezpečnostní nastavení",
"password-policy": "Politika hesel",
"minimum-password-length": "Minimální délka hesla",
@ -119,8 +151,74 @@
"general-policy": "Obecná politika",
"max-failed-login-attempts": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu",
"minimum-max-failed-login-attempts-range": "Maximální počet neúspěšných pokusů o přihlášení před zablokováním účtu nemůže být záporný",
"user-lockout-notification-email": "V případě zablokování uživatelského účtu odeslat upozornění na email"
},
"user-lockout-notification-email": "V případě zablokování uživatelského účtu odeslat upozornění na email",
"domain-name": "Doménové jméno",
"domain-name-unique": "Doménové jméno a protokol musí být unikátní.",
"error-verification-url": "Doménové jméno by nemělo obsahovat symbol '/' ani ':'. Příklad: thingsboard.io",
"oauth2": {
"access-token-uri": "URI přístupového tokenu",
"access-token-uri-required": "URI přístupového tokenu je povinné.",
"activate-user": "Aktivovat uživatele",
"add-domain": "Přidat doménu",
"delete-domain": "Smazat doménu",
"add-provider": "Přidat poskytovatele",
"delete-provider": "Smazat poskytovatele",
"allow-user-creation": "Povolit vytvoření uživatele",
"always-fullscreen": "Vždy v režimu celé obrazovky",
"authorization-uri": "Autorizační URI",
"authorization-uri-required": "Autorizační URI je povinné.",
"client-authentication-method": "Metoda autentizace klienta",
"client-id": "ID klienta",
"client-id-required": "ID klienta je povinné.",
"client-secret": "Heslo klienta",
"client-secret-required": "Heslo klienta je povinné.",
"custom-setting": "Vlastní nastavení",
"customer-name-pattern": "Vzor názvu zákazníka",
"default-dashboard-name": "Název defaultního dashboardu",
"delete-domain-text": "Budťe opatrní, protože po potvrzení nebudou doména ani žádná data poskytovatele dostupné.",
"delete-domain-title": "Jste si jisti, že chcete smazat nastavení domény '{{domainName}}'?",
"delete-registration-text": "Buďte opatrní, protože po potvrzení nebudou data poskytovatele dostupná.",
"delete-registration-title": "Jste si jisti, že chcete smazat poskytovatele '{{name}}'?",
"email-attribute-key": "Atribut klíče email",
"email-attribute-key-required": "Atribut klíče email je povinný.",
"first-name-attribute-key": "Atribut klíče jméno",
"general": "Obecné",
"jwk-set-uri": "JSON Web Key URI",
"last-name-attribute-key": "Atribut klíče příjmení",
"login-button-icon": "Ikona tlačítka přihlášení",
"login-button-label": "Označení poskytovatele",
"login-button-label-placeholder": "Přihlásit se přes $(Provider label)",
"login-button-label-required": "Označení je povinné.",
"login-provider": "Poskytovatel přihlášení",
"mapper": "Mapper",
"new-domain": "Nová doména",
"oauth2": "OAuth2",
"redirect-uri-template": "Šablona URI přesměrování",
"copy-redirect-uri": "Zkopírovat URI přesměrování",
"registration-id": "ID registrace",
"registration-id-required": "ID registrace je povinné.",
"registration-id-unique": "Id registrace musí být v systému unikátní.",
"scope": "Rozsah",
"scope-required": "Rozsah je povinný.",
"tenant-name-pattern": "Vzor názvu tenanta",
"tenant-name-pattern-required": "Vzor názvu tenanta je povinný.",
"tenant-name-strategy": "Strategie názvu tenanta",
"type": "Typ mapperu",
"uri-pattern-error": "Neplatný formát URI.",
"url": "URL",
"url-pattern": "Neplatný formát URL.",
"url-required": "URL je povinná.",
"user-info-uri": "User info URI",
"user-info-uri-required": "User info URI je povinné.",
"user-name-attribute-name": "Atribut klíče název uživatele",
"user-name-attribute-name-required": "Atribut klíče název uživatele je povinný",
"protocol": "Protokol",
"domain-schema-http": "HTTP",
"domain-schema-https": "HTTPS",
"domain-schema-mixed": "HTTP+HTTPS",
"enable": "Povolit nastavení OAuth2"
}
},
"alarm": {
"alarm": "Alarm",
"alarms": "Alarmy",
@ -128,6 +226,8 @@
"no-alarms-matching": "Žádné alarmy odpovídající '{{entity}}' nebyly nalezeny.",
"alarm-required": "Alarm je povinný",
"alarm-status": "Stav alarmu",
"alarm-status-list": "Seznam stavů alarmu",
"any-status": "Všechny stavy",
"search-status": {
"ANY": "Všechny",
"ACTIVE": "Aktivní",
@ -154,6 +254,8 @@
"end-time": "Datum ukončení",
"ack-time": "Datum přijetí",
"clear-time": "Datum vyřešení",
"alarm-severity-list": "Seznam závažností alarmu",
"any-severity": "Všechny závažnosti",
"severity-critical": "Kritická",
"severity-major": "Vysoká",
"severity-minor": "Nízká",
@ -176,12 +278,16 @@
"clear-alarm-title": "Odstranit alarm",
"clear-alarm-text": "Jste si jisti, že chcete alarm odstranit?",
"alarm-status-filter": "Filtr stavu alarmu",
"alarm-filter": "Filtr alarmu",
"max-count-load": "Maximální počet nahraných alarmů (0 - neomezeně)",
"max-count-load-required": "Maximální počet nahraných alarmů je povinný.",
"max-count-load-error-min": "Minimální hodnota je 0.",
"fetch-size": "Velikost dávky",
"fetch-size-required": "Velikost dávky je povinná.",
"fetch-size-error-min": "Minimální hodnota je 10."
"fetch-size-error-min": "Minimální hodnota je 10.",
"alarm-type-list": "Seznam typů alarmu",
"any-type": "Všechny typy",
"search-propagated-alarms": "Vyhledat zpropagované alarmy"
},
"alias": {
"add": "Přidat alias",
@ -211,6 +317,7 @@
"filter-type-device-search-query-description": "Zařízení typů {{deviceTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}",
"filter-type-entity-view-search-query": "Dotaz na vyhledání zobrazení entity",
"filter-type-entity-view-search-query-description": "Entitní pohledy typů {{entityViewTypes}} se {{relationType}} vztahem {{direction}} {{rootEntity}}",
"filter-type-apiUsageState": "Stav využití Api",
"entity-filter": "Filtr entity",
"resolve-multiple": "Použít jako více entit",
"filter-type": "Typ filtru",
@ -325,6 +432,59 @@
"no-attributes-text": "Žádné atributy nebyly nalezeny",
"no-telemetry-text": "Žádná telemetrie nebyla nalezena"
},
"api-usage": {
"api-usage": "Využití Api",
"data-points": "Datové body",
"data-points-storage-days": "Dny uložení datových bodů",
"email": "Email",
"email-messages": "Emailové zprávy",
"email-messages-daily-activity": "Denní aktivita emailových zpráv",
"email-messages-hourly-activity": "Hodinová aktivita emailových zpráv",
"email-messages-monthly-activity": "Měsíční aktivita emailových zpráv",
"exceptions": "Výjimky",
"executions": "Zpracování",
"javascript": "JavaScript",
"javascript-executions": "JavaScript výjimky",
"javascript-functions": "JavaScript funkce",
"javascript-functions-daily-activity": "Denní aktivita JavaScript funkcí",
"javascript-functions-hourly-activity": "Hodinová aktivita JavaScript funkcí",
"javascript-functions-monthly-activity": "Měsíční aktivita JavaScript funkcí",
"latest-error": "Poslední chyba",
"messages": "Zprávy",
"permanent-failures": "${entityName} permanentní chyby",
"permanent-timeouts": "${entityName} permanentní timeouty",
"processing-failures": "${entityName} chyby zpracování",
"processing-failures-and-timeouts": "Chyby a timeouty zpracování",
"processing-timeouts": "${entityName} timeouty zpracování",
"queue-stats": "Statistiky fronty",
"rule-chain": "Řetěz pravidel",
"rule-engine": "Engine pro zpracování pravidel",
"rule-engine-daily-activity": "Denní aktivita enginu pro zpracování pravidel",
"rule-engine-executions": "Zpracování Enginu pro zpracování pravidel",
"rule-engine-hourly-activity": "Hodinová aktivita enginu pro zpracování pravidel",
"rule-engine-monthly-activity": "Měsíční aktivita enginu pro zpracování pravidel",
"rule-engine-statistics": "Statistiky enginu pro zpracování pravidel",
"rule-node": "Uzel pravidla",
"sms": "SMS",
"sms-messages": "SMS zprávy",
"sms-messages-daily-activity": "Denní aktivita SMS zpráv",
"sms-messages-hourly-activity": "Hodinová aktivita SMS zpráv",
"sms-messages-monthly-activity": "Měsíční aktivita SMS zpráv",
"successful": "${entityName} úspěšnost",
"telemetry": "Telemetrie",
"telemetry-persistence": "Uložení telemetrie",
"telemetry-persistence-daily-activity": "Denní aktivita uložení telemetrie",
"telemetry-persistence-hourly-activity": "Hodinová aktivita uložení telemetrie",
"telemetry-persistence-monthly-activity": "Měsíční aktivita uložení telemetrie",
"transport": "Přenos",
"transport-daily-activity": "Denní aktivita přenosu",
"transport-data-points": "Datové body přenosu",
"transport-hourly-activity": "Hodinová aktivita přenosu",
"transport-messages": "Zprávy přenosu",
"transport-monthly-activity": "Měsíční aktivita přenosu",
"view-details": "Zobrazit detail",
"view-statistics": "Zobrazit statistiky"
},
"audit-log": {
"audit": "Audit",
"audit-logs": "Záznamy auditu",
@ -363,7 +523,13 @@
"action-data": "Data akce",
"failure-details": "Detail chyby",
"search": "Prohledat záznamy auditu",
"clear-search": "Vymazat vyhledávání"
"clear-search": "Vymazat vyhledávání",
"type-assigned-from-tenant": "Odebráno tenantovi",
"type-assigned-to-tenant": "Přiřazeno tenantovi",
"type-provision-success": "Zřízení zařízení",
"type-provision-failure": "Selhání zřízení zařízení",
"type-timeseries-updated": "Aktualizace telemetrie",
"type-timeseries-deleted": "Smazání telemetrie"
},
"confirm-on-exit": {
"message": "Některé změny nebyly uloženy. Jste si jisti, že chcete tuto stránku opustit?",
@ -549,6 +715,7 @@
"title-color": "Barva názvu",
"display-dashboards-selection": "Zobrazit výběr dashboardů",
"display-entities-selection": "Zobrazit výběr entit",
"display-filters": "Zobrazit filtry",
"display-dashboard-timewindow": "Zobrazit časové okno",
"display-dashboard-export": "Zobrazit export",
"import": "Importovat dashboard",
@ -615,6 +782,7 @@
"alarm": "Pole alarmu",
"timeseries-required": "Časové řady entity jsou povinné.",
"timeseries-or-attributes-required": "Časové řady / atributy entity jsou povinné.",
"alarm-fields-timeseries-or-attributes-required": "Pole alarmu nebo časové řady / atributy jsou povinné.",
"maximum-timeseries-or-attributes": "Maximálně { count, plural, 1 {1 časová řada/atribut je povolena.} other {# časových řad/atributů je povoleno} }",
"alarm-fields-required": "Pole alarmu jsou povinná.",
"function-types": "Typy funkcí",
@ -706,6 +874,12 @@
"access-token-invalid": "Délka přístupového tokenu musí být od 1 do 20 znaků.",
"rsa-key": "RSA veřejný klíč",
"rsa-key-required": "RSA veřejný klíč je povinný.",
"client-id": "ID klienta",
"client-id-pattern": "Obsahuje neplatné znaky.",
"user-name": "Název uživatele",
"user-name-required": "Název uživatele je povinný.",
"client-id-or-user-name-necessary": "ID klienta a/nebo název uživatele jsou povinné",
"password": "Heslo",
"secret": "Heslo",
"secret-required": "Heslo je povinné.",
"device-type": "Typ zařízení",
@ -724,19 +898,183 @@
"details": "Detail",
"copyId": "Kopírovat Id zařízení",
"copyAccessToken": "Kopírovat přístupový token",
"copy-mqtt-authentication": "Kopírovat přístupové údaje MQTT",
"idCopiedMessage": "Id zařízení bylo zkopírováno do schránky",
"accessTokenCopiedMessage": "Přístupový token zařízení byl zkopírován do schránky",
"mqtt-authentication-copied-message": "MQTT autentizace zařízení byla zkopírována do schránky",
"assignedToCustomer": "Přiřazeno zákazníkovi",
"unable-delete-device-alias-title": "Nebylo možné smazat alias zařízení",
"unable-delete-device-alias-text": "Alias zařízení '{{deviceAlias}}' nelze smazat, protože je používán následujícími widgety:<br/>{{widgetsList}}",
"is-gateway": "Je bránou",
"overwrite-activity-time": "Přepsat čas aktivity připojeného zařízení",
"public": "Veřejné",
"device-public": "Zařízení je veřejné",
"select-device": "Vybrat zařízení",
"import": "Importovat zařízení",
"device-file": "Soubor zařízení",
"search": "Vyhledat zařízení",
"selected-devices": "Vybráno { count, plural, 1 {1 zařízení} other {# zařízení} }"
"selected-devices": "Vybráno { count, plural, 1 {1 zařízení} other {# zařízení} }",
"device-configuration": "Konfigurace zařízení",
"transport-configuration": "Konfigurace přenosu",
"wizard": {
"device-wizard": "Průvodce zařízením",
"device-details": "Detail zařízení",
"new-device-profile": "Vytvořit nový profil zařízení",
"existing-device-profile": "Vybrat existující profil zařízení",
"specific-configuration": "Specifická konfigurace",
"customer-to-assign-device": "Přiřadit zařízení zákazníkovi",
"add-credential": "Přidat přístupový údaj"
}
},
"device-profile": {
"device-profile": "Profil zařízení",
"device-profiles": "Profily zařízení",
"all-device-profiles": "Všechny",
"add": "Přidat profil zařízení",
"edit": "Editovat profil zařízení",
"device-profile-details": "Detail profilu zařízení",
"no-device-profiles-text": "Žádné profily zařízení nebyly nalezeny",
"search": "Vyhledat profily zařízení",
"selected-device-profiles": "Vybráno { count, plural, 1 {1 profil zařízení} other {# profilů zařízení} }",
"no-device-profiles-matching": "Žádný profil zařízení odpovídající '{{entity}}' nebyl nalezen.",
"device-profile-required": "Profil zařízení je povinný",
"idCopiedMessage": "Id profilu zařízení bylo zkopírováno do schránky",
"set-default": "Učinit profil zařízení defaultním",
"delete": "Smazat profil zařízení",
"copyId": "Kopírovat Id profilu zařízení",
"new-device-profile-name": "Název profilu zařízení",
"new-device-profile-name-required": "Název profilu zařízení je povinný.",
"name": "Název",
"name-required": "Název je povinný.",
"type": "Typ profilu",
"type-required": "Typ profilu je povinný.",
"type-default": "Defaultní",
"transport-type": "Typ přenosu",
"transport-type-required": "Typ přenosu je povinný.",
"transport-type-default": "Defaultní",
"transport-type-default-hint": "Podporuje základní MQTT, HTTP and CoAP přenos",
"transport-type-mqtt": "MQTT",
"transport-type-mqtt-hint": "Umožňuje pokročilé nastavení MQTT přenosu",
"transport-type-lwm2m": "LWM2M",
"transport-type-lwm2m-hint": "Typ transportu LWM2M",
"description": "Popis",
"default": "Defaultní",
"profile-configuration": "Konfigurace profilu",
"transport-configuration": "Konfigurace přenosu",
"default-rule-chain": "Defaultní řetěz pravidel",
"select-queue-hint": "Vyberte z rozbalovacího seznamu nebo přidejte vlastní název.",
"delete-device-profile-title": "Jste si jisti, že chcete smazat profil zařízení '{{deviceProfileName}}'?",
"delete-device-profile-text": "Buďte opatrní, protože po potvrzení nebude možné profil zařízení ani žádná související data obnovit.",
"delete-device-profiles-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 profil zařízení} other {# profilů zařízení} }?",
"delete-device-profiles-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané profily zařízení odstraněny a žádná související data nebude možné obnovit.",
"set-default-device-profile-title": "Jste si jisti, že chcete profil zařízení '{{deviceProfileName}}' učinit defaultním?",
"set-default-device-profile-text": "Po potvrzení bude profil zařízení označen jako defaultní a bude použit pro nová zařízení bez specifikovaného profilu.",
"no-device-profiles-found": "Žádné profily zařízení nebyly nalezeny.",
"create-new-device-profile": "Vytvořit nový!",
"mqtt-device-topic-filters": "Filtry MQTT fronty zařízení",
"mqtt-device-topic-filters-unique": "Filtry MQTT fronty zařízení musí být unikátní.",
"mqtt-device-payload-type": "MQTT zpráva zařízení",
"mqtt-device-payload-type-json": "JSON",
"mqtt-device-payload-type-proto": "Protobuf",
"mqtt-payload-type-required": "Typ zprávy je povinný.",
"support-level-wildcards": "Jsou podporovány jednoúrovňové <code>[+]</code> a víceúrovňové <code>[#]</code> zástupné znaky.",
"telemetry-topic-filter": "Filtr fronty telemetrie",
"telemetry-topic-filter-required": "Filtr fronty telemetrie je povinný.",
"attributes-topic-filter": "Filtr atributů fronty",
"attributes-topic-filter-required": "Filtr atributů fronty je povinný.",
"telemetry-proto-schema": "Proto schéma telemetrie",
"telemetry-proto-schema-required": "Proto schéma telemetrie je povinné.",
"attributes-proto-schema": "Atributy proto schémata",
"attributes-proto-schema-required": "Atributy proto schémata jsou povinné.",
"rpc-response-topic-filter": "Filtr fronty RPC odpovědi",
"rpc-response-topic-filter-required": "Filtr fronty RPC odpovědi je povinný.",
"not-valid-pattern-topic-filter": "Neplatný vzor filtru fronty",
"not-valid-single-character": "Neplatné použití jednoúrovňového zástupného znaku",
"not-valid-multi-character": "Neplatné použití víceúrovňového zástupného znaku",
"single-level-wildcards-hint": "<code>[+]</code> je vhodný pro jakoukoli úroveň filtru fronty. Př.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.",
"multi-level-wildcards-hint": "<code>[#]</code> může nahradit filtr fronty a může se jednat o poslední symbol fronty. Př.: <b>#</b> or <b>v1/devices/me/#</b>.",
"alarm-rules": "Pravidla alarmu",
"alarm-rules-with-count": "Pravidla alarmu ({{count}})",
"no-alarm-rules": "Žádná pravidla alarmu nejsou konfigurována",
"add-alarm-rule": "Přidat pravidlo alarmu",
"edit-alarm-rule": "Editovat pravidlo alarmu",
"alarm-type": "Typ alarmu",
"alarm-type-required": "Typ alarmu je povinný.",
"alarm-type-unique": "Typ alarmu musí být v rámci pravidel alarmu profilu zařízení unikátní.",
"create-alarm-pattern": "Vytvořit <b>{{alarmType}}</b> alarm",
"create-alarm-rules": "Vytvořit pravidla alarmu",
"no-create-alarm-rules": "Nejsou konfigurovány žádné podmínky vytvoření",
"add-create-alarm-rule-prompt": "Přidejte prosím pravidlo vytvoření alarmu",
"clear-alarm-rule": "Pravidlo zrušení alarmu",
"no-clear-alarm-rule": "Není konfigurována žádná podmínka zrušení",
"add-create-alarm-rule": "Přidat podmínku vytvoření",
"add-clear-alarm-rule": "Přidat podmínku zrušení",
"select-alarm-severity": "Vybrat závažnost alarmu",
"alarm-severity-required": "Závažnost alarmu je povinná.",
"condition-duration": "Doba trvání podmínky",
"condition-duration-value": "Hodnota doby trvání",
"condition-duration-time-unit": "Jednotka času",
"condition-duration-value-range": "Hodnota doby trvání musí být v rozsahu od 1 do 2147483647.",
"condition-duration-value-pattern": "Doba trvání musí být celé číslo.",
"condition-duration-value-required": "Doba trvání je povinná.",
"condition-duration-time-unit-required": "Jednotka času je povinná.",
"advanced-settings": "Pokročilá nastavení",
"alarm-rule-details": "Detail",
"add-alarm-rule-details": "Přidat detail",
"propagate-alarm": "Propagovat alarm",
"alarm-rule-relation-types-list": "Typy vztahů ke zpropagování",
"alarm-rule-relation-types-list-hint": "Pokud nejsou vybrány žádné typy vztahů, alarmy budou propagovány bez filtru typu vztahu.",
"alarm-details": "Detail alarmu",
"alarm-rule-condition": "Podmínka pravidla alarmu",
"enter-alarm-rule-condition-prompt": "Přidejte prosím podmínku pravidla alarmu",
"edit-alarm-rule-condition": "Editovat podmínku pravidla alarmu",
"device-provisioning": "Zřízení zařízení",
"provision-strategy": "Strategie zřízení",
"provision-strategy-required": "Strategie zřízení je povinná.",
"provision-strategy-disabled": "Zakázáno",
"provision-strategy-created-new": "Povolit vytváření nových zařízení",
"provision-strategy-check-pre-provisioned": "Zkontrolovat předvytvořená zařízení",
"provision-device-key": "Klíč pro zřízení zařízení",
"provision-device-key-required": "Klíč pro zřízení zařízení je povinný.",
"copy-provision-key": "Kopírovat klíč pro zřízení",
"provision-key-copied-message": "Klíč pro zřízení byl zkopírován do schránky",
"provision-device-secret": "Heslo pro zřízení zařízení",
"provision-device-secret-required": "Heslo pro zřízení zařízení je povinné.",
"copy-provision-secret": "Kopírovat heslo pro zřízení",
"provision-secret-copied-message": "Heslo pro zřízení zařízení bylo zkopírováno do schránky",
"condition": "Podmínka",
"condition-type": "Typ podmínky",
"condition-type-simple": "Jednoduchá",
"condition-type-duration": "Doba trvání",
"condition-during": "V průběhu {{during}}",
"condition-type-repeating": "Opakování",
"condition-type-required": "Typ podmínky je povinný.",
"condition-repeating-value": "Počet událostí",
"condition-repeating-value-range": "Počet událostí musí být v rozsahu od 1 do 2147483647.",
"condition-repeating-value-pattern": "Počet událostí musí být celé číslo.",
"condition-repeating-value-required": "Počet událostí je povinný.",
"condition-repeat-times": "Opakování { count, plural, 1 {1 krát} other {# krát} }",
"schedule-type": "Typ plánovače",
"schedule-type-required": "Typ plánovače je povinný.",
"schedule": "Časový plán",
"edit-schedule": "Editovat časový plán alarmu",
"schedule-any-time": "Aktivní neustále",
"schedule-specific-time": "Aktivní v konkrétním čase",
"schedule-custom": "Vlastní",
"schedule-day": {
"monday": "Pondělí",
"tuesday": "Úterý",
"wednesday": "Středa",
"thursday": "Čtvrtek",
"friday": "Pátek",
"saturday": "Sobota",
"sunday": "Neděle"
},
"schedule-days": "Dny",
"schedule-time": "Čas",
"schedule-time-from": "Od",
"schedule-time-to": "Do",
"schedule-days-of-week-required": "Musí být vybrán minimálně jeden den v týdnu."
},
"dialog": {
"close": "Zavřít dialog"
@ -757,7 +1095,7 @@
"entity-alias": "Alias entity",
"unable-delete-entity-alias-title": "Alias entity nebylo možné smazat",
"unable-delete-entity-alias-text": "Alias entity '{{entityAlias}}' nelze smazat, protože je používán následujícími widgety:<br/>{{widgetsList}}",
"duplicate-alias-error": "Nalezen dupliticní alias '{{alias}}'.<br>Aliasy entit musí být v rámci dashboardu unikátní.",
"duplicate-alias-error": "Nalezen duplicitní alias '{{alias}}'.<br>Aliasy entit musí být v rámci dashboardu unikátní.",
"missing-entity-filter-error": "Ve filtru chybí alias '{{alias}}'.",
"configure-alias": "Konfigurovat '{{alias}}' alias",
"alias": "Alias",
@ -794,6 +1132,10 @@
"type-devices": "Zařízení",
"list-of-devices": "{ count, plural, 1 {Jedno zařízení} other {Seznam # zařízení} }",
"device-name-starts-with": "Zařízení, jejichž název začíná '{{prefix}}'",
"type-device-profile": "Profil zařízení",
"type-device-profiles": "Profily zařízení",
"list-of-device-profiles": "{ count, plural, 1 {Jeden profil zařízení} other {Seznam # profilů zařízení} }",
"device-profile-name-starts-with": "Profily zařízení, jejichž název začíná '{{prefix}}'",
"type-asset": "Aktivum",
"type-assets": "Aktiva",
"list-of-assets": "{ count, plural, 1 {Jedno aktivum} other {Seznam # aktiv} }",
@ -814,6 +1156,10 @@
"type-tenants": "Tenanti",
"list-of-tenants": "{ count, plural, 1 {Jeden tenant} other {Seznam # tenantů} }",
"tenant-name-starts-with": "Tenanti, jejichž název začíná '{{prefix}}'",
"type-tenant-profile": "Profil tenanta",
"type-tenant-profiles": "Profily tenantů",
"list-of-tenant-profiles": "{ count, plural, 1 {Jeden profil tenanta} other {Seznam # profilů tenantů} }",
"tenant-profile-name-starts-with": "Profily tenantů, jejichž název začíná '{{prefix}}'",
"type-customer": "Zákazník",
"type-customers": "Zákazníci",
"list-of-customers": "{ count, plural, 1 {Jeden zákazník} other {Seznam # zákazníků} }",
@ -840,6 +1186,8 @@
"rulenode-name-starts-with": "Uzly pravidel, jejichž název začíná '{{prefix}}'",
"type-current-customer": "Stávající zákazník",
"type-current-tenant": "Stávající tenant",
"type-current-user": "Stávající uživatel",
"type-current-user-owner": "Vlastník stávajícího uživatele",
"search": "Vyhledat entity",
"selected-entities": "{ count, plural, 1 {1 entita} other {# entit} } zvoleno",
"entity-name": "Název entity",
@ -847,7 +1195,8 @@
"details": "Detail entity",
"no-entities-prompt": "Žádné entity nebyly nalezeny",
"no-data": "Nelze zobrazit žádná data",
"columns-to-display": "Zobrazit sloupce"
"columns-to-display": "Zobrazit sloupce",
"type-api-usage-state": "Stav využití API"
},
"entity-field": {
"created-time": "Datum vytvoření",
@ -1048,7 +1397,7 @@
"anonymous": "Anonymní",
"basic": "Základní",
"pem": "PEM",
"ca-cert": "soubor CA certifikátu *",
"ca-cert": "Soubor CA certifikátu *",
"private-key": "Soubor privátního klíče *",
"cert": "Soubor certifikátu *",
"no-file": "Žádný soubor nebyl vybrán.",
@ -1154,6 +1503,93 @@
"file": "Soubor rozšíření",
"invalid-file-error": "Neplatný soubor rozšíření"
},
"filter": {
"add": "Přidat filtr",
"edit": "Editovat filtr",
"name": "Název filtru",
"name-required": "Název filtru je povinný.",
"duplicate-filter": "Filtr s identickým názvem již existuje.",
"filters": "Filtry",
"unable-delete-filter-title": "Smazat filtr není možné",
"unable-delete-filter-text": "Filtr '{{filter}}' není možné smazat, protože je používán následujícím widgetem(y):<br/>{{widgetsList}}",
"duplicate-filter-error": "Nalezen duplicitní filtr '{{filter}}'.<br>Filtry musí být v rámci dashboardu unikátní.",
"missing-key-filters-error": "U filtru '{{filter}}' chybí klíčové filtry.",
"filter": "Filtr",
"editable": "Editovatelné",
"no-filters-found": "Žádné filtry nebyly nalezeny.",
"no-filter-text": "Není specifikován žádný filtr",
"add-filter-prompt": "Přidejte prosím filtr",
"no-filter-matching": "'{{filter}}' nebyl nalezen.",
"create-new-filter": "Vytvořit nový!",
"filter-required": "Filtr je povinný.",
"operation": {
"operation": "Operace",
"equal": "je rovno",
"not-equal": "není rovno",
"starts-with": "začíná na",
"ends-with": "končí na",
"contains": "obsahuje",
"not-contains": "neobsahuje",
"greater": "větší než",
"less": "menší než",
"greater-or-equal": "větší nebo rovno",
"less-or-equal": "menší nebo rovno",
"and": "a",
"or": "nebo"
},
"ignore-case": "ignorovat velikost písmen",
"value": "Hodnota",
"remove-filter": "Odebrat filtr",
"preview": "Náhled filtru",
"no-filters": "Nejsou konfigurovány žádné filtry",
"add-filter": "Přidat filtr",
"add-complex-filter": "Přidat komplexní filtr",
"add-complex": "Přidat komplex",
"complex-filter": "Komplexní filtr",
"edit-complex-filter": "Editovat komplexní filtr",
"edit-filter-user-params": "Editovat filtr predikátu parametrů uživatele",
"filter-user-params": "Filtr predikátu parametrů uživatele",
"user-parameters": "Parametry uživatele",
"display-label": "Zobrazované označení",
"autogenerated-label": "Automaticky vygenerovat označení",
"order-priority": "Priority pořadí polí",
"key-filter": "Klíčový filtr",
"key-filters": "Klíčové filtry",
"key-name": "Název klíče",
"key-name-required": "Název klíče je povinný.",
"key-type": {
"key-type": "Typ klíče",
"attribute": "Atribut",
"timeseries": "Časové řady",
"entity-field": "Pole entity"
},
"value-type": {
"value-type": "Typ hodnoty",
"string": "Řetězec",
"numeric": "Číslo",
"boolean": "Pravdivostní hodnota",
"date-time": "Datum a čas"
},
"value-type-required": "Typ hodnoty klíče je povinný.",
"key-value-type-change-title": "Jste si jisti, že chcete změnit typ klíče hodnoty?",
"key-value-type-change-message": "Pokud potvrdíte nový typ hodnoty, všechny zadané klíčové filtry budou odstraněny.",
"no-key-filters": "Nejsou konfigurovány žádné klíčové filtry",
"add-key-filter": "Přidat klíčový filtr",
"remove-key-filter": "Odebrat klíčový filtr",
"edit-key-filter": "Editovat klíčový filtr",
"date": "Datum",
"time": "Čas",
"current-tenant": "Stávající tenant",
"current-customer": "Stávající zákazník",
"current-user": "Stávající uživatel",
"current-device": "Stávající zařízení",
"default-value": "Defaultní hodnota",
"dynamic-source-type": "Dynamický typ zdroje",
"no-dynamic-value": "Žádná dynamická hodnota",
"source-attribute": "Atribut zdroje",
"switch-to-dynamic-value": "Přepnout na dynamickou hodnotu",
"switch-to-default-value": "Přepnout na defaultní hodnotu"
},
"fullscreen": {
"expand": "Rozšířit do režimu celé obrazovky",
"exit": "Ukončit režim celé obrazovky",
@ -1286,6 +1722,7 @@
"entity-field": "Pole entity",
"access-token": "Přístupový token",
"isgateway": "Je bránou",
"activity-time-from-gateway-device": "Čas aktivity ze zařízení brány",
"description": "Popis"
},
"stepper-text":{
@ -1329,6 +1766,7 @@
"legend": {
"direction": "Směr legendy",
"position": "Pozice legendy",
"sort-legend": "Setřídit datové klíče v legendě",
"show-max": "Zobrazit max hodnotu",
"show-min": "Zobrazit min hodnotu",
"show-avg": "Zobrazit průměrnou hodnotu",
@ -1525,6 +1963,12 @@
"help": "Nápověda",
"reset-debug-mode": "Resetovat režim ladění na všech uzlech"
},
"timezone": {
"timezone": "Časová zóna",
"select-timezone": "Vyberte časovou zónu",
"no-timezones-matching": "žádné časové zóny odpovídající '{{timezone}}' nebyly nalezeny.",
"timezone-required": "Časová zóna je povinná."
},
"queue": {
"select_name": "Vybrat název fronty",
"name": "Název fronty",
@ -1563,6 +2007,87 @@
"isolated-tb-core-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta",
"isolated-tb-rule-engine-details": "Vyžaduje samostatnou mikroslužbu(y) pro každého izolovaného tenanta"
},
"tenant-profile": {
"tenant-profile": "Profil tenanta",
"tenant-profiles": "Profily tenantů",
"add": "Přidat profil tenanta",
"edit": "Editovat profil tenanta",
"tenant-profile-details": "Detail profilu tenanta",
"no-tenant-profiles-text": "Nebyly nalezeny žádné profily tenantů",
"search": "Vyhledat profily tenantů",
"selected-tenant-profiles": "Vybráno { count, plural, 1 {1 profilů tenantů} other {# profilů tenantů} }",
"no-tenant-profiles-matching": "Žádné profily tenantů odpovídající '{{entity}}' nebyly nalezeny.",
"tenant-profile-required": "Profil tenanta je povinný",
"idCopiedMessage": "Id profilu tenanta bylo zkopírováno do schránky",
"set-default": "Učinit profil tenanta defaultním",
"delete": "Smazat profil tenanta",
"copyId": "Kopírovat Id profilu tenanta",
"name": "Název",
"name-required": "Název je povinný.",
"data": "Data profilu",
"profile-configuration": "Konfigurace profilu",
"description": "Popis",
"default": "Defaultní",
"delete-tenant-profile-title": "Jste si jisti, že chcete smazat profil tenanta '{{tenantProfileName}}'?",
"delete-tenant-profile-text": "Buďte opatrní, protože po potvrzení nebude možné profil tenanta ani žádná související data obnovit.",
"delete-tenant-profiles-title": "Jste si jisti, že chcete smazat { count, plural, 1 {1 profil tenanta} other {# profilů tenanta} }?",
"delete-tenant-profiles-text": "Buďte opatrní, protože po potvrzení budou všechny vybrané profily tenantů odstraněny a žádná související data nebude možné obnovit.",
"set-default-tenant-profile-title": "Jste si jisti, že chcete učinit profil tenanta '{{tenantProfileName}}' defaultním?",
"set-default-tenant-profile-text": "Po potvrzení bude profil tenanta označen jako defaultní a bude použit pro nové tenanty bez specifikovaného profilu.",
"no-tenant-profiles-found": "Nebyly nalezeny žádné profily tenantů.",
"create-new-tenant-profile": "Vytvořit nový!",
"maximum-devices": "Maximální počet zařízení (0 - neomezeno)",
"maximum-devices-required": "Maximální počet zařízení je povinný.",
"maximum-devices-range": "Minimální počet zařízení nemůže být záporný",
"maximum-assets": "Maximální počet aktiv (0 - neomezeno)",
"maximum-assets-required": "Maximální počet aktiv je povinný.",
"maximum-assets-range": "Maximální počet aktiv nemůže být záporný",
"maximum-customers": "Maximální počet zákazníků (0 - neomezeno)",
"maximum-customers-required": "Maximální počet zákazníkůje povinný.",
"maximum-customers-range": "Maximální počet zákazníků nemůže být záporný",
"maximum-users": "Maximální počet uživatelů (0 - neomezeno)",
"maximum-users-required": "Maximální počet uživatelů je povinný.",
"maximum-users-range": "Maximální počet uživatelů nemůže být záporný",
"maximum-dashboards": "Maximální počet dashboardů (0 - neomezeno)",
"maximum-dashboards-required": "Maximální počet dashboardů je povinný.",
"maximum-dashboards-range": "Maximální počet dashboardů nemůže být záporný",
"maximum-rule-chains": "Maximální počet řetězů pravidel (0 - neomezeno)",
"maximum-rule-chains-required": "Maximální počet řetězů pravidel je povinný.",
"maximum-rule-chains-range": "Maximální počet řetězů pravidel nemůže být záporný",
"transport-tenant-msg-rate-limit": "Limit přenosu zpráv tenanta.",
"transport-tenant-telemetry-msg-rate-limit": "Limit přenosu zpráv telemetrie tenanta.",
"transport-tenant-telemetry-data-points-rate-limit": "Limit přenosu datových bodů telemetrie tenanta.",
"transport-device-msg-rate-limit": "Limit přenosu zpráv zařízení.",
"transport-device-telemetry-msg-rate-limit": "Limit přenosu zpráv zařízení telemetrie tenanta.",
"transport-device-telemetry-data-points-rate-limit": "Limit přenosu datových bodů zařízení telemetrie tenanta.",
"max-transport-messages": "Maximální počet zpráv přenosu (0 - neomezeno)",
"max-transport-messages-required": "Maximální počet zpráv přenosu je povinný.",
"max-transport-messages-range": "Maximální počet zpráv přenosu nemůže být záporný",
"max-transport-data-points": "Maximální počet datových bodů přenosu (0 - neomezeno)",
"max-transport-data-points-required": "Maximální počet datových bodů přenosu je povinný.",
"max-transport-data-points-range": "Maximální počet datových bodů přenosu nemůže být záporný",
"max-r-e-executions": "Maximální počet zpracování enginu pro zpracování pravidel (0 - neomezeno)",
"max-r-e-executions-required": "Maximální počet zpracování enginu pro zpracování pravidel je povinný.",
"max-r-e-executions-range": "Maximální počet zpracování enginu pro zpracování pravidel nemůže být záporný",
"max-j-s-executions": "Maximální počet JavaScript zpracování (0 - neomezeno)",
"max-j-s-executions-required": "Maximální počet JavaScript zpracování je povinný.",
"max-j-s-executions-range": "Maximální počet JavaScript zpracování nemůže být záporný",
"max-d-p-storage-days": "Maximální počet dnů uložení datových bodů (0 - neomezeno)",
"max-d-p-storage-days-required": "Maximální počet dnů uložení datových bodů je povinný.",
"max-d-p-storage-days-range": "Maximální počet dnů uložení datových bodů nemůže být záporný",
"default-storage-ttl-days": "Defaultní počet dnů TTL úložiště (0 - neomezeno)",
"default-storage-ttl-days-required": "Defaultní počet dnů TTL úložiště je povinný.",
"default-storage-ttl-days-range": "Defaultní počet dnů TTL úložiště nemůže být záporný",
"max-rule-node-executions-per-message": "Maximální počet zpracování uzlů pravidel na zprávu (0 - neomezeno)",
"max-rule-node-executions-per-message-required": "Maximální počet zpracování uzlů pravidel na zprávu je povinný.",
"max-rule-node-executions-per-message-range": "Maximální počet zpracování uzlů pravidel na zprávu nemůže být záporný",
"max-emails": "Maximální počet odeslaných emailů (0 - neomezeno)",
"max-emails-required": "Maximální počet odeslaných emailů je povinný.",
"max-emails-range": "Maximální počet odeslaných emailů nemůže být záporný",
"max-sms": "Maximální počet odeslaných SMS (0 - neomezeno)",
"max-sms-required": "Maximální počet odeslaných SMS je povinný.",
"max-sms-range": "Maximální počet odeslaných SMS nemůže být záporný"
},
"timeinterval": {
"seconds-interval": "{ seconds, plural, 1 {1 vteřina} other {# vteřin} }",
"minutes-interval": "{ minutes, plural, 1 {1 minuta} other {# minut} }",
@ -1574,8 +2099,14 @@
"seconds": "Vteřiny",
"advanced": "Rozšířené"
},
"timeunit": {
"seconds": "Vteřiny",
"minutes": "Minuty",
"hours": "Hodiny",
"days": "Dny"
},
"timewindow": {
"days": "{ days, plural, 1 { den } other {# days } }",
"days": "{ days, plural, 1 { den } other {# dnů } }",
"hours": "{ hours, plural, 0 { hodina } 1 {1 hodina } other {# hodin } }",
"minutes": "{ minutes, plural, 0 { minuta } 1 {1 minuta } other {# minut } }",
"seconds": "{ seconds, plural, 0 { vteřina } 1 {1 vteřina } other {# vteřin } }",
@ -1694,6 +2225,7 @@
"type": "Typ widgetu",
"resources": "Zdroje",
"resource-url": "JavaScript/CSS URL",
"resource-is-module": "Je modulem",
"remove-resource": "Odebrat zdroj",
"add-resource": "Přidat zdroj",
"html": "HTML",
@ -1711,7 +2243,10 @@
"widget-template-load-failed-error": "Nahrání šablony widgetu selhalo!",
"add": "Přidat widget",
"undo": "Vrátit změny widgetu",
"export": "Exportovat widget"
"export": "Exportovat widget",
"no-data": "Nejsou k dispozici žádná data pro zobrazení ve widgetu",
"data-overflow": "Widget zobrazuje {{count}} z {{total}} entit",
"alarm-data-overflow": "Widget zobrazuje alarmy {{allowedEntities}} (maxima možných) entit z {{totalEntities}} entit"
},
"widget-action": {
"header-button": "Tlačítko hlavičky widgetu",
@ -1724,7 +2259,14 @@
"target-dashboard-state-required": "Cílový stav dashboardu je povinný",
"set-entity-from-widget": "Nastavit entitu z widgetu",
"target-dashboard": "Cílový dashboard",
"open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)"
"open-right-layout": "Otevřít rozmístění dashboardu vpravo (mobilní zobrazení)",
"open-in-separate-dialog": "Otevřít v samostatném okně",
"dialog-title": "Název okna",
"dialog-hide-dashboard-toolbar": "Skrýt v okně nástrojovou lištu dashboardu",
"dialog-width": "Šířka okna v procentech vzhledem k šířce obrazovky",
"dialog-height": "Výška okna v procentech vzhledem k výšce obrazovky",
"dialog-size-range-error": "Hodnota procentuální velikosti musí být v rozsahu od 1 do 100.",
"open-new-browser-tab": "Otevřít na nové záložce prohlížeče"
},
"widgets-bundle": {
"current": "Vybraná kategorie",
@ -1891,8 +2433,11 @@
"entity-coordinate-required": "Obě pole, zeměpisná šířka i zeměpisná délka, jsou povinná",
"entity-timeseries-required": "Časové řady entity jsou povinné",
"get-location": "Získat aktuální polohu",
"invalid-date": "Neplatné datum",
"latitude": "Zeměpisná šířka",
"longitude": "Zeměpisná délka",
"min-value-error": "Minimální hodnota je {{value}}",
"max-value-error": "Maximální hodnota je {{value}}",
"not-allowed-entity": "Vybraná entita nemůže mít sdílené atributy",
"no-attribute-selected": "Není vybrán žádný atribut",
"no-datakey-selected": "Není vybrán žádný datový klíč",
@ -1900,7 +2445,10 @@
"no-entity-selected": "Není vybrána žádná entita",
"no-image": "Žádný obrázek",
"no-support-geolocation": "Váš prohlížeč nepodporuje geolokaci",
"no-support-web-camera": "Žádná podporovaná webová kamera",
"no-support-web-camera": "Váš prohlížeč nepodporuje kamery",
"enable-https-use-widget": "Prosím povolte HTTPS abyste mohli používat tento widget",
"no-found-your-camera": "Nelze nalézt vyši kameru",
"no-permission-camera": "Přístup byl zakázán uživatelem / Tato stránka nemá oprávnění použít kameru",
"no-timeseries-selected": "Nejsou vybrány žádné časové řady",
"secret-key": "Tajný klíč",
"secret-key-required": "Tajný klíč je povinný",

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

@ -74,6 +74,7 @@
"admin": {
"general": "General",
"general-settings": "General Settings",
"home-settings": "Home Settings",
"outgoing-mail": "Mail Server",
"outgoing-mail-settings": "Outgoing Mail Server Settings",
"system-settings": "System Settings",
@ -764,7 +765,9 @@
"select-state": "Select target state",
"state-controller": "State controller",
"search": "Search dashboards",
"selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected"
"selected-dashboards": "{ count, plural, 1 {1 dashboard} other {# dashboards} } selected",
"home-dashboard": "Home dashboard",
"home-dashboard-hide-toolbar": "Hide home dashboard toolbar"
},
"datakey": {
"settings": "Settings",

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save