diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json
index 80eb49442f..c2e2ff6858 100644
--- a/application/src/main/data/json/system/widget_bundles/cards.json
+++ b/application/src/main/data/json/system/widget_bundles/cards.json
@@ -69,22 +69,6 @@
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"nodeRelationQueryFunction\":\"/**\\n\\n// Function should return relations query object for current node used to fetch entity children.\\n// Function can return 'default' string value. In this case default relations query will be used.\\n\\n// The following example code will construct simple relations query that will fetch relations of type 'Contains'\\n// from the current entity.\\n\\nvar entity = nodeCtx.entity;\\nvar query = {\\n parameters: {\\n rootId: entity.id.id,\\n rootType: entity.id.entityType,\\n direction: types.entitySearchDirection.from,\\n relationTypeGroup: \\\"COMMON\\\",\\n maxLevel: 1\\n },\\n filters: [{\\n relationType: \\\"Contains\\\",\\n entityTypes: []\\n }]\\n};\\nreturn query;\\n\\n**/\\n\",\"nodeHasChildrenFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node has children (whether it can be expanded).\\n\\n// The following example code will restrict entities hierarchy expansion up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n// The next example code will restrict entities expansion according to the value of example 'nodeHasChildren' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeHasChildren') && data['nodeHasChildren'] !== null) {\\n return data['nodeHasChildren'] === 'true';\\n} else {\\n return true;\\n}\\n \\n**/\\n \",\"nodeTextFunction\":\"/**\\n\\n// Function should return text (can be HTML code) for the current node.\\n\\n// The following example code will generate node text consisting of entity name and temperature if temperature value is present in entity attributes/timeseries.\\n\\nvar data = nodeCtx.data;\\nvar entity = nodeCtx.entity;\\nvar text = entity.name;\\nif (data.hasOwnProperty('temperature') && data['temperature'] !== null) {\\n text += \\\" \\\"+ data['temperature'] +\\\" °C\\\";\\n}\\nreturn text;\\n\\n**/\",\"nodeIconFunction\":\"/** \\n\\n// Function should return node icon info object.\\n// Resulting object should contain either 'materialIcon' or 'iconUrl' property. \\n// Where:\\n - 'materialIcon' - name of the material icon to be used from the Material Icons Library (https://material.io/tools/icons);\\n - 'iconUrl' - url of the external image to be used as node icon.\\n// Function can return 'default' string value. In this case default icons according to entity type will be used.\\n\\n// The following example code shows how to use external image for devices which name starts with 'Test' and use \\n// default icons for the rest of entities.\\n\\nvar entity = nodeCtx.entity;\\nif (entity.id.entityType === 'DEVICE' && entity.name.startsWith('Test')) {\\n return {iconUrl: 'https://avatars1.githubusercontent.com/u/14793288?v=4&s=117'};\\n} else {\\n return 'default';\\n}\\n \\n**/\",\"nodeDisabledFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be disabled (not selectable).\\n\\n// The following example code will disable current node according to the value of example 'nodeDisabled' attribute.\\n\\nvar data = nodeCtx.data;\\nif (data.hasOwnProperty('nodeDisabled') && data['nodeDisabled'] !== null) {\\n return data['nodeDisabled'] === 'true';\\n} else {\\n return false;\\n}\\n \\n**/\\n\",\"nodesSortFunction\":\"/**\\n\\n// This function is used to sort nodes of the same level. Function should compare two nodes and return \\n// integer value: \\n// - less than 0 - sort nodeCtx1 to an index lower than nodeCtx2\\n// - 0 - leave nodeCtx1 and nodeCtx2 unchanged with respect to each other\\n// - greater than 0 - sort nodeCtx2 to an index lower than nodeCtx1\\n\\n// The following example code will sort entities first by entity type in alphabetical order then\\n// by entity name in alphabetical order.\\n\\nvar result = nodeCtx1.entity.id.entityType.localeCompare(nodeCtx2.entity.id.entityType);\\nif (result === 0) {\\n result = nodeCtx1.entity.name.localeCompare(nodeCtx2.entity.name);\\n}\\nreturn result;\\n \\n**/\",\"nodeOpenedFunction\":\"/**\\n\\n// Function should return boolean value indicating whether current node should be opened (expanded) when it first loaded.\\n\\n// The following example code will open by default nodes up to third level.\\n\\nreturn nodeCtx.level <= 2;\\n\\n**/\\n \"},\"title\":\"Entities hierarchy\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"widgetStyle\":{},\"actions\":{}}"
}
},
- {
- "alias": "entities_table",
- "name": "Entities table",
- "descriptor": {
- "type": "latest",
- "sizeX": 7.5,
- "sizeY": 6.5,
- "resources": [],
- "templateHtml": "\n",
- "templateCss": "",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
- "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
- "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\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)\",\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, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}]}"
- }
- },
{
"alias": "html_value_card",
"name": "HTML Value Card",
@@ -132,6 +116,22 @@
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"var\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"backgroundImageUrl\":\"data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnMiIgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMTAwIiB3aWR0aD0iMTAwIiB2ZXJzaW9uPSIxLjEiIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgdmlld0JveD0iMCAwIDEwMCAxMDAiPgogPGcgaWQ9ImxheWVyMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtOTUyLjM2KSI+CiAgPHJlY3QgaWQ9InJlY3Q0Njg0IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBoZWlnaHQ9Ijk5LjAxIiB3aWR0aD0iOTkuMDEiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiB5PSI5NTIuODYiIHg9Ii40OTUwNSIgc3Ryb2tlLXdpZHRoPSIuOTkwMTAiIGZpbGw9IiNlZWUiLz4KICA8dGV4dCBpZD0idGV4dDQ2ODYiIHN0eWxlPSJ3b3JkLXNwYWNpbmc6MHB4O2xldHRlci1zcGFjaW5nOjBweDt0ZXh0LWFuY2hvcjptaWRkbGU7dGV4dC1hbGlnbjpjZW50ZXIiIGZvbnQtd2VpZ2h0PSJib2xkIiB4bWw6c3BhY2U9InByZXNlcnZlIiBmb250LXNpemU9IjEwcHgiIGxpbmUtaGVpZ2h0PSIxMjUlIiB5PSI5NzAuNzI4MDkiIHg9IjQ5LjM5NjQ3NyIgZm9udC1mYW1pbHk9IlJvYm90byIgZmlsbD0iIzY2NjY2NiI+PHRzcGFuIGlkPSJ0c3BhbjQ2OTAiIHg9IjUwLjY0NjQ3NyIgeT0iOTcwLjcyODA5Ij5JbWFnZSBiYWNrZ3JvdW5kIDwvdHNwYW4+PHRzcGFuIGlkPSJ0c3BhbjQ2OTIiIHg9IjQ5LjM5NjQ3NyIgeT0iOTgzLjIyODA5Ij5pcyBub3QgY29uZmlndXJlZDwvdHNwYW4+PC90ZXh0PgogIDxyZWN0IGlkPSJyZWN0NDY5NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgaGVpZ2h0PSIxOS4zNiIgd2lkdGg9IjY5LjM2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgeT0iOTkyLjY4IiB4PSIxNS4zMiIgc3Ryb2tlLXdpZHRoPSIuNjM5ODYiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+Cg==\",\"labels\":[{\"pattern\":\"Value: ${#0:2} units.\",\"x\":20,\"y\":47,\"font\":{\"color\":\"#515151\",\"family\":\"Roboto\",\"size\":6,\"style\":\"normal\",\"weight\":\"500\"}}]},\"title\":\"Label widget\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
}
+ },
+ {
+ "alias": "entities_table",
+ "name": "Entities table",
+ "descriptor": {
+ "type": "latest",
+ "sizeX": 7.5,
+ "sizeY": 6.5,
+ "resources": [],
+ "templateHtml": "\n",
+ "templateCss": "",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
+ "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\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)\",\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, entity, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}]}"
+ }
}
]
}
\ No newline at end of file
diff --git a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
index c8ac0150ee..f9d05d83f3 100644
--- a/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
+++ b/application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
@@ -6,8 +6,8 @@
},
"widgetTypes": [
{
- "alias": "asset_admin_table",
- "name": "Asset admin table",
+ "alias": "device_admin_table",
+ "name": "Device admin table",
"descriptor": {
"type": "latest",
"sizeX": 7.5,
@@ -15,15 +15,15 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\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)\",\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, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"
\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
}
},
{
- "alias": "device_admin_table",
- "name": "Device admin table",
+ "alias": "asset_admin_table",
+ "name": "Asset admin table",
"descriptor": {
"type": "latest",
"sizeX": 7.5,
@@ -31,10 +31,10 @@
"resources": [],
"templateHtml": "\n",
"templateCss": "",
- "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
+ "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"EntitiesTableSettings\",\n \"properties\": {\n \"entitiesTitle\": {\n \"title\": \"Entities table title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"enableSearch\": {\n \"title\": \"Enable entities search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableSelectColumnDisplay\": {\n \"title\": \"Enable select columns to display\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayEntityName\": {\n \"title\": \"Display entity name column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"entityNameColumnTitle\": {\n \"title\": \"Entity name column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityLabel\": {\n \"title\": \"Display entity label column\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"entityLabelColumnTitle\": {\n \"title\": \"Entity label column title\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"displayEntityType\": {\n \"title\": \"Display entity type column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"defaultSortOrder\": {\n \"title\": \"Default sort order\",\n \"type\": \"string\",\n \"default\": \"entityName\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"entitiesTitle\",\n \"enableSearch\",\n \"enableSelectColumnDisplay\",\n \"displayEntityName\",\n \"entityNameColumnTitle\",\n \"displayEntityLabel\",\n \"entityLabelColumnTitle\",\n \"displayEntityType\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"defaultSortOrder\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"columnWidth\": {\n \"title\": \"Column width (px or %)\",\n \"type\": \"string\",\n \"default\": \"0px\"\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)\",\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, entity, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"columnWidth\",\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
- "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Device admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Device admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add device\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddDeviceDialog();\\n\\nfunction openAddDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\\n}\\n\\nfunction AddDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.addDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addDeviceFormGroup.markAsPristine();\\n let device = {\\n name: vm.addDeviceFormGroup.get('deviceName').value,\\n type: vm.addDeviceFormGroup.get('deviceType').value,\\n label: vm.addDeviceFormGroup.get('deviceLabel').value\\n };\\n deviceService.saveDevice(device).subscribe(\\n function (device) {\\n saveAttributes(device.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit device\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditDeviceDialog();\\n\\nfunction openEditDeviceDialog() {\\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\\n}\\n\\nfunction EditDeviceDialogController(instance) {\\n let vm = instance;\\n \\n vm.device = null;\\n vm.attributes = {};\\n \\n vm.editDeviceFormGroup = vm.fb.group({\\n deviceName: ['', [vm.validators.required]],\\n deviceType: ['', [vm.validators.required]],\\n deviceLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editDeviceFormGroup.markAsPristine();\\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value,\\n vm.device.type = vm.editDeviceFormGroup.get('deviceType').value,\\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value\\n deviceService.saveDevice(vm.device).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n deviceService.getDevice(entityId.id).subscribe(\\n function (device) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.device = device;\\n vm.editDeviceFormGroup.patchValue(\\n {\\n deviceName: vm.device.name,\\n deviceType: vm.device.type,\\n deviceLabel: vm.device.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete device\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\\n\\nopenDeleteDeviceDialog();\\n\\nfunction openDeleteDeviceDialog() {\\n let title = \\\"Are you sure you want to delete the device \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the device and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteDevice();\\n }\\n }\\n );\\n}\\n\\nfunction deleteDevice() {\\n deviceService.deleteDevice(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
+ "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"displayEntityName\":true,\"displayEntityType\":true,\"entitiesTitle\":\"Asset admin table\",\"enableSelectColumnDisplay\":true},\"title\":\"Asset admin table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6401141393938932,\"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;\"}]}],\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{\"headerButton\":[{\"name\":\"Add asset\",\"icon\":\"add\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenAddAssetDialog();\\n\\nfunction openAddAssetDialog() {\\n customDialog.customDialog(htmlTemplate, AddAssetDialogController).subscribe();\\n}\\n\\nfunction AddAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.addAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.addAssetFormGroup.markAsPristine();\\n let asset = {\\n name: vm.addAssetFormGroup.get('assetName').value,\\n type: vm.addAssetFormGroup.get('assetType').value,\\n label: vm.addAssetFormGroup.get('assetLabel').value\\n };\\n assetService.saveAsset(asset).subscribe(\\n function (asset) {\\n saveAttributes(asset.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n function saveAttributes(entityId) {\\n let attributes = vm.addAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, \\\"SERVER_SCOPE\\\", attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"70837a9d-c3de-a9a7-03c5-dccd14998758\"}],\"actionCellButton\":[{\"name\":\"Edit asset\",\"icon\":\"edit\",\"type\":\"customPretty\",\"customHtml\":\"\\n\",\"customCss\":\"\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\\n\\nopenEditAssetDialog();\\n\\nfunction openEditAssetDialog() {\\n customDialog.customDialog(htmlTemplate, EditAssetDialogController).subscribe();\\n}\\n\\nfunction EditAssetDialogController(instance) {\\n let vm = instance;\\n \\n vm.asset = null;\\n vm.attributes = {};\\n \\n vm.editAssetFormGroup = vm.fb.group({\\n assetName: ['', [vm.validators.required]],\\n assetType: ['', [vm.validators.required]],\\n assetLabel: [''],\\n attributes: vm.fb.group({\\n latitude: [null],\\n longitude: [null]\\n }) \\n });\\n \\n vm.cancel = function() {\\n vm.dialogRef.close(null);\\n };\\n \\n vm.save = function() {\\n vm.editAssetFormGroup.markAsPristine();\\n vm.asset.name = vm.editAssetFormGroup.get('assetName').value,\\n vm.asset.type = vm.editAssetFormGroup.get('assetType').value,\\n vm.asset.label = vm.editAssetFormGroup.get('assetLabel').value\\n assetService.saveAsset(vm.asset).subscribe(\\n function () {\\n saveAttributes().subscribe(\\n function () {\\n widgetContext.updateAliases();\\n vm.dialogRef.close(null);\\n }\\n );\\n }\\n );\\n };\\n \\n getEntityInfo();\\n \\n function getEntityInfo() {\\n assetService.getAsset(entityId.id).subscribe(\\n function (asset) {\\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\\n ['latitude', 'longitude']).subscribe(\\n function (attributes) {\\n for (let i = 0; i < attributes.length; i++) {\\n vm.attributes[attributes[i].key] = attributes[i].value; \\n }\\n vm.asset = asset;\\n vm.editAssetFormGroup.patchValue(\\n {\\n assetName: vm.asset.name,\\n assetType: vm.asset.type,\\n assetLabel: vm.asset.label,\\n attributes: {\\n latitude: vm.attributes.latitude,\\n longitude: vm.attributes.longitude\\n }\\n }, {emitEvent: false}\\n );\\n } \\n );\\n }\\n ); \\n }\\n \\n function saveAttributes() {\\n let attributes = vm.editAssetFormGroup.get('attributes').value;\\n let attributesArray = [];\\n for (let key in attributes) {\\n attributesArray.push({key: key, value: attributes[key]});\\n }\\n if (attributesArray.length > 0) {\\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\\n } else {\\n return widgetContext.rxjs.of([]);\\n }\\n }\\n}\",\"customResources\":[],\"id\":\"93931e52-5d7c-903e-67aa-b9435df44ff4\"},{\"name\":\"Delete asset\",\"icon\":\"delete\",\"type\":\"custom\",\"customFunction\":\"let $injector = widgetContext.$scope.$injector;\\nlet dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));\\nlet assetService = $injector.get(widgetContext.servicesMap.get('assetService'));\\n\\nopenDeleteAssetDialog();\\n\\nfunction openDeleteAssetDialog() {\\n let title = \\\"Are you sure you want to delete the asset \\\" + entityName + \\\"?\\\";\\n let content = \\\"Be careful, after the confirmation, the asset and all related data will become unrecoverable!\\\";\\n dialogs.confirm(title, content, 'Cancel', 'Delete').subscribe(\\n function (result) {\\n if (result) {\\n deleteAsset();\\n }\\n }\\n );\\n}\\n\\nfunction deleteAsset() {\\n assetService.deleteAsset(entityId.id).subscribe(\\n function () {\\n widgetContext.updateAliases();\\n }\\n );\\n}\\n\",\"id\":\"ec2708f6-9ff0-186b-e4fc-7635ebfa3074\"}]}}"
}
}
]
diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts
index f51e3c04f2..bcd1829763 100644
--- a/ui-ngx/src/app/core/api/entity-data-subscription.ts
+++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts
@@ -467,14 +467,15 @@ export class EntityDataSubscription {
{
pageData,
data,
- datasourceIndex: this.listener.configDatasourceIndex
+ datasourceIndex: this.listener.configDatasourceIndex,
+ pageLink: this.entityDataSubscriptionOptions.pageLink
}
);
this.entityDataResolveSubject.complete();
} else {
if (isInitialData || this.entityDataSubscriptionOptions.isPaginatedDataSubscription) {
this.listener.dataLoaded(pageData, data,
- this.listener.configDatasourceIndex);
+ this.listener.configDatasourceIndex, this.entityDataSubscriptionOptions.pageLink);
}
if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription && isInitialData) {
if (this.datasourceType === DatasourceType.function) {
@@ -484,7 +485,8 @@ export class EntityDataSubscription {
{
pageData,
data,
- datasourceIndex: this.listener.configDatasourceIndex
+ datasourceIndex: this.listener.configDatasourceIndex,
+ pageLink: this.entityDataSubscriptionOptions.pageLink
}
);
this.entityDataResolveSubject.complete();
diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts
index 2a2c6dd44a..675348d37a 100644
--- a/ui-ngx/src/app/core/api/entity-data.service.ts
+++ b/ui-ngx/src/app/core/api/entity-data.service.ts
@@ -34,7 +34,9 @@ export interface EntityDataListener {
subscriptionTimewindow?: SubscriptionTimewindow;
configDatasource: Datasource;
configDatasourceIndex: number;
- dataLoaded: (pageData: PageData, data: Array>, datasourceIndex: number) => void;
+ dataLoaded: (pageData: PageData,
+ data: Array>,
+ datasourceIndex: number, pageLink: EntityDataPageLink) => void;
dataUpdated: (data: DataSetHolder, datasourceIndex: number, dataIndex: number, dataKeyIndex: number, detectChanges: boolean) => void;
initialPageDataChanged?: (nextPageData: PageData) => void;
updateRealtimeSubscription?: () => SubscriptionTimewindow;
@@ -46,6 +48,7 @@ export interface EntityDataLoadResult {
pageData: PageData;
data: Array>;
datasourceIndex: number;
+ pageLink: EntityDataPageLink;
}
@Injectable({
diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts
index 543d7dafe3..9a2a877369 100644
--- a/ui-ngx/src/app/core/api/widget-api.models.ts
+++ b/ui-ngx/src/app/core/api/widget-api.models.ts
@@ -180,9 +180,17 @@ export class WidgetSubscriptionContext {
getServerTimeDiff: () => Observable;
}
+export type SubscriptionMessageSeverity = 'info' | 'warn' | 'error' | 'success';
+
+export interface SubscriptionMessage {
+ severity: SubscriptionMessageSeverity,
+ message: string;
+}
+
export interface WidgetSubscriptionCallbacks {
onDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void;
onDataUpdateError?: (subscription: IWidgetSubscription, e: any) => void;
+ onSubscriptionMessage?: (subscription: IWidgetSubscription, message: SubscriptionMessage) => void;
onInitialPageDataChanged?: (subscription: IWidgetSubscription, nextPageData: PageData) => void;
dataLoading?: (subscription: IWidgetSubscription) => void;
legendDataUpdated?: (subscription: IWidgetSubscription, detectChanges: boolean) => void;
@@ -204,6 +212,7 @@ export interface WidgetSubscriptionOptions {
datasources?: Array;
hasDataPageLink?: boolean;
singleEntity?: boolean;
+ warnOnPageDataOverflow?: boolean;
targetDeviceAliasIds?: Array;
targetDeviceIds?: Array;
useDashboardTimewindow?: boolean;
diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts
index 531d5b498d..b4ef58e09c 100644
--- a/ui-ngx/src/app/core/api/widget-subscription.ts
+++ b/ui-ngx/src/app/core/api/widget-subscription.ts
@@ -16,7 +16,7 @@
import {
IWidgetSubscription,
- SubscriptionEntityInfo,
+ SubscriptionEntityInfo, SubscriptionMessage,
WidgetSubscriptionCallbacks,
WidgetSubscriptionContext,
WidgetSubscriptionOptions
@@ -78,6 +78,7 @@ export class WidgetSubscription implements IWidgetSubscription {
hasDataPageLink: boolean;
singleEntity: boolean;
+ warnOnPageDataOverflow: boolean;
datasourcePages: PageData[];
dataPages: PageData>[];
@@ -174,6 +175,7 @@ export class WidgetSubscription implements IWidgetSubscription {
} else if (this.type === widgetType.alarm) {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {});
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || (() => {});
+ this.callbacks.onSubscriptionMessage = this.callbacks.onSubscriptionMessage || (() => {});
this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {});
this.callbacks.timeWindowUpdated = this.callbacks.timeWindowUpdated || (() => {});
this.alarmSource = options.alarmSource;
@@ -208,6 +210,7 @@ export class WidgetSubscription implements IWidgetSubscription {
} else {
this.callbacks.onDataUpdated = this.callbacks.onDataUpdated || (() => {});
this.callbacks.onDataUpdateError = this.callbacks.onDataUpdateError || (() => {});
+ this.callbacks.onSubscriptionMessage = this.callbacks.onSubscriptionMessage || (() => {});
this.callbacks.onInitialPageDataChanged = this.callbacks.onInitialPageDataChanged || (() => {});
this.callbacks.dataLoading = this.callbacks.dataLoading || (() => {});
this.callbacks.legendDataUpdated = this.callbacks.legendDataUpdated || (() => {});
@@ -217,6 +220,7 @@ export class WidgetSubscription implements IWidgetSubscription {
this.entityDataListeners = [];
this.hasDataPageLink = options.hasDataPageLink;
this.singleEntity = options.singleEntity;
+ this.warnOnPageDataOverflow = options.warnOnPageDataOverflow;
this.datasourcePages = [];
this.datasources = [];
this.dataPages = [];
@@ -417,8 +421,8 @@ export class WidgetSubscription implements IWidgetSubscription {
subscriptionType: this.type,
configDatasource: datasource,
configDatasourceIndex: index,
- dataLoaded: (pageData, data1, datasourceIndex) => {
- this.dataLoaded(pageData, data1, datasourceIndex, true)
+ dataLoaded: (pageData, data1, datasourceIndex, pageLink) => {
+ this.dataLoaded(pageData, data1, datasourceIndex, pageLink, true)
},
initialPageDataChanged: this.initialPageDataChanged.bind(this),
dataUpdated: this.dataUpdated.bind(this),
@@ -443,7 +447,7 @@ export class WidgetSubscription implements IWidgetSubscription {
return forkJoin(resolveResultObservables).pipe(
map((resolveResults) => {
resolveResults.forEach((resolveResult) => {
- this.dataLoaded(resolveResult.pageData, resolveResult.data, resolveResult.datasourceIndex, false);
+ this.dataLoaded(resolveResult.pageData, resolveResult.data, resolveResult.datasourceIndex, resolveResult.pageLink, false);
});
this.configureLoadedData();
this.hasResolvedData = this.datasources.length > 0;
@@ -533,6 +537,16 @@ export class WidgetSubscription implements IWidgetSubscription {
});
}
+ private onSubscriptionMessage(message: SubscriptionMessage) {
+ if (this.cafs.message) {
+ this.cafs.message();
+ this.cafs.message = null;
+ }
+ this.cafs.message = this.ctx.raf.raf(() => {
+ this.callbacks.onSubscriptionMessage(this, message);
+ });
+ }
+
onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) {
if (this.type === widgetType.timeseries || this.type === widgetType.alarm) {
if (this.useDashboardTimewindow) {
@@ -776,8 +790,8 @@ export class WidgetSubscription implements IWidgetSubscription {
configDatasource: datasource,
configDatasourceIndex: datasourceIndex,
subscriptionTimewindow: this.subscriptionTimewindow,
- dataLoaded: (pageData, data1, datasourceIndex1) => {
- this.dataLoaded(pageData, data1, datasourceIndex1, true)
+ dataLoaded: (pageData, data1, datasourceIndex1, pageLink1) => {
+ this.dataLoaded(pageData, data1, datasourceIndex1, pageLink1, true)
},
dataUpdated: this.dataUpdated.bind(this),
updateRealtimeSubscription: () => {
@@ -1039,7 +1053,7 @@ export class WidgetSubscription implements IWidgetSubscription {
private dataLoaded(pageData: PageData,
data: Array>,
- datasourceIndex: number, isUpdate: boolean) {
+ datasourceIndex: number, pageLink: EntityDataPageLink, isUpdate: boolean) {
const datasource = this.configuredDatasources[datasourceIndex];
datasource.dataReceived = true;
const datasources = pageData.data.map((entityData, index) =>
@@ -1062,6 +1076,17 @@ export class WidgetSubscription implements IWidgetSubscription {
totalPages: pageData.totalPages
};
this.dataPages[datasourceIndex] = datasourceDataPage;
+ if (datasource.type === DatasourceType.entity &&
+ pageData.hasNext && pageLink.pageSize > 1) {
+ if (this.warnOnPageDataOverflow) {
+ const message = this.ctx.translate.instant('widget.data-overflow',
+ {count: pageData.data.length, total: pageData.totalElements});
+ this.onSubscriptionMessage({
+ severity: 'warn',
+ message
+ })
+ }
+ }
if (isUpdate) {
this.configureLoadedData();
this.onDataUpdated(true);
diff --git a/ui-ngx/src/app/core/notification/notification.models.ts b/ui-ngx/src/app/core/notification/notification.models.ts
index 312298d3e4..69e9f0bb1d 100644
--- a/ui-ngx/src/app/core/notification/notification.models.ts
+++ b/ui-ngx/src/app/core/notification/notification.models.ts
@@ -20,7 +20,7 @@ export interface NotificationState {
hideNotification: HideNotification;
}
-export declare type NotificationType = 'info' | 'success' | 'error';
+export declare type NotificationType = 'info' | 'warn' | 'success' | 'error';
export declare type NotificationHorizontalPosition = 'start' | 'center' | 'end' | 'left' | 'right';
export declare type NotificationVerticalPosition = 'top' | 'bottom';
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
index 081fca2270..b4035be6d8 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
@@ -358,6 +358,9 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.singleEntity)) {
result.typeParameters.singleEntity = false;
}
+ if (isUndefined(result.typeParameters.warnOnPageDataOverflow)) {
+ result.typeParameters.warnOnPageDataOverflow = true;
+ }
if (isUndefined(result.typeParameters.dataKeysOptional)) {
result.typeParameters.dataKeysOptional = false;
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget.component.html b/ui-ngx/src/app/modules/home/components/widget/widget.component.html
index 4a306dacad..30a30ed95f 100644
--- a/ui-ngx/src/app/modules/home/components/widget/widget.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/widget.component.html
@@ -15,7 +15,8 @@
limitations under the License.
-->
-
+
{
this.handleWidgetException(e);
},
+ onSubscriptionMessage: (subscription, message) => {
+ if (this.displayWidgetInstance()) {
+ if (this.widgetInstanceInited) {
+ this.displayMessage(message.severity, message.message);
+ } else {
+ this.pendingMessage = message;
+ }
+ }
+ },
onInitialPageDataChanged: (subscription, nextPageData) => {
this.reInit();
},
@@ -855,6 +881,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
stateData: this.typeParameters.stateData,
hasDataPageLink: this.typeParameters.hasDataPageLink,
singleEntity: this.typeParameters.singleEntity,
+ warnOnPageDataOverflow: this.typeParameters.warnOnPageDataOverflow,
comparisonEnabled: comparisonSettings.comparisonEnabled,
timeForComparison: comparisonSettings.timeForComparison
};
@@ -910,6 +937,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
+ this.clearMessage();
this.detectChanges();
}
},
@@ -918,6 +946,9 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest;
this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText;
this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection;
+ if (subscription.rpcErrorText) {
+ this.displayMessage('error', subscription.rpcErrorText);
+ }
this.detectChanges();
}
},
@@ -925,6 +956,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
if (this.dynamicWidgetComponent) {
this.dynamicWidgetComponent.rpcErrorText = null;
this.dynamicWidgetComponent.rpcRejection = null;
+ this.clearMessage();
this.detectChanges();
}
}
diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts
index 07858516f0..96fae7bac3 100644
--- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts
+++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts
@@ -57,7 +57,7 @@ import {
NotificationType,
NotificationVerticalPosition
} from '@core/notification/notification.models';
-import { ActionNotificationShow } from '@core/notification/notification.actions';
+import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { DeviceService } from '@core/http/device.service';
@@ -244,6 +244,20 @@ export class WidgetContext {
this.showToast('success', message, duration, verticalPosition, horizontalPosition, target);
}
+ showInfoToast(message: string,
+ verticalPosition: NotificationVerticalPosition = 'bottom',
+ horizontalPosition: NotificationHorizontalPosition = 'left',
+ target?: string) {
+ this.showToast('info', message, undefined, verticalPosition, horizontalPosition, target);
+ }
+
+ showWarnToast(message: string,
+ verticalPosition: NotificationVerticalPosition = 'bottom',
+ horizontalPosition: NotificationHorizontalPosition = 'left',
+ target?: string) {
+ this.showToast('warn', message, undefined, verticalPosition, horizontalPosition, target);
+ }
+
showErrorToast(message: string,
verticalPosition: NotificationVerticalPosition = 'bottom',
horizontalPosition: NotificationHorizontalPosition = 'left',
@@ -268,6 +282,13 @@ export class WidgetContext {
}));
}
+ hideToast(target?: string) {
+ this.store.dispatch(new ActionNotificationHide(
+ {
+ target,
+ }));
+ }
+
detectChanges(updateWidgetParams: boolean = false) {
if (!this.destroyed) {
if (updateWidgetParams) {
diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.html b/ui-ngx/src/app/shared/components/snack-bar-component.html
index f90fb5cac3..53f23fcf8a 100644
--- a/ui-ngx/src/app/shared/components/snack-bar-component.html
+++ b/ui-ngx/src/app/shared/components/snack-bar-component.html
@@ -16,8 +16,11 @@
-->
diff --git a/ui-ngx/src/app/shared/components/snack-bar-component.scss b/ui-ngx/src/app/shared/components/snack-bar-component.scss
index 054937fb45..6ef4191efb 100644
--- a/ui-ngx/src/app/shared/components/snack-bar-component.scss
+++ b/ui-ngx/src/app/shared/components/snack-bar-component.scss
@@ -33,6 +33,9 @@
&.info-toast {
background: #323232;
}
+ &.warn-toast {
+ background: #dc6d1b;
+ }
&.error-toast {
background: #800000;
}
diff --git a/ui-ngx/src/app/shared/components/toast.directive.ts b/ui-ngx/src/app/shared/components/toast.directive.ts
index a8b304a16f..1b6c1cd6d8 100644
--- a/ui-ngx/src/app/shared/components/toast.directive.ts
+++ b/ui-ngx/src/app/shared/components/toast.directive.ts
@@ -15,27 +15,27 @@
///
import {
- AfterViewInit,
- ChangeDetectorRef,
- Component,
+ AfterViewInit, ChangeDetectorRef,
+ Component, ComponentRef,
Directive,
ElementRef,
Inject,
Input,
NgZone,
- OnDestroy,
+ OnDestroy, Optional,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { MAT_SNACK_BAR_DATA, MatSnackBar, MatSnackBarConfig, MatSnackBarRef } from '@angular/material/snack-bar';
import { NotificationMessage } from '@app/core/notification/notification.models';
-import { onParentScrollOrWindowResize } from '@app/core/utils';
import { Subscription } from 'rxjs';
import { NotificationService } from '@app/core/services/notification.service';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { MatButton } from '@angular/material/button';
import Timeout = NodeJS.Timeout;
+import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
+import { ComponentPortal, PortalInjector } from '@angular/cdk/portal';
@Directive({
selector: '[tb-toast]'
@@ -48,17 +48,21 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
private notificationSubscription: Subscription = null;
private hideNotificationSubscription: Subscription = null;
- private snackBarRef: MatSnackBarRef
= null;
+ private snackBarRef: MatSnackBarRef = null;
+ private overlayRef: OverlayRef;
+ private toastComponentRef: ComponentRef;
private currentMessage: NotificationMessage = null;
private dismissTimeout: Timeout = null;
- constructor(public elementRef: ElementRef,
- public viewContainerRef: ViewContainerRef,
+ constructor(private elementRef: ElementRef,
+ private viewContainerRef: ViewContainerRef,
private notificationService: NotificationService,
- public snackBar: MatSnackBar,
+ private overlay: Overlay,
+ private snackBar: MatSnackBar,
private ngZone: NgZone,
- private breakpointObserver: BreakpointObserver) {
+ private breakpointObserver: BreakpointObserver,
+ private cd: ChangeDetectorRef) {
}
ngAfterViewInit(): void {
@@ -66,45 +70,12 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
(notificationMessage) => {
if (this.shouldDisplayMessage(notificationMessage)) {
this.currentMessage = notificationMessage;
- const data = {
- parent: this.elementRef,
- notification: notificationMessage
- };
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
- const config: MatSnackBarConfig = {
- horizontalPosition: notificationMessage.horizontalPosition || 'left',
- verticalPosition: !isGtSm ? 'bottom' : (notificationMessage.verticalPosition || 'top'),
- viewContainerRef: this.viewContainerRef,
- duration: notificationMessage.duration,
- panelClass: notificationMessage.panelClass,
- data
- };
- this.ngZone.run(() => {
- if (this.snackBarRef) {
- this.snackBarRef.dismiss();
- }
- this.snackBarRef = this.snackBar.openFromComponent(TbSnackBarComponent, config);
- if (notificationMessage.duration && notificationMessage.duration > 0 && notificationMessage.forceDismiss) {
- if (this.dismissTimeout !== null) {
- clearTimeout(this.dismissTimeout);
- this.dismissTimeout = null;
- }
- this.dismissTimeout = setTimeout(() => {
- if (this.snackBarRef) {
- this.snackBarRef.instance.actionButton._elementRef.nativeElement.click();
- }
- this.dismissTimeout = null;
- }, notificationMessage.duration);
- }
- this.snackBarRef.afterDismissed().subscribe(() => {
- if (this.dismissTimeout !== null) {
- clearTimeout(this.dismissTimeout);
- this.dismissTimeout = null;
- }
- this.snackBarRef = null;
- this.currentMessage = null;
- });
- });
+ if (isGtSm) {
+ this.showToastPanel(notificationMessage);
+ } else {
+ this.showSnackBar(notificationMessage);
+ }
}
}
);
@@ -118,6 +89,9 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
if (this.snackBarRef) {
this.snackBarRef.dismiss();
}
+ if (this.toastComponentRef) {
+ this.toastComponentRef.instance.actionButton._elementRef.nativeElement.click();
+ }
});
}
}
@@ -125,6 +99,127 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
);
}
+ private showToastPanel(notificationMessage: NotificationMessage) {
+ this.ngZone.run(() => {
+ if (this.snackBarRef) {
+ this.snackBarRef.dismiss();
+ }
+
+ const position = this.overlay.position();
+ let panelClass = ['tb-toast-panel'];
+ if (notificationMessage.panelClass) {
+ if (typeof notificationMessage.panelClass === 'string') {
+ panelClass.push(notificationMessage.panelClass);
+ } else if (notificationMessage.panelClass.length) {
+ panelClass = panelClass.concat(notificationMessage.panelClass);
+ }
+ }
+ const overlayConfig = new OverlayConfig({
+ panelClass,
+ backdropClass: 'cdk-overlay-transparent-backdrop',
+ hasBackdrop: false,
+ disposeOnNavigation: true
+ });
+ let originX;
+ let originY;
+ const horizontalPosition = notificationMessage.horizontalPosition || 'left';
+ const verticalPosition = notificationMessage.verticalPosition || 'top';
+ if (horizontalPosition === 'start' || horizontalPosition === 'left') {
+ originX = 'start';
+ } else if (horizontalPosition === 'end' || horizontalPosition === 'right') {
+ originX = 'end';
+ } else {
+ originX = 'center';
+ }
+ if (verticalPosition === 'top') {
+ originY = 'top';
+ } else {
+ originY = 'bottom';
+ }
+ const connectedPosition: ConnectedPosition = {
+ originX,
+ originY,
+ overlayX: originX,
+ overlayY: originY
+ };
+ overlayConfig.positionStrategy = position.flexibleConnectedTo(this.elementRef)
+ .withPositions([connectedPosition]);
+ this.overlayRef = this.overlay.create(overlayConfig);
+ const data: ToastPanelData = {
+ notification: notificationMessage
+ };
+ const injectionTokens = new WeakMap([
+ [MAT_SNACK_BAR_DATA, data],
+ [OverlayRef, this.overlayRef]
+ ]);
+ const injector = new PortalInjector(this.viewContainerRef.injector, injectionTokens);
+ this.toastComponentRef = this.overlayRef.attach(new ComponentPortal(TbSnackBarComponent, this.viewContainerRef, injector));
+ this.cd.detectChanges();
+
+ if (notificationMessage.duration && notificationMessage.duration > 0) {
+ if (this.dismissTimeout !== null) {
+ clearTimeout(this.dismissTimeout);
+ this.dismissTimeout = null;
+ }
+ this.dismissTimeout = setTimeout(() => {
+ if (this.toastComponentRef) {
+ this.toastComponentRef.instance.actionButton._elementRef.nativeElement.click();
+ }
+ this.dismissTimeout = null;
+ }, notificationMessage.duration + 500);
+ }
+ this.toastComponentRef.onDestroy(() => {
+ if (this.dismissTimeout !== null) {
+ clearTimeout(this.dismissTimeout);
+ this.dismissTimeout = null;
+ }
+ this.overlayRef = null;
+ this.toastComponentRef = null;
+ this.currentMessage = null;
+ });
+ });
+ }
+
+ private showSnackBar(notificationMessage: NotificationMessage) {
+ const data: ToastPanelData = {
+ notification: notificationMessage
+ };
+ const config: MatSnackBarConfig = {
+ horizontalPosition: notificationMessage.horizontalPosition || 'left',
+ verticalPosition: 'bottom',
+ viewContainerRef: this.viewContainerRef,
+ duration: notificationMessage.duration,
+ panelClass: notificationMessage.panelClass,
+ data
+ };
+ this.ngZone.run(() => {
+ if (this.snackBarRef) {
+ this.snackBarRef.dismiss();
+ }
+ this.snackBarRef = this.snackBar.openFromComponent(TbSnackBarComponent, config);
+ });
+ if (notificationMessage.duration && notificationMessage.duration > 0 && notificationMessage.forceDismiss) {
+ if (this.dismissTimeout !== null) {
+ clearTimeout(this.dismissTimeout);
+ this.dismissTimeout = null;
+ }
+ this.dismissTimeout = setTimeout(() => {
+ if (this.snackBarRef) {
+ this.snackBarRef.instance.actionButton._elementRef.nativeElement.click();
+ }
+ this.dismissTimeout = null;
+ }, notificationMessage.duration);
+ }
+ this.snackBarRef.afterDismissed().subscribe(() => {
+ if (this.dismissTimeout !== null) {
+ clearTimeout(this.dismissTimeout);
+ this.dismissTimeout = null;
+ }
+ this.snackBarRef = null;
+ this.currentMessage = null;
+ });
+ }
+
private shouldDisplayMessage(notificationMessage: NotificationMessage): boolean {
if (notificationMessage && notificationMessage.message) {
const target = notificationMessage.target || 'root';
@@ -139,6 +234,9 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
}
ngOnDestroy(): void {
+ if (this.overlayRef) {
+ this.overlayRef.dispose();
+ }
if (this.notificationSubscription) {
this.notificationSubscription.unsubscribe();
}
@@ -148,72 +246,84 @@ export class ToastDirective implements AfterViewInit, OnDestroy {
}
}
+interface ToastPanelData {
+ notification: NotificationMessage;
+}
+
+import {
+ AnimationTriggerMetadata,
+ AnimationEvent,
+ trigger,
+ state,
+ transition,
+ style,
+ animate,
+} from '@angular/animations';
+
+export const toastAnimations: {
+ readonly showHideToast: AnimationTriggerMetadata;
+} = {
+ showHideToast: trigger('showHideAnimation', [
+ state('in', style({ transform: 'scale(1)', opacity: 1 })),
+ transition('void => opened', [style({ transform: 'scale(0)', opacity: 0 }), animate('{{ open }}ms')]),
+ transition(
+ 'opened => closing',
+ animate('{{ close }}ms', style({ transform: 'scale(0)', opacity: 0 })),
+ ),
+ ]),
+};
+
+export type ToastAnimationState = 'default' | 'opened' | 'closing';
+
@Component({
selector: 'tb-snack-bar-component',
templateUrl: 'snack-bar-component.html',
- styleUrls: ['snack-bar-component.scss']
+ styleUrls: ['snack-bar-component.scss'],
+ animations: [toastAnimations.showHideToast]
})
export class TbSnackBarComponent implements AfterViewInit, OnDestroy {
@ViewChild('actionButton', {static: true}) actionButton: MatButton;
- private parentEl: HTMLElement;
- public snackBarContainerEl: HTMLElement;
- private parentScrollSubscription: Subscription = null;
public notification: NotificationMessage;
- constructor(@Inject(MAT_SNACK_BAR_DATA) public data: any, private elementRef: ElementRef,
- public cd: ChangeDetectorRef,
- public snackBarRef: MatSnackBarRef) {
+
+ animationState: ToastAnimationState;
+
+ animationParams = {
+ open: 100,
+ close: 100
+ };
+
+ constructor(@Inject(MAT_SNACK_BAR_DATA)
+ private data: ToastPanelData,
+ @Optional()
+ private snackBarRef: MatSnackBarRef,
+ @Optional()
+ private overlayRef: OverlayRef) {
+ this.animationState = !!this.snackBarRef ? 'default' : 'opened';
this.notification = data.notification;
}
ngAfterViewInit() {
- this.parentEl = this.data.parent.nativeElement;
- this.snackBarContainerEl = this.elementRef.nativeElement.parentNode;
- this.snackBarContainerEl.style.position = 'absolute';
- this.updateContainerRect();
- this.updatePosition(this.snackBarRef.containerInstance.snackBarConfig);
- const snackBarComponent = this;
- this.parentScrollSubscription = onParentScrollOrWindowResize(this.parentEl).subscribe(() => {
- snackBarComponent.updateContainerRect();
- });
- }
-
- updatePosition(config: MatSnackBarConfig) {
- const isRtl = config.direction === 'rtl';
- const isLeft = (config.horizontalPosition === 'left' ||
- (config.horizontalPosition === 'start' && !isRtl) ||
- (config.horizontalPosition === 'end' && isRtl));
- const isRight = !isLeft && config.horizontalPosition !== 'center';
- if (isLeft) {
- this.snackBarContainerEl.style.justifyContent = 'flex-start';
- } else if (isRight) {
- this.snackBarContainerEl.style.justifyContent = 'flex-end';
- } else {
- this.snackBarContainerEl.style.justifyContent = 'center';
- }
- if (config.verticalPosition === 'top') {
- this.snackBarContainerEl.style.alignItems = 'flex-start';
- } else {
- this.snackBarContainerEl.style.alignItems = 'flex-end';
- }
}
ngOnDestroy() {
- if (this.parentScrollSubscription) {
- this.parentScrollSubscription.unsubscribe();
- }
}
- updateContainerRect() {
- const viewportOffset = this.parentEl.getBoundingClientRect();
- this.snackBarContainerEl.style.top = viewportOffset.top + 'px';
- this.snackBarContainerEl.style.left = viewportOffset.left + 'px';
- this.snackBarContainerEl.style.width = viewportOffset.width + 'px';
- this.snackBarContainerEl.style.height = viewportOffset.height + 'px';
+ action(): void {
+ if (this.snackBarRef) {
+ this.snackBarRef.dismissWithAction();
+ } else {
+ this.animationState = 'closing';
+ }
}
- action(): void {
- this.snackBarRef.dismissWithAction();
+ onHideFinished(event: AnimationEvent) {
+ const { toState } = event;
+ const isFadeOut = (toState as ToastAnimationState) === 'closing';
+ const itFinished = this.animationState === 'closing';
+ if (isFadeOut && itFinished) {
+ this.overlayRef.dispose();
+ }
}
}
diff --git a/ui-ngx/src/app/shared/models/page/page-link.ts b/ui-ngx/src/app/shared/models/page/page-link.ts
index 712757a7f3..dba4ce097c 100644
--- a/ui-ngx/src/app/shared/models/page/page-link.ts
+++ b/ui-ngx/src/app/shared/models/page/page-link.ts
@@ -70,18 +70,18 @@ export function sortItems(item1: any, item2: any, property: string, asc: boolean
const item2Value = getDescendantProp(item2, property);
let result = 0;
if (item1Value !== item2Value) {
- if (typeof item1Value === 'number' && typeof item2Value === 'number') {
+ const item1Type = typeof item1Value;
+ const item2Type = typeof item2Value;
+ if (item1Type === 'number' && item2Type === 'number') {
result = item1Value - item2Value;
- } else if (typeof item1Value === 'string' && typeof item2Value === 'string') {
+ } else if (item1Type === 'string' && item2Type === 'string') {
result = item1Value.localeCompare(item2Value);
- } else if (typeof item1Value === 'boolean' && typeof item2Value === 'boolean') {
+ } else if ((item1Type === 'boolean' && item2Type === 'boolean') || (item1Type !== item2Type)) {
if (item1Value && !item2Value) {
result = 1;
} else if (!item1Value && item2Value) {
result = -1;
}
- } else if (typeof item1Value !== typeof item2Value) {
- result = 1;
}
}
return asc ? result : result * -1;
diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts
index caffc0a66a..8bd4653eb1 100644
--- a/ui-ngx/src/app/shared/models/widget.models.ts
+++ b/ui-ngx/src/app/shared/models/widget.models.ts
@@ -152,6 +152,7 @@ export interface WidgetTypeParameters {
stateData?: boolean;
hasDataPageLink?: boolean;
singleEntity?: boolean;
+ warnOnPageDataOverflow?: boolean;
}
export interface WidgetControllerDescriptor {
diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json
index 89f8ebc9a2..bc54bfd46a 100644
--- a/ui-ngx/src/assets/locale/locale.constant-en_US.json
+++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json
@@ -1712,7 +1712,8 @@
"add": "Add Widget",
"undo": "Undo widget changes",
"export": "Export widget",
- "no-data": "No data to display on widget"
+ "no-data": "No data to display on widget",
+ "data-overflow": "Widget displays {{count}} out of {{total}} entities"
},
"widget-action": {
"header-button": "Widget header button",
diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss
index e253727727..44feb80244 100644
--- a/ui-ngx/src/styles.scss
+++ b/ui-ngx/src/styles.scss
@@ -1056,4 +1056,8 @@ mat-label {
line-height: 1.5;
white-space: pre-line;
}
+
+ .tb-toast-panel {
+ pointer-events: none !important;
+ }
}