Browse Source

Merge with develop/3.4

pull/6545/head
Igor Kulikov 4 years ago
parent
commit
2422cd7882
  1. 3
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 11
      application/src/main/data/json/system/widget_bundles/charts.json
  3. 8
      application/src/main/data/json/system/widget_bundles/maps.json
  4. 11
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  5. 14
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java
  6. 80
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
  7. 50
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/AbstractCoapAttributesUpdatesIntegrationTest.java
  8. 8
      application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/AbstractCoapAttributesUpdatesProtoIntegrationTest.java
  9. 2
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  10. 253
      ui-ngx/src/app/core/api/entity-data-subscription.ts
  11. 30
      ui-ngx/src/app/core/api/entity-data.service.ts
  12. 3
      ui-ngx/src/app/core/api/widget-api.models.ts
  13. 141
      ui-ngx/src/app/core/api/widget-subscription.ts
  14. 191
      ui-ngx/src/app/core/utils.ts
  15. 32
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts
  16. 12
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  17. 12
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  18. 2
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.html
  19. 4
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts
  20. 27
      ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts
  21. 3
      ui-ngx/src/app/modules/home/components/widget/legend.component.html
  22. 1
      ui-ngx/src/app/modules/home/components/widget/legend.component.scss
  23. 40
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.models.ts
  24. 108
      ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts
  25. 13
      ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts
  26. 169
      ui-ngx/src/app/modules/home/components/widget/lib/maps/common-maps-utils.ts
  27. 2
      ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts
  28. 37
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  29. 21
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts
  30. 24
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  31. 14
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
  32. 13
      ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts
  33. 3
      ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts
  34. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts
  35. 24
      ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts
  36. 23
      ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts
  37. 3
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  38. 8
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  39. 169
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  40. 66
      ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts
  41. 11
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  42. 164
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  43. 10
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  44. 99
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  45. 23
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  46. 8
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  47. 13
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  48. 16
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  49. 7
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss
  50. 33
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  51. 31
      ui-ngx/src/app/shared/models/widget.models.ts
  52. 14
      ui-ngx/src/assets/locale/locale.constant-en_US.json

3
application/src/main/data/json/system/widget_bundles/cards.json

@ -54,9 +54,10 @@
"resources": [],
"templateHtml": "<tb-timeseries-table-widget \n [ctx]=\"ctx\">\n</tb-timeseries-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onLatestDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"enableSearch\": {\n \"title\": \"Enable search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"useEntityLabel\": {\n \"title\": \"Use entity label in tab name\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"disableStickyHeader\": {\n \"title\": \"Disable sticky header\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableSearch\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"useEntityLabel\",\n \"defaultPageSize\",\n \"hideEmptyLines\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n }\n ]\n}",
"latestDataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"LatestDataKeySettings\",\n \"properties\": {\n \"show\": {\n \"title\": \"Show latest data column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"order\": {\n \"title\": \"Latest data column order\",\n \"type\": \"number\"\n },\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"show\",\n {\n \"key\": \"order\",\n \"condition\": \"model.show === true\"\n },\n {\n \"key\": \"useCellStyleFunction\",\n \"condition\": \"model.show === true\"\n },\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_style_fn\",\n \"condition\": \"model.show === true && model.useCellStyleFunction === true\"\n },\n {\n \"key\": \"useCellContentFunction\",\n \"condition\": \"model.show === true\"\n },\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_content_fn\",\n \"condition\": \"model.show === true && model.useCellContentFunction === true\"\n }\n ]\n}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\"}"
}
},

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

@ -146,7 +146,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.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.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, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true,\n hasAdditionalLatestDataKeys: 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.getLatestDataKeySettingsSchema = function() {\n return TbFlot.latestDatakeySettingsSchema();\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}}"
@ -164,10 +164,11 @@
"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.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.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\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.getLatestDataKeySettingsSchema = function() {\n return TbFlot.latestDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\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}"
"latestDataKeySettingsSchema": "{}",
"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 Line Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null}"
}
},
{
@ -182,10 +183,10 @@
"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",
"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.onLatestDataUpdated = function() {\n self.ctx.flot.latestDataUpdate();\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.getLatestDataKeySettingsSchema = function() {\n return TbFlot.latestDatakeySettingsSchema();\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n\nself.typeParameters = function() {\n return {\n hasAdditionalLatestDataKeys: true\n };\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\":\"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 Bar Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{}}"
}
}
]

8
application/src/main/data/json/system/widget_bundles/maps.json

File diff suppressed because one or more lines are too long

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

@ -219,10 +219,13 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
@Override
public void onSuccess(@Nullable TbEntityDataSubCtx theCtx) {
try {
if (cmd.getLatestCmd() != null) {
handleLatestCmd(theCtx, cmd.getLatestCmd());
} else if (cmd.getTsCmd() != null) {
handleTimeSeriesCmd(theCtx, cmd.getTsCmd());
if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) {
if (cmd.getLatestCmd() != null) {
handleLatestCmd(theCtx, cmd.getLatestCmd());
}
if (cmd.getTsCmd() != null) {
handleTimeSeriesCmd(theCtx, cmd.getTsCmd());
}
} else if (!theCtx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription());
wsService.sendWsMsg(theCtx.getSessionId(), update);

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

@ -167,7 +167,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
}
private TbSubscription createAttrSub(EntityData entityData, int subIdx, EntityKeyType keysType, TbAttributeSubscriptionScope scope, List<EntityKey> subKeys) {
Map<String, Long> keyStates = buildKeyStats(entityData, keysType, subKeys);
Map<String, Long> keyStates = buildKeyStats(entityData, keysType, subKeys, true);
log.trace("[{}][{}][{}] Creating attributes subscription for [{}] with keys: {}", serviceId, cmdId, subIdx, entityData.getEntityId(), keyStates);
return TbAttributeSubscription.builder()
.serviceId(serviceId)
@ -183,7 +183,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
}
private TbSubscription createTsSub(EntityData entityData, int subIdx, List<EntityKey> subKeys, boolean latestValues, long startTs, long endTs) {
Map<String, Long> keyStates = buildKeyStats(entityData, EntityKeyType.TIME_SERIES, subKeys);
Map<String, Long> keyStates = buildKeyStats(entityData, EntityKeyType.TIME_SERIES, subKeys, latestValues);
if (!latestValues && entityData.getTimeseries() != null) {
entityData.getTimeseries().forEach((k, v) -> {
long ts = Arrays.stream(v).map(TsValue::getTs).max(Long::compareTo).orElse(0L);
@ -211,15 +211,17 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
sendWsMsg(sessionId, subscriptionUpdate, keyType, true);
}
private Map<String, Long> buildKeyStats(EntityData entityData, EntityKeyType keysType, List<EntityKey> subKeys) {
private Map<String, Long> buildKeyStats(EntityData entityData, EntityKeyType keysType, List<EntityKey> subKeys, boolean latestValues) {
Map<String, Long> keyStates = new HashMap<>();
subKeys.forEach(key -> keyStates.put(key.getKey(), 0L));
if (entityData.getLatest() != null) {
if (latestValues && entityData.getLatest() != null) {
Map<String, TsValue> currentValues = entityData.getLatest().get(keysType);
if (currentValues != null) {
currentValues.forEach((k, v) -> {
log.trace("[{}][{}] Updating key: {} with ts: {}", serviceId, cmdId, k, v.getTs());
keyStates.put(k, v.getTs());
if (subKeys.contains(new EntityKey(keysType, k))) {
log.trace("[{}][{}] Updating key: {} with ts: {}", serviceId, cmdId, k, v.getTs());
keyStates.put(k, v.getTs());
}
});
}
}

80
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java

@ -19,6 +19,7 @@ import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
@ -55,6 +56,7 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
private LatestValueCmd latestValueCmd;
@Getter
private final int maxEntitiesPerDataSubscription;
private Map<EntityId, Map<String, TsValue>> latestTsEntityData;
public TbEntityDataSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService,
TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService,
@ -63,6 +65,12 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
this.maxEntitiesPerDataSubscription = maxEntitiesPerDataSubscription;
}
@Override
public void fetchData() {
super.fetchData();
this.updateLatestTsData(this.data);
}
@Override
protected void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues) {
EntityId entityId = subToEntityIdMap.get(subscriptionUpdate.getSubscriptionId());
@ -123,40 +131,37 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
Object[] data = (Object[]) v.get(0);
tsUpdate.computeIfAbsent(k, key -> new ArrayList<>()).add(new TsValue((Long) data[0], (String) data[1]));
});
EntityData entityData = getDataForEntity(entityId);
if (entityData != null && entityData.getLatest() != null) {
Map<String, TsValue> latestCtxValues = entityData.getLatest().get(keyType);
log.trace("[{}][{}][{}] Going to compare update with {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), latestCtxValues);
if (latestCtxValues != null) {
latestCtxValues.forEach((k, v) -> {
List<TsValue> updateList = tsUpdate.get(k);
if (updateList != null) {
for (TsValue update : new ArrayList<>(updateList)) {
if (update.getTs() < v.getTs()) {
log.trace("[{}][{}][{}] Removed stale update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
// Looks like this is redundant feature and our UI is ready to merge the updates.
//updateList.remove(update);
} else if ((update.getTs() == v.getTs() && update.getValue().equals(v.getValue()))) {
log.trace("[{}][{}][{}] Removed duplicate update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
updateList.remove(update);
}
if (updateList.isEmpty()) {
tsUpdate.remove(k);
}
Map<String, TsValue> latestCtxValues = getLatestTsValuesForEntity(entityId);
log.trace("[{}][{}][{}] Going to compare update with {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), latestCtxValues);
if (latestCtxValues != null) {
latestCtxValues.forEach((k, v) -> {
List<TsValue> updateList = tsUpdate.get(k);
if (updateList != null) {
for (TsValue update : new ArrayList<>(updateList)) {
if (update.getTs() < v.getTs()) {
log.trace("[{}][{}][{}] Removed stale update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
// Looks like this is redundant feature and our UI is ready to merge the updates.
//updateList.remove(update);
} else if ((update.getTs() == v.getTs() && update.getValue().equals(v.getValue()))) {
log.trace("[{}][{}][{}] Removed duplicate update for key: {} and ts: {}", sessionId, cmdId, subscriptionUpdate.getSubscriptionId(), k, update.getTs());
updateList.remove(update);
}
if (updateList.isEmpty()) {
tsUpdate.remove(k);
}
}
});
//Setting new values
tsUpdate.forEach((k, v) -> {
Optional<TsValue> maxValue = v.stream().max(Comparator.comparingLong(TsValue::getTs));
maxValue.ifPresent(max -> latestCtxValues.put(k, max));
});
}
}
});
//Setting new values
tsUpdate.forEach((k, v) -> {
Optional<TsValue> maxValue = v.stream().max(Comparator.comparingLong(TsValue::getTs));
maxValue.ifPresent(max -> latestCtxValues.put(k, max));
});
}
if (!tsUpdate.isEmpty()) {
Map<String, TsValue[]> tsMap = new HashMap<>();
tsUpdate.forEach((key, tsValue) -> tsMap.put(key, tsValue.toArray(new TsValue[tsValue.size()])));
entityData = new EntityData(entityId, null, tsMap);
EntityData entityData = new EntityData(entityId, null, tsMap);
wsService.sendWsMsg(sessionId, new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData), maxEntitiesPerDataSubscription));
}
}
@ -165,8 +170,27 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
return data.getData().stream().filter(item -> item.getEntityId().equals(entityId)).findFirst().orElse(null);
}
private Map<String, TsValue> getLatestTsValuesForEntity(EntityId entityId) {
return latestTsEntityData.get(entityId);
}
private void updateLatestTsData(PageData<EntityData> data) {
latestTsEntityData = new HashMap<>();
data.getData().stream().forEach(entityData -> {
Map<String, TsValue> latestTsMap = new HashMap<>();
latestTsEntityData.put(entityData.getEntityId(), latestTsMap);
if (entityData.getLatest() != null) {
Map<String, TsValue> latestTsValues = entityData.getLatest().get(EntityKeyType.TIME_SERIES);
if (latestTsValues != null) {
latestTsValues.forEach(latestTsMap::put);
}
}
});
}
@Override
public synchronized void doUpdate(Map<EntityId, EntityData> newDataMap) {
this.updateLatestTsData(this.data);
List<Integer> subIdsToCancel = new ArrayList<>();
List<TbSubscription> subsToAdd = new ArrayList<>();
Set<EntityId> currentSubs = new HashSet<>();

50
application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/AbstractCoapAttributesUpdatesIntegrationTest.java

@ -17,19 +17,23 @@ package org.thingsboard.server.transport.coap.attributes.updates;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.core.CoapClient;
import org.awaitility.Awaitility;
import org.eclipse.californium.core.CoapHandler;
import org.eclipse.californium.core.CoapObserveRelation;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.coap.CoAP;
import org.eclipse.californium.core.coap.Request;
import org.eclipse.californium.core.server.resources.Resource;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.coapserver.DefaultCoapServerService;
import org.thingsboard.server.common.transport.service.DefaultTransportService;
import org.thingsboard.server.transport.coap.CoapTransportResource;
import org.thingsboard.server.transport.coap.attributes.AbstractCoapAttributesIntegrationTest;
import org.thingsboard.server.common.msg.session.FeatureType;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -37,6 +41,7 @@ import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.spy;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@ -47,8 +52,20 @@ public abstract class AbstractCoapAttributesUpdatesIntegrationTest extends Abstr
protected static final String POST_ATTRIBUTES_PAYLOAD_ON_CURRENT_STATE_NOTIFICATION = "{\"attribute1\":\"value\",\"attribute2\":false,\"attribute3\":41.0,\"attribute4\":72," +
"\"attribute5\":{\"someNumber\":41,\"someArray\":[],\"someNestedObject\":{\"key\":\"value\"}}}";
CoapTransportResource coapTransportResource;
@Autowired
DefaultCoapServerService defaultCoapServerService;
@Autowired
DefaultTransportService defaultTransportService;
@Before
public void beforeTest() throws Exception {
Resource api = defaultCoapServerService.getCoapServer().getRoot().getChild("api");
coapTransportResource = spy( (CoapTransportResource) api.getChild("v1") );
api.delete(api.getChild("v1") );
api.add(coapTransportResource);
processBeforeTest("Test Subscribe to attribute updates", null, null);
}
@ -89,21 +106,23 @@ public abstract class AbstractCoapAttributesUpdatesIntegrationTest extends Abstr
}
latch = new CountDownLatch(1);
int expectedObserveCnt = callback.getObserve().intValue() + 1;
doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
latch.await(3, TimeUnit.SECONDS);
validateUpdateAttributesResponse(callback);
validateUpdateAttributesResponse(callback, expectedObserveCnt);
latch = new CountDownLatch(1);
latch = new CountDownLatch(1);
int expectedObserveBeforeDeleteCnt = callback.getObserve().intValue() + 1;
doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class);
latch.await(3, TimeUnit.SECONDS);
validateDeleteAttributesResponse(callback);
validateDeleteAttributesResponse(callback, expectedObserveBeforeDeleteCnt);
observeRelation.proactiveCancel();
assertTrue(observeRelation.isCanceled());
awaitClientAfterCancelObserve();
}
protected void validateCurrentStateAttributesResponse(TestCoapCallback callback) throws InvalidProtocolBufferException {
@ -124,20 +143,20 @@ public abstract class AbstractCoapAttributesUpdatesIntegrationTest extends Abstr
assertEquals("{}", response);
}
protected void validateUpdateAttributesResponse(TestCoapCallback callback) throws InvalidProtocolBufferException {
protected void validateUpdateAttributesResponse(TestCoapCallback callback, int expectedObserveCnt) throws InvalidProtocolBufferException {
assertNotNull(callback.getPayloadBytes());
assertNotNull(callback.getObserve());
assertEquals(CoAP.ResponseCode.CONTENT, callback.getResponseCode());
assertEquals(1, callback.getObserve().intValue());
assertEquals(expectedObserveCnt, callback.getObserve().intValue());
String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8);
assertEquals(JacksonUtil.toJsonNode(POST_ATTRIBUTES_PAYLOAD), JacksonUtil.toJsonNode(response));
}
protected void validateDeleteAttributesResponse(TestCoapCallback callback) throws InvalidProtocolBufferException {
protected void validateDeleteAttributesResponse(TestCoapCallback callback, int expectedObserveCnt) throws InvalidProtocolBufferException {
assertNotNull(callback.getPayloadBytes());
assertNotNull(callback.getObserve());
assertEquals(CoAP.ResponseCode.CONTENT, callback.getResponseCode());
assertEquals(2, callback.getObserve().intValue());
assertEquals(expectedObserveCnt, callback.getObserve().intValue());
String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8);
assertEquals(JacksonUtil.toJsonNode(RESPONSE_ATTRIBUTES_PAYLOAD_DELETED), JacksonUtil.toJsonNode(response));
}
@ -180,4 +199,13 @@ public abstract class AbstractCoapAttributesUpdatesIntegrationTest extends Abstr
}
}
private void awaitClientAfterCancelObserve() {
Awaitility.await("awaitClientAfterCancelObserve")
.pollInterval(10, TimeUnit.MILLISECONDS)
.atMost(5, TimeUnit.SECONDS)
.until(()->{
log.trace("awaiting defaultTransportService.sessions is empty");
return defaultTransportService.sessions.isEmpty();});
}
}

8
application/src/test/java/org/thingsboard/server/transport/coap/attributes/updates/AbstractCoapAttributesUpdatesProtoIntegrationTest.java

@ -92,11 +92,11 @@ public abstract class AbstractCoapAttributesUpdatesProtoIntegrationTest extends
assertEquals(0, callback.getObserve().intValue());
}
protected void validateUpdateAttributesResponse(TestCoapCallback callback) throws InvalidProtocolBufferException {
protected void validateUpdateAttributesResponse(TestCoapCallback callback, int expectedObserveCnt) throws InvalidProtocolBufferException {
assertNotNull(callback.getPayloadBytes());
assertNotNull(callback.getObserve());
assertEquals(CoAP.ResponseCode.CONTENT, callback.getResponseCode());
assertEquals(1, callback.getObserve().intValue());
assertEquals(expectedObserveCnt, callback.getObserve().intValue());
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
List<TransportProtos.TsKvProto> tsKvProtoList = getTsKvProtoList();
attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList);
@ -112,11 +112,11 @@ public abstract class AbstractCoapAttributesUpdatesProtoIntegrationTest extends
}
protected void validateDeleteAttributesResponse(TestCoapCallback callback) throws InvalidProtocolBufferException {
protected void validateDeleteAttributesResponse(TestCoapCallback callback, int expectedObserveCnt) throws InvalidProtocolBufferException {
assertNotNull(callback.getPayloadBytes());
assertNotNull(callback.getObserve());
assertEquals(CoAP.ResponseCode.CONTENT, callback.getResponseCode());
assertEquals(2, callback.getObserve().intValue());
assertEquals(expectedObserveCnt, callback.getObserve().intValue());
TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5");

2
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -180,7 +180,7 @@ public class DefaultTransportService implements TransportService {
protected ExecutorService transportCallbackExecutor;
private ExecutorService mainConsumerExecutor;
private final ConcurrentMap<UUID, SessionMetaData> sessions = new ConcurrentHashMap<>();
public final ConcurrentMap<UUID, SessionMetaData> sessions = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, SessionActivityData> sessionsActivity = new ConcurrentHashMap<>();
private final Map<String, RpcRequestMetadata> toServerRpcPendingMap = new ConcurrentHashMap<>();

253
ui-ngx/src/app/core/api/entity-data-subscription.ts

@ -49,7 +49,8 @@ import Timeout = NodeJS.Timeout;
declare type DataKeyFunction = (time: number, prevValue: any) => any;
declare type DataKeyPostFunction = (time: number, value: any, prevValue: any, timePrev: number, prevOrigValue: any) => any;
declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number,
dataKeyIndex: number, detectChanges: boolean, isLatest: boolean) => void;
export interface SubscriptionDataKey {
name: string;
@ -59,8 +60,10 @@ export interface SubscriptionDataKey {
postFuncBody: string;
postFunc?: DataKeyPostFunction;
index?: number;
listIndex?: number;
key?: string;
lastUpdateTime?: number;
latest?: boolean;
}
export interface EntityDataSubscriptionOptions {
@ -104,9 +107,11 @@ export class EntityDataSubscription {
private entityIdToDataIndex: {[id: string]: number};
private frequency: number;
private latestFrequency: number;
private tickScheduledTime = 0;
private tickElapsed = 0;
private timer: Timeout;
private timeseriesTimer: Timeout;
private latestTimer: Timeout;
private dataResolved = false;
private started = false;
@ -135,9 +140,9 @@ export class EntityDataSubscription {
if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.datasourceType === DatasourceType.function) {
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}`;
key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
} else {
key = `${dataKey.name}_${dataKey.type}`;
key = `${dataKey.name}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`;
}
let dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
if (!dataKeysList) {
@ -145,6 +150,7 @@ export class EntityDataSubscription {
this.dataKeys[key] = dataKeysList;
}
dataKeysList.push(dataKey);
dataKey.listIndex = dataKeysList.length - 1;
} else {
key = String(objectHashCode(dataKey));
this.dataKeys[key] = dataKey;
@ -154,9 +160,13 @@ export class EntityDataSubscription {
}
public unsubscribe() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
if (this.timeseriesTimer) {
clearTimeout(this.timeseriesTimer);
this.timeseriesTimer = null;
}
if (this.latestTimer) {
clearTimeout(this.latestTimer);
this.latestTimer = null;
}
if (this.datasourceType === DatasourceType.entity || this.datasourceType === DatasourceType.entityCount) {
if (this.subscriber) {
@ -213,11 +223,20 @@ export class EntityDataSubscription {
dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name })
);
this.tsFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.timeseries).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
this.tsFields = this.entityDataSubscriptionOptions.dataKeys.
filter(dataKey => dataKey.type === DataKeyType.timeseries && !dataKey.latest).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
);
this.latestValues = this.attrFields.concat(this.tsFields);
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const latestTsFields = this.entityDataSubscriptionOptions.dataKeys.
filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest).map(
dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name })
);
this.latestValues = this.attrFields.concat(latestTsFields);
} else {
this.latestValues = this.attrFields.concat(this.tsFields);
}
this.subscriber = new TelemetrySubscriber(this.telemetryService);
this.dataCommand = new EntityDataCmd();
@ -467,6 +486,11 @@ export class EntityDataSubscription {
};
}
}
if (this.latestValues.length > 0) {
cmd.latestCmd = {
keys: this.latestValues
};
}
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
if (this.latestValues.length > 0) {
cmd.latestCmd = {
@ -478,21 +502,22 @@ export class EntityDataSubscription {
private startFunction() {
this.frequency = 1000;
this.latestFrequency = 1000;
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
this.frequency = Math.min(this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.interval, 5000);
}
this.tickScheduledTime = this.utils.currentPerfTime();
if (this.history) {
this.onTick(true);
} else {
this.timer = setTimeout(this.onTick.bind(this, true), 0);
}
this.generateData(true);
}
private prepareData() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
if (this.timeseriesTimer) {
clearTimeout(this.timeseriesTimer);
this.timeseriesTimer = null;
}
if (this.latestTimer) {
clearTimeout(this.latestTimer);
this.latestTimer = null;
}
if (this.dataAggregators) {
@ -509,19 +534,23 @@ export class EntityDataSubscription {
for (const key of Object.keys(this.dataKeys)) {
const dataKeysList = this.dataKeys[key] as Array<SubscriptionDataKey>;
dataKeysList.forEach((subscriptionDataKey) => {
tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`);
if (!subscriptionDataKey.latest) {
tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`);
}
});
}
} else {
tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : [];
}
for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) {
if (this.datasourceType === DatasourceType.function) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
DataKeyType.function, dataIndex, this.notifyListener.bind(this));
} else if (tsKeyNames.length) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this));
if (tsKeyNames.length) {
if (this.datasourceType === DatasourceType.function) {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
DataKeyType.function, dataIndex, this.notifyListener.bind(this));
} else {
this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames,
DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this));
}
}
}
}
@ -540,7 +569,8 @@ export class EntityDataSubscription {
this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
const dataKeysList = dataKey as Array<SubscriptionDataKey>;
for (let index = 0; index < dataKeysList.length; index++) {
this.datasourceData[dataIndex][key + '_' + index] = {
const datasourceKey = `${key}_${index}`;
this.datasourceData[dataIndex][datasourceKey] = {
data: []
};
}
@ -637,19 +667,21 @@ export class EntityDataSubscription {
}
}
private notifyListener(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {
private notifyListener(data: DataSetHolder, dataIndex: number, dataKeyIndex: number, detectChanges: boolean, isLatest: boolean) {
this.listener.dataUpdated(data,
this.listener.configDatasourceIndex,
dataIndex, dataKeyIndex, detectChanges);
dataIndex, dataKeyIndex, detectChanges, isLatest);
}
private processEntityData(entityData: EntityData, dataIndex: number, isUpdate: boolean,
dataUpdatedCb: DataUpdatedCb) {
if (this.entityDataSubscriptionOptions.type === widgetType.latest && entityData.latest) {
if ((this.entityDataSubscriptionOptions.type === widgetType.latest ||
this.entityDataSubscriptionOptions.type === widgetType.timeseries) && entityData.latest) {
for (const type of Object.keys(entityData.latest)) {
const subscriptionData = this.toSubscriptionData(entityData.latest[type], false);
const dataKeyType = entityKeyTypeToDataKeyType(EntityKeyType[type]);
this.onData(subscriptionData, dataKeyType, dataIndex, true, dataUpdatedCb);
this.onData(subscriptionData, dataKeyType, dataIndex, true,
this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb);
}
}
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && entityData.timeseries) {
@ -660,7 +692,7 @@ export class EntityDataSubscription {
if (!isUpdate) {
prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => {
this.onData(data, this.datasourceType === DatasourceType.function ?
DataKeyType.function : DataKeyType.timeseries, dataIndex, detectChanges, dataUpdatedCb);
DataKeyType.function : DataKeyType.timeseries, dataIndex, detectChanges, false, dataUpdatedCb);
});
}
dataAggregator.onData({data: subscriptionData}, false, this.history, true);
@ -668,16 +700,16 @@ export class EntityDataSubscription {
dataAggregator.updateOnDataCb(prevDataCb);
}
} else if (!this.history && !isUpdate) {
this.onData(subscriptionData, DataKeyType.timeseries, dataIndex, true, dataUpdatedCb);
this.onData(subscriptionData, DataKeyType.timeseries, dataIndex, true, false, dataUpdatedCb);
}
}
}
private onData(sourceData: SubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean,
dataUpdatedCb: DataUpdatedCb) {
isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) {
for (const keyName of Object.keys(sourceData)) {
const keyData = sourceData[keyName];
const key = `${keyName}_${type}`;
const key = `${keyName}_${type}${isTsLatest ? '_latest' : ''}`;
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) {
const datasourceKey = `${key}_${keyIndex}`;
@ -689,7 +721,7 @@ export class EntityDataSubscription {
let datasourceKeyData: DataSet;
let datasourceOrigKeyData: DataSet;
let update = false;
if (this.realtime) {
if (this.realtime && !isTsLatest) {
datasourceKeyData = [];
datasourceOrigKeyData = [];
} else {
@ -704,7 +736,7 @@ export class EntityDataSubscription {
prevOrigSeries = [0, 0];
}
this.datasourceOrigData[dataIndex][datasourceKey].data = [];
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && !isTsLatest) {
keyData.forEach((keySeries) => {
let series = keySeries;
const time = series[0];
@ -719,7 +751,7 @@ export class EntityDataSubscription {
prevSeries = series;
});
update = true;
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest || isTsLatest) {
if (keyData.length > 0) {
let series = keyData[0];
const time = series[0];
@ -735,7 +767,7 @@ export class EntityDataSubscription {
}
if (update) {
this.datasourceData[dataIndex][datasourceKey].data = data;
dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges);
dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges, isTsLatest);
}
}
}
@ -774,7 +806,7 @@ export class EntityDataSubscription {
dataUpdatedCb: DataUpdatedCb): DataAggregator {
return new DataAggregator(
(data, detectChanges) => {
this.onData(data, dataKeyType, dataIndex, detectChanges, dataUpdatedCb);
this.onData(data, dataKeyType, dataIndex, detectChanges, false, dataUpdatedCb);
},
tsKeyNames,
subsTw,
@ -783,17 +815,17 @@ export class EntityDataSubscription {
);
}
private generateSeries(dataKey: SubscriptionDataKey, index: number, startTime: number, endTime: number): [number, any][] {
private generateSeries(dataKey: SubscriptionDataKey, startTime: number, endTime: number): [number, any][] {
const data: [number, any][] = [];
let prevSeries: [number, any];
const datasourceDataKey = `${dataKey.key}_${index}`;
const datasourceDataKey = `${dataKey.key}_${dataKey.listIndex}`;
const datasourceKeyData = this.datasourceData[0][datasourceDataKey].data;
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else {
prevSeries = [0, 0];
}
for (let time = startTime; time <= endTime && (this.timer || this.history); time += this.frequency) {
for (let time = startTime; time <= endTime && (this.timeseriesTimer || this.history); time += this.frequency) {
const value = dataKey.func(time, prevSeries[1]);
const series: [number, any] = [time, value];
data.push(series);
@ -807,7 +839,8 @@ export class EntityDataSubscription {
private generateLatest(dataKey: SubscriptionDataKey, detectChanges: boolean) {
let prevSeries: [number, any];
const datasourceKeyData = this.datasourceData[0][dataKey.key].data;
const datasourceKey = dataKey.latest ? `${dataKey.key}_${dataKey.listIndex}` : dataKey.key;
const datasourceKeyData = this.datasourceData[0][datasourceKey].data;
if (datasourceKeyData.length > 0) {
prevSeries = datasourceKeyData[datasourceKeyData.length - 1];
} else {
@ -816,78 +849,100 @@ export class EntityDataSubscription {
const time = Date.now() + this.latestTsOffset;
const value = dataKey.func(time, prevSeries[1]);
const series: [number, any] = [time, value];
this.datasourceData[0][dataKey.key].data = [series];
this.listener.dataUpdated(this.datasourceData[0][dataKey.key],
this.datasourceData[0][datasourceKey].data = [series];
this.listener.dataUpdated(this.datasourceData[0][datasourceKey],
this.listener.configDatasourceIndex,
0,
dataKey.index, detectChanges);
dataKey.index, detectChanges, dataKey.latest);
}
private onTick(detectChanges: boolean) {
const now = this.utils.currentPerfTime();
this.tickElapsed += now - this.tickScheduledTime;
this.tickScheduledTime = now;
if (this.timer) {
clearTimeout(this.timer);
}
private generateData(detectChanges: boolean) {
let key: string;
let tsDataKeys: SubscriptionDataKey[] = [];
let latestDataKeys: SubscriptionDataKey[] = [];
if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) {
let startTime: number;
let endTime: number;
let delta: number;
const generatedData: SubscriptionDataHolder = {
data: {}
};
if (!this.history) {
delta = Math.floor(this.tickElapsed / this.frequency);
}
const deltaElapsed = this.history ? this.frequency : delta * this.frequency;
this.tickElapsed = this.tickElapsed - deltaElapsed;
for (key of Object.keys(this.dataKeys)) {
const dataKeyList = this.dataKeys[key] as Array<SubscriptionDataKey>;
for (let index = 0; index < dataKeyList.length && (this.timer || this.history); index ++) {
const dataKey = dataKeyList[index];
if (!startTime) {
if (this.realtime) {
if (dataKey.lastUpdateTime) {
startTime = dataKey.lastUpdateTime + this.frequency;
endTime = dataKey.lastUpdateTime + deltaElapsed;
} else {
startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs +
this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency;
if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) {
const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit;
startTime = Math.max(time, startTime);
}
}
} else {
startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs +
this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs +
this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
}
if (this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval) {
const currentTime = getCurrentTime().valueOf() + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
endTime = Math.min(currentTime, endTime);
}
}
generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, index, startTime, endTime);
}
}
if (this.dataAggregators && this.dataAggregators.length) {
this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges);
tsDataKeys = tsDataKeys.concat(dataKeyList.filter(dataKey => !dataKey.latest));
latestDataKeys = latestDataKeys.concat(dataKeyList.filter(dataKey => dataKey.latest));
}
} else if (this.entityDataSubscriptionOptions.type === widgetType.latest) {
for (key of Object.keys(this.dataKeys)) {
this.generateLatest(this.dataKeys[key] as SubscriptionDataKey, detectChanges);
latestDataKeys.push(this.dataKeys[key] as SubscriptionDataKey);
}
}
if (tsDataKeys.length) {
this.timeseriesTimer = setTimeout(this.onTimeseriesTick.bind(this, tsDataKeys, true), 0);
}
if (latestDataKeys.length) {
this.onLatestTick(latestDataKeys, detectChanges);
}
}
private onTimeseriesTick(tsDataKeys: SubscriptionDataKey[], detectChanges: boolean) {
const now = this.utils.currentPerfTime();
this.tickElapsed += now - this.tickScheduledTime;
this.tickScheduledTime = now;
if (this.timeseriesTimer) {
clearTimeout(this.timeseriesTimer);
}
let startTime: number;
let endTime: number;
let delta: number;
const generatedData: SubscriptionDataHolder = {
data: {}
};
if (!this.history) {
delta = Math.floor(this.tickElapsed / this.frequency);
}
const deltaElapsed = this.history ? this.frequency : delta * this.frequency;
this.tickElapsed = this.tickElapsed - deltaElapsed;
for (let index = 0; index < tsDataKeys.length && (this.timeseriesTimer || this.history); index ++) {
const dataKey = tsDataKeys[index];
if (!startTime) {
if (this.realtime) {
if (dataKey.lastUpdateTime) {
startTime = dataKey.lastUpdateTime + this.frequency;
endTime = dataKey.lastUpdateTime + deltaElapsed;
} else {
startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.startTs +
this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
endTime = startTime + this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs + this.frequency;
if (this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.type === AggregationType.NONE) {
const time = endTime - this.frequency * this.entityDataSubscriptionOptions.subscriptionTimewindow.aggregation.limit;
startTime = Math.max(time, startTime);
}
}
} else {
startTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.startTimeMs +
this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
endTime = this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow.endTimeMs +
this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
}
if (this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval) {
const currentTime = getCurrentTime().valueOf() + this.entityDataSubscriptionOptions.subscriptionTimewindow.tsOffset;
endTime = Math.min(currentTime, endTime);
}
}
generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, startTime, endTime);
}
if (this.dataAggregators && this.dataAggregators.length) {
this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges);
}
if (!this.history) {
this.timer = setTimeout(this.onTick.bind(this, true), this.frequency);
this.timeseriesTimer = setTimeout(this.onTimeseriesTick.bind(this, tsDataKeys, true), this.frequency);
}
}
private onLatestTick(latestDataKeys: SubscriptionDataKey[], detectChanges: boolean) {
if (this.latestTimer) {
clearTimeout(this.latestTimer);
}
latestDataKeys.forEach(dataKey => {
this.generateLatest(dataKey, detectChanges);
});
this.latestTimer = setTimeout(this.onLatestTick.bind(this, latestDataKeys, true), this.latestFrequency);
}
}

30
ui-ngx/src/app/core/api/entity-data.service.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { DataSetHolder, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models';
import { DataKey, DataSetHolder, Datasource, DatasourceType, widgetType } from '@shared/models/widget.models';
import { SubscriptionTimewindow } from '@shared/models/time/time.models';
import { EntityData, EntityDataPageLink, KeyFilter } from '@shared/models/query/query.models';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
@ -38,7 +38,8 @@ export interface EntityDataListener {
dataLoaded: (pageData: PageData<EntityData>,
data: Array<Array<DataSetHolder>>,
datasourceIndex: number, pageLink: EntityDataPageLink) => void;
dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number,
detectChanges: boolean, isLatest: boolean) => void;
initialPageDataChanged?: (nextPageData: PageData<EntityData>) => void;
forceReInit?: () => void;
updateRealtimeSubscription?: () => SubscriptionTimewindow;
@ -94,6 +95,7 @@ export class EntityDataService {
if (listener.subscription) {
if (listener.subscriptionType === widgetType.timeseries) {
listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
} else if (listener.subscriptionType === widgetType.latest) {
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
}
@ -122,6 +124,7 @@ export class EntityDataService {
listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils);
if (listener.subscriptionType === widgetType.timeseries) {
listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow);
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
} else if (listener.subscriptionType === widgetType.latest) {
listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset;
}
@ -143,14 +146,13 @@ export class EntityDataService {
ignoreDataUpdateOnIntervalTick: boolean): EntityDataSubscriptionOptions {
const subscriptionDataKeys: Array<SubscriptionDataKey> = [];
datasource.dataKeys.forEach((dataKey) => {
const subscriptionDataKey: SubscriptionDataKey = {
name: dataKey.name,
type: dataKey.type,
funcBody: dataKey.funcBody,
postFuncBody: dataKey.postFuncBody
};
subscriptionDataKeys.push(subscriptionDataKey);
subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, false));
});
if (datasource.latestDataKeys) {
datasource.latestDataKeys.forEach((dataKey) => {
subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, true));
});
}
const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = {
datasourceType: datasource.type,
dataKeys: subscriptionDataKeys,
@ -169,4 +171,14 @@ export class EntityDataService {
entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick = ignoreDataUpdateOnIntervalTick;
return entityDataSubscriptionOptions;
}
private toSubscriptionDataKey(dataKey: DataKey, latest: boolean): SubscriptionDataKey {
return {
name: dataKey.name,
type: dataKey.type,
funcBody: dataKey.funcBody,
postFuncBody: dataKey.postFuncBody,
latest
};
}
}

3
ui-ngx/src/app/core/api/widget-api.models.ts

@ -219,7 +219,9 @@ export interface SubscriptionMessage {
export interface WidgetSubscriptionCallbacks {
onDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void;
onLatestDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void;
onDataUpdateError?: (subscription: IWidgetSubscription, e: any) => void;
onLatestDataUpdateError?: (subscription: IWidgetSubscription, e: any) => void;
onSubscriptionMessage?: (subscription: IWidgetSubscription, message: SubscriptionMessage) => void;
onInitialPageDataChanged?: (subscription: IWidgetSubscription, nextPageData: PageData<EntityData>) => void;
forceReInit?: () => void;
@ -282,6 +284,7 @@ export interface IWidgetSubscription {
dataPages?: PageData<Array<DatasourceData>>[];
datasources?: Array<Datasource>;
data?: Array<DatasourceData>;
latestData?: Array<DatasourceData>;
hiddenData?: Array<{data: DataSet}>;
timeWindowConfig?: Timewindow;
timeWindow?: WidgetTimewindow;

141
ui-ngx/src/app/core/api/widget-subscription.ts

@ -52,7 +52,14 @@ import {
import { forkJoin, Observable, of, ReplaySubject, Subject, throwError, timer } from 'rxjs';
import { CancelAnimationFrame } from '@core/services/raf.service';
import { EntityType } from '@shared/models/entity-type.models';
import { createLabelFromDatasource, deepClone, isDefined, isDefinedAndNotNull, isEqual } from '@core/utils';
import {
createLabelFromDatasource, createLabelFromPattern,
deepClone, flatFormattedData,
formattedDataFormDatasourceData,
isDefined,
isDefinedAndNotNull,
isEqual
} from '@core/utils';
import { EntityId } from '@app/shared/models/id/entity-id';
import * as moment_ from 'moment';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
@ -99,6 +106,7 @@ export class WidgetSubscription implements IWidgetSubscription {
configuredDatasources: Array<Datasource>;
data: Array<DatasourceData>;
latestData: Array<DatasourceData>;
datasources: Array<Datasource>;
hiddenData: Array<DataSetHolder>;
legendData: LegendData;
@ -140,6 +148,7 @@ export class WidgetSubscription implements IWidgetSubscription {
executingSubjects: Array<Subject<any>>;
subscribed = false;
hasLatestData = false;
widgetTimewindowChangedSubject: Subject<WidgetTimewindow> = new ReplaySubject<WidgetTimewindow>();
widgetTimewindowChanged$ = this.widgetTimewindowChangedSubject.asObservable().pipe(
@ -205,7 +214,9 @@ export class WidgetSubscription implements IWidgetSubscription {
});
} else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {});
this.callbacks.onLatestDataUpdated = this.callbacks.onLatestDataUpdated || (() => {});
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || (() => {});
this.callbacks.onLatestDataUpdateError = this.callbacks.onLatestDataUpdateError || (() => {});
this.callbacks.onSubscriptionMessage = this.callbacks.onSubscriptionMessage || (() => {});
this.callbacks.onInitialPageDataChanged = this.callbacks.onInitialPageDataChanged || (() => {});
this.callbacks.forceReInit = this.callbacks.forceReInit || (() => {});
@ -224,6 +235,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.datasources = [];
this.dataPages = [];
this.data = [];
this.latestData = [];
this.hiddenData = [];
this.originalTimewindow = null;
this.timeWindow = {};
@ -366,6 +378,7 @@ export class WidgetSubscription implements IWidgetSubscription {
const initDataSubscriptionSubject = new ReplaySubject(1);
this.loadStDiff().subscribe(() => {
if (!this.ctx.aliasController) {
this.configuredDatasources = deepClone(this.configuredDatasources);
this.hasResolvedData = true;
this.prepareDataSubscriptions().subscribe(
() => {
@ -455,18 +468,25 @@ export class WidgetSubscription implements IWidgetSubscription {
this.updateDataTimewindow();
this.notifyDataLoaded();
this.onDataUpdated(true);
if (this.hasLatestData) {
this.onLatestDataUpdated(true);
}
})
);
}
private resetData() {
this.data.length = 0;
this.latestData.length = 0;
this.hiddenData.length = 0;
if (this.displayLegend) {
this.legendData.keys.length = 0;
this.legendData.data.length = 0;
}
this.onDataUpdated();
if (this.hasLatestData) {
this.onLatestDataUpdated();
}
}
getFirstEntityInfo(): SubscriptionEntityInfo {
@ -576,6 +596,20 @@ export class WidgetSubscription implements IWidgetSubscription {
});
}
private onLatestDataUpdated(detectChanges?: boolean) {
if (this.cafs.latestDataUpdated) {
this.cafs.latestDataUpdated();
this.cafs.latestDataUpdated = null;
}
this.cafs.latestDataUpdated = this.ctx.raf.raf(() => {
try {
this.callbacks.onLatestDataUpdated(this, detectChanges);
} catch (e) {
this.callbacks.onLatestDataUpdateError(this, e);
}
});
}
private onSubscriptionMessage(message: SubscriptionMessage) {
if (this.cafs.message) {
this.cafs.message();
@ -1090,6 +1124,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private updateDataSubscriptions() {
this.configuredDatasources = this.ctx.utils.validateDatasources(this.options.datasources);
if (!this.ctx.aliasController) {
this.configuredDatasources = deepClone(this.configuredDatasources);
this.hasResolvedData = true;
this.prepareDataSubscriptions().subscribe(
() => {
@ -1249,21 +1284,28 @@ export class WidgetSubscription implements IWidgetSubscription {
if (isUpdate) {
this.configureLoadedData();
this.onDataUpdated(true);
if (this.hasLatestData) {
this.onLatestDataUpdated(true);
}
}
}
private configureLoadedData() {
this.datasources.length = 0;
this.data.length = 0;
this.latestData.length = 0;
this.hiddenData.length = 0;
this.hasLatestData = false;
if (this.displayLegend) {
this.legendData.keys.length = 0;
this.legendData.data.length = 0;
}
let dataKeyIndex = 0;
let latestDataKeyIndex = 0;
this.configuredDatasources.forEach((configuredDatasource, datasourceIndex) => {
configuredDatasource.dataKeyStartIndex = dataKeyIndex;
configuredDatasource.latestDataKeyStartIndex = latestDataKeyIndex;
const datasourcesPage = this.datasourcePages[datasourceIndex];
const datasourceDataPage = this.dataPages[datasourceIndex];
if (datasourcesPage) {
@ -1289,6 +1331,15 @@ export class WidgetSubscription implements IWidgetSubscription {
}
dataKeyIndex++;
});
if (datasource.latestDataKeys && datasource.latestDataKeys.length) {
this.hasLatestData = true;
datasource.latestDataKeys.forEach((dataKey, currentLatestDataKeyIndex) => {
const currentDataKeyIndex = datasource.dataKeys.length + currentLatestDataKeyIndex;
const datasourceData = datasourceDataPage.data[currentDatasourceIndex][currentDataKeyIndex];
this.latestData.push(datasourceData);
latestDataKeyIndex++;
});
}
this.datasources.push(datasource);
});
}
@ -1303,6 +1354,15 @@ export class WidgetSubscription implements IWidgetSubscription {
}
index++;
});
if (datasource.latestDataKeys) {
datasource.latestDataKeys.forEach((dataKey) => {
if (datasource.generated || datasource.isAdditional) {
dataKey._hash = Math.random();
// dataKey.color = this.ctx.utils.getMaterialColor(index);
}
// index++;
});
}
});
if (this.comparisonEnabled) {
this.datasourcePages.forEach(datasourcePage => {
@ -1329,19 +1389,11 @@ export class WidgetSubscription implements IWidgetSubscription {
}
private entityDataToDatasourceData(datasource: Datasource, data: Array<DataSetHolder>): Array<DatasourceData> {
return datasource.dataKeys.map((dataKey, keyIndex) => {
let datasourceDataArray: Array<DatasourceData> = [];
datasourceDataArray = datasourceDataArray.concat(datasource.dataKeys.map((dataKey, keyIndex) => {
dataKey.hidden = !!dataKey.settings.hideDataByDefault;
dataKey.inLegend = !dataKey.settings.removeFromLegend;
dataKey.label = this.ctx.utils.customTranslation(dataKey.label, dataKey.label);
if (this.comparisonEnabled && dataKey.isAdditional && dataKey.settings.comparisonSettings.comparisonValuesLabel) {
dataKey.label = createLabelFromDatasource(datasource, dataKey.settings.comparisonSettings.comparisonValuesLabel);
} else {
if (this.comparisonEnabled && dataKey.isAdditional) {
dataKey.label = dataKey.label + ' ' + this.ctx.translate.instant('legend.comparison-time-ago.' + this.timeForComparison);
}
dataKey.pattern = dataKey.label;
dataKey.label = createLabelFromDatasource(datasource, dataKey.pattern);
}
const datasourceData: DatasourceData = {
datasource,
dataKey,
@ -1351,7 +1403,38 @@ export class WidgetSubscription implements IWidgetSubscription {
datasourceData.data = data[keyIndex].data;
}
return datasourceData;
}));
if (datasource.latestDataKeys) {
datasourceDataArray = datasourceDataArray.concat(datasource.latestDataKeys.map((dataKey, latestKeyIndex) => {
const datasourceData: DatasourceData = {
datasource,
dataKey,
data: []
};
const keyIndex = datasource.dataKeys.length + latestKeyIndex;
if (data && data[keyIndex] && data[keyIndex].data) {
datasourceData.data = data[keyIndex].data;
}
return datasourceData;
}));
}
const formattedDataArray = formattedDataFormDatasourceData(datasourceDataArray);
const formattedData = flatFormattedData(formattedDataArray);
datasource.dataKeys.forEach((dataKey) => {
if (this.comparisonEnabled && dataKey.isAdditional && dataKey.settings.comparisonSettings.comparisonValuesLabel) {
dataKey.label = createLabelFromPattern(dataKey.settings.comparisonSettings.comparisonValuesLabel, formattedData);
} else {
if (this.comparisonEnabled && dataKey.isAdditional) {
dataKey.label = dataKey.label + ' ' + this.ctx.translate.instant('legend.comparison-time-ago.' + this.timeForComparison);
}
dataKey.pattern = dataKey.label;
dataKey.label = createLabelFromPattern(dataKey.pattern, formattedData);
}
});
return datasourceDataArray;
}
private entityDataToDatasource(configDatasource: Datasource, entityData: EntityData, index: number): Datasource {
@ -1362,7 +1445,17 @@ export class WidgetSubscription implements IWidgetSubscription {
return newDatasource;
}
private dataUpdated(data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) {
private dataUpdated(data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number,
detectChanges: boolean, isLatest: boolean) {
if (isLatest) {
this.processLatestDataUpdated(data, datasourceIndex, dataIndex, dataKeyIndex, detectChanges);
} else {
this.processDataUpdated(data, datasourceIndex, dataIndex, dataKeyIndex, detectChanges);
}
}
private processDataUpdated(data: DataSetHolder, datasourceIndex: number, dataIndex: number,
dataKeyIndex: number, detectChanges: boolean) {
const configuredDatasource = this.configuredDatasources[datasourceIndex];
const startIndex = configuredDatasource.dataKeyStartIndex;
const dataKeysCount = configuredDatasource.dataKeys.length;
@ -1402,6 +1495,30 @@ export class WidgetSubscription implements IWidgetSubscription {
}
}
private processLatestDataUpdated(data: DataSetHolder, datasourceIndex: number, dataIndex: number,
dataKeyIndex: number, detectChanges: boolean) {
const configuredDatasource = this.configuredDatasources[datasourceIndex];
const startIndex = configuredDatasource.latestDataKeyStartIndex;
const dataKeysCount = configuredDatasource.latestDataKeys.length;
const index = startIndex + dataIndex * dataKeysCount + dataKeyIndex - configuredDatasource.dataKeys.length;
let update = true;
const currentData = this.latestData[index];
const prevData = currentData.data;
if (!data.data.length) {
update = false;
} else if (prevData && prevData[0] && prevData[0].length > 1 && data.data.length > 0) {
const prevTs = prevData[0][0];
const prevValue = prevData[0][1];
if (prevTs === data.data[0][0] && prevValue === data.data[0][1]) {
update = false;
}
}
if (update) {
currentData.data = data.data;
this.onLatestDataUpdated(detectChanges);
}
}
private alarmsLoaded(alarms: PageData<AlarmData>, allowedEntities: number, totalEntities: number) {
this.alarms = alarms;
if (totalEntities > allowedEntities) {

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

@ -17,7 +17,7 @@
import _ from 'lodash';
import { Observable, Subject } from 'rxjs';
import { finalize, share } from 'rxjs/operators';
import { Datasource } from '@app/shared/models/widget.models';
import { Datasource, DatasourceData, FormattedData, ReplaceInfo } from '@app/shared/models/widget.models';
import { EntityId } from '@shared/models/id/entity-id';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { EntityType, baseDetailsPageByEntityType } from '@shared/models/entity-type.models';
@ -388,6 +388,195 @@ export function createLabelFromDatasource(datasource: Datasource, pattern: strin
return label;
}
export function formattedDataFormDatasourceData(input: DatasourceData[], dataIndex?: number): FormattedData[] {
return _(input).groupBy(el => el.datasource.entityName + el.datasource.entityType)
.values().value().map((entityArray, i) => {
const datasource = entityArray[0].datasource;
const obj = formattedDataFromDatasource(datasource, i);
entityArray.filter(el => el.data.length).forEach(el => {
const index = isDefined(dataIndex) ? dataIndex : el.data.length - 1;
if (!obj.hasOwnProperty(el.dataKey.label) || el.data[index][1] !== '') {
obj[el.dataKey.label] = el.data[index][1];
obj[el.dataKey.label + '|ts'] = el.data[index][0];
if (el.dataKey.label.toLowerCase() === 'type') {
obj.deviceType = el.data[index][1];
}
}
});
return obj;
});
}
export function formattedDataArrayFromDatasourceData(input: DatasourceData[]): FormattedData[][] {
return _(input).groupBy(el => el.datasource.entityName)
.values().value().map((entityArray, dsIndex) => {
const timeDataMap: {[time: number]: FormattedData} = {};
entityArray.filter(e => e.data.length).forEach(entity => {
entity.data.forEach(tsData => {
const time = tsData[0];
const value = tsData[1];
let data = timeDataMap[time];
if (!data) {
const datasource = entity.datasource;
data = formattedDataFromDatasource(datasource, dsIndex);
data.time = time;
timeDataMap[time] = data;
}
data[entity.dataKey.label] = value;
data[entity.dataKey.label + '|ts'] = time;
if (entity.dataKey.label.toLowerCase() === 'type') {
data.deviceType = value;
}
});
});
return _.values(timeDataMap);
});
}
export function formattedDataFromDatasource(datasource: Datasource, dsIndex: number): FormattedData {
return {
entityName: datasource.entityName,
deviceName: datasource.entityName,
entityId: datasource.entityId,
entityType: datasource.entityType,
entityLabel: datasource.entityLabel || datasource.entityName,
entityDescription: datasource.entityDescription,
aliasName: datasource.aliasName,
$datasource: datasource,
dsIndex,
dsName: datasource.name,
deviceType: null
};
}
export function flatFormattedData(input: FormattedData[]): FormattedData {
let result: FormattedData = {} as FormattedData;
if (input.length) {
for (const toMerge of input) {
result = {...result, ...toMerge};
}
const sourceData = input[0];
result.entityName = sourceData.entityName;
result.deviceName = sourceData.deviceName;
result.entityId = sourceData.entityId;
result.entityType = sourceData.entityType;
result.entityLabel = sourceData.entityLabel;
result.entityDescription = sourceData.entityDescription;
result.aliasName = sourceData.aliasName;
result.$datasource = sourceData.$datasource;
result.dsIndex = sourceData.dsIndex;
result.dsName = sourceData.dsName;
result.deviceType = sourceData.deviceType;
}
return result;
}
export function mergeFormattedData(first: FormattedData[], second: FormattedData[]): FormattedData[] {
const merged = first.concat(second);
return _(merged).groupBy(el => el.$datasource)
.values().value().map((formattedDataArray, i) => {
let res = formattedDataArray[0];
if (formattedDataArray.length > 1) {
const toMerge = formattedDataArray[1];
res = {...res, ...toMerge};
}
return res;
});
}
export function processDataPattern(pattern: string, data: FormattedData): Array<ReplaceInfo> {
const replaceInfo: Array<ReplaceInfo> = [];
try {
const reg = /\${([^}]*)}/g;
let match = reg.exec(pattern);
while (match !== null) {
const variableInfo: ReplaceInfo = {
dataKeyName: '',
valDec: 2,
variable: ''
};
const variable = match[0];
let label = match[1];
let valDec = 2;
const splitValues = label.split(':');
if (splitValues.length > 1) {
label = splitValues[0];
valDec = parseFloat(splitValues[1]);
}
variableInfo.variable = variable;
variableInfo.valDec = valDec;
if (label.startsWith('#')) {
const keyIndexStr = label.substring(1);
const n = Math.floor(Number(keyIndexStr));
if (String(n) === keyIndexStr && n >= 0) {
variableInfo.dataKeyName = data.$datasource.dataKeys[n].label;
}
} else {
variableInfo.dataKeyName = label;
}
replaceInfo.push(variableInfo);
match = reg.exec(pattern);
}
} catch (ex) {
console.log(ex, pattern);
}
return replaceInfo;
}
export function fillDataPattern(pattern: string, replaceInfo: Array<ReplaceInfo>, data: FormattedData) {
let text = createLabelFromDatasource(data.$datasource, pattern);
if (replaceInfo) {
for (const variableInfo of replaceInfo) {
let txtVal = '';
if (variableInfo.dataKeyName && isDefinedAndNotNull(data[variableInfo.dataKeyName])) {
const varData = data[variableInfo.dataKeyName];
if (isNumber(varData)) {
txtVal = padValue(varData, variableInfo.valDec);
} else {
txtVal = varData;
}
}
text = text.replace(variableInfo.variable, txtVal);
}
}
return text;
}
export function createLabelFromPattern(pattern: string, data: FormattedData): string {
const replaceInfo = processDataPattern(pattern, data);
return fillDataPattern(pattern, replaceInfo, data);
}
export function parseFunction(source: any, params: string[] = ['def']): (...args: any[]) => any {
let res = null;
if (source?.length) {
try {
res = new Function(...params, source);
}
catch (err) {
res = null;
}
}
return res;
}
export function safeExecute(func: (...args: any[]) => any, params = []) {
let res = null;
if (func && typeof (func) === 'function') {
try {
res = func(...params);
}
catch (err) {
console.log('error in external function:', err);
res = null;
}
}
return res;
}
export function padValue(val: any, dec: number): string {
let strVal;
let n;

32
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.ts

@ -141,21 +141,23 @@ export class EntityAliasDialogComponent extends DialogComponent<EntityAliasDialo
this.alias.alias = this.entityAliasFormGroup.get('alias').value.trim();
this.alias.filter = this.entityAliasFormGroup.get('filter').value;
this.alias.filter.resolveMultiple = this.entityAliasFormGroup.get('resolveMultiple').value;
this.validate().subscribe(() => {
if (this.isAdd) {
this.alias.id = this.utils.guid();
if (this.alias.filter.type) {
this.validate().subscribe(() => {
if (this.isAdd) {
this.alias.id = this.utils.guid();
}
this.dialogRef.close(this.alias);
},
() => {
this.entityAliasFormGroup.setErrors({
noEntityMatched: true
});
const changesSubscriptuion = this.entityAliasFormGroup.valueChanges.subscribe(() => {
this.entityAliasFormGroup.setErrors(null);
changesSubscriptuion.unsubscribe();
});
}
this.dialogRef.close(this.alias);
},
() => {
this.entityAliasFormGroup.setErrors({
noEntityMatched: true
});
const changesSubscriptuion = this.entityAliasFormGroup.valueChanges.subscribe(() => {
this.entityAliasFormGroup.setErrors(null);
changesSubscriptuion.unsubscribe();
});
}
);
);
}
}
}

12
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -68,6 +68,7 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
const rawLatestDataKeySettingsSchema = widgetInfo.typeLatestDataKeySettingsSchema || widgetInfo.latestDataKeySettingsSchema;
const typeParameters = widgetInfo.typeParameters;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
@ -83,6 +84,13 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
} else {
dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema;
}
let latestDataKeySettingsSchema;
if (!rawLatestDataKeySettingsSchema || rawLatestDataKeySettingsSchema === '') {
latestDataKeySettingsSchema = {};
} else {
latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ?
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
}
const widgetConfig: WidgetConfigComponentData = {
config: this.widget.config,
layout: {},
@ -92,8 +100,10 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
isDataEnabled,
settingsSchema,
dataKeySettingsSchema,
latestDataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective
};
this.widgetFormGroup = this.fb.group({

12
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -89,6 +89,7 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
const widgetInfo = this.widgetComponentService.getInstantWidgetInfo(this.widget);
const rawSettingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
const rawDataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
const rawLatestDataKeySettingsSchema = widgetInfo.typeLatestDataKeySettingsSchema || widgetInfo.latestDataKeySettingsSchema;
const typeParameters = widgetInfo.typeParameters;
const actionSources = widgetInfo.actionSources;
const isDataEnabled = isDefined(widgetInfo.typeParameters) ? !widgetInfo.typeParameters.useCustomDatasources : true;
@ -104,6 +105,13 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
} else {
dataKeySettingsSchema = isString(rawDataKeySettingsSchema) ? JSON.parse(rawDataKeySettingsSchema) : rawDataKeySettingsSchema;
}
let latestDataKeySettingsSchema;
if (!rawLatestDataKeySettingsSchema || rawLatestDataKeySettingsSchema === '') {
latestDataKeySettingsSchema = {};
} else {
latestDataKeySettingsSchema = isString(rawLatestDataKeySettingsSchema) ?
JSON.parse(rawLatestDataKeySettingsSchema) : rawLatestDataKeySettingsSchema;
}
this.widgetConfig = {
config: this.widget.config,
layout: this.widgetLayout,
@ -113,8 +121,10 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
isDataEnabled,
settingsSchema,
dataKeySettingsSchema,
latestDataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective
};
this.widgetFormGroup.reset({widgetConfig: this.widgetConfig});
}

2
ui-ngx/src/app/modules/home/components/widget/data-keys.component.html

@ -73,7 +73,7 @@
<mat-icon matChipRemove *ngIf="!disabled && !isEntityCountDatasource">close</mat-icon>
</div>
</mat-chip>
<input matInput type="text" placeholder="{{ !disabled ? placeholder : '' }}"
<input matInput type="text" placeholder="{{ !disabled ? (keys.length ? secondaryPlaceholder : placeholder) : '' }}"
style="max-width: 200px;"
#keyInput
formControlName="key"

4
ui-ngx/src/app/modules/home/components/widget/data-keys.component.models.ts

@ -15,10 +15,10 @@
///
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey } from '@shared/models/widget.models';
import { DataKey, JsonSettingsSchema } from '@shared/models/widget.models';
import { Observable } from 'rxjs';
export interface DataKeysCallbacks {
generateDataKey: (chip: any, type: DataKeyType) => DataKey;
generateDataKey: (chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema) => DataKey;
fetchEntityKeys: (entityAliasId: string, types: Array<DataKeyType>) => Observable<Array<DataKey>>;
}

27
ui-ngx/src/app/modules/home/components/widget/data-keys.component.ts

@ -46,7 +46,7 @@ import { MatAutocomplete } from '@angular/material/autocomplete';
import { MatChipInputEvent, MatChipList } from '@angular/material/chips';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { DataKey, DatasourceType, Widget, widgetType } from '@shared/models/widget.models';
import { DataKey, DatasourceType, Widget, JsonSettingsSchema, widgetType } from '@shared/models/widget.models';
import { IAliasController } from '@core/api/widget-api.models';
import { DataKeysCallbacks } from './data-keys.component.models';
import { alarmFields } from '@shared/models/alarm.models';
@ -112,7 +112,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
aliasController: IAliasController;
@Input()
datakeySettingsSchema: any;
datakeySettingsSchema: JsonSettingsSchema;
@Input()
dataKeySettingsDirective: string;
@ -155,6 +155,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
dataKeyType: DataKeyType;
placeholder: string;
secondaryPlaceholder: string;
requiredText: string;
searchText = '';
@ -240,20 +241,34 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
private updateParams() {
if (this.datasourceType === DatasourceType.function) {
this.dataKeyType = DataKeyType.function;
this.placeholder = this.translate.instant('datakey.function-types');
this.requiredText = this.translate.instant('datakey.function-types-required');
if (this.widgetType === widgetType.latest) {
this.placeholder = this.translate.instant('datakey.latest-key-functions');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key-function');
} else if (this.widgetType === widgetType.alarm) {
this.placeholder = this.translate.instant('datakey.alarm-key-functions');
this.secondaryPlaceholder = '+' + this.translate.instant('alarm-key-function');
} else {
this.placeholder = this.translate.instant('datakey.timeseries-key-functions');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key-function');
}
} else {
if (this.widgetType === widgetType.latest) {
this.dataKeyType = null;
this.placeholder = this.translate.instant('datakey.latest-keys');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.latest-key');
this.requiredText = this.translate.instant('datakey.timeseries-or-attributes-required');
} else if (this.widgetType === widgetType.alarm) {
this.dataKeyType = null;
this.placeholder = this.translate.instant('datakey.alarm-keys');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.alarm-key');
this.requiredText = this.translate.instant('datakey.alarm-fields-timeseries-or-attributes-required');
} else {
this.dataKeyType = DataKeyType.timeseries;
this.placeholder = this.translate.instant('datakey.timeseries-keys');
this.secondaryPlaceholder = '+' + this.translate.instant('datakey.timeseries-key');
this.requiredText = this.translate.instant('datakey.timeseries-required');
}
this.placeholder = '';
}
}
@ -261,7 +276,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
if (this.widgetType === widgetType.alarm) {
this.keys = this.utils.getDefaultAlarmDataKeys();
} else if (this.isEntityCountDatasource) {
this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count)];
this.keys = [this.callbacks.generateDataKey('count', DataKeyType.count, this.datakeySettingsSchema)];
} else {
this.keys = [];
}
@ -335,7 +350,7 @@ export class DataKeysComponent implements ControlValueAccessor, OnInit, AfterVie
}
private addFromChipValue(chip: DataKey) {
const key = this.callbacks.generateDataKey(chip.name, chip.type);
const key = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema);
this.addKey(key);
}

3
ui-ngx/src/app/modules/home/components/widget/legend.component.html

@ -15,8 +15,7 @@
limitations under the License.
-->
<table class="tb-legend" [ngClass]="isRowDirection ? 'tb-legend-row' : 'tb-legend-column'"
[ngStyle]="{ marginRight: isRowDirection && !displayHeader ? 'auto' : '20px' }">
<table class="tb-legend" [ngClass]="isRowDirection ? 'tb-legend-row' : 'tb-legend-column'">
<thead>
<tr class="tb-legend-header" *ngIf="!isRowDirection">
<th colspan="2"></th>

1
ui-ngx/src/app/modules/home/components/widget/legend.component.scss

@ -24,6 +24,7 @@
&.tb-legend-row {
width: auto;
margin-left: auto;
margin-right: auto;
}
.tb-legend-header,

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

@ -235,6 +235,12 @@ export interface TbFlotKeySettings {
comparisonSettings: TbFlotKeyComparisonSettings;
}
export interface TbFlotLatestKeySettings {
useAsThreshold: boolean;
thresholdLineWidth: number;
thresholdColor: string;
}
export function flotSettingsSchema(chartType: ChartType): JsonSettingsSchema {
const schema: JsonSettingsSchema = {
@ -1124,3 +1130,37 @@ export function flotDatakeySettingsSchema(defaultShowLines: boolean, chartType:
return schema;
}
export const flotLatestDatakeySettingsSchema: JsonSettingsSchema = {
schema: {
type: 'object',
title: 'LatestDataKeySettings',
properties: {
useAsThreshold: {
title: 'Use key value as threshold',
type: 'boolean',
default: false
},
thresholdLineWidth: {
title: 'Threshold line width',
type: 'number'
},
thresholdColor: {
title: 'Threshold color',
type: 'string'
}
}
},
form: [
'useAsThreshold',
{
key: 'thresholdLineWidth',
condition: 'model.useAsThreshold === true'
},
{
key: 'thresholdColor',
type: 'color',
condition: 'model.useAsThreshold === true'
},
]
};

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

@ -38,13 +38,13 @@ import {
} from '@app/shared/models/widget.models';
import {
ChartType,
flotDatakeySettingsSchema,
flotDatakeySettingsSchema, flotLatestDatakeySettingsSchema,
flotPieDatakeySettingsSchema,
flotPieSettingsSchema,
flotSettingsSchema,
TbFlotAxisOptions,
TbFlotHoverInfo,
TbFlotKeySettings,
TbFlotKeySettings, TbFlotLatestKeySettings,
TbFlotPlotAxis,
TbFlotPlotDataSeries,
TbFlotPlotItem,
@ -69,6 +69,7 @@ const moment = moment_;
const flotPieSettingsSchemaValue = flotPieSettingsSchema;
const flotPieDatakeySettingsSchemaValue = flotPieDatakeySettingsSchema;
const latestDatakeySettingsSchemaValue = flotLatestDatakeySettingsSchema;
export class TbFlot {
@ -98,6 +99,8 @@ export class TbFlot {
private thresholdsSourcesSubscription: IWidgetSubscription;
private predefinedThresholds: TbFlotThresholdMarking[];
private latestDataThresholds: TbFlotThresholdMarking[];
private attributesThresholds: TbFlotThresholdMarking[];
private labelPatternsSourcesSubscription: IWidgetSubscription;
private labelPatternsSourcesData: DatasourceData[];
@ -107,6 +110,7 @@ export class TbFlot {
private createPlotTimeoutHandle: Timeout;
private updateTimeoutHandle: Timeout;
private latestUpdateTimeoutHandle: Timeout;
private resizeTimeoutHandle: Timeout;
private mouseEventsEnabled: boolean;
@ -145,6 +149,10 @@ export class TbFlot {
return flotDatakeySettingsSchema(defaultShowLines, chartType);
}
static latestDatakeySettingsSchema(): JsonSettingsSchema {
return latestDatakeySettingsSchemaValue;
}
constructor(private ctx: WidgetContext, private readonly chartType: ChartType) {
this.chartType = this.chartType || 'line';
this.settings = ctx.settings as TbFlotSettings;
@ -541,7 +549,6 @@ export class TbFlot {
}
this.subscribeForThresholdsAttributes(thresholdsDatasources);
this.options.grid.markings = predefinedThresholds;
this.predefinedThresholds = predefinedThresholds;
this.options.colors = colors;
@ -561,6 +568,12 @@ export class TbFlot {
this.options.xaxes[1].min = this.subscription.comparisonTimeWindow.minTime;
this.options.xaxes[1].max = this.subscription.comparisonTimeWindow.maxTime;
}
let allThresholds = deepClone(this.predefinedThresholds);
if (this.attributesThresholds) {
allThresholds = allThresholds.concat(this.attributesThresholds);
}
this.latestDataThresholds = this.thresholdsSourcesDataUpdated(allThresholds, this.subscription.latestData, true);
this.options.grid.markings = allThresholds.concat(this.latestDataThresholds);
}
this.checkMouseEvents();
@ -591,7 +604,6 @@ export class TbFlot {
if (this.subscription) {
if (!this.isMouseInteraction && this.plot) {
if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
let axisVisibilityChanged = false;
if (this.yaxis) {
for (let i = 0; i < this.subscription.data.length; i++) {
@ -681,6 +693,31 @@ export class TbFlot {
}
}
public latestDataUpdate() {
if (this.latestUpdateTimeoutHandle) {
clearTimeout(this.latestUpdateTimeoutHandle);
this.latestUpdateTimeoutHandle = null;
}
if (this.subscription) {
if (!this.isMouseInteraction && this.plot) {
if (this.chartType === 'line' || this.chartType === 'bar' || this.chartType === 'state') {
let allThresholds = deepClone(this.predefinedThresholds);
if (this.attributesThresholds) {
allThresholds = allThresholds.concat(this.attributesThresholds);
}
this.latestDataThresholds = this.thresholdsSourcesDataUpdated(allThresholds, this.subscription.latestData, true);
this.options.grid.markings = allThresholds.concat(this.latestDataThresholds);
if (this.plot) {
this.plot.getOptions().grid.markings = this.options.grid.markings;
this.updateData();
}
}
} else if (this.isMouseInteraction && this.plot) {
this.latestUpdateTimeoutHandle = setTimeout(this.latestDataUpdate.bind(this), 30);
}
}
}
public resize() {
if (this.resizeTimeoutHandle) {
clearTimeout(this.resizeTimeoutHandle);
@ -734,6 +771,10 @@ export class TbFlot {
clearTimeout(this.updateTimeoutHandle);
this.updateTimeoutHandle = null;
}
if (this.latestUpdateTimeoutHandle) {
clearTimeout(this.latestUpdateTimeoutHandle);
this.latestUpdateTimeoutHandle = null;
}
if (this.createPlotTimeoutHandle) {
clearTimeout(this.createPlotTimeoutHandle);
this.createPlotTimeoutHandle = null;
@ -829,7 +870,18 @@ export class TbFlot {
useDashboardTimewindow: false,
type: widgetType.latest,
callbacks: {
onDataUpdated: (subscription) => this.thresholdsSourcesDataUpdated(subscription.data)
onDataUpdated: (subscription) => {
let allThresholds = deepClone(this.predefinedThresholds);
if (this.latestDataThresholds) {
allThresholds = allThresholds.concat(this.latestDataThresholds);
}
this.attributesThresholds = this.thresholdsSourcesDataUpdated(allThresholds, subscription.data);
this.options.grid.markings = allThresholds.concat(this.attributesThresholds);
if (this.plot) {
this.plot.getOptions().grid.markings = this.options.grid.markings;
this.updateData();
}
}
}
};
this.ctx.subscriptionApi.createSubscription(thresholdsSourcesSubscriptionOptions, true).subscribe(
@ -839,28 +891,51 @@ export class TbFlot {
);
}
private thresholdsSourcesDataUpdated(data: DatasourceData[]) {
const allThresholds = deepClone(this.predefinedThresholds);
private thresholdsSourcesDataUpdated(existingThresholds: TbFlotThresholdMarking[], data: DatasourceData[],
isLatest = false): TbFlotThresholdMarking[] {
const thresholds: TbFlotThresholdMarking[] = [];
data.forEach((keyData) => {
if (keyData && keyData.data && keyData.data[0]) {
let skip = false;
let latestSettings: TbFlotLatestKeySettings;
if (isLatest) {
latestSettings = keyData.dataKey.settings;
if (!latestSettings.useAsThreshold) {
skip = true;
}
}
if (!skip && keyData && keyData.data && keyData.data[0]) {
const attrValue = keyData.data[0][1];
if (isNumeric(attrValue) && isFinite(attrValue)) {
const settings: TbFlotThresholdKeySettings = keyData.dataKey.settings;
const colorIndex = this.subscription.data.length + allThresholds.length;
this.generateThreshold(allThresholds, settings.yaxis, settings.lineWidth, settings.color, colorIndex, attrValue);
let yaxis: number;
let lineWidth: number;
let color: string;
if (isLatest) {
yaxis = 1;
lineWidth = latestSettings.thresholdLineWidth;
color = latestSettings.thresholdColor || keyData.dataKey.color;
} else {
const settings: TbFlotThresholdKeySettings = keyData.dataKey.settings;
yaxis = settings.yaxis;
lineWidth = settings.lineWidth;
color = settings.color || keyData.dataKey.color;
}
const colorIndex = this.subscription.data.length + existingThresholds.length;
const threshold = this.generateThreshold(existingThresholds, yaxis, lineWidth, color, colorIndex, attrValue);
if (threshold != null) {
thresholds.push(threshold);
}
}
}
});
this.options.grid.markings = allThresholds;
this.redrawPlot();
return thresholds;
}
private generateThreshold(existingThresholds: TbFlotThresholdMarking[], yaxis: number, lineWidth: number,
color: string, defaultColorIndex: number, thresholdValue: number) {
color: string, defaultColorIndex: number, thresholdValue: number): TbFlotThresholdMarking {
const marking: TbFlotThresholdMarking = {};
let markingYAxis;
if (yaxis !== 1) {
if (isDefined(yaxis) && yaxis !== 1) {
markingYAxis = 'y' + yaxis + 'axis';
} else {
markingYAxis = 'yaxis';
@ -885,8 +960,9 @@ export class TbFlot {
return isEqual(existingMarking[markingYAxis], marking[markingYAxis]);
});
if (!similarMarkings.length) {
existingThresholds.push(marking);
return marking;
}
return null;
}
private subscribeForLabelPatternsSources(datasources: Datasource[]) {

13
ui-ngx/src/app/modules/home/components/widget/lib/maps/circle.ts

@ -15,16 +15,15 @@
///
import L, { LeafletMouseEvent } from 'leaflet';
import { CircleData, FormattedData, UnitedMapSettings } from '@home/components/widget/lib/maps/map-models';
import { CircleData, UnitedMapSettings } from '@home/components/widget/lib/maps/map-models';
import {
fillPattern,
functionValueCalculator,
parseWithTranslation,
processPattern,
safeExecute
parseWithTranslation
} from '@home/components/widget/lib/maps/common-maps-utils';
import LeafletMap from '@home/components/widget/lib/maps/leaflet-map';
import { createTooltip } from '@home/components/widget/lib/maps/maps-utils';
import { FormattedData } from '@shared/models/widget.models';
import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils';
export class Circle {
@ -96,9 +95,9 @@ export class Circle {
const pattern = this.settings.useCircleLabelFunction ?
safeExecute(this.settings.circleLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.circleLabel;
this.map.circleLabelText = parseWithTranslation.prepareProcessPattern(pattern, true);
this.map.replaceInfoTooltipCircle = processPattern(this.map.circleLabelText, this.data);
this.map.replaceInfoTooltipCircle = processDataPattern(this.map.circleLabelText, this.data);
}
const circleLabelText = fillPattern(this.map.circleLabelText, this.map.replaceInfoTooltipCircle, this.data);
const circleLabelText = fillDataPattern(this.map.circleLabelText, this.map.replaceInfoTooltipCircle, this.data);
this.leafletCircle.bindTooltip(`<div style="color: ${this.settings.labelColor};"><b>${circleLabelText}</b></div>`,
{ className: 'tb-polygon-label', permanent: true, sticky: true, direction: 'center'})
.openTooltip(this.leafletCircle.getLatLng());

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { FormattedData, MapProviders, ReplaceInfo } from '@home/components/widget/lib/maps/map-models';
import { MapProviders } from '@home/components/widget/lib/maps/map-models';
import {
createLabelFromDatasource,
hashCode,
@ -27,7 +27,7 @@ import {
} from '@core/utils';
import { Observable, Observer, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { Datasource, DatasourceData } from '@shared/models/widget.models';
import { FormattedData } from '@shared/models/widget.models';
import _ from 'lodash';
import { mapProviderSchema, providerSets } from '@home/components/widget/lib/maps/schemes';
import { addCondition, mergeSchemes } from '@core/schema-utils';
@ -139,7 +139,7 @@ function createButtonElement(actionName: string, actionText: string) {
return `<button mat-button class="tb-custom-action" data-action-name=${actionName}>${actionText}</button>`;
}
function parseTemplate(template: string, data: { $datasource?: Datasource, [key: string]: any },
function parseTemplate(template: string, data: FormattedData,
translateFn?: TranslateFunc) {
let res = '';
try {
@ -208,67 +208,6 @@ function parseTemplate(template: string, data: { $datasource?: Datasource, [key:
return res;
}
export function processPattern(template: string, data: { $datasource?: Datasource, [key: string]: any }): Array<ReplaceInfo> {
const replaceInfo = [];
try {
const reg = /\${([^}]*)}/g;
let match = reg.exec(template);
while (match !== null) {
const variableInfo: ReplaceInfo = {
dataKeyName: '',
valDec: 2,
variable: ''
};
const variable = match[0];
let label = match[1];
let valDec = 2;
const splitValues = label.split(':');
if (splitValues.length > 1) {
label = splitValues[0];
valDec = parseFloat(splitValues[1]);
}
variableInfo.variable = variable;
variableInfo.valDec = valDec;
if (label.startsWith('#')) {
const keyIndexStr = label.substring(1);
const n = Math.floor(Number(keyIndexStr));
if (String(n) === keyIndexStr && n >= 0) {
variableInfo.dataKeyName = data.$datasource.dataKeys[n].label;
}
} else {
variableInfo.dataKeyName = label;
}
replaceInfo.push(variableInfo);
match = reg.exec(template);
}
} catch (ex) {
console.log(ex, template);
}
return replaceInfo;
}
export function fillPattern(markerLabelText: string, replaceInfoLabelMarker: Array<ReplaceInfo>, data: FormattedData) {
let text = createLabelFromDatasource(data.$datasource, markerLabelText);
if (replaceInfoLabelMarker) {
for (const variableInfo of replaceInfoLabelMarker) {
let txtVal = '';
if (variableInfo.dataKeyName && isDefinedAndNotNull(data[variableInfo.dataKeyName])) {
const varData = data[variableInfo.dataKeyName];
if (isNumber(varData)) {
txtVal = padValue(varData, variableInfo.valDec);
} else {
txtVal = varData;
}
}
text = text.replace(variableInfo.variable, txtVal);
}
}
return text;
}
function prepareProcessPattern(template: string, translateFn?: TranslateFunc): string {
if (translateFn) {
template = translateFn(template);
@ -308,7 +247,7 @@ export const parseWithTranslation = {
throw Error('Translate not assigned');
}
},
parseTemplate(template: string, data: object, forceTranslate = false): string {
parseTemplate(template: string, data: FormattedData, forceTranslate = false): string {
return parseTemplate(forceTranslate ? this.translate(template) : template, data, this.translate.bind(this));
},
prepareProcessPattern(template: string, forceTranslate = false): string {
@ -319,106 +258,6 @@ export const parseWithTranslation = {
}
};
export function parseData(input: DatasourceData[], dataIndex?: number): FormattedData[] {
return _(input).groupBy(el => el?.datasource.entityName + el?.datasource.entityType)
.values().value().map((entityArray, i) => {
const obj: FormattedData = {
entityName: entityArray[0]?.datasource?.entityName,
entityId: entityArray[0].datasource.entityId,
entityType: entityArray[0].datasource.entityType,
$datasource: entityArray[0].datasource,
dsIndex: i,
deviceType: null
};
entityArray.filter(el => el.data.length).forEach(el => {
const index = isDefined(dataIndex) ? dataIndex : el.data.length - 1;
if (!obj.hasOwnProperty(el.dataKey.label) || el.data[index][1] !== '') {
obj[el.dataKey.label] = el.data[index][1];
obj[el.dataKey.label + '|ts'] = el.data[index][0];
if (el.dataKey.label.toLowerCase() === 'type') {
obj.deviceType = el.data[index][1];
}
}
});
return obj;
});
}
export function flatData(input: FormattedData[]): FormattedData {
let result: FormattedData = {} as FormattedData;
if (input.length) {
for (const toMerge of input) {
result = {...result, ...toMerge};
}
result.entityName = input[0].entityName;
result.entityId = input[0].entityId;
result.entityType = input[0].entityType;
result.$datasource = input[0].$datasource;
result.dsIndex = input[0].dsIndex;
result.deviceType = input[0].deviceType;
}
return result;
}
export function parseArray(input: DatasourceData[]): FormattedData[][] {
return _(input).groupBy(el => el.datasource.entityName)
.values().value().map((entityArray, dsIndex) => {
const timeDataMap: {[time: number]: FormattedData} = {};
entityArray.filter(e => e.data.length).forEach(entity => {
entity.data.forEach(tsData => {
const time = tsData[0];
const value = tsData[1];
let data = timeDataMap[time];
if (!data) {
data = {
entityName: entity.datasource.entityName,
entityId: entity.datasource.entityId,
entityType: entity.datasource.entityType,
$datasource: entity.datasource,
dsIndex,
time,
deviceType: null
};
timeDataMap[time] = data;
}
data[entity.dataKey.label] = value;
data[entity.dataKey.label + '|ts'] = time;
if (entity.dataKey.label.toLowerCase() === 'type') {
data.deviceType = value;
}
});
});
return _.values(timeDataMap);
});
}
export function parseFunction(source: any, params: string[] = ['def']): (...args: any[]) => any {
let res = null;
if (source?.length) {
try {
res = new Function(...params, source);
}
catch (err) {
res = null;
}
}
return res;
}
export function safeExecute(func: (...args: any[]) => any, params = []) {
let res = null;
if (func && typeof (func) === 'function') {
try {
res = func(...params);
}
catch (err) {
console.log('error in external function:', err);
res = null;
}
}
return res;
}
export function functionValueCalculator(useFunction: boolean, func: (...args: any[]) => any, params = [], defaultValue: any) {
let res;
if (useFunction && isDefined(func) && isFunction(func)) {

2
ui-ngx/src/app/modules/home/components/widget/lib/maps/dialogs/select-entity-dialog.component.ts

@ -21,7 +21,7 @@ import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { FormattedData } from '@shared/models/widget.models';
export interface SelectEntityDialogData {
entities: FormattedData[];

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

@ -23,14 +23,12 @@ import '@geoman-io/leaflet-geoman-free';
import {
CircleData,
defaultSettings,
FormattedData,
MapSettings,
MarkerIconInfo,
MarkerImageInfo,
MarkerSettings,
PolygonSettings,
PolylineSettings,
ReplaceInfo,
UnitedMapSettings
} from './map-models';
import { Marker } from './markers';
@ -41,13 +39,17 @@ import { Circle } from './circle';
import { createTooltip, isCutPolygon, isJSON } from '@home/components/widget/lib/maps/maps-utils';
import {
checkLngLat,
createLoadingDiv,
parseArray,
parseData,
safeExecute
createLoadingDiv
} from '@home/components/widget/lib/maps/common-maps-utils';
import { WidgetContext } from '@home/models/widget-component.models';
import { deepClone, isDefinedAndNotNull, isNotEmptyStr, isString } from '@core/utils';
import {
deepClone,
formattedDataArrayFromDatasourceData,
formattedDataFormDatasourceData,
isDefinedAndNotNull,
isNotEmptyStr,
isString, mergeFormattedData, safeExecute
} from '@core/utils';
import { TranslateService } from '@ngx-translate/core';
import {
SelectEntityDialogComponent,
@ -55,6 +57,7 @@ import {
} from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { FormattedData, ReplaceInfo } from '@shared/models/widget.models';
export default abstract class LeafletMap {
@ -641,13 +644,25 @@ export default abstract class LeafletMap {
}
updateData(drawRoutes: boolean, showPolygon: boolean) {
const data = this.ctx.data;
let formattedData = formattedDataFormDatasourceData(data);
if (this.ctx.latestData && this.ctx.latestData.length) {
const formattedLatestData = formattedDataFormDatasourceData(this.ctx.latestData);
formattedData = mergeFormattedData(formattedData, formattedLatestData);
}
let polyData: FormattedData[][] = null;
if (drawRoutes) {
polyData = formattedDataArrayFromDatasourceData(data);
}
this.updateFromData(drawRoutes, showPolygon, formattedData, polyData);
}
updateFromData(drawRoutes: boolean, showPolygon: boolean, formattedData: FormattedData[],
polyData: FormattedData[][], markerClickCallback?: any) {
this.drawRoutes = drawRoutes;
this.showPolygon = showPolygon;
if (this.map) {
const data = this.ctx.data;
const formattedData = parseData(data);
if (drawRoutes) {
const polyData = parseArray(data);
this.updatePolylines(polyData, formattedData, false);
}
if (showPolygon) {
@ -656,7 +671,7 @@ export default abstract class LeafletMap {
if (this.options.showCircle) {
this.updateCircle(formattedData, false);
}
this.updateMarkers(formattedData, false);
this.updateMarkers(formattedData, false, markerClickCallback);
this.updateBoundsInternal();
if (this.options.draggableMarker || this.editPolygons || this.editCircle) {
let foundEntityWithLocation = false;

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

@ -14,8 +14,7 @@
/// limitations under the License.
///
import { Datasource } from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { Datasource, FormattedData } from '@app/shared/models/widget.models';
import tinycolor from 'tinycolor2';
import { BaseIconOptions, Icon } from 'leaflet';
@ -122,22 +121,6 @@ export type MarkerSettings = {
tooltipOffsetY: number;
};
export interface FormattedData {
$datasource: Datasource;
entityName: string;
entityId: string;
entityType: EntityType;
dsIndex: number;
deviceType: string;
[key: string]: any;
}
export interface ReplaceInfo {
variable: string;
valDec?: number;
dataKeyName: string;
}
export type PolygonSettings = {
showPolygon: boolean;
polygonKeyName: string;
@ -238,7 +221,7 @@ export interface MapImage {
update?: boolean;
}
export interface TripAnimationSettings extends PolygonSettings {
export interface TripAnimationSettings extends PolygonSettings, CircleSettings {
showPoints: boolean;
pointColor: string;
pointSize: number;

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { defaultSettings, FormattedData, hereProviders, MapProviders, UnitedMapSettings } from './map-models';
import { defaultSettings, hereProviders, MapProviders, UnitedMapSettings } from './map-models';
import LeafletMap from './leaflet-map';
import {
commonMapSettingsSchema,
@ -28,13 +28,19 @@ import {
import { MapWidgetInterface, MapWidgetStaticInterface } from './map-widget.interface';
import { addCondition, addGroupInfo, addToSchema, initSchema, mergeSchemes } from '@core/schema-utils';
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { getDefCenterPosition, getProviderSchema, parseFunction, parseWithTranslation } from './common-maps-utils';
import { Datasource, DatasourceData, JsonSettingsSchema, WidgetActionDescriptor } from '@shared/models/widget.models';
import { getDefCenterPosition, getProviderSchema, parseWithTranslation } from './common-maps-utils';
import {
Datasource,
DatasourceData,
FormattedData,
JsonSettingsSchema,
WidgetActionDescriptor
} from '@shared/models/widget.models';
import { TranslateService } from '@ngx-translate/core';
import { UtilsService } from '@core/services/utils.service';
import { EntityDataPageLink } from '@shared/models/query/query.models';
import { providerClass } from '@home/components/widget/lib/maps/providers';
import { isDefined } from '@core/utils';
import { isDefined, parseFunction } from '@core/utils';
import L from 'leaflet';
import { forkJoin, Observable, of } from 'rxjs';
import { AttributeService } from '@core/http/attribute.service';
@ -220,8 +226,12 @@ export class MapWidgetController implements MapWidgetInterface {
id: e.$datasource.entityId
};
let dataKeys = e.$datasource.dataKeys;
if (e.$datasource.latestDataKeys) {
dataKeys = dataKeys.concat(e.$datasource.latestDataKeys);
}
for (const dataKeyName of Object.keys(values)) {
for (const key of e.$datasource.dataKeys) {
for (const key of dataKeys) {
if (dataKeyName === key.name) {
const value = {
key: key.name,
@ -311,6 +321,10 @@ export class MapWidgetController implements MapWidgetInterface {
this.map.setLoading(false);
}
latestDataUpdate() {
this.map.updateData(this.drawRoutes, this.settings.showPolygon);
}
resize() {
this.map.onResize();
this.map?.invalidateSize();

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

@ -16,7 +16,6 @@
import L, { LeafletMouseEvent } from 'leaflet';
import {
FormattedData,
MarkerIconInfo,
MarkerIconReadyFunction,
MarkerImageInfo,
@ -24,10 +23,11 @@ import {
UnitedMapSettings
} from './map-models';
import { bindPopupActions, createTooltip } from './maps-utils';
import { aspectCache, fillPattern, parseWithTranslation, processPattern, safeExecute } from './common-maps-utils';
import { aspectCache, parseWithTranslation } from './common-maps-utils';
import tinycolor from 'tinycolor2';
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import { fillDataPattern, isDefined, isDefinedAndNotNull, processDataPattern, safeExecute } from '@core/utils';
import LeafletMap from './leaflet-map';
import { FormattedData } from '@shared/models/widget.models';
export class Marker {
@ -102,9 +102,9 @@ export class Marker {
const pattern = this.settings.useTooltipFunction ?
safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
this.map.markerTooltipText = parseWithTranslation.prepareProcessPattern(pattern, true);
this.map.replaceInfoTooltipMarker = processPattern(this.map.markerTooltipText, data);
this.map.replaceInfoTooltipMarker = processDataPattern(this.map.markerTooltipText, data);
}
this.tooltip.setContent(fillPattern(this.map.markerTooltipText, this.map.replaceInfoTooltipMarker, data));
this.tooltip.setContent(fillDataPattern(this.map.markerTooltipText, this.map.replaceInfoTooltipMarker, data));
if (this.tooltip.isOpen() && this.tooltip.getElement()) {
bindPopupActions(this.tooltip, this.settings, data.$datasource);
}
@ -124,9 +124,9 @@ export class Marker {
const pattern = settings.useLabelFunction ?
safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label;
this.map.markerLabelText = parseWithTranslation.prepareProcessPattern(pattern, true);
this.map.replaceInfoLabelMarker = processPattern(this.map.markerLabelText, this.data);
this.map.replaceInfoLabelMarker = processDataPattern(this.map.markerLabelText, this.data);
}
settings.labelText = fillPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data);
settings.labelText = fillDataPattern(this.map.markerLabelText, this.map.replaceInfoLabelMarker, this.data);
this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
{ className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.labelOffset });
}

13
ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts

@ -17,13 +17,12 @@
import L, { LatLngExpression, LeafletMouseEvent } from 'leaflet';
import { createTooltip, isCutPolygon } from './maps-utils';
import {
fillPattern,
functionValueCalculator,
parseWithTranslation,
processPattern,
safeExecute
parseWithTranslation
} from './common-maps-utils';
import { FormattedData, PolygonSettings, UnitedMapSettings } from './map-models';
import { PolygonSettings, UnitedMapSettings } from './map-models';
import { FormattedData } from '@shared/models/widget.models';
import { fillDataPattern, processDataPattern, safeExecute } from '@core/utils';
export class Polygon {
@ -104,9 +103,9 @@ export class Polygon {
const pattern = settings.usePolygonLabelFunction ?
safeExecute(settings.polygonLabelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.polygonLabel;
this.map.polygonLabelText = parseWithTranslation.prepareProcessPattern(pattern, true);
this.map.replaceInfoLabelPolygon = processPattern(this.map.polygonLabelText, this.data);
this.map.replaceInfoLabelPolygon = processDataPattern(this.map.polygonLabelText, this.data);
}
const polygonLabelText = fillPattern(this.map.polygonLabelText, this.map.replaceInfoLabelPolygon, this.data);
const polygonLabelText = fillDataPattern(this.map.polygonLabelText, this.map.replaceInfoLabelPolygon, this.data);
this.leafletPoly.bindTooltip(`<div style="color: ${settings.polygonLabelColor};"><b>${polygonLabelText}</b></div>`,
{ className: 'tb-polygon-label', permanent: true, sticky: true, direction: 'center' })
.openTooltip(this.leafletPoly.getBounds().getCenter());

3
ui-ngx/src/app/modules/home/components/widget/lib/maps/polyline.ts

@ -18,8 +18,9 @@
import L, { PolylineDecorator, PolylineDecoratorOptions, Symbol } from 'leaflet';
import 'leaflet-polylinedecorator';
import { FormattedData, PolylineSettings } from './map-models';
import { PolylineSettings } from './map-models';
import { functionValueCalculator } from '@home/components/widget/lib/maps/common-maps-utils';
import { FormattedData } from '@shared/models/widget.models';
export class Polyline {

7
ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts

@ -18,17 +18,16 @@ import L, { LatLngBounds, LatLngLiteral, LatLngTuple } from 'leaflet';
import LeafletMap from '../leaflet-map';
import { CircleData, MapImage, PosFuncton, UnitedMapSettings } from '../map-models';
import { Observable, ReplaySubject } from 'rxjs';
import { filter, map, mergeMap } from 'rxjs/operators';
import { map, mergeMap } from 'rxjs/operators';
import {
aspectCache,
calculateNewPointCoordinate,
parseFunction
calculateNewPointCoordinate
} from '@home/components/widget/lib/maps/common-maps-utils';
import { WidgetContext } from '@home/models/widget-component.models';
import { DataSet, DatasourceType, widgetType } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetSubscriptionOptions } from '@core/api/widget-api.models';
import { isDefinedAndNotNull, isEmptyStr } from '@core/utils';
import { isDefinedAndNotNull, isEmptyStr, parseFunction } from '@core/utils';
import { EntityDataPageLink } from '@shared/models/query/query.models';
const maxZoom = 4; // ?

24
ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts

@ -19,17 +19,18 @@ import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DatasourceData } from '@shared/models/widget.models';
import { DatasourceData, FormattedData } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
fillPattern, flatData,
parseData,
parseFunction,
processPattern,
createLabelFromPattern,
fillDataPattern,
flatFormattedData,
formattedDataFormDatasourceData,
hashCode,
isNotEmptyStr,
parseFunction, processDataPattern,
safeExecute
} from '@home/components/widget/lib/maps/common-maps-utils';
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { hashCode, isNotEmptyStr } from '@core/utils';
} from '@core/utils';
import cssjs from '@core/css/css';
import { UtilsService } from '@core/services/utils.service';
import { HOME_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
@ -113,12 +114,11 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit {
} else {
initialData = [];
}
const data = parseData(initialData);
const data = formattedDataFormDatasourceData(initialData);
let markdownText = this.settings.useMarkdownTextFunction ?
safeExecute(this.markdownTextFunction, [data]) : this.settings.markdownTextPattern;
const allData = flatData(data);
const replaceInfo = processPattern(markdownText, allData);
markdownText = fillPattern(markdownText, replaceInfo, allData);
const allData = flatFormattedData(data);
markdownText = createLabelFromPattern(markdownText, allData);
if (this.markdownText !== markdownText) {
this.markdownText = this.utils.customTranslation(markdownText, markdownText);
this.cd.detectChanges();

23
ui-ngx/src/app/modules/home/components/widget/lib/qrcode-widget.component.ts

@ -19,17 +19,17 @@ import { PageComponent } from '@shared/components/page.component';
import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DatasourceData, FormattedData } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import {
fillPattern, flatData,
parseData,
createLabelFromPattern,
flatFormattedData,
formattedDataFormDatasourceData,
isNumber,
isObject,
parseFunction,
processPattern,
safeExecute
} from '@home/components/widget/lib/maps/common-maps-utils';
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { DatasourceData } from '@shared/models/widget.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isNumber, isObject } from '@core/utils';
} from '@core/utils';
interface QrCodeWidgetSettings {
qrCodeTextPattern: string;
@ -98,12 +98,11 @@ export class QrCodeWidgetComponent extends PageComponent implements OnInit, Afte
} else {
initialData = [];
}
const data = parseData(initialData);
const data = formattedDataFormDatasourceData(initialData);
const pattern = this.settings.useQrCodeTextFunction ?
safeExecute(this.qrCodeTextFunction, [data]) : this.settings.qrCodeTextPattern;
const allData = flatData(data);
const replaceInfo = processPattern(pattern, allData);
qrCodeText = fillPattern(pattern, replaceInfo, allData);
const allData = flatFormattedData(data);
qrCodeText = createLabelFromPattern(pattern, allData);
this.updateQrCodeText(qrCodeText);
}

3
ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts

@ -15,14 +15,13 @@
///
import { EntityId } from '@shared/models/id/entity-id';
import { DataKey, WidgetActionDescriptor, WidgetConfig } from '@shared/models/widget.models';
import { DataKey, FormattedData, WidgetActionDescriptor, WidgetConfig } from '@shared/models/widget.models';
import { getDescendantProp, isDefined, isNotEmptyStr } from '@core/utils';
import { AlarmDataInfo, alarmFields } from '@shared/models/alarm.models';
import * as tinycolor_ from 'tinycolor2';
import { Direction, EntityDataSortOrder, EntityKey } from '@shared/models/query/query.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetContext } from '@home/models/widget-component.models';
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';

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

@ -47,15 +47,15 @@
<ng-container *ngIf="showTimestamp" [matColumnDef]="'0'">
<mat-header-cell *matHeaderCellDef mat-sort-header>Timestamp</mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, 0, row, row[0], rowIndex)"
[ngStyle]="cellStyle(source, 0, row, row[0], rowIndex)">
[innerHTML]="cellContent(source, null, 0, row, row[0], rowIndex)"
[ngStyle]="cellStyle(source, null, 0, row, row[0], rowIndex)">
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="h.index + ''" *ngFor="let h of source.header; trackBy: trackByColumnIndex;">
<mat-header-cell *matHeaderCellDef mat-sort-header [disabled]="!h.sortable"> {{ h.dataKey.label }} </mat-header-cell>
<mat-cell *matCellDef="let row; let rowIndex = index"
[innerHTML]="cellContent(source, h.index, row, row[h.index], rowIndex)"
[ngStyle]="cellStyle(source, h.index, row, row[h.index], rowIndex)">
[innerHTML]="cellContent(source, h, h.index, row, row[h.index], rowIndex)"
[ngStyle]="cellStyle(source, h, h.index, row, row[h.index], rowIndex)">
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" [stickyEnd]="enableStickyAction">

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

@ -40,7 +40,15 @@ import {
} from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { hashCode, isDefined, isNumber, isObject, isUndefined } from '@core/utils';
import {
formattedDataFormDatasourceData,
hashCode,
isDefined,
isDefinedAndNotNull,
isNumber,
isObject,
isUndefined
} from '@core/utils';
import cssjs from '@core/css/css';
import { PageLink } from '@shared/models/page/page-link';
import { Direction, SortOrder, sortOrderFromString } from '@shared/models/page/sort-order';
@ -70,7 +78,6 @@ import {
import { Overlay } from '@angular/cdk/overlay';
import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import { DatePipe } from '@angular/common';
import { parseData } from '@home/components/widget/lib/maps/common-maps-utils';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants';
@ -81,6 +88,11 @@ export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
hideEmptyLines: boolean;
}
interface TimeseriesWidgetLatestDataKeySettings extends TableWidgetDataKeySettings {
show: boolean;
order: number;
}
interface TimeseriesRow {
actionCellButtons?: TableCellButtonActionDescriptor[];
hasActions?: boolean;
@ -92,20 +104,25 @@ interface TimeseriesHeader {
index: number;
dataKey: DataKey;
sortable: boolean;
show: boolean;
styleInfo: CellStyleInfo;
contentInfo: CellContentInfo;
order?: number;
}
interface TimeseriesTableSource {
keyStartIndex: number;
keyEndIndex: number;
latestKeyStartIndex: number;
latestKeyEndIndex: number;
datasource: Datasource;
rawData: Array<DatasourceData>;
latestRawData: Array<DatasourceData>;
data: TimeseriesRow[];
pageLink: PageLink;
displayedColumns: string[];
timeseriesDatasource: TimeseriesDatasource;
header: TimeseriesHeader[];
stylesInfo: CellStyleInfo[];
contentsInfo: CellContentInfo[];
rowDataTemplate: {[key: string]: any};
}
@ -142,6 +159,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private settings: TimeseriesTableWidgetSettings;
private widgetConfig: WidgetConfig;
private data: Array<DatasourceData>;
private latestData: Array<DatasourceData>;
private datasources: Array<Datasource>;
private defaultPageSize = 10;
@ -183,6 +201,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.settings = this.ctx.settings;
this.widgetConfig = this.ctx.widgetConfig;
this.data = this.ctx.data;
this.latestData = this.ctx.latestData;
this.datasources = this.ctx.datasources;
this.initialize();
this.ctx.updateWidgetParams();
@ -249,6 +268,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.ctx.detectChanges();
}
public onLatestDataUpdated() {
this.updateCurrentSourceLatestData();
this.clearCache();
this.ctx.detectChanges();
}
private initialize() {
this.ctx.widgetActions = [this.searchAction ];
@ -302,56 +327,94 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.sources = [];
this.sourceIndex = 0;
let keyOffset = 0;
let latestKeyOffset = 0;
const pageSize = this.displayPagination ? this.defaultPageSize : Number.POSITIVE_INFINITY;
if (this.datasources) {
for (const datasource of this.datasources) {
const sortOrder: SortOrder = sortOrderFromString(this.defaultSortOrder);
const source = {} as TimeseriesTableSource;
source.header = this.prepareHeader(datasource);
source.keyStartIndex = keyOffset;
keyOffset += datasource.dataKeys.length;
source.keyEndIndex = keyOffset;
source.latestKeyStartIndex = latestKeyOffset;
latestKeyOffset += datasource.latestDataKeys ? datasource.latestDataKeys.length : 0;
source.latestKeyEndIndex = latestKeyOffset;
source.datasource = datasource;
source.data = [];
source.rawData = [];
source.displayedColumns = [];
source.pageLink = new PageLink(pageSize, 0, null, sortOrder);
source.header = [];
source.stylesInfo = [];
source.contentsInfo = [];
source.rowDataTemplate = {};
source.rowDataTemplate.Timestamp = null;
if (this.showTimestamp) {
source.displayedColumns.push('0');
}
for (let a = 0; a < datasource.dataKeys.length; a++ ) {
const dataKey = datasource.dataKeys[a];
const keySettings: TableWidgetDataKeySettings = dataKey.settings;
const sortable = !dataKey.usePostProcessing;
const index = a + 1;
source.header.push({
index,
dataKey,
sortable
});
source.displayedColumns.push(index + '');
source.header.forEach(header => {
const dataKey = header.dataKey;
if (header.show) {
source.displayedColumns.push(header.index + '');
}
source.rowDataTemplate[dataKey.label] = null;
source.stylesInfo.push(getCellStyleInfo(keySettings, 'value, rowData, ctx'));
const cellContentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
cellContentInfo.units = dataKey.units;
cellContentInfo.decimals = dataKey.decimals;
source.contentsInfo.push(cellContentInfo);
}
});
if (this.setCellButtonAction) {
source.displayedColumns.push('actions');
}
const tsDatasource = new TimeseriesDatasource(source, this.hideEmptyLines, this.dateFormatFilter, this.datePipe, this.ctx);
tsDatasource.dataUpdated(this.data);
tsDatasource.allDataUpdated(this.data, this.latestData);
this.sources.push(source);
}
}
this.updateActiveEntityInfo();
}
private prepareHeader(datasource: Datasource): TimeseriesHeader[] {
const dataKeys = datasource.dataKeys;
const latestDataKeys = datasource.latestDataKeys;
let header: TimeseriesHeader[] = [];
dataKeys.forEach((dataKey, index) => {
const sortable = !dataKey.usePostProcessing;
const keySettings: TableWidgetDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx');
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
contentInfo.units = dataKey.units;
contentInfo.decimals = dataKey.decimals;
header.push({
index: index + 1,
dataKey,
sortable,
styleInfo,
contentInfo,
show: true,
order: index + 2
});
});
if (latestDataKeys) {
latestDataKeys.forEach((dataKey, latestIndex) => {
const index = dataKeys.length + latestIndex;
const sortable = !dataKey.usePostProcessing;
const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx');
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
contentInfo.units = dataKey.units;
contentInfo.decimals = dataKey.decimals;
header.push({
index: index + 1,
dataKey,
sortable,
styleInfo,
contentInfo,
show: isDefinedAndNotNull(keySettings.show) ? keySettings.show : true,
order: isDefinedAndNotNull(keySettings.order) ? keySettings.order : (index + 2)
});
});
}
header = header.sort((a, b) => {
return a.order - b.order;
});
return header;
}
private updateActiveEntityInfo() {
const source = this.sources[this.sourceIndex];
let activeEntityInfo: SubscriptionEntityInfo = null;
@ -393,7 +456,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
}
onSourceIndexChanged() {
this.updateCurrentSourceData();
this.updateCurrentSourceAllData();
this.updateActiveEntityInfo();
this.clearCache();
}
@ -466,7 +529,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.name] = row[headerInfo.index];
rowData[headerInfo.dataKey.label] = row[headerInfo.index];
});
res = this.rowStylesInfo.rowStyleFunction(rowData, this.ctx);
if (!isObject(res)) {
@ -486,19 +549,20 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
return res;
}
public cellStyle(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): any {
public cellStyle(source: TimeseriesTableSource, header: TimeseriesHeader,
index: number, row: TimeseriesRow, value: any, rowIndex: number): any {
const cacheIndex = rowIndex * (source.header.length + 1) + index;
let res = this.cellStyleCache[cacheIndex];
if (!res) {
res = {};
if (index > 0) {
const styleInfo = source.stylesInfo[index - 1];
const styleInfo = header.styleInfo;
if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
try {
const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.name] = row[headerInfo.index];
rowData[headerInfo.dataKey.label] = row[headerInfo.index];
});
res = styleInfo.cellStyleFunction(value, rowData, this.ctx);
if (!isObject(res)) {
@ -519,7 +583,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
return res;
}
public cellContent(source: TimeseriesTableSource, index: number, row: TimeseriesRow, value: any, rowIndex: number): SafeHtml {
public cellContent(source: TimeseriesTableSource, header: TimeseriesHeader,
index: number, row: TimeseriesRow, value: any, rowIndex: number): SafeHtml {
const cacheIndex = rowIndex * (source.header.length + 1) + index ;
let res = this.cellContentCache[cacheIndex];
if (isUndefined(res)) {
@ -528,13 +593,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
res = row.formattedTs;
} else {
let content;
const contentInfo = source.contentsInfo[index - 1];
const contentInfo = header.contentInfo;
if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
try {
const rowData = source.rowDataTemplate;
rowData.Timestamp = row[0];
source.header.forEach((headerInfo) => {
rowData[headerInfo.dataKey.name] = row[headerInfo.index];
rowData[headerInfo.dataKey.label] = row[headerInfo.index];
});
content = contentInfo.cellContentFunction(value, rowData, this.ctx);
} catch (e) {
@ -599,10 +664,18 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
return index === this.sourceIndex;
}
private updateCurrentSourceAllData() {
this.sources[this.sourceIndex].timeseriesDatasource.allDataUpdated(this.data, this.latestData);
}
private updateCurrentSourceData() {
this.sources[this.sourceIndex].timeseriesDatasource.dataUpdated(this.data);
}
private updateCurrentSourceLatestData() {
this.sources[this.sourceIndex].timeseriesDatasource.latestDataUpdated(this.latestData);
}
private loadCurrentSourceRow() {
this.sources[this.sourceIndex].timeseriesDatasource.loadRows();
this.clearCache();
@ -668,17 +741,28 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
);
}
allDataUpdated(data: DatasourceData[], latestData: DatasourceData[]) {
this.source.rawData = data.slice(this.source.keyStartIndex, this.source.keyEndIndex);
this.source.latestRawData = latestData.slice(this.source.latestKeyStartIndex, this.source.latestKeyEndIndex);
this.updateSourceData();
}
dataUpdated(data: DatasourceData[]) {
this.source.rawData = data.slice(this.source.keyStartIndex, this.source.keyEndIndex);
this.updateSourceData();
}
latestDataUpdated(latestData: DatasourceData[]) {
this.source.latestRawData = latestData.slice(this.source.latestKeyStartIndex, this.source.latestKeyEndIndex);
this.updateSourceData();
}
private updateSourceData() {
this.source.data = this.convertData(this.source.rawData);
this.source.data = this.convertData(this.source.rawData, this.source.latestRawData);
this.allRowsSubject.next(this.source.data);
}
private convertData(data: DatasourceData[]): TimeseriesRow[] {
private convertData(data: DatasourceData[], latestData: DatasourceData[]): TimeseriesRow[] {
const rowsMap: {[timestamp: number]: TimeseriesRow} = {};
for (let d = 0; d < data.length; d++) {
const columnData = data[d].data;
@ -691,9 +775,9 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
};
if (this.cellButtonActions.length) {
if (this.usedShowCellActionFunction) {
const parsedData = parseData(data, index);
const parsedData = formattedDataFormDatasourceData(data, index);
row.actionCellButtons = prepareTableCellButtonActions(this.widgetContext, this.cellButtonActions,
parsedData[0], this.reserveSpaceForHiddenAction);
parsedData[0], this.reserveSpaceForHiddenAction);
row.hasActions = checkHasActions(row.actionCellButtons);
} else {
row.hasActions = true;
@ -701,7 +785,7 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
}
}
row[0] = timestamp;
for (let c = 0; c < data.length; c++) {
for (let c = 0; c < (data.length + latestData.length); c++) {
row[c + 1] = undefined;
}
rowsMap[timestamp] = row;
@ -726,6 +810,15 @@ class TimeseriesDatasource implements DataSource<TimeseriesRow> {
} else {
rows = Object.keys(rowsMap).map(itm => rowsMap[itm]);
}
for (let d = 0; d < latestData.length; d++) {
const columnData = latestData[d].data;
if (columnData.length) {
const value = columnData[0][1];
rows.forEach((row) => {
row[data.length + d + 1] = value;
});
}
}
return rows;
}

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

@ -27,9 +27,10 @@ import {
SecurityContext,
ViewChild
} from '@angular/core';
import { FormattedData, MapProviders, TripAnimationSettings } from '@home/components/widget/lib/maps/map-models';
import { MapProviders, TripAnimationSettings } from '@home/components/widget/lib/maps/map-models';
import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils';
import {
mapCircleSchema,
mapPolygonSchema,
pathSchema,
pointSchema,
@ -42,14 +43,18 @@ import {
getProviderSchema,
getRatio,
interpolateOnLineSegment,
parseArray,
parseFunction,
parseWithTranslation,
safeExecute
parseWithTranslation
} from '@home/components/widget/lib/maps/common-maps-utils';
import { JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models';
import { FormattedData, JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models';
import moment from 'moment';
import { deepClone, isDefined, isUndefined } from '@core/utils';
import {
deepClone,
formattedDataArrayFromDatasourceData, formattedDataFormDatasourceData,
isDefined,
isUndefined, mergeFormattedData,
parseFunction,
safeExecute
} from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer';
import { MapWidgetInterface } from '@home/components/widget/lib/maps/map-widget.interface';
@ -78,6 +83,8 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
normalizationStep: number;
interpolatedTimeData: {[time: number]: FormattedData}[] = [];
formattedInterpolatedTimeData: FormattedData[][] = [];
formattedCurrentPosition: FormattedData[] = [];
formattedLatestData: FormattedData[] = [];
widgetConfig: WidgetConfig;
settings: TripAnimationSettings;
mainTooltips = [];
@ -100,11 +107,10 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
addGroupInfo(schema, 'Path Settings');
addToSchema(schema, addCondition(pointSchema, 'model.showPoints === true', ['showPoints']));
addGroupInfo(schema, 'Path Points Settings');
const mapPolygonSchemaWithoutEdit = deepClone(mapPolygonSchema);
delete mapPolygonSchemaWithoutEdit.schema.properties.editablePolygon;
mapPolygonSchemaWithoutEdit.form.splice(mapPolygonSchemaWithoutEdit.form.indexOf('editablePolygon'), 1);
addToSchema(schema, addCondition(mapPolygonSchemaWithoutEdit, 'model.showPolygon === true', ['showPolygon']));
addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon']));
addGroupInfo(schema, 'Polygon Settings');
addToSchema(schema, addCondition(mapCircleSchema, 'model.showCircle === true', ['showCircle']));
addGroupInfo(schema, 'Circle Settings');
return schema;
}
@ -118,7 +124,6 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
rotationAngle: 0
};
this.settings = { ...settings, ...this.ctx.settings };
this.settings.editablePolygon = false;
this.useAnchors = this.settings.showPoints && this.settings.usePointAsAnchor;
this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']);
this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']);
@ -127,7 +132,8 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
this.normalizationStep = this.settings.normalizationStep;
const subscription = this.ctx.defaultSubscription;
subscription.callbacks.onDataUpdated = () => {
this.historicalData = parseArray(this.ctx.data).map(item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length);
this.historicalData = formattedDataArrayFromDatasourceData(this.ctx.data).map(
item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length);
this.interpolatedTimeData.length = 0;
this.formattedInterpolatedTimeData.length = 0;
if (this.historicalData.length) {
@ -143,6 +149,10 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
this.mapWidget.map.setLoading(false);
this.cd.detectChanges();
};
subscription.callbacks.onLatestDataUpdated = () => {
this.formattedLatestData = formattedDataFormDatasourceData(this.ctx.latestData);
this.updateCurrentData();
};
}
ngAfterViewInit() {
@ -166,17 +176,17 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
timeUpdated(time: number) {
this.currentTime = time;
// get point for each datasource associated with time
const currentPosition = this.interpolatedTimeData
this.formattedCurrentPosition = this.interpolatedTimeData
.map(dataSource => dataSource[time]);
for (let j = 0; j < this.interpolatedTimeData.length; j++) {
if (isUndefined(currentPosition[j])) {
if (isUndefined(this.formattedCurrentPosition[j])) {
const timePoints = Object.keys(this.interpolatedTimeData[j]).map(item => parseInt(item, 10));
for (let i = 1; i < timePoints.length; i++) {
if (timePoints[i - 1] < time && timePoints[i] > time) {
const beforePosition = this.interpolatedTimeData[j][timePoints[i - 1]];
const afterPosition = this.interpolatedTimeData[j][timePoints[i]];
const ratio = getRatio(timePoints[i - 1], timePoints[i], time);
currentPosition[j] = {
this.formattedCurrentPosition[j] = {
...beforePosition,
time,
...interpolateOnLineSegment(beforePosition, afterPosition, this.settings.latKeyName, this.settings.lngKeyName, ratio)
@ -187,25 +197,29 @@ export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy
}
}
for (let j = 0; j < this.interpolatedTimeData.length; j++) {
if (isUndefined(currentPosition[j])) {
currentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time);
if (isUndefined(this.formattedCurrentPosition[j])) {
this.formattedCurrentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time);
}
}
this.updateCurrentData();
}
private updateCurrentData() {
let currentPosition = this.formattedCurrentPosition;
if (this.formattedLatestData.length) {
currentPosition = mergeFormattedData(this.formattedCurrentPosition, this.formattedLatestData);
}
this.calcLabel(currentPosition);
this.calcMainTooltip(currentPosition);
if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) {
this.mapWidget.map.updatePolylines(this.formattedInterpolatedTimeData, currentPosition, true);
if (this.settings.showPolygon) {
this.mapWidget.map.updatePolygons(currentPosition);
}
if (this.settings.showPoints) {
this.mapWidget.map.updatePoints(this.formattedInterpolatedTimeData, this.calcTooltip);
}
this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => {
this.mapWidget.map.updateFromData(true, this.settings.showPolygon, currentPosition, this.formattedInterpolatedTimeData, (trip) => {
this.activeTrip = trip;
this.timeUpdated(this.currentTime);
this.cd.markForCheck();
});
if (this.settings.showPoints) {
this.mapWidget.map.updatePoints(this.formattedInterpolatedTimeData, this.calcTooltip);
}
}
}

11
ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts

@ -107,8 +107,10 @@ export class WidgetComponentService {
controllerScript: this.utils.editWidgetInfo.controllerScript,
settingsSchema: this.utils.editWidgetInfo.settingsSchema,
dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
latestDataKeySettingsSchema: this.utils.editWidgetInfo.latestDataKeySettingsSchema,
settingsDirective: this.utils.editWidgetInfo.settingsDirective,
dataKeySettingsDirective: this.utils.editWidgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: this.utils.editWidgetInfo.latestDataKeySettingsDirective,
defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined
);
@ -288,6 +290,9 @@ export class WidgetComponentService {
if (widgetControllerDescriptor.dataKeySettingsSchema) {
widgetInfo.typeDataKeySettingsSchema = widgetControllerDescriptor.dataKeySettingsSchema;
}
if (widgetControllerDescriptor.latestDataKeySettingsSchema) {
widgetInfo.typeLatestDataKeySettingsSchema = widgetControllerDescriptor.latestDataKeySettingsSchema;
}
widgetInfo.typeParameters = widgetControllerDescriptor.typeParameters;
widgetInfo.actionSources = widgetControllerDescriptor.actionSources;
widgetInfo.widgetTypeFunction = widgetControllerDescriptor.widgetTypeFunction;
@ -467,6 +472,9 @@ export class WidgetComponentService {
if (isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
}
if (isFunction(widgetTypeInstance.getLatestDataKeySettingsSchema)) {
result.latestDataKeySettingsSchema = widgetTypeInstance.getLatestDataKeySettingsSchema();
}
if (isFunction(widgetTypeInstance.typeParameters)) {
result.typeParameters = widgetTypeInstance.typeParameters();
} else {
@ -489,6 +497,9 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.singleEntity)) {
result.typeParameters.singleEntity = false;
}
if (isUndefined(result.typeParameters.hasAdditionalLatestDataKeys)) {
result.typeParameters.hasAdditionalLatestDataKeys = false;
}
if (isUndefined(result.typeParameters.warnOnPageDataOverflow)) {
result.typeParameters.warnOnPageDataOverflow = true;
}

164
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -91,6 +91,7 @@
<div class="tb-panel-title" translate>widget-config.datasources</div>
<div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1"
class="tb-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div>
<tb-error [error]="dataError"></tb-error>
</mat-panel-title>
</mat-expansion-panel-header>
<div *ngIf="datasourcesFormArray().length === 0; else datasourcesTemplate">
@ -132,85 +133,102 @@
</button>
<span>{{$index + 1}}.</span>
</div>
<div class="mat-elevation-z4" fxFlex
fxLayout="row"
fxLayoutAlign="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<section fxFlex
fxLayout="column"
fxLayoutAlign="center"
fxLayout.gt-sm="row"
fxLayoutAlign.gt-sm="start center">
<mat-form-field class="tb-datasource-type">
<mat-select [formControl]="datasourceControl.get('type')">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
{{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.function">
<mat-form-field floatLabel="always"
class="tb-datasource-name" style="min-width: 200px;">
<mat-label></mat-label>
<input matInput
placeholder="{{ 'datasource.label' | translate }}"
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity ||
<div class="mat-elevation-z4 tb-datasource-params" fxFlex>
<div fxFlex
fxLayout="row"
fxLayoutAlign="start center">
<section fxFlex
fxLayout="column"
fxLayoutAlign="center"
fxLayout.gt-sm="row"
fxLayoutAlign.gt-sm="start center">
<mat-form-field class="tb-datasource-type">
<mat-select [formControl]="datasourceControl.get('type')">
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
{{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">
<ng-template [ngSwitchCase]="datasourceType.function">
<mat-form-field class="tb-datasource-name" style="min-width: 200px;">
<mat-label translate>datasource.label</mat-label>
<input matInput
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity ||
datasourceControl.get('type').value === datasourceType.entityCount ? datasourceControl.get('type').value : ''">
<tb-entity-alias-select
[showLabel]="true"
[tbRequired]="true"
[aliasController]="aliasController"
[formControl]="datasourceControl.get('entityAliasId')"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<tb-filter-select
[showLabel]="true"
[aliasController]="aliasController"
[formControl]="datasourceControl.get('filterId')"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
<mat-form-field *ngIf="datasourceControl.get('type').value === datasourceType.entityCount"
floatLabel="always"
class="tb-datasource-name no-border-top" style="min-width: 200px;">
<mat-label></mat-label>
<input matInput
placeholder="{{ 'datasource.label' | translate }}"
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
<tb-entity-alias-select
[showLabel]="true"
[tbRequired]="true"
[aliasController]="aliasController"
[formControl]="datasourceControl.get('entityAliasId')"
[callbacks]="widgetConfigCallbacks">
</tb-entity-alias-select>
<tb-filter-select
[showLabel]="true"
[aliasController]="aliasController"
[formControl]="datasourceControl.get('filterId')"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
<mat-form-field *ngIf="datasourceControl.get('type').value === datasourceType.entityCount"
floatLabel="always"
class="tb-datasource-name no-border-top" style="min-width: 200px;">
<mat-label></mat-label>
<input matInput
placeholder="{{ 'datasource.label' | translate }}"
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
</section>
<section fxLayout="column" fxLayoutAlign="stretch" fxFlex>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[datasourceType]="datasourceControl.get('type').value"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[optDataKeys]="dataKeysOptional(datasourceControl.value)"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value"
[formControl]="datasourceControl.get('dataKeys')">
</tb-data-keys>
<tb-data-keys *ngIf="widgetType === widgetTypes.timeseries &&
modelValue?.typeParameters?.hasAdditionalLatestDataKeys" class="tb-data-keys" fxFlex
[widgetType]="widgetTypes.latest"
[datasourceType]="datasourceControl.get('type').value"
[optDataKeys]="true"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.latestDataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.latestDataKeySettingsDirective"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value"
[formControl]="datasourceControl.get('latestDataKeys')">
</tb-data-keys>
</section>
</section>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[datasourceType]="datasourceControl.get('type').value"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[dataKeySettingsDirective]="modelValue?.dataKeySettingsDirective"
[dashboard]="dashboard"
[widget]="widget"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value"
[formControl]="datasourceControl.get('dataKeys')">
</tb-data-keys>
</section>
<button [disabled]="isLoading$ | async"
type="button"
mat-icon-button color="primary"
style="min-width: 40px;"
(click)="removeDatasource($index)"
matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
type="button"
mat-icon-button color="primary"
style="min-width: 40px;"
(click)="removeDatasource($index)"
matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
<tb-error class="tb-datasource-error" [error]="datasourceError[$index] ? datasourceError[$index] : ''"></tb-error>
</div>
</div>
</mat-list-item>
</mat-list>
</div>
</ng-template>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">

10
ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss

@ -61,6 +61,16 @@
max-width: 200px;
}
}
.tb-datasource-params {
position: relative;
padding: 0 0 0 10px;
margin: 5px;
tb-error.tb-datasource-error {
position: absolute;
bottom: 4px;
left: 8px;
}
}
.tb-data-keys {
@media #{$mat-gt-sm} {
padding-left: 8px;

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

@ -25,7 +25,9 @@ import {
datasourceTypeTranslationMap,
defaultLegendConfig,
GroupInfo,
JsonSchema, Widget,
JsonSchema,
JsonSettingsSchema,
Widget,
widgetType
} from '@shared/models/widget.models';
import {
@ -168,6 +170,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
public advancedSettings: FormGroup;
public actionsSettings: FormGroup;
public dataError = '';
public datasourceError: string[] = [];
private dataSettingsChangesSubscription: Subscription;
private targetDeviceSettingsSubscription: Subscription;
private alarmSourceSettingsSubscription: Subscription;
@ -564,9 +570,17 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
}
public dataKeysOptional(datasource?: Datasource): boolean {
if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
return true;
} else {
return this.modelValue.typeParameters && this.modelValue.typeParameters.dataKeysOptional
&& datasource?.type !== DatasourceType.entityCount;
}
}
private buildDatasourceForm(datasource?: Datasource): FormGroup {
let dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional
|| datasource?.type === DatasourceType.entityCount;
const dataKeysRequired = !this.dataKeysOptional(datasource);
const datasourceFormGroup = this.fb.group(
{
type: [datasource ? datasource.type : null, [Validators.required]],
@ -578,13 +592,15 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
dataKeys: [datasource ? datasource.dataKeys : null, dataKeysRequired ? [Validators.required] : []]
}
);
if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
datasourceFormGroup.addControl('latestDataKeys', this.fb.control(datasource ? datasource.latestDataKeys : null));
}
datasourceFormGroup.get('type').valueChanges.subscribe((type: DatasourceType) => {
datasourceFormGroup.get('entityAliasId').setValidators(
(type === DatasourceType.entity || type === DatasourceType.entityCount) ? [Validators.required] : []
);
dataKeysRequired = !this.modelValue.typeParameters || !this.modelValue.typeParameters.dataKeysOptional
|| type === DatasourceType.entityCount;
datasourceFormGroup.get('dataKeys').setValidators(dataKeysRequired ? [Validators.required] : []);
const newDataKeysRequired = !this.dataKeysOptional(datasourceFormGroup.value);
datasourceFormGroup.get('dataKeys').setValidators(newDataKeysRequired ? [Validators.required] : []);
datasourceFormGroup.get('entityAliasId').updateValueAndValidity();
datasourceFormGroup.get('dataKeys').updateValueAndValidity();
});
@ -736,16 +752,19 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
let newDatasource: Datasource;
if (this.functionsOnly) {
newDatasource = deepClone(this.utils.getDefaultDatasource(this.modelValue.dataKeySettingsSchema.schema));
newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function)];
newDatasource.dataKeys = [this.generateDataKey('Sin', DataKeyType.function, this.modelValue.dataKeySettingsSchema)];
} else {
newDatasource = { type: DatasourceType.entity,
dataKeys: []
};
}
if (this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
newDatasource.latestDataKeys = [];
}
this.datasourcesFormArray().push(this.buildDatasourceForm(newDatasource));
}
public generateDataKey(chip: any, type: DataKeyType): DataKey {
public generateDataKey(chip: any, type: DataKeyType, datakeySettingsSchema: JsonSettingsSchema): DataKey {
if (isObject(chip)) {
(chip as DataKey)._hash = Math.random();
return chip;
@ -775,8 +794,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
} else if (type === DataKeyType.count) {
result.name = 'count';
}
if (isDefined(this.modelValue.dataKeySettingsSchema.schema)) {
result.settings = this.utils.generateObjectFromJsonSchema(this.modelValue.dataKeySettingsSchema.schema);
if (datakeySettingsSchema && isDefined(datakeySettingsSchema.schema)) {
result.settings = this.utils.generateObjectFromJsonSchema(datakeySettingsSchema.schema);
}
return result;
}
@ -791,14 +810,25 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
do {
matches = false;
datasources.forEach((datasource) => {
if (datasource && datasource.dataKeys) {
datasource.dataKeys.forEach((dataKey) => {
if (dataKey.label === label) {
i++;
label = name + ' ' + i;
matches = true;
}
});
if (datasource) {
if (datasource.dataKeys) {
datasource.dataKeys.forEach((dataKey) => {
if (dataKey.label === label) {
i++;
label = name + ' ' + i;
matches = true;
}
});
}
if (datasource.latestDataKeys) {
datasource.latestDataKeys.forEach((dataKey) => {
if (dataKey.label === label) {
i++;
label = name + ' ' + i;
matches = true;
}
});
}
}
});
} while (matches);
@ -811,8 +841,9 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
const datasources = this.widgetType === widgetType.alarm ? [this.modelValue.config.alarmSource] : this.modelValue.config.datasources;
if (datasources) {
datasources.forEach((datasource) => {
if (datasource && datasource.dataKeys) {
i += datasource.dataKeys.length;
if (datasource && (datasource.dataKeys || datasource.latestDataKeys)) {
i += ((datasource.dataKeys ? datasource.dataKeys.length : 0) +
(datasource.latestDataKeys ? datasource.latestDataKeys.length : 0));
}
});
}
@ -893,6 +924,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
public validate(c: FormControl) {
this.dataError = '';
this.datasourceError = [];
if (!this.dataSettings.valid) {
return {
dataSettings: {
@ -943,6 +976,32 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}
};
}
if (this.widgetType === widgetType.timeseries && this.modelValue?.typeParameters?.hasAdditionalLatestDataKeys) {
let valid = config.datasources.filter(datasource => datasource?.dataKeys?.length).length > 0;
if (!valid) {
this.dataError = 'At least one timeseries data key should be specified';
return {
timeseriesDataKeys: {
valid: false
}
};
} else {
const emptyDatasources = config.datasources.filter(datasource => !datasource?.dataKeys?.length &&
!datasource?.latestDataKeys?.length);
valid = emptyDatasources.length === 0;
if (!valid) {
for (const emptyDatasource of emptyDatasources) {
const i = config.datasources.indexOf(emptyDatasource);
this.datasourceError[i] = 'At least one data key should be specified';
}
return {
dataKeys: {
valid: false
}
};
}
}
}
}
}
return null;

23
ui-ngx/src/app/modules/home/components/widget/widget.component.ts

@ -162,6 +162,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
widgetSizeDetected = false;
widgetInstanceInited = false;
dataUpdatePending = false;
latestDataUpdatePending = false;
pendingMessage: SubscriptionMessage;
cafs: {[cafId: string]: CancelAnimationFrame} = {};
@ -485,6 +486,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (!this.widgetTypeInstance.onDataUpdated) {
this.widgetTypeInstance.onDataUpdated = () => {};
}
if (!this.widgetTypeInstance.onLatestDataUpdated) {
this.widgetTypeInstance.onLatestDataUpdated = () => {};
}
if (!this.widgetTypeInstance.onResize) {
this.widgetTypeInstance.onResize = () => {};
}
@ -551,6 +555,10 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}, 0);
this.dataUpdatePending = false;
}
if (this.latestDataUpdatePending) {
this.widgetTypeInstance.onLatestDataUpdated();
this.latestDataUpdatePending = false;
}
if (this.pendingMessage) {
this.displayMessage(this.pendingMessage.severity, this.pendingMessage.message);
this.pendingMessage = null;
@ -895,9 +903,23 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
}
} catch (e){}
},
onLatestDataUpdated: () => {
try {
if (this.displayWidgetInstance()) {
if (this.widgetInstanceInited) {
this.widgetTypeInstance.onLatestDataUpdated();
} else {
this.latestDataUpdatePending = true;
}
}
} catch (e){}
},
onDataUpdateError: (subscription, e) => {
this.handleWidgetException(e);
},
onLatestDataUpdateError: (subscription, e) => {
this.handleWidgetException(e);
},
onSubscriptionMessage: (subscription, message) => {
if (this.displayWidgetInstance()) {
if (this.widgetInstanceInited) {
@ -972,6 +994,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
// backward compatibility
this.widgetContext.datasources = subscription.datasources;
this.widgetContext.data = subscription.data;
this.widgetContext.latestData = subscription.latestData;
this.widgetContext.hiddenData = subscription.hiddenData;
this.widgetContext.timeWindow = subscription.timeWindow;
this.widgetContext.defaultSubscription = subscription;

8
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -15,18 +15,16 @@
///
import { GridsterComponent, GridsterConfig, GridsterItem, GridsterItemComponentInterface } from 'angular-gridster2';
import { Widget, WidgetPosition, widgetType } from '@app/shared/models/widget.models';
import { FormattedData, Widget, WidgetPosition, widgetType } from '@app/shared/models/widget.models';
import { WidgetLayout, WidgetLayouts } from '@app/shared/models/dashboard.models';
import { IDashboardWidget, WidgetAction, WidgetContext, WidgetHeaderAction } from './widget-component.models';
import { Timewindow } from '@shared/models/time/time.models';
import { Observable, of, Subject } from 'rxjs';
import { guid, isDefined, isEqual, isUndefined } from '@app/core/utils';
import { formattedDataFormDatasourceData, guid, isDefined, isEqual, isUndefined } from '@app/core/utils';
import { IterableDiffer, KeyValueDiffer } from '@angular/core';
import { IAliasController, IStateController } from '@app/core/api/widget-api.models';
import { enumerable } from '@shared/decorators/enumerable';
import { UtilsService } from '@core/services/utils.service';
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { parseData } from '@home/components/widget/lib/maps/common-maps-utils';
export interface WidgetsData {
widgets: Array<Widget>;
@ -456,7 +454,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
if (this.widgetContext.customHeaderActions) {
let data: FormattedData[] = [];
if (this.widgetContext.customHeaderActions.some(action => action.useShowWidgetHeaderActionFunction)) {
data = parseData(this.widgetContext.data || []);
data = formattedDataFormDatasourceData(this.widgetContext.data || []);
}
customHeaderActions = this.widgetContext.customHeaderActions.filter(action => this.filterCustomHeaderAction(action, data));
} else {

13
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -18,7 +18,7 @@ import { IDashboardComponent } from '@home/models/dashboard-component.models';
import {
DataSet,
Datasource,
DatasourceData,
DatasourceData, FormattedData,
JsonSettingsSchema,
Widget,
WidgetActionDescriptor,
@ -79,7 +79,6 @@ import { SortOrder } from '@shared/models/page/sort-order';
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { catchError, map, mergeMap, switchMap } from 'rxjs/operators';
import { FormattedData } from '@home/components/widget/lib/maps/map-models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { EntityId } from '@shared/models/id/entity-id';
@ -243,6 +242,7 @@ export class WidgetContext {
datasources?: Array<Datasource>;
data?: Array<DatasourceData>;
latestData?: Array<DatasourceData>;
hiddenData?: Array<{data: DataSet}>;
timeWindow?: WidgetTimewindow;
@ -410,6 +410,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
alias: string;
typeSettingsSchema?: string | any;
typeDataKeySettingsSchema?: string | any;
typeLatestDataKeySettingsSchema?: string | any;
image?: string;
description?: string;
componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
@ -424,8 +425,10 @@ export interface WidgetConfigComponentData {
isDataEnabled: boolean;
settingsSchema: JsonSettingsSchema;
dataKeySettingsSchema: JsonSettingsSchema;
latestDataKeySettingsSchema: JsonSettingsSchema;
settingsDirective: string;
dataKeySettingsDirective: string;
latestDataKeySettingsDirective: string;
}
export const MissingWidgetType: WidgetInfo = {
@ -480,12 +483,14 @@ export const ErrorWidgetType: WidgetInfo = {
export interface WidgetTypeInstance {
getSettingsSchema?: () => string;
getDataKeySettingsSchema?: () => string;
getLatestDataKeySettingsSchema?: () => string;
typeParameters?: () => WidgetTypeParameters;
useCustomDatasources?: () => boolean;
actionSources?: () => {[actionSourceId: string]: WidgetActionSource};
onInit?: () => void;
onDataUpdated?: () => void;
onLatestDataUpdated?: () => void;
onResize?: () => void;
onEditModeChanged?: () => void;
onMobileModeChanged?: () => void;
@ -512,8 +517,10 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
controllerScript: widgetTypeEntity.descriptor.controllerScript,
settingsSchema: widgetTypeEntity.descriptor.settingsSchema,
dataKeySettingsSchema: widgetTypeEntity.descriptor.dataKeySettingsSchema,
latestDataKeySettingsSchema: widgetTypeEntity.descriptor.latestDataKeySettingsSchema,
settingsDirective: widgetTypeEntity.descriptor.settingsDirective,
dataKeySettingsDirective: widgetTypeEntity.descriptor.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetTypeEntity.descriptor.latestDataKeySettingsDirective,
defaultConfig: widgetTypeEntity.descriptor.defaultConfig
};
}
@ -540,8 +547,10 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
controllerScript: widgetInfo.controllerScript,
settingsSchema: widgetInfo.settingsSchema,
dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema,
latestDataKeySettingsSchema: widgetInfo.latestDataKeySettingsSchema,
settingsDirective: widgetInfo.settingsDirective,
dataKeySettingsDirective: widgetInfo.dataKeySettingsDirective,
latestDataKeySettingsDirective: widgetInfo.latestDataKeySettingsDirective,
defaultConfig: widgetInfo.defaultConfig
};
return {

16
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -221,6 +221,22 @@
<div #dataKeySettingsJsonInput></div>
</div>
</mat-tab>
<mat-tab [aria-labelledby]="widget.type !== widgetTypes.timeseries ? 'hidden': ''" label="{{ 'widget.latest-datakey-settings-schema' | translate }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonLatestDataKeySettingsFullscreen">
<div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyLatestDataKeyJson()">
{{ 'widget.tidy' | translate }}
</button>
<button mat-icon-button class="tb-mat-32"
(click)="jsonLatestDataKeySettingsFullscreen = !jsonLatestDataKeySettingsFullscreen"
matTooltip="{{(jsonLatestDataKeySettingsFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ jsonLatestDataKeySettingsFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
<div #latestDataKeySettingsJsonInput></div>
</div>
</mat-tab>
<mat-tab label="{{ 'widget.widget-settings' | translate }}">
<div class="tb-resize-container" style="background-color: #fff;">
<div class="mat-padding">

7
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss

@ -91,6 +91,13 @@ tb-widget-editor {
width: 100%;
height: 100%;
}
.mat-tab-label[aria-labelledby='hidden'] {
width: 0px !important;
min-width: 0px;
padding: 0px;
}
}
.tb-split-vertical {

33
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts

@ -98,6 +98,9 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
@ViewChild('dataKeySettingsJsonInput', {static: true})
dataKeySettingsJsonInputElmRef: ElementRef;
@ViewChild('latestDataKeySettingsJsonInput', {static: true})
latestDataKeySettingsJsonInputElmRef: ElementRef;
@ViewChild('javascriptInput', {static: true})
javascriptInputElmRef: ElementRef;
@ -126,6 +129,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
cssFullscreen = false;
jsonSettingsFullscreen = false;
jsonDataKeySettingsFullscreen = false;
jsonLatestDataKeySettingsFullscreen = false;
javascriptFullscreen = false;
iFrameFullscreen = false;
@ -135,6 +139,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
cssEditor: Ace.Editor;
jsonSettingsEditor: Ace.Editor;
dataKeyJsonSettingsEditor: Ace.Editor;
latestDataKeyJsonSettingsEditor: Ace.Editor;
jsEditor: Ace.Editor;
aceResize$: ResizeObserver;
@ -343,6 +348,19 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
})
));
editorsObservables.push(this.createAceEditor(this.latestDataKeySettingsJsonInputElmRef, 'json').pipe(
tap((editor) => {
this.latestDataKeyJsonSettingsEditor = editor;
this.latestDataKeyJsonSettingsEditor.on('input', () => {
const editorValue = this.latestDataKeyJsonSettingsEditor.getValue();
if (this.widget.latestDataKeySettingsSchema !== editorValue) {
this.widget.latestDataKeySettingsSchema = editorValue;
this.isDirty = true;
}
});
})
));
editorsObservables.push(this.createAceEditor(this.javascriptInputElmRef, 'javascript').pipe(
tap((editor) => {
this.jsEditor = editor;
@ -373,6 +391,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.cssEditor.setValue(this.widget.templateCss ? this.widget.templateCss : '', -1);
this.jsonSettingsEditor.setValue(this.widget.settingsSchema ? this.widget.settingsSchema : '', -1);
this.dataKeyJsonSettingsEditor.setValue(this.widget.dataKeySettingsSchema ? this.widget.dataKeySettingsSchema : '', -1);
this.latestDataKeyJsonSettingsEditor.setValue(this.widget.latestDataKeySettingsSchema ?
this.widget.latestDataKeySettingsSchema : '', -1);
this.jsEditor.setValue(this.widget.controllerScript ? this.widget.controllerScript : '', -1);
}
@ -673,6 +693,19 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
);
}
beautifyLatestDataKeyJson(): void {
beautifyJs(this.widget.latestDataKeySettingsSchema, {indent_size: 4}).subscribe(
(res) => {
if (this.widget.latestDataKeySettingsSchema !== res) {
this.isDirty = true;
this.widget.latestDataKeySettingsSchema = res;
this.latestDataKeyJsonSettingsEditor.setValue(this.widget.latestDataKeySettingsSchema ?
this.widget.latestDataKeySettingsSchema : '', -1);
}
}
);
}
beautifyJs(): void {
beautifyJs(this.widget.controllerScript, {indent_size: 4, wrap_line_length: 60}).subscribe(
(res) => {

31
ui-ngx/src/app/shared/models/widget.models.ts

@ -30,9 +30,7 @@ import { AfterViewInit, Directive, EventEmitter, Inject, OnInit } from '@angular
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, FormGroup } from '@angular/forms';
import {RuleChainType} from "@shared/models/rule-chain.models";
import {Observable} from "rxjs";
import {RuleNodeConfiguration} from "@shared/models/rule-node.models";
import { Observable } from 'rxjs';
import { Dashboard } from '@shared/models/dashboard.models';
export enum widgetType {
@ -152,8 +150,10 @@ export interface WidgetTypeDescriptor {
controllerScript: string;
settingsSchema?: string | any;
dataKeySettingsSchema?: string | any;
latestDataKeySettingsSchema?: string | any;
settingsDirective?: string;
dataKeySettingsDirective?: string;
latestDataKeySettingsDirective?: string;
defaultConfig: string;
sizeX: number;
sizeY: number;
@ -168,6 +168,7 @@ export interface WidgetTypeParameters {
stateData?: boolean;
hasDataPageLink?: boolean;
singleEntity?: boolean;
hasAdditionalLatestDataKeys?: boolean;
warnOnPageDataOverflow?: boolean;
ignoreDataUpdateOnIntervalTick?: boolean;
@ -177,6 +178,7 @@ export interface WidgetControllerDescriptor {
widgetTypeFunction?: any;
settingsSchema?: string | any;
dataKeySettingsSchema?: string | any;
latestDataKeySettingsSchema?: string | any;
typeParameters?: WidgetTypeParameters;
actionSources?: {[actionSourceId: string]: WidgetActionSource};
}
@ -294,6 +296,7 @@ export interface Datasource {
name?: string;
aliasName?: string;
dataKeys?: Array<DataKey>;
latestDataKeys?: Array<DataKey>;
entityType?: EntityType;
entityId?: string;
entityName?: string;
@ -311,9 +314,31 @@ export interface Datasource {
keyFilters?: Array<KeyFilter>;
entityFilter?: EntityFilter;
dataKeyStartIndex?: number;
latestDataKeyStartIndex?: number;
[key: string]: any;
}
export interface FormattedData {
$datasource: Datasource;
entityName: string;
deviceName: string;
entityId: string;
entityType: EntityType;
entityLabel: string;
entityDescription: string;
aliasName: string;
dsIndex: number;
dsName: string;
deviceType: string;
[key: string]: any;
}
export interface ReplaceInfo {
variable: string;
valDec?: number;
dataKeyName: string;
}
export type DataSet = [number, any][];
export interface DataSetHolder {

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

@ -890,7 +890,20 @@
"maximum-timeseries-or-attributes": "Maximum { count, plural, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }",
"alarm-fields-required": "Alarm fields are required.",
"function-types": "Function types",
"function-type": "Function type",
"function-types-required": "Function types are required.",
"alarm-keys": "Alarm data keys",
"alarm-key": "Alarm data key",
"alarm-key-functions": "Alarm key functions",
"alarm-key-function": "Alarm key function",
"latest-keys": "Latest data keys",
"latest-key": "Latest data key",
"latest-key-functions": "Latest key functions",
"latest-key-function": "Latest key function",
"timeseries-keys": "Timeseries data keys",
"timeseries-key": "Timeseries data key",
"timeseries-key-functions": "Timeseries key functions",
"timeseries-key-function": "Timeseries key function",
"maximum-function-types": "Maximum { count, plural, 1 {1 function type is allowed.} other {# function types are allowed} }",
"time-description": "timestamp of the current value;",
"value-description": "the current value;",
@ -2982,6 +2995,7 @@
"css": "CSS",
"settings-schema": "Settings schema",
"datakey-settings-schema": "Data key settings schema",
"latest-datakey-settings-schema": "Latest data key settings schema",
"widget-settings": "Widget settings",
"description": "Description",
"image-preview": "Image preview",

Loading…
Cancel
Save